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"
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"
21 echo " --run-image Override the image used for running the container"
30 if [[ $command == "run" ]]; then
34 while [ ${#} -gt 0 ]; do
58 if [ -z "$command" -o -z "$config" -a "$command" != "cleanup" ]; then
63 # Docker doesn't like uppercase characters, spaces or special characters, catch it now before we build everything out and then find out
64 re
='[[:upper:]/ !@#$%^&*()+~`=]'
65 if [[ $config =~
$re ]];
68 echo "ERROR: Config name '$config' must not contain upper case characters, spaces or special characters. Correct config name and rerun $0."
75 docker_min_version
='17.03.1'
76 docker_rec_version
='17.06.2'
77 git_min_version
='1.8.0'
78 git_rec_version
='1.8.0'
80 config_file
=containers
/"$config".yml
81 cidbootstrap
=cids
/"$config"_bootstrap.cid
82 local_discourse
=local_discourse
83 image
=discourse
/base
:2.0.20190217
84 docker_path
=`which docker.io || which docker`
87 if [ "${SUPERVISED}" = "true" ]; then
88 restart_policy
="--restart=no"
90 attach_on_run
="-a stdout -a stderr"
95 if [ -n "$DOCKER_HOST" ]; then
96 docker_ip
=`sed -e 's/^tcp:\/\/\(.*\):.*$/\1/' <<< "$DOCKER_HOST"`
97 elif [ -x "$(which ip 2>/dev/null)" ]; then
98 docker_ip
=`ip addr show docker0 | \
100 awk '{ split($2,a,"/"); print a[1] }';`
102 docker_ip
=`ifconfig | \
103 grep -B1 "inet addr" | \
104 awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
106 awk -F: '{ print $3 }';`
109 # From https://stackoverflow.com/a/44660519/702738
111 if [[ $1 == $2 ]]; then
115 local i a
=(${1%%[^0-9.]*}) b
=(${2%%[^0-9.]*})
116 local arem
=${1#${1%%[^0-9.]*}} brem=${2#${2%%[^0-9.]*}}
117 for ((i
=0; i
<${#a[@]} || i
<${#b[@]}; i
++)); do
118 if ((10#${a[i]:-0} < 10#${b[i]:-0})); then
120 elif ((10#${a[i]:-0} > 10#${b[i]:-0})); then
124 if [ "$arem" '<' "$brem" ]; then
126 elif [ "$arem" '>' "$brem" ]; then
134 echo "Docker is not installed, you will need to install Docker in order to run Launcher"
135 echo "See https://docs.docker.com/installation/"
141 if [ -z $docker_path ]; then
145 # 1. docker daemon running?
146 # we send stderr to /dev/null cause we don't care about warnings,
147 # it usually complains about swap which does not matter
148 test=`$docker_path info 2> /dev/null`
149 if [[ $?
-ne 0 ]] ; then
150 echo "Cannot connect to the docker daemon - verify it is running and you have access"
154 # 2. running an approved storage driver?
155 if ! $docker_path info
2> /dev
/null |
egrep -q '^Storage Driver: (aufs|btrfs|zfs|overlay|overlay2)$'; then
156 echo "Your Docker installation is not using a supported storage driver. If we were to proceed you may have a broken install."
157 echo "aufs is the recommended storage driver, although zfs/btrfs/overlay and overlay2 may work as well."
158 echo "Other storage drivers are known to be problematic."
159 echo "You can tell what filesystem you are using by running \"docker info\" and looking at the 'Storage Driver' line."
161 echo "If you wish to continue anyway using your existing unsupported storage driver,"
162 echo "read the source code of launcher and figure out how to bypass this check."
166 # 3. running recommended docker version
167 test=($
($docker_path --version)) # Get docker version string
168 test=${test[2]//,/} # Get version alone and strip comma if exists
170 # At least minimum docker version
171 if compare_version
"${docker_min_version}" "${test}"; then
172 echo "ERROR: Docker version ${test} not supported, please upgrade to at least ${docker_min_version}, or recommended ${docker_rec_version}"
176 # Recommend newer docker version
177 if compare_version
"${docker_rec_version}" "${test}"; then
178 echo "WARNING: Docker version ${test} deprecated, recommend upgrade to ${docker_rec_version} or newer."
181 # 4. discourse docker image is downloaded
182 test=`$docker_path images | awk '{print $1 ":" $2 }' | grep "$image"`
184 if [ -z "$test" ]; then
186 echo "WARNING: We are about to start downloading the Discourse base image"
187 echo "This process may take anywhere between a few minutes to an hour, depending on your network speed"
189 echo "Please be patient"
193 # 5. running recommended git version
194 test=($
($git_path --version)) # Get git version string
195 test=${test[2]//,/} # Get version alone and strip comma if exists
197 # At least minimum version
198 if compare_version
"${git_min_version}" "${test}"; then
199 echo "ERROR: Git version ${test} not supported, please upgrade to at least ${git_min_version}, or recommended ${git_rec_version}"
203 # Recommend best version
204 if compare_version
"${git_rec_version}" "${test}"; then
205 echo "WARNING: Git version ${test} deprecated, recommend upgrade to ${git_rec_version} or newer."
208 # 6. able to attach stderr / out / tty
209 test=`$docker_path run $user_args -i --rm -a stdout -a stderr $image echo working`
210 if [[ "$test" =~
"working" ]] ; then : ; else
211 echo "Your Docker installation is not working correctly"
213 echo "See: https://meta.discourse.org/t/docker-error-on-bootstrap/13657/18?u=sam"
217 # 7. enough space for the bootstrap on docker folder
218 folder
=`$docker_path info --format '{{.DockerRootDir}}'`
219 safe_folder
=${folder:-/var/lib/docker}
220 test=$
(($
(stat
-f --format="%a*%S" $safe_folder)/1024**3 < 5))
221 if [[ $test -ne 0 ]] ; then
222 echo "You have less than 5GB of free space on the disk where $safe_folder is located. You will need more space to continue"
225 read -p "Would you like to attempt to recover space by cleaning docker images and containers in the system?(y/N)" -n 1 -r
227 if [[ $REPLY =~ ^
[Yy
]$
]]
229 $docker_path system prune
-af
230 echo "If the cleanup was successful, you may try again now"
237 if [ -z "$SKIP_PREREQS" ] && [ "$command" != "cleanup" ]; then
242 read -r -d '' env_ruby
<< 'RUBY'
245 input
= STDIN.readlines.
join
246 yaml
= YAML.load
(input
)
248 if host_run
= yaml
['host_run']
249 params
= yaml
['params'] ||
{}
250 host_run.each
do |run|
252 run
= run.gsub
("$#{k}", v
)
254 STDOUT.
write "#{run}--SEP--"
259 host_run
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
261 while [ "$host_run" ] ; do
262 iter
=${host_run%%--SEP--*}
264 echo "Host run: $iter"
267 host_run
="${host_run#*--SEP--}"
273 volumes
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
274 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
278 links
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
279 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
283 local templates
=`cat $1 | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
284 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
286 local arrTemplates
=${templates// / }
288 if [ ! -z "$templates" ]; then
289 for template
in "${arrTemplates[@]}"
291 local nested_templates
=$
(find_templates
$template)
293 if [ ! -z "$nested_templates" ]; then
294 templates
="$templates $nested_templates"
304 set_template_info
() {
305 templates
=$
(find_templates
$config_file)
307 arrTemplates
=(${templates// / })
308 config_data
=$
(cat $config_file)
312 for template
in "${arrTemplates[@]}"
314 [ ! -z $template ] && {
315 input
="$input _FILE_SEPERATOR_ $(cat $template)"
319 # we always want our config file last so it takes priority
320 input
="$input _FILE_SEPERATOR_ $config_data"
322 read -r -d '' env_ruby
<< 'RUBY'
325 input
=STDIN.readlines.
join
326 # default to UTF-8 for the dbs sake
327 env
= {'LANG' => 'en_US.UTF-8'}
328 input.
split('_FILE_SEPERATOR_').each
do |yml|
331 env.merge
!(YAML.load
(yml
)['env'] ||
{})
332 rescue Psych
::SyntaxError
=> e
340 puts env.map
{|k
,v|
"-e\n#{k}=#{v}" }.
join("\n")
343 tmp_input_file
=$
(mktemp
)
345 echo "$input" > "$tmp_input_file"
346 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
348 rm -f "$tmp_input_file"
353 if [ "$i" == "*ERROR." ]; then
355 elif [ -n "$i" ]; then
356 env
[${#env[@]}]="${i//\{\{config\}\}/${config}}"
360 if [ "$ok" -ne 1 ]; then
362 echo "YAML syntax error. Please check your containers
/*.yml config files.
"
367 read -r -d '' labels_ruby << 'RUBY'
370 input=STDIN.readlines.join
372 input.split('_FILE_SEPERATOR_').each do |yml|
375 labels.merge!(YAML.load(yml)['labels'] || {})
376 rescue Psych::SyntaxError => e
384 puts labels.map{|k,v| "-l\n#{k}=#{v}" }.join("\n")
387 tmp_input_file
=$
(mktemp
)
389 echo "$input" > "$tmp_input_file"
390 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$labels_ruby"`
392 rm -f "$tmp_input_file"
397 if [ "$i" == "*ERROR." ]; then
399 elif [ -n "$i" ]; then
400 labels
[${#labels[@]}]=$
(echo $i |
sed s
/{{config
}}/${config}/g
)
404 if [ "$ok" -ne 1 ]; then
406 echo "YAML syntax error. Please check your containers/*.yml config files."
411 read -r -d '' ports_ruby
<< 'RUBY'
414 input
=STDIN.readlines.
join
416 input.
split('_FILE_SEPERATOR_').each
do |yml|
419 ports
+= (YAML.load
(yml
)['expose'] ||
[])
420 rescue Psych
::SyntaxError
=> e
428 puts ports.map
{|p|
"-p\n#{p}"}.
join("\n")
431 tmp_input_file
=$
(mktemp
)
433 echo "$input" > "$tmp_input_file"
434 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$ports_ruby"`
436 rm -f "$tmp_input_file"
441 if [ "$i" == "*ERROR." ]; then
443 elif [ -n "$i" ]; then
444 ports
[${#ports[@]}]=$i
448 if [ "$ok" -ne 1 ]; then
450 echo "YAML syntax error. Please check your containers/*.yml config files."
457 if [ -z $docker_path ]; then
461 [ "$command" == "cleanup" ] && {
462 $docker_path system prune
-a
464 if [ -d /var
/discourse
/shared
/standalone
/postgres_data_old
]; then
466 echo "Old PostgreSQL backup data cluster detected taking up $(du -hs /var/discourse/shared/standalone/postgres_data_old | awk '{print $1}') detected"
467 read -p "Would you like to remove it? (Y/n): " -n 1 -r && echo
469 if [[ $REPLY =~ ^
[Yy
]$
]]; then
470 echo "removing old PostgreSQL data cluster at /var/discourse/shared/standalone/postgres_data_old..."
471 rm -rf /var
/discourse
/shared
/standalone
/postgres_data_old
480 if [ ! "$command" == "setup" ]; then
481 if [[ ! -e $config_file ]]; then
482 echo "Config file was not found, ensure $config_file exists"
484 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
489 docker_version
=($
($docker_path --version))
490 docker_version
=${test[2]//,/}
491 restart_policy
=${restart_policy:---restart=always}
493 set_existing_container
(){
494 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
499 set_existing_container
501 if [ ! -z $existing ]
505 $docker_path stop
-t 10 $config
508 echo "$config was not started !"
514 run_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
515 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['run_image']"`
517 if [ -n "$user_run_image" ]; then
518 run_image
=$user_run_image
519 elif [ -z "$run_image" ]; then
520 run_image
="$local_discourse/$config"
525 boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
526 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
528 if [ -z "$boot_command" ]; then
530 no_boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
531 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
533 if [ -z "$no_boot_command" ]; then
534 boot_command
="/sbin/boot"
542 docker_args
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
543 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
545 if [[ -n "$docker_args" ]]; then
546 user_args
="$user_args $docker_args"
552 existing
=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
554 if [ ! -z $existing ]
556 echo "Nothing to do, your container has already started!"
560 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
561 if [ ! -z $existing ]
563 echo "starting up existing container"
566 $docker_path start
$config
579 # get hostname and settings from container configuration
580 for envar
in "${env[@]}"
582 if [[ $envar == DOCKER_USE_HOSTNAME
* ]] ||
[[ $envar == DISCOURSE_HOSTNAME
* ]]
584 # use as environment variable
590 hostname
=`hostname -s`
592 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
594 hostname
=$DISCOURSE_HOSTNAME
596 hostname
=$hostname-$config
599 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
600 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
601 # docker added more hostname rules
602 hostname
=${hostname//_/-}
605 if [ -z "$SKIP_MAC_ADDRESS" ] ; then
606 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/'")"
610 $docker_path run
--shm-size=512m
$links $attach_on_run $restart_policy "${env[@]}" "${labels[@]}" -h "$hostname" \
611 -e DOCKER_HOST_IP
="$docker_ip" --name $config -t "${ports[@]}" $volumes $mac_address $user_args \
612 $run_image $boot_command
626 (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 \
627 /bin
/bash
-c "$run_command") || ERR
=$?
629 if [[ $ERR > 0 ]]; then
637 # Is the image available?
638 # If not, pull it here so the user is aware what's happening.
639 $docker_path history $image >/dev
/null
2>&1 ||
$docker_path pull
$image
643 base_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
644 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
646 update_pups
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
647 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
649 if [[ ! X
"" = X
"$base_image" ]]; then
658 run_command
="cd /pups &&"
659 if [[ ! "false" = $update_pups ]]; then
660 run_command
="$run_command git pull &&"
662 run_command
="$run_command /pups/bin/pups --stdin"
668 tmp_input_file
=$
(mktemp
)
670 echo "$input" > "$tmp_input_file"
671 (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 \
672 /bin
/bash
-c "$run_command") || ERR
=$?
674 rm -f "$tmp_input_file"
677 # magic exit code that indicates a retry
678 if [[ "$ERR" == 77 ]]; then
679 $docker_path rm `cat $cidbootstrap`
682 elif [[ "$ERR" > 0 ]]; then
686 if [[ $FAILED = "TRUE" ]]; then
687 if [[ ! -z "$DEBUG" ]]; then
688 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config-debug ||
echo 'FAILED TO COMMIT'
689 echo "** DEBUG ** Maintaining image for diagnostics $local_discourse/$config-debug"
692 $docker_path rm `cat $cidbootstrap`
694 echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one"
700 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config ||
echo 'FAILED TO COMMIT'
701 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
707 echo "Successfully bootstrapped, to startup use ./launcher start $config"
717 exec $docker_path exec -it $config /bin
/bash
--login
727 $docker_path logs
$config
743 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
744 echo "Ensuring launcher is up to date"
748 LOCAL
=$
(git rev-parse @
)
749 REMOTE
=$
(git rev-parse @
{u
})
750 BASE
=$
(git merge-base @ @
{u
})
752 if [ $LOCAL = $REMOTE ]; then
753 echo "Launcher is up-to-date"
755 elif [ $LOCAL = $BASE ]; then
756 echo "Updating Launcher"
757 git pull ||
(echo 'failed to update' && exit 1)
759 for (( i
=${#BASH_ARGV[@]}-1,j
=0; i
>=0,j
<${#BASH_ARGV[@]}; i--
,j
++ ))
761 args
[$j]=${BASH_ARGV[$i]}
763 exec /bin
/bash
$0 "${args[@]}" # $@ is empty, because of shift at the beginning. Use BASH_ARGV instead.
765 elif [ $REMOTE = $BASE ]; then
766 echo "Your version of Launcher is ahead of origin"
769 echo "Launcher has diverged source, this is only expected in Dev mode"
774 set_existing_container
776 if [ ! -z $existing ]
778 echo "Stopping old container"
781 $docker_path stop
-t 10 $config
787 if [ ! -z $existing ]
789 echo "Removing old container"
792 $docker_path rm $config
802 (set -x; $docker_path stop
-t 10 $config && $docker_path rm $config) ||
(echo "$config was not found" && exit 0)