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.20190901-2315"
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 !"
519 echo "./discourse-doctor may help diagnose the problem."
525 run_image
=`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)['run_image']"`
528 if [ -n "$user_run_image" ]; then
529 run_image
=$user_run_image
530 elif [ -z "$run_image" ]; then
531 run_image
="$local_discourse/$config"
536 boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
537 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
539 if [ -z "$boot_command" ]; then
541 no_boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
542 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
544 if [ -z "$no_boot_command" ]; then
545 boot_command
="/sbin/boot"
553 docker_args
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
554 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
556 if [[ -n "$docker_args" ]]; then
557 user_args
="$user_args_argv $docker_args"
563 if [ -z "$START_CMD_ONLY" ]
565 existing
=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
567 if [ ! -z $existing ]
569 echo "Nothing to do, your container has already started!"
573 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
574 if [ ! -z $existing ]
576 echo "starting up existing container"
579 $docker_path start
$config
593 # get hostname and settings from container configuration
594 for envar
in "${env[@]}"
596 if [[ $envar == DOCKER_USE_HOSTNAME
* ]] ||
[[ $envar == DISCOURSE_HOSTNAME
* ]]
598 # use as environment variable
604 hostname
=`hostname -s`
606 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
608 hostname
=$DISCOURSE_HOSTNAME
610 hostname
=$hostname-$config
613 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
614 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
615 # docker added more hostname rules
616 hostname
=${hostname//_/-}
619 if [ -z "$SKIP_MAC_ADDRESS" ] ; then
620 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/'")"
623 if [ ! -z "$START_CMD_ONLY" ] ; then
629 $docker_path run
--shm-size=512m
$links $attach_on_run $restart_policy "${env[@]}" "${labels[@]}" -h "$hostname" \
630 -e DOCKER_HOST_IP
="$docker_ip" --name $config -t "${ports[@]}" $volumes $mac_address $user_args \
631 $run_image $boot_command
645 (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 \
646 /bin
/bash
-c "$run_command") || ERR
=$?
648 if [[ $ERR > 0 ]]; then
656 # Is the image available?
657 # If not, pull it here so the user is aware what's happening.
658 $docker_path history $image >/dev
/null
2>&1 ||
$docker_path pull
$image
662 base_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
663 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
665 update_pups
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
666 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
668 if [[ ! X
"" = X
"$base_image" ]]; then
677 run_command
="cd /pups &&"
678 if [[ ! "false" = $update_pups ]]; then
679 run_command
="$run_command git pull &&"
681 run_command
="$run_command /pups/bin/pups --stdin"
687 tmp_input_file
=$
(mktemp
)
689 echo "$input" > "$tmp_input_file"
690 (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 \
691 /bin
/bash
-c "$run_command") || ERR
=$?
693 rm -f "$tmp_input_file"
696 # magic exit code that indicates a retry
697 if [[ "$ERR" == 77 ]]; then
698 $docker_path rm `cat $cidbootstrap`
701 elif [[ "$ERR" > 0 ]]; then
705 if [[ $FAILED = "TRUE" ]]; then
706 if [[ ! -z "$DEBUG" ]]; then
707 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config-debug ||
echo 'FAILED TO COMMIT'
708 echo "** DEBUG ** Maintaining image for diagnostics $local_discourse/$config-debug"
711 $docker_path rm `cat $cidbootstrap`
713 echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one."
714 echo "./discourse-doctor may help diagnose the problem."
720 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config ||
echo 'FAILED TO COMMIT'
721 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
727 echo "Successfully bootstrapped, to startup use ./launcher start $config"
737 exec $docker_path exec -it $config /bin
/bash
--login
747 $docker_path logs
$config
769 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
770 echo "Ensuring launcher is up to date"
774 LOCAL
=$
(git rev-parse HEAD
)
775 REMOTE
=$
(git rev-parse @
{u
})
776 BASE
=$
(git merge-base HEAD @
{u
})
778 if [ $LOCAL = $REMOTE ]; then
779 echo "Launcher is up-to-date"
781 elif [ $LOCAL = $BASE ]; then
782 echo "Updating Launcher"
783 git pull ||
(echo 'failed to update' && exit 1)
785 for (( i
=${#BASH_ARGV[@]}-1,j
=0; i
>=0,j
<${#BASH_ARGV[@]}; i--
,j
++ ))
787 args
[$j]=${BASH_ARGV[$i]}
789 exec /bin
/bash
$0 "${args[@]}" # $@ is empty, because of shift at the beginning. Use BASH_ARGV instead.
791 elif [ $REMOTE = $BASE ]; then
792 echo "Your version of Launcher is ahead of origin"
795 echo "Launcher has diverged source, this is only expected in Dev mode"
800 set_existing_container
802 if [ ! -z $existing ]
804 echo "Stopping old container"
807 $docker_path stop
-t 10 $config
813 if [ ! -z $existing ]
815 echo "Removing old container"
818 $docker_path rm $config
828 (set -x; $docker_path stop
-t 10 $config && $docker_path rm $config) ||
(echo "$config was not found" && exit 0)