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"
29 # user_args_argv is assigned once when the argument vector is parsed.
31 # user_args is mutable: its value may change when templates are parsed.
32 # Superset of user_args_argv.
37 if [[ $command == "run" ]]; then
41 while [ ${#} -gt 0 ]; do
54 user_args
="$user_args_argv"
66 if [ -z "$command" -o -z "$config" -a "$command" != "cleanup" ]; then
71 # Docker doesn't like uppercase characters, spaces or special characters, catch it now before we build everything out and then find out
72 re
='[[:upper:]/ !@#$%^&*()+~`=]'
73 if [[ $config =~
$re ]];
76 echo "ERROR: Config name '$config' must not contain upper case characters, spaces or special characters. Correct config name and rerun $0."
83 docker_min_version
='17.03.1'
84 docker_rec_version
='17.06.2'
85 git_min_version
='1.8.0'
86 git_rec_version
='1.8.0'
88 config_file
=containers
/"$config".yml
89 cidbootstrap
=cids
/"$config"_bootstrap.cid
90 local_discourse
=local_discourse
91 image
="discourse/base:2.0.20200220-2221"
92 docker_path
=`which docker.io 2> /dev/null || which docker`
95 if [ "${SUPERVISED}" = "true" ]; then
96 restart_policy
="--restart=no"
98 attach_on_run
="-a stdout -a stderr"
103 if [ -n "$DOCKER_HOST" ]; then
104 docker_ip
=`sed -e 's/^tcp:\/\/\(.*\):.*$/\1/' <<< "$DOCKER_HOST"`
105 elif [ -x "$(which ip 2>/dev/null)" ]; then
106 docker_ip
=`ip addr show docker0 | \
108 awk '{ split($2,a,"/"); print a[1] }';`
110 docker_ip
=`ifconfig | \
111 grep -B1 "inet addr" | \
112 awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
114 awk -F: '{ print $3 }';`
117 # From https://stackoverflow.com/a/44660519/702738
119 if [[ $1 == $2 ]]; then
123 local i a
=(${1%%[^0-9.]*}) b
=(${2%%[^0-9.]*})
124 local arem
=${1#${1%%[^0-9.]*}} brem=${2#${2%%[^0-9.]*}}
125 for ((i
=0; i
<${#a[@]} || i
<${#b[@]}; i
++)); do
126 if ((10#${a[i]:-0} < 10#${b[i]:-0})); then
128 elif ((10#${a[i]:-0} > 10#${b[i]:-0})); then
132 if [ "$arem" '<' "$brem" ]; then
134 elif [ "$arem" '>' "$brem" ]; then
142 echo "Docker is not installed, you will need to install Docker in order to run Launcher"
143 echo "See https://docs.docker.com/installation/"
149 if [ -z $docker_path ]; then
153 # 1. docker daemon running?
154 # we send stderr to /dev/null cause we don't care about warnings,
155 # it usually complains about swap which does not matter
156 test=`$docker_path info 2> /dev/null`
157 if [[ $?
-ne 0 ]] ; then
158 echo "Cannot connect to the docker daemon - verify it is running and you have access"
162 # 2. running an approved storage driver?
163 if ! $docker_path info
2> /dev
/null |
egrep -q 'Storage Driver: (aufs|zfs|overlay2)$'; then
164 echo "Your Docker installation is not using a supported storage driver. If we were to proceed you may have a broken install."
165 echo "aufs is the recommended storage driver, although zfs and overlay2 may work as well."
166 echo "Other storage drivers are known to be problematic."
167 echo "You can tell what filesystem you are using by running \"docker info\" and looking at the 'Storage Driver' line."
169 echo "If you wish to continue anyway using your existing unsupported storage driver,"
170 echo "read the source code of launcher and figure out how to bypass this check."
174 # 3. running recommended docker version
175 test=($
($docker_path --version)) # Get docker version string
176 test=${test[2]//,/} # Get version alone and strip comma if exists
178 # At least minimum docker version
179 if compare_version
"${docker_min_version}" "${test}"; then
180 echo "ERROR: Docker version ${test} not supported, please upgrade to at least ${docker_min_version}, or recommended ${docker_rec_version}"
184 # Recommend newer docker version
185 if compare_version
"${docker_rec_version}" "${test}"; then
186 echo "WARNING: Docker version ${test} deprecated, recommend upgrade to ${docker_rec_version} or newer."
189 # 4. discourse docker image is downloaded
190 test=`$docker_path images | awk '{print $1 ":" $2 }' | grep "$image"`
192 if [ -z "$test" ]; then
194 echo "WARNING: We are about to start downloading the Discourse base image"
195 echo "This process may take anywhere between a few minutes to an hour, depending on your network speed"
197 echo "Please be patient"
201 # 5. running recommended git version
202 test=($
($git_path --version)) # Get git version string
203 test=${test[2]//,/} # Get version alone and strip comma if exists
205 # At least minimum version
206 if compare_version
"${git_min_version}" "${test}"; then
207 echo "ERROR: Git version ${test} not supported, please upgrade to at least ${git_min_version}, or recommended ${git_rec_version}"
211 # Recommend best version
212 if compare_version
"${git_rec_version}" "${test}"; then
213 echo "WARNING: Git version ${test} deprecated, recommend upgrade to ${git_rec_version} or newer."
216 # 6. able to attach stderr / out / tty
217 test=`$docker_path run $user_args -i --rm -a stdout -a stderr $image echo working`
218 if [[ "$test" =~
"working" ]] ; then : ; else
219 echo "Your Docker installation is not working correctly"
221 echo "See: https://meta.discourse.org/t/docker-error-on-bootstrap/13657/18?u=sam"
225 # 7. enough space for the bootstrap on docker folder
226 folder
=`$docker_path info --format '{{.DockerRootDir}}'`
227 safe_folder
=${folder:-/var/lib/docker}
228 test=$
(($
(stat
-f --format="%a*%S" $safe_folder)/1024**3 < 5))
229 if [[ $test -ne 0 ]] ; then
230 echo "You have less than 5GB of free space on the disk where $safe_folder is located. You will need more space to continue"
233 if tty
>/dev
/null
; then
234 read -p "Would you like to attempt to recover space by cleaning docker images and containers in the system?(y/N)" -n 1 -r
236 if [[ $REPLY =~ ^
[Yy
]$
]]
238 $docker_path container prune
--force --filter until=1h
>/dev
/null
239 $docker_path image prune
--all --force --filter until=1h
>/dev
/null
240 echo "If the cleanup was successful, you may try again now"
248 if [ -z "$SKIP_PREREQS" ] && [ "$command" != "cleanup" ]; then
253 read -r -d '' env_ruby
<< 'RUBY'
256 input
= STDIN.readlines.
join
257 yaml
= YAML.load
(input
)
259 if host_run
= yaml
['host_run']
260 params
= yaml
['params'] ||
{}
261 host_run.each
do |run|
263 run
= run.gsub
("$#{k}", v
)
265 STDOUT.
write "#{run}--SEP--"
270 host_run
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
272 while [ "$host_run" ] ; do
273 iter
=${host_run%%--SEP--*}
275 echo "Host run: $iter"
278 host_run
="${host_run#*--SEP--}"
284 volumes
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
285 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
289 links
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
290 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
294 local templates
=`cat $1 | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
295 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
297 local arrTemplates
=${templates// / }
299 if [ ! -z "$templates" ]; then
300 for template
in "${arrTemplates[@]}"
302 local nested_templates
=$
(find_templates
$template)
304 if [ ! -z "$nested_templates" ]; then
305 templates
="$templates $nested_templates"
315 set_template_info
() {
316 templates
=$
(find_templates
$config_file)
318 arrTemplates
=(${templates// / })
319 config_data
=$
(cat $config_file)
323 for template
in "${arrTemplates[@]}"
325 [ ! -z $template ] && {
326 input
="$input _FILE_SEPERATOR_ $(cat $template)"
330 # we always want our config file last so it takes priority
331 input
="$input _FILE_SEPERATOR_ $config_data"
333 read -r -d '' env_ruby
<< 'RUBY'
336 input
=STDIN.readlines.
join
337 # default to UTF-8 for the dbs sake
338 env
= {'LANG' => 'en_US.UTF-8'}
339 input.
split('_FILE_SEPERATOR_').each
do |yml|
342 env.merge
!(YAML.load
(yml
)['env'] ||
{})
343 rescue Psych
::SyntaxError
=> e
351 puts env.map
{|k
,v|
"-e\n#{k}=#{v}" }.
join("\n")
354 tmp_input_file
=$
(mktemp
)
356 echo "$input" > "$tmp_input_file"
357 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
359 rm -f "$tmp_input_file"
364 if [ "$i" == "*ERROR." ]; then
366 elif [ -n "$i" ]; then
367 env
[${#env[@]}]="${i//\{\{config\}\}/${config}}"
371 if [ "$ok" -ne 1 ]; then
373 echo "YAML syntax error. Please check your containers
/*.yml config files.
"
378 read -r -d '' labels_ruby << 'RUBY'
381 input=STDIN.readlines.join
383 input.split('_FILE_SEPERATOR_').each do |yml|
386 labels.merge!(YAML.load(yml)['labels'] || {})
387 rescue Psych::SyntaxError => e
395 puts labels.map{|k,v| "-l\n#{k}=#{v}" }.join("\n")
398 tmp_input_file
=$
(mktemp
)
400 echo "$input" > "$tmp_input_file"
401 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$labels_ruby"`
403 rm -f "$tmp_input_file"
408 if [ "$i" == "*ERROR." ]; then
410 elif [ -n "$i" ]; then
411 labels
[${#labels[@]}]=$
(echo $i |
sed s
/{{config
}}/${config}/g
)
415 if [ "$ok" -ne 1 ]; then
417 echo "YAML syntax error. Please check your containers/*.yml config files."
422 read -r -d '' ports_ruby
<< 'RUBY'
425 input
=STDIN.readlines.
join
427 input.
split('_FILE_SEPERATOR_').each
do |yml|
430 ports
+= (YAML.load
(yml
)['expose'] ||
[])
431 rescue Psych
::SyntaxError
=> e
439 puts ports.map
{ |p| p.to_s.include?
(':') ?
"-p\n#{p}" : "--expose\n#{p}" }.
join("\n")
442 tmp_input_file
=$
(mktemp
)
444 echo "$input" > "$tmp_input_file"
445 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$ports_ruby"`
447 rm -f "$tmp_input_file"
452 if [ "$i" == "*ERROR." ]; then
454 elif [ -n "$i" ]; then
455 ports
[${#ports[@]}]=$i
459 if [ "$ok" -ne 1 ]; then
461 echo "YAML syntax error. Please check your containers/*.yml config files."
468 if [ -z $docker_path ]; then
472 [ "$command" == "cleanup" ] && {
473 $docker_path container prune
--filter until=1h
474 $docker_path image prune
--all --filter until=1h
476 if [ -d /var
/discourse
/shared
/standalone
/postgres_data_old
]; then
478 echo "Old PostgreSQL backup data cluster detected taking up $(du -hs /var/discourse/shared/standalone/postgres_data_old | awk '{print $1}') detected"
479 read -p "Would you like to remove it? (Y/n): " -n 1 -r && echo
481 if [[ $REPLY =~ ^
[Yy
]$
]]; then
482 echo "removing old PostgreSQL data cluster at /var/discourse/shared/standalone/postgres_data_old..."
483 rm -rf /var
/discourse
/shared
/standalone
/postgres_data_old
492 if [ ! "$command" == "setup" ]; then
493 if [[ ! -e $config_file ]]; then
494 echo "Config file was not found, ensure $config_file exists"
496 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
501 docker_version
=($
($docker_path --version))
502 docker_version
=${test[2]//,/}
503 restart_policy
=${restart_policy:---restart=always}
505 set_existing_container
(){
506 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
511 set_existing_container
513 if [ ! -z $existing ]
517 $docker_path stop
-t 10 $config
520 echo "$config was not started !"
521 echo "./discourse-doctor may help diagnose the problem."
527 run_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
528 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['run_image']"`
530 if [ -n "$user_run_image" ]; then
531 run_image
=$user_run_image
532 elif [ -z "$run_image" ]; then
533 run_image
="$local_discourse/$config"
538 boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
539 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
541 if [ -z "$boot_command" ]; then
543 no_boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
544 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
546 if [ -z "$no_boot_command" ]; then
547 boot_command
="/sbin/boot"
555 docker_args
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
556 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
558 if [[ -n "$docker_args" ]]; then
559 user_args
="$user_args_argv $docker_args"
565 if [ -z "$START_CMD_ONLY" ]
567 existing
=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
569 if [ ! -z $existing ]
571 echo "Nothing to do, your container has already started!"
575 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
576 if [ ! -z $existing ]
578 echo "starting up existing container"
581 $docker_path start
$config
595 # get hostname and settings from container configuration
596 for envar
in "${env[@]}"
598 if [[ $envar == DOCKER_USE_HOSTNAME
* ]] ||
[[ $envar == DISCOURSE_HOSTNAME
* ]]
600 # use as environment variable
606 hostname
=`hostname -s`
608 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
610 hostname
=$DISCOURSE_HOSTNAME
612 hostname
=$hostname-$config
615 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
616 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
617 # docker added more hostname rules
618 hostname
=${hostname//_/-}
621 if [ -z "$SKIP_MAC_ADDRESS" ] ; then
622 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/'")"
625 if [ ! -z "$START_CMD_ONLY" ] ; then
631 $docker_path run
--shm-size=512m
$links $attach_on_run $restart_policy "${env[@]}" "${labels[@]}" -h "$hostname" \
632 -e DOCKER_HOST_IP
="$docker_ip" --name $config -t "${ports[@]}" $volumes $mac_address $user_args \
633 $run_image $boot_command
647 (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 \
648 /bin
/bash
-c "$run_command") || ERR
=$?
650 if [[ $ERR > 0 ]]; then
658 # Is the image available?
659 # If not, pull it here so the user is aware what's happening.
660 $docker_path history $image >/dev
/null
2>&1 ||
$docker_path pull
$image
664 base_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
665 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
667 update_pups
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
668 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
670 if [[ ! X
"" = X
"$base_image" ]]; then
679 run_command
="cd /pups &&"
680 if [[ ! "false" = $update_pups ]]; then
681 run_command
="$run_command git pull &&"
683 run_command
="$run_command /pups/bin/pups --stdin"
689 tmp_input_file
=$
(mktemp
)
691 echo "$input" > "$tmp_input_file"
692 (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 \
693 /bin
/bash
-c "$run_command") || ERR
=$?
695 rm -f "$tmp_input_file"
698 # magic exit code that indicates a retry
699 if [[ "$ERR" == 77 ]]; then
700 $docker_path rm `cat $cidbootstrap`
703 elif [[ "$ERR" > 0 ]]; then
707 if [[ $FAILED = "TRUE" ]]; then
708 if [[ ! -z "$DEBUG" ]]; then
709 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config-debug ||
echo 'FAILED TO COMMIT'
710 echo "** DEBUG ** Maintaining image for diagnostics $local_discourse/$config-debug"
713 $docker_path rm `cat $cidbootstrap`
715 echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one."
716 echo "./discourse-doctor may help diagnose the problem."
722 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config ||
echo 'FAILED TO COMMIT'
723 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
729 echo "Successfully bootstrapped, to startup use ./launcher start $config"
739 exec $docker_path exec -it $config /bin
/bash
--login
749 $docker_path logs
$config
771 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
772 echo "Ensuring launcher is up to date"
776 LOCAL
=$
(git rev-parse HEAD
)
777 REMOTE
=$
(git rev-parse @
{u
})
778 BASE
=$
(git merge-base HEAD @
{u
})
780 if [ $LOCAL = $REMOTE ]; then
781 echo "Launcher is up-to-date"
783 elif [ $LOCAL = $BASE ]; then
784 echo "Updating Launcher"
785 git pull ||
(echo 'failed to update' && exit 1)
787 for (( i
=${#BASH_ARGV[@]}-1,j
=0; i
>=0,j
<${#BASH_ARGV[@]}; i--
,j
++ ))
789 args
[$j]=${BASH_ARGV[$i]}
791 exec bash
$0 "${args[@]}" # $@ is empty, because of shift at the beginning. Use BASH_ARGV instead.
793 elif [ $REMOTE = $BASE ]; then
794 echo "Your version of Launcher is ahead of origin"
797 echo "Launcher has diverged source, this is only expected in Dev mode"
802 set_existing_container
804 if [ ! -z $existing ]
806 echo "Stopping old container"
809 $docker_path stop
-t 10 $config
815 if [ ! -z $existing ]
817 echo "Removing old container"
820 $docker_path rm $config
830 (set -x; $docker_path stop
-t 10 $config && $docker_path rm $config) ||
(echo "$config was not found" && exit 0)