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.20190625-0946"
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 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 if tty
>/dev
/null
; then
227 read -p "Would you like to attempt to recover space by cleaning docker images and containers in the system?(y/N)" -n 1 -r
229 if [[ $REPLY =~ ^
[Yy
]$
]]
231 $docker_path system prune
-af
232 echo "If the cleanup was successful, you may try again now"
240 if [ -z "$SKIP_PREREQS" ] && [ "$command" != "cleanup" ]; then
245 read -r -d '' env_ruby
<< 'RUBY'
248 input
= STDIN.readlines.
join
249 yaml
= YAML.load
(input
)
251 if host_run
= yaml
['host_run']
252 params
= yaml
['params'] ||
{}
253 host_run.each
do |run|
255 run
= run.gsub
("$#{k}", v
)
257 STDOUT.
write "#{run}--SEP--"
262 host_run
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
264 while [ "$host_run" ] ; do
265 iter
=${host_run%%--SEP--*}
267 echo "Host run: $iter"
270 host_run
="${host_run#*--SEP--}"
276 volumes
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
277 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
281 links
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
282 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
286 local templates
=`cat $1 | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
287 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
289 local arrTemplates
=${templates// / }
291 if [ ! -z "$templates" ]; then
292 for template
in "${arrTemplates[@]}"
294 local nested_templates
=$
(find_templates
$template)
296 if [ ! -z "$nested_templates" ]; then
297 templates
="$templates $nested_templates"
307 set_template_info
() {
308 templates
=$
(find_templates
$config_file)
310 arrTemplates
=(${templates// / })
311 config_data
=$
(cat $config_file)
315 for template
in "${arrTemplates[@]}"
317 [ ! -z $template ] && {
318 input
="$input _FILE_SEPERATOR_ $(cat $template)"
322 # we always want our config file last so it takes priority
323 input
="$input _FILE_SEPERATOR_ $config_data"
325 read -r -d '' env_ruby
<< 'RUBY'
328 input
=STDIN.readlines.
join
329 # default to UTF-8 for the dbs sake
330 env
= {'LANG' => 'en_US.UTF-8'}
331 input.
split('_FILE_SEPERATOR_').each
do |yml|
334 env.merge
!(YAML.load
(yml
)['env'] ||
{})
335 rescue Psych
::SyntaxError
=> e
343 puts env.map
{|k
,v|
"-e\n#{k}=#{v}" }.
join("\n")
346 tmp_input_file
=$
(mktemp
)
348 echo "$input" > "$tmp_input_file"
349 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
351 rm -f "$tmp_input_file"
356 if [ "$i" == "*ERROR." ]; then
358 elif [ -n "$i" ]; then
359 env
[${#env[@]}]="${i//\{\{config\}\}/${config}}"
363 if [ "$ok" -ne 1 ]; then
365 echo "YAML syntax error. Please check your containers
/*.yml config files.
"
370 read -r -d '' labels_ruby << 'RUBY'
373 input=STDIN.readlines.join
375 input.split('_FILE_SEPERATOR_').each do |yml|
378 labels.merge!(YAML.load(yml)['labels'] || {})
379 rescue Psych::SyntaxError => e
387 puts labels.map{|k,v| "-l\n#{k}=#{v}" }.join("\n")
390 tmp_input_file
=$
(mktemp
)
392 echo "$input" > "$tmp_input_file"
393 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$labels_ruby"`
395 rm -f "$tmp_input_file"
400 if [ "$i" == "*ERROR." ]; then
402 elif [ -n "$i" ]; then
403 labels
[${#labels[@]}]=$
(echo $i |
sed s
/{{config
}}/${config}/g
)
407 if [ "$ok" -ne 1 ]; then
409 echo "YAML syntax error. Please check your containers/*.yml config files."
414 read -r -d '' ports_ruby
<< 'RUBY'
417 input
=STDIN.readlines.
join
419 input.
split('_FILE_SEPERATOR_').each
do |yml|
422 ports
+= (YAML.load
(yml
)['expose'] ||
[])
423 rescue Psych
::SyntaxError
=> e
431 puts ports.map
{ |p| p.to_s.include?
(':') ?
"-p\n#{p}" : "--expose\n#{p}" }.
join("\n")
434 tmp_input_file
=$
(mktemp
)
436 echo "$input" > "$tmp_input_file"
437 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$ports_ruby"`
439 rm -f "$tmp_input_file"
444 if [ "$i" == "*ERROR." ]; then
446 elif [ -n "$i" ]; then
447 ports
[${#ports[@]}]=$i
451 if [ "$ok" -ne 1 ]; then
453 echo "YAML syntax error. Please check your containers/*.yml config files."
460 if [ -z $docker_path ]; then
464 [ "$command" == "cleanup" ] && {
465 $docker_path system prune
-a
467 if [ -d /var
/discourse
/shared
/standalone
/postgres_data_old
]; then
469 echo "Old PostgreSQL backup data cluster detected taking up $(du -hs /var/discourse/shared/standalone/postgres_data_old | awk '{print $1}') detected"
470 read -p "Would you like to remove it? (Y/n): " -n 1 -r && echo
472 if [[ $REPLY =~ ^
[Yy
]$
]]; then
473 echo "removing old PostgreSQL data cluster at /var/discourse/shared/standalone/postgres_data_old..."
474 rm -rf /var
/discourse
/shared
/standalone
/postgres_data_old
483 if [ ! "$command" == "setup" ]; then
484 if [[ ! -e $config_file ]]; then
485 echo "Config file was not found, ensure $config_file exists"
487 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
492 docker_version
=($
($docker_path --version))
493 docker_version
=${test[2]//,/}
494 restart_policy
=${restart_policy:---restart=always}
496 set_existing_container
(){
497 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
502 set_existing_container
504 if [ ! -z $existing ]
508 $docker_path stop
-t 10 $config
511 echo "$config was not started !"
517 run_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
518 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['run_image']"`
520 if [ -n "$user_run_image" ]; then
521 run_image
=$user_run_image
522 elif [ -z "$run_image" ]; then
523 run_image
="$local_discourse/$config"
528 boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
529 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
531 if [ -z "$boot_command" ]; then
533 no_boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
534 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
536 if [ -z "$no_boot_command" ]; then
537 boot_command
="/sbin/boot"
545 docker_args
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
546 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
548 if [[ -n "$docker_args" ]]; then
549 user_args
="$user_args $docker_args"
555 if [ -z "$START_CMD_ONLY" ]
557 existing
=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
559 if [ ! -z $existing ]
561 echo "Nothing to do, your container has already started!"
565 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
566 if [ ! -z $existing ]
568 echo "starting up existing container"
571 $docker_path start
$config
585 # get hostname and settings from container configuration
586 for envar
in "${env[@]}"
588 if [[ $envar == DOCKER_USE_HOSTNAME
* ]] ||
[[ $envar == DISCOURSE_HOSTNAME
* ]]
590 # use as environment variable
596 hostname
=`hostname -s`
598 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
600 hostname
=$DISCOURSE_HOSTNAME
602 hostname
=$hostname-$config
605 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
606 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
607 # docker added more hostname rules
608 hostname
=${hostname//_/-}
611 if [ -z "$SKIP_MAC_ADDRESS" ] ; then
612 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/'")"
615 if [ ! -z "$START_CMD_ONLY" ] ; then
621 $docker_path run
--shm-size=512m
$links $attach_on_run $restart_policy "${env[@]}" "${labels[@]}" -h "$hostname" \
622 -e DOCKER_HOST_IP
="$docker_ip" --name $config -t "${ports[@]}" $volumes $mac_address $user_args \
623 $run_image $boot_command
637 (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 \
638 /bin
/bash
-c "$run_command") || ERR
=$?
640 if [[ $ERR > 0 ]]; then
648 # Is the image available?
649 # If not, pull it here so the user is aware what's happening.
650 $docker_path history $image >/dev
/null
2>&1 ||
$docker_path pull
$image
654 base_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
655 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
657 update_pups
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
658 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
660 if [[ ! X
"" = X
"$base_image" ]]; then
669 run_command
="cd /pups &&"
670 if [[ ! "false" = $update_pups ]]; then
671 run_command
="$run_command git pull &&"
673 run_command
="$run_command /pups/bin/pups --stdin"
679 tmp_input_file
=$
(mktemp
)
681 echo "$input" > "$tmp_input_file"
682 (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 \
683 /bin
/bash
-c "$run_command") || ERR
=$?
685 rm -f "$tmp_input_file"
688 # magic exit code that indicates a retry
689 if [[ "$ERR" == 77 ]]; then
690 $docker_path rm `cat $cidbootstrap`
693 elif [[ "$ERR" > 0 ]]; then
697 if [[ $FAILED = "TRUE" ]]; then
698 if [[ ! -z "$DEBUG" ]]; then
699 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config-debug ||
echo 'FAILED TO COMMIT'
700 echo "** DEBUG ** Maintaining image for diagnostics $local_discourse/$config-debug"
703 $docker_path rm `cat $cidbootstrap`
705 echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one"
711 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config ||
echo 'FAILED TO COMMIT'
712 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
718 echo "Successfully bootstrapped, to startup use ./launcher start $config"
728 exec $docker_path exec -it $config /bin
/bash
--login
738 $docker_path logs
$config
760 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
761 echo "Ensuring launcher is up to date"
765 LOCAL
=$
(git rev-parse HEAD
)
766 REMOTE
=$
(git rev-parse @
{u
})
767 BASE
=$
(git merge-base HEAD @
{u
})
769 if [ $LOCAL = $REMOTE ]; then
770 echo "Launcher is up-to-date"
772 elif [ $LOCAL = $BASE ]; then
773 echo "Updating Launcher"
774 git pull ||
(echo 'failed to update' && exit 1)
776 for (( i
=${#BASH_ARGV[@]}-1,j
=0; i
>=0,j
<${#BASH_ARGV[@]}; i--
,j
++ ))
778 args
[$j]=${BASH_ARGV[$i]}
780 exec /bin
/bash
$0 "${args[@]}" # $@ is empty, because of shift at the beginning. Use BASH_ARGV instead.
782 elif [ $REMOTE = $BASE ]; then
783 echo "Your version of Launcher is ahead of origin"
786 echo "Launcher has diverged source, this is only expected in Dev mode"
791 set_existing_container
793 if [ ! -z $existing ]
795 echo "Stopping old container"
798 $docker_path stop
-t 10 $config
804 if [ ! -z $existing ]
806 echo "Removing old container"
809 $docker_path rm $config
819 (set -x; $docker_path stop
-t 10 $config && $docker_path rm $config) ||
(echo "$config was not found" && exit 0)