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.20190625-0946"
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 system prune
-af
239 echo "If the cleanup was successful, you may try again now"
247 if [ -z "$SKIP_PREREQS" ] && [ "$command" != "cleanup" ]; then
252 read -r -d '' env_ruby
<< 'RUBY'
255 input
= STDIN.readlines.
join
256 yaml
= YAML.load
(input
)
258 if host_run
= yaml
['host_run']
259 params
= yaml
['params'] ||
{}
260 host_run.each
do |run|
262 run
= run.gsub
("$#{k}", v
)
264 STDOUT.
write "#{run}--SEP--"
269 host_run
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
271 while [ "$host_run" ] ; do
272 iter
=${host_run%%--SEP--*}
274 echo "Host run: $iter"
277 host_run
="${host_run#*--SEP--}"
283 volumes
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
284 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
288 links
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
289 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
293 local templates
=`cat $1 | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
294 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
296 local arrTemplates
=${templates// / }
298 if [ ! -z "$templates" ]; then
299 for template
in "${arrTemplates[@]}"
301 local nested_templates
=$
(find_templates
$template)
303 if [ ! -z "$nested_templates" ]; then
304 templates
="$templates $nested_templates"
314 set_template_info
() {
315 templates
=$
(find_templates
$config_file)
317 arrTemplates
=(${templates// / })
318 config_data
=$
(cat $config_file)
322 for template
in "${arrTemplates[@]}"
324 [ ! -z $template ] && {
325 input
="$input _FILE_SEPERATOR_ $(cat $template)"
329 # we always want our config file last so it takes priority
330 input
="$input _FILE_SEPERATOR_ $config_data"
332 read -r -d '' env_ruby
<< 'RUBY'
335 input
=STDIN.readlines.
join
336 # default to UTF-8 for the dbs sake
337 env
= {'LANG' => 'en_US.UTF-8'}
338 input.
split('_FILE_SEPERATOR_').each
do |yml|
341 env.merge
!(YAML.load
(yml
)['env'] ||
{})
342 rescue Psych
::SyntaxError
=> e
350 puts env.map
{|k
,v|
"-e\n#{k}=#{v}" }.
join("\n")
353 tmp_input_file
=$
(mktemp
)
355 echo "$input" > "$tmp_input_file"
356 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
358 rm -f "$tmp_input_file"
363 if [ "$i" == "*ERROR." ]; then
365 elif [ -n "$i" ]; then
366 env
[${#env[@]}]="${i//\{\{config\}\}/${config}}"
370 if [ "$ok" -ne 1 ]; then
372 echo "YAML syntax error. Please check your containers
/*.yml config files.
"
377 read -r -d '' labels_ruby << 'RUBY'
380 input=STDIN.readlines.join
382 input.split('_FILE_SEPERATOR_').each do |yml|
385 labels.merge!(YAML.load(yml)['labels'] || {})
386 rescue Psych::SyntaxError => e
394 puts labels.map{|k,v| "-l\n#{k}=#{v}" }.join("\n")
397 tmp_input_file
=$
(mktemp
)
399 echo "$input" > "$tmp_input_file"
400 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$labels_ruby"`
402 rm -f "$tmp_input_file"
407 if [ "$i" == "*ERROR." ]; then
409 elif [ -n "$i" ]; then
410 labels
[${#labels[@]}]=$
(echo $i |
sed s
/{{config
}}/${config}/g
)
414 if [ "$ok" -ne 1 ]; then
416 echo "YAML syntax error. Please check your containers/*.yml config files."
421 read -r -d '' ports_ruby
<< 'RUBY'
424 input
=STDIN.readlines.
join
426 input.
split('_FILE_SEPERATOR_').each
do |yml|
429 ports
+= (YAML.load
(yml
)['expose'] ||
[])
430 rescue Psych
::SyntaxError
=> e
438 puts ports.map
{ |p| p.to_s.include?
(':') ?
"-p\n#{p}" : "--expose\n#{p}" }.
join("\n")
441 tmp_input_file
=$
(mktemp
)
443 echo "$input" > "$tmp_input_file"
444 raw
=`exec cat "$tmp_input_file" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$ports_ruby"`
446 rm -f "$tmp_input_file"
451 if [ "$i" == "*ERROR." ]; then
453 elif [ -n "$i" ]; then
454 ports
[${#ports[@]}]=$i
458 if [ "$ok" -ne 1 ]; then
460 echo "YAML syntax error. Please check your containers/*.yml config files."
467 if [ -z $docker_path ]; then
471 [ "$command" == "cleanup" ] && {
472 $docker_path system prune
-a
474 if [ -d /var
/discourse
/shared
/standalone
/postgres_data_old
]; then
476 echo "Old PostgreSQL backup data cluster detected taking up $(du -hs /var/discourse/shared/standalone/postgres_data_old | awk '{print $1}') detected"
477 read -p "Would you like to remove it? (Y/n): " -n 1 -r && echo
479 if [[ $REPLY =~ ^
[Yy
]$
]]; then
480 echo "removing old PostgreSQL data cluster at /var/discourse/shared/standalone/postgres_data_old..."
481 rm -rf /var
/discourse
/shared
/standalone
/postgres_data_old
490 if [ ! "$command" == "setup" ]; then
491 if [[ ! -e $config_file ]]; then
492 echo "Config file was not found, ensure $config_file exists"
494 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
499 docker_version
=($
($docker_path --version))
500 docker_version
=${test[2]//,/}
501 restart_policy
=${restart_policy:---restart=always}
503 set_existing_container
(){
504 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
509 set_existing_container
511 if [ ! -z $existing ]
515 $docker_path stop
-t 10 $config
518 echo "$config was not started !"
524 run_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
525 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['run_image']"`
527 if [ -n "$user_run_image" ]; then
528 run_image
=$user_run_image
529 elif [ -z "$run_image" ]; then
530 run_image
="$local_discourse/$config"
535 boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
536 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
538 if [ -z "$boot_command" ]; then
540 no_boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
541 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
543 if [ -z "$no_boot_command" ]; then
544 boot_command
="/sbin/boot"
552 docker_args
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
553 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
555 if [[ -n "$docker_args" ]]; then
556 user_args
="$user_args_argv $docker_args"
562 if [ -z "$START_CMD_ONLY" ]
564 existing
=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
566 if [ ! -z $existing ]
568 echo "Nothing to do, your container has already started!"
572 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
573 if [ ! -z $existing ]
575 echo "starting up existing container"
578 $docker_path start
$config
592 # get hostname and settings from container configuration
593 for envar
in "${env[@]}"
595 if [[ $envar == DOCKER_USE_HOSTNAME
* ]] ||
[[ $envar == DISCOURSE_HOSTNAME
* ]]
597 # use as environment variable
603 hostname
=`hostname -s`
605 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
607 hostname
=$DISCOURSE_HOSTNAME
609 hostname
=$hostname-$config
612 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
613 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
614 # docker added more hostname rules
615 hostname
=${hostname//_/-}
618 if [ -z "$SKIP_MAC_ADDRESS" ] ; then
619 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/'")"
622 if [ ! -z "$START_CMD_ONLY" ] ; then
628 $docker_path run
--shm-size=512m
$links $attach_on_run $restart_policy "${env[@]}" "${labels[@]}" -h "$hostname" \
629 -e DOCKER_HOST_IP
="$docker_ip" --name $config -t "${ports[@]}" $volumes $mac_address $user_args \
630 $run_image $boot_command
644 (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 \
645 /bin
/bash
-c "$run_command") || ERR
=$?
647 if [[ $ERR > 0 ]]; then
655 # Is the image available?
656 # If not, pull it here so the user is aware what's happening.
657 $docker_path history $image >/dev
/null
2>&1 ||
$docker_path pull
$image
661 base_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
662 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
664 update_pups
=`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)['update_pups']"`
667 if [[ ! X
"" = X
"$base_image" ]]; then
676 run_command
="cd /pups &&"
677 if [[ ! "false" = $update_pups ]]; then
678 run_command
="$run_command git pull &&"
680 run_command
="$run_command /pups/bin/pups --stdin"
686 tmp_input_file
=$
(mktemp
)
688 echo "$input" > "$tmp_input_file"
689 (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 \
690 /bin
/bash
-c "$run_command") || ERR
=$?
692 rm -f "$tmp_input_file"
695 # magic exit code that indicates a retry
696 if [[ "$ERR" == 77 ]]; then
697 $docker_path rm `cat $cidbootstrap`
700 elif [[ "$ERR" > 0 ]]; then
704 if [[ $FAILED = "TRUE" ]]; then
705 if [[ ! -z "$DEBUG" ]]; then
706 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config-debug ||
echo 'FAILED TO COMMIT'
707 echo "** DEBUG ** Maintaining image for diagnostics $local_discourse/$config-debug"
710 $docker_path rm `cat $cidbootstrap`
712 echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one"
718 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config ||
echo 'FAILED TO COMMIT'
719 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
725 echo "Successfully bootstrapped, to startup use ./launcher start $config"
735 exec $docker_path exec -it $config /bin
/bash
--login
745 $docker_path logs
$config
767 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
768 echo "Ensuring launcher is up to date"
772 LOCAL
=$
(git rev-parse HEAD
)
773 REMOTE
=$
(git rev-parse @
{u
})
774 BASE
=$
(git merge-base HEAD @
{u
})
776 if [ $LOCAL = $REMOTE ]; then
777 echo "Launcher is up-to-date"
779 elif [ $LOCAL = $BASE ]; then
780 echo "Updating Launcher"
781 git pull ||
(echo 'failed to update' && exit 1)
783 for (( i
=${#BASH_ARGV[@]}-1,j
=0; i
>=0,j
<${#BASH_ARGV[@]}; i--
,j
++ ))
785 args
[$j]=${BASH_ARGV[$i]}
787 exec /bin
/bash
$0 "${args[@]}" # $@ is empty, because of shift at the beginning. Use BASH_ARGV instead.
789 elif [ $REMOTE = $BASE ]; then
790 echo "Your version of Launcher is ahead of origin"
793 echo "Launcher has diverged source, this is only expected in Dev mode"
798 set_existing_container
800 if [ ! -z $existing ]
802 echo "Stopping old container"
805 $docker_path stop
-t 10 $config
811 if [ ! -z $existing ]
813 echo "Removing old container"
816 $docker_path rm $config
826 (set -x; $docker_path stop
-t 10 $config && $docker_path rm $config) ||
(echo "$config was not found" && exit 0)