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"
26 # for potential re-exec later
32 # user_args_argv is assigned once when the argument vector is parsed.
34 # user_args is mutable: its value may change when templates are parsed.
35 # Superset of user_args_argv.
40 if [[ $command == "run" ]]; then
44 while [ ${#} -gt 0 ]; do
57 user_args
="$user_args_argv"
69 if [ -z "$command" -o -z "$config" -a "$command" != "cleanup" ]; then
74 # Docker doesn't like uppercase characters, spaces or special characters, catch it now before we build everything out and then find out
75 re
='[[:upper:]/ !@#$%^&*()+~`=]'
76 if [[ $config =~
$re ]];
79 echo "ERROR: Config name '$config' must not contain upper case characters, spaces or special characters. Correct config name and rerun $0."
86 docker_min_version
='17.03.1'
87 docker_rec_version
='17.06.2'
88 git_min_version
='1.8.0'
89 git_rec_version
='1.8.0'
91 config_file
=containers
/"$config".yml
92 cidbootstrap
=cids
/"$config"_bootstrap.cid
93 local_discourse
=local_discourse
94 image
="discourse/base:2.0.20201004-2310"
95 docker_path
=`which docker.io 2> /dev/null || which docker`
98 if [ "${SUPERVISED}" = "true" ]; then
99 restart_policy
="--restart=no"
101 attach_on_run
="-a stdout -a stderr"
106 if [ -n "$DOCKER_HOST" ]; then
107 docker_ip
=`sed -e 's/^tcp:\/\/\(.*\):.*$/\1/' <<< "$DOCKER_HOST"`
108 elif [ -x "$(which ip 2>/dev/null)" ]; then
109 docker_ip
=`ip addr show docker0 | \
111 awk '{ split($2,a,"/"); print a[1] }';`
113 docker_ip
=`ifconfig | \
114 grep -B1 "inet addr" | \
115 awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
117 awk -F: '{ print $3 }';`
120 # From https://stackoverflow.com/a/44660519/702738
122 if [[ $1 == $2 ]]; then
126 local i a
=(${1%%[^0-9.]*}) b
=(${2%%[^0-9.]*})
127 local arem
=${1#${1%%[^0-9.]*}} brem=${2#${2%%[^0-9.]*}}
128 for ((i
=0; i
<${#a[@]} || i
<${#b[@]}; i
++)); do
129 if ((10#${a[i]:-0} < 10#${b[i]:-0})); then
131 elif ((10#${a[i]:-0} > 10#${b[i]:-0})); then
135 if [ "$arem" '<' "$brem" ]; then
137 elif [ "$arem" '>' "$brem" ]; then
145 echo "Docker is not installed, you will need to install Docker in order to run Launcher"
146 echo "See https://docs.docker.com/installation/"
151 # Add a single retry to work around dockerhub TLS errors
152 $docker_path pull
$image ||
$docker_path pull
$image
157 if [ -z $docker_path ]; then
161 # 1. docker daemon running?
162 # we send stderr to /dev/null cause we don't care about warnings,
163 # it usually complains about swap which does not matter
164 test=`$docker_path info 2> /dev/null`
165 if [[ $?
-ne 0 ]] ; then
166 echo "Cannot connect to the docker daemon - verify it is running and you have access"
170 # 2. running an approved storage driver?
171 if ! $docker_path info
2> /dev
/null |
egrep -q 'Storage Driver: (aufs|zfs|overlay2)$'; then
172 echo "Your Docker installation is not using a supported storage driver. If we were to proceed you may have a broken install."
173 echo "overlay2 is the recommended storage driver, although zfs and aufs may work as well."
174 echo "Other storage drivers are known to be problematic."
175 echo "You can tell what filesystem you are using by running \"docker info\" and looking at the 'Storage Driver' line."
177 echo "If you wish to continue anyway using your existing unsupported storage driver,"
178 echo "read the source code of launcher and figure out how to bypass this check."
182 # 3. running recommended docker version
183 test=($
($docker_path --version)) # Get docker version string
184 test=${test[2]//,/} # Get version alone and strip comma if exists
186 # At least minimum docker version
187 if compare_version
"${docker_min_version}" "${test}"; then
188 echo "ERROR: Docker version ${test} not supported, please upgrade to at least ${docker_min_version}, or recommended ${docker_rec_version}"
192 # Recommend newer docker version
193 if compare_version
"${docker_rec_version}" "${test}"; then
194 echo "WARNING: Docker version ${test} deprecated, recommend upgrade to ${docker_rec_version} or newer."
197 # 4. discourse docker image is downloaded
198 test=`$docker_path images | awk '{print $1 ":" $2 }' | grep "$image"`
200 if [ -z "$test" ]; then
202 echo "WARNING: We are about to start downloading the Discourse base image"
203 echo "This process may take anywhere between a few minutes to an hour, depending on your network speed"
205 echo "Please be patient"
211 # 5. running recommended git version
212 test=($
($git_path --version)) # Get git version string
213 test=${test[2]//,/} # Get version alone and strip comma if exists
215 # At least minimum version
216 if compare_version
"${git_min_version}" "${test}"; then
217 echo "ERROR: Git version ${test} not supported, please upgrade to at least ${git_min_version}, or recommended ${git_rec_version}"
221 # Recommend best version
222 if compare_version
"${git_rec_version}" "${test}"; then
223 echo "WARNING: Git version ${test} deprecated, recommend upgrade to ${git_rec_version} or newer."
226 # 6. able to attach stderr / out / tty
227 test=`$docker_path run $user_args -i --rm -a stdout -a stderr $image echo working`
228 if [[ "$test" =~
"working" ]] ; then : ; else
229 echo "Your Docker installation is not working correctly"
231 echo "See: https://meta.discourse.org/t/docker-error-on-bootstrap/13657/18?u=sam"
235 # 7. enough space for the bootstrap on docker folder
236 folder
=`$docker_path info --format '{{.DockerRootDir}}'`
237 safe_folder
=${folder:-/var/lib/docker}
238 test=$
(($
(stat
-f --format="%a*%S" $safe_folder)/1024**3 < 5))
239 if [[ $test -ne 0 ]] ; then
240 echo "You have less than 5GB of free space on the disk where $safe_folder is located. You will need more space to continue"
243 if tty
>/dev
/null
; then
244 read -p "Would you like to attempt to recover space by cleaning docker images and containers in the system?(y/N)" -n 1 -r
246 if [[ $REPLY =~ ^
[Yy
]$
]]
248 $docker_path container prune
--force --filter until=1h
>/dev
/null
249 $docker_path image prune
--all --force --filter until=1h
>/dev
/null
250 echo "If the cleanup was successful, you may try again now"
258 if [ -z "$SKIP_PREREQS" ] && [ "$command" != "cleanup" ]; then
263 read -r -d '' env_ruby
<< 'RUBY'
266 input
= STDIN.readlines.
join
267 yaml
= YAML.load
(input
)
269 if host_run
= yaml
['host_run']
270 params
= yaml
['params'] ||
{}
271 host_run.each
do |run|
273 run
= run.gsub
("$#{k}", v
)
275 STDOUT.
write "#{run}--SEP--"
280 host_run
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
282 while [ "$host_run" ] ; do
283 iter
=${host_run%%--SEP--*}
285 echo "Host run: $iter"
288 host_run
="${host_run#*--SEP--}"
294 volumes
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
295 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
299 links
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
300 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
304 local templates
=`cat $1 | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
305 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
307 local arrTemplates
=${templates// / }
309 if [ ! -z "$templates" ]; then
310 for template
in "${arrTemplates[@]}"
312 local nested_templates
=$
(find_templates
$template)
314 if [ ! -z "$nested_templates" ]; then
315 templates
="$templates $nested_templates"
325 set_template_info
() {
326 templates
=$
(find_templates
$config_file)
328 arrTemplates
=(${templates// / })
329 config_data
=$
(cat $config_file)
333 for template
in "${arrTemplates[@]}"
335 [ ! -z $template ] && {
336 input
="$input _FILE_SEPERATOR_ $(cat $template)"
340 # we always want our config file last so it takes priority
341 input
="$input _FILE_SEPERATOR_ $config_data"
343 read -r -d '' env_ruby
<< 'RUBY'
346 input
=STDIN.readlines.
join
347 # default to UTF-8 for the dbs sake
348 env
= {'LANG' => 'en_US.UTF-8'}
349 input.
split('_FILE_SEPERATOR_').each
do |yml|
352 env.merge
!(YAML.load
(yml
)['env'] ||
{})
353 rescue Psych
::SyntaxError
=> e
361 env.each
{|k
,v| puts
"*ERROR." if v.is_a?
(Hash
)}
362 puts env.map
{|k
,v|
"-e\n#{k}=#{v}" }.
join("\n")
365 tmp_input_file
=$
(mktemp
)
367 echo "$input" > "$tmp_input_file"
368 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
370 rm -f "$tmp_input_file"
375 if [ "$i" == "*ERROR." ]; then
377 elif [ -n "$i" ]; then
378 env
[${#env[@]}]="${i//\{\{config\}\}/${config}}"
382 if [ "$ok" -ne 1 ]; then
384 echo "YAML syntax error. Please check your containers
/*.yml config files.
"
389 read -r -d '' labels_ruby << 'RUBY'
392 input=STDIN.readlines.join
394 input.split('_FILE_SEPERATOR_').each do |yml|
397 labels.merge!(YAML.load(yml)['labels'] || {})
398 rescue Psych::SyntaxError => e
406 puts labels.map{|k,v| "-l\n#{k}=#{v}" }.join("\n")
409 tmp_input_file
=$
(mktemp
)
411 echo "$input" > "$tmp_input_file"
412 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$labels_ruby"`
414 rm -f "$tmp_input_file"
419 if [ "$i" == "*ERROR." ]; then
421 elif [ -n "$i" ]; then
422 labels
[${#labels[@]}]=$
(echo $i |
sed s
/{{config
}}/${config}/g
)
426 if [ "$ok" -ne 1 ]; then
428 echo "YAML syntax error. Please check your containers/*.yml config files."
433 read -r -d '' ports_ruby
<< 'RUBY'
436 input
=STDIN.readlines.
join
438 input.
split('_FILE_SEPERATOR_').each
do |yml|
441 ports
+= (YAML.load
(yml
)['expose'] ||
[])
442 rescue Psych
::SyntaxError
=> e
450 puts ports.map
{ |p| p.to_s.include?
(':') ?
"-p\n#{p}" : "--expose\n#{p}" }.
join("\n")
453 tmp_input_file
=$
(mktemp
)
455 echo "$input" > "$tmp_input_file"
456 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$ports_ruby"`
458 rm -f "$tmp_input_file"
463 if [ "$i" == "*ERROR." ]; then
465 elif [ -n "$i" ]; then
466 ports
[${#ports[@]}]=$i
470 if [ "$ok" -ne 1 ]; then
472 echo "YAML syntax error. Please check your containers/*.yml config files."
479 if [ -z $docker_path ]; then
483 [ "$command" == "cleanup" ] && {
484 $docker_path container prune
--filter until=1h
485 $docker_path image prune
--all --filter until=1h
487 if [ -d /var
/discourse
/shared
/standalone
/postgres_data_old
]; then
489 echo "Old PostgreSQL backup data cluster detected taking up $(du -hs /var/discourse/shared/standalone/postgres_data_old | awk '{print $1}') detected"
490 read -p "Would you like to remove it? (Y/n): " -n 1 -r && echo
492 if [[ $REPLY =~ ^
[Yy
]$
]]; then
493 echo "removing old PostgreSQL data cluster at /var/discourse/shared/standalone/postgres_data_old..."
494 rm -rf /var
/discourse
/shared
/standalone
/postgres_data_old
*
503 if [ ! "$command" == "setup" ]; then
504 if [[ ! -e $config_file ]]; then
505 echo "Config file was not found, ensure $config_file exists"
507 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
512 docker_version
=($
($docker_path --version))
513 docker_version
=${test[2]//,/}
514 restart_policy
=${restart_policy:---restart=always}
516 set_existing_container
(){
517 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
522 set_existing_container
524 if [ ! -z $existing ]
528 $docker_path stop
-t 10 $config
531 echo "$config was not started !"
532 echo "./discourse-doctor may help diagnose the problem."
538 run_image
=`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)['run_image']"`
541 if [ -n "$user_run_image" ]; then
542 run_image
=$user_run_image
543 elif [ -z "$run_image" ]; then
544 run_image
="$local_discourse/$config"
549 boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
550 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
552 if [ -z "$boot_command" ]; then
554 no_boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
555 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
557 if [ -z "$no_boot_command" ]; then
558 boot_command
="/sbin/boot"
566 docker_args
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
567 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
569 if [[ -n "$docker_args" ]]; then
570 user_args
="$user_args_argv $docker_args"
576 if [ -z "$START_CMD_ONLY" ]
578 existing
=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
580 if [ ! -z $existing ]
582 echo "Nothing to do, your container has already started!"
586 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
587 if [ ! -z $existing ]
589 echo "starting up existing container"
592 $docker_path start
$config
606 # get hostname and settings from container configuration
607 for envar
in "${env[@]}"
609 if [[ $envar == DOCKER_USE_HOSTNAME
* ]] ||
[[ $envar == DISCOURSE_HOSTNAME
* ]]
611 # use as environment variable
617 hostname
=`hostname -s`
619 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
621 hostname
=$DISCOURSE_HOSTNAME
623 hostname
=$hostname-$config
626 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
627 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
628 # docker added more hostname rules
629 hostname
=${hostname//_/-}
632 if [ -z "$SKIP_MAC_ADDRESS" ] ; then
633 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/'")"
636 if [ ! -z "$START_CMD_ONLY" ] ; then
642 $docker_path run
--shm-size=512m
$links $attach_on_run $restart_policy "${env[@]}" "${labels[@]}" -h "$hostname" \
643 -e DOCKER_HOST_IP
="$docker_ip" --name $config -t "${ports[@]}" $volumes $mac_address $user_args \
644 $run_image $boot_command
658 (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 \
659 /bin
/bash
-c "$run_command") || ERR
=$?
661 if [[ $ERR > 0 ]]; then
669 # Is the image available?
670 # If not, pull it here so the user is aware what's happening.
671 $docker_path history $image >/dev
/null
2>&1 || pull_image
675 base_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
676 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
678 update_pups
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
679 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
681 if [[ ! X
"" = X
"$base_image" ]]; then
690 run_command
="cd /pups &&"
691 if [[ ! "false" = $update_pups ]]; then
692 run_command
="$run_command git pull &&"
694 run_command
="$run_command /pups/bin/pups --stdin"
700 tmp_input_file
=$
(mktemp
)
702 echo "$input" > "$tmp_input_file"
703 (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 \
704 /bin
/bash
-c "$run_command") || ERR
=$?
706 rm -f "$tmp_input_file"
709 # magic exit code that indicates a retry
710 if [[ "$ERR" == 77 ]]; then
711 $docker_path rm `cat $cidbootstrap`
714 elif [[ "$ERR" > 0 ]]; then
718 if [[ $FAILED = "TRUE" ]]; then
719 if [[ ! -z "$DEBUG" ]]; then
720 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config-debug ||
echo 'FAILED TO COMMIT'
721 echo "** DEBUG ** Maintaining image for diagnostics $local_discourse/$config-debug"
724 $docker_path rm `cat $cidbootstrap`
726 echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one."
727 echo "./discourse-doctor may help diagnose the problem."
733 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config ||
echo 'FAILED TO COMMIT'
734 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
740 echo "Successfully bootstrapped, to startup use ./launcher start $config"
750 exec $docker_path exec -it $config /bin
/bash
--login
760 $docker_path logs
$config
782 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
783 echo "Ensuring launcher is up to date"
787 LOCAL
=$
(git rev-parse HEAD
)
788 REMOTE
=$
(git rev-parse @
{u
})
789 BASE
=$
(git merge-base HEAD @
{u
})
791 if [ $LOCAL = $REMOTE ]; then
792 echo "Launcher is up-to-date"
794 elif [ $LOCAL = $BASE ]; then
795 echo "Updating Launcher..."
796 git pull ||
(echo 'failed to update' && exit 1)
798 echo "Launcher updated, restarting..."
799 exec "$0" "${SAVED_ARGV[@]}"
801 elif [ $REMOTE = $BASE ]; then
802 echo "Your version of Launcher is ahead of origin"
805 echo "Launcher has diverged source, this is only expected in Dev mode"
810 set_existing_container
812 if [ ! -z $existing ]
814 echo "Stopping old container"
817 $docker_path stop
-t 60 $config
823 if [ ! -z $existing ]
825 echo "Removing old container"
828 $docker_path rm $config
838 (set -x; $docker_path stop
-t 10 $config && $docker_path rm $config) ||
(echo "$config was not found" && exit 0)