apache-ignite
678 строк · 24.3 Кб
1#!/usr/bin/env bash
2
3# Licensed to the Apache Software Foundation (ASF) under one or more
4# contributor license agreements. See the NOTICE file distributed with
5# this work for additional information regarding copyright ownership.
6# The ASF licenses this file to You under the Apache License, Version 2.0
7# (the "License"); you may not use this file except in compliance with
8# the License. You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18#
19# Ducker-Ignite: a tool for running Apache Ignite system tests inside Docker images.
20#
21# Note: this should be compatible with the version of bash that ships on most
22# Macs, bash 3.2.57.
23#
24
25script_path="${0}"
26
27# The absolute path to the directory which this script is in. This will also be the directory
28# which we run docker build from.
29ducker_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
30
31# The absolute path to the root Ignite directory
32ignite_dir="$( cd "${ducker_dir}/../../../.." && pwd )"
33
34# The memory consumption to allow during the docker build.
35# This does not include swap.
36docker_build_memory_limit="8000m"
37
38# The maximum mmemory consumption to allow in containers.
39docker_run_memory_limit="8000m"
40
41# The default number of cluster nodes to bring up if a number is not specified.
42default_num_nodes=4
43
44# The default OpenJDK base image.
45default_jdk="openjdk:8"
46
47# The default ducker-ignite image name.
48default_image_name="ducker-ignite"
49
50# Display a usage message on the terminal and exit.
51#
52# $1: The exit status to use
53usage() {
54local exit_status="${1}"
55cat <<EOF
56ducker-ignite: a tool for running Apache Ignite tests inside Docker images.
57
58Usage: ${script_path} [command] [options]
59
60help|-h|--help
61Display this help message
62
63build [-j|--jdk JDK] [-c|--context] [image-name]
64Build a docker image that represents ducker node. Image is tagged with specified ${image_name}.
65
66If --jdk is specified then we will use this argument as base image for ducker docker images.
67Otherwise ${default_jdk} is used.
68
69If --context is specified then build docker image from this path. Context directory must contain Dockerfile.
70
71up [-n|--num-nodes NUM_NODES] [-f|--force] [docker-image]
72[-C|--custom-ducktape DIR] [-e|--expose-ports ports] [-j|--jdk JDK_VERSION]
73[--subnet SUBNET]
74Bring up a cluster with the specified amount of nodes (defaults to ${default_num_nodes}).
75The docker image name defaults to ${default_image_name}. If --force is specified, we will
76attempt to bring up an image even some parameters are not valid.
77
78If --custom-ducktape is specified, we will install the provided custom
79ducktape source code directory before bringing up the nodes. The provided
80directory should be the ducktape git repo, not the ducktape installed module directory.
81
82if --expose-ports is specified then we will expose those ports to random ephemeral ports
83on the host. The argument can be a single port (like 5005), a port range like (5005-5009)
84or a combination of port/port-range separated by comma (like 2181,9092 or 2181,5005-5008).
85By default no port is exposed. See README.md for more detail on this option.
86
87If --jdk is specified then we will use this argument as base image for ducktest's docker images.
88Otherwise ${default_jdk} is used.
89
90If --subnet is specified then nodes are assigned IP addresses in this subnetwork. SUBNET is specified
91in the CIDR format and passed directly to the docker create network command.
92
93test [test-name(s)]
94Run a test or set of tests inside the currently active Ducker nodes.
95For example, to run the system test produce_bench_test, you would run:
96./tests/docker/ducker-ignite test ./tests/ignitetest/test/core/rebalance_test.py
97
98ssh [node-name|user-name@node-name] [command]
99Log in to a running ducker container. If node-name is not given, it prints
100the names of all running nodes. If node-name is 'all', we will run the
101command on every node. If user-name is given, we will try to log in as
102that user. Otherwise, we will log in as the 'ducker' user. If a command
103is specified, we will run that command. Otherwise, we will provide a login
104shell.
105
106down [-q|--quiet] [-f|--force]
107Tear down all the currently active ducker-ignite nodes. If --quiet is specified,
108only error messages are printed. If --force or -f is specified, "docker rm -f"
109will be used to remove the nodes, which kills currently running ducker-ignite test.
110
111purge [--f|--force]
112Purge Docker images created by ducker-ignite. This will free disk space.
113If --force is set, we run 'docker rmi -f'.
114
115compare [docker-image]
116Compare image id of last run and last build for specified docker-image. If they are different then cluster runs
117non-actual version of image. Rerun is required.
118
119EOF
120exit "${exit_status}"
121}
122
123# Exit with an error message.
124die() {
125echo $@
126exit 1
127}
128
129# Check for the presence of certain commands.
130#
131# $@: The commands to check for. This function will die if any of these commands are not found by
132# the 'which' command.
133require_commands() {
134local cmds="${@}"
135for cmd in ${cmds}; do
136which -- "${cmd}" &> /dev/null || die "You must install ${cmd} to run this script."
137done
138}
139
140# Set a global variable to a value.
141#
142# $1: The variable name to set. This function will die if the variable already has a value. The
143# variable will be made readonly to prevent any future modifications.
144# $2: The value to set the variable to. This function will die if the value is empty or starts
145# with a dash.
146# $3: A human-readable description of the variable.
147set_once() {
148local key="${1}"
149local value="${2}"
150local what="${3}"
151[[ -n "${!key}" ]] && die "Error: more than one value specified for ${what}."
152verify_command_line_argument "${value}" "${what}"
153# It would be better to use declare -g, but older bash versions don't support it.
154export ${key}="${value}"
155}
156
157# Verify that a command-line argument is present and does not start with a slash.
158#
159# $1: The command-line argument to verify.
160# $2: A human-readable description of the variable.
161verify_command_line_argument() {
162local value="${1}"
163local what="${2}"
164[[ -n "${value}" ]] || die "Error: no value specified for ${what}"
165[[ ${value} == -* ]] && die "Error: invalid value ${value} specified for ${what}"
166}
167
168# Echo a message if a flag is set.
169#
170# $1: If this is 1, the message will be echoed.
171# $@: The message
172maybe_echo() {
173local verbose="${1}"
174shift
175[[ "${verbose}" -eq 1 ]] && echo "${@}"
176}
177
178# Counts the number of elements passed to this subroutine.
179count() {
180echo $#
181}
182
183# Push a new directory on to the bash directory stack, or exit with a failure message.
184#
185# $1: The directory push on to the directory stack.
186must_pushd() {
187local target_dir="${1}"
188pushd -- "${target_dir}" &> /dev/null || die "failed to change directory to ${target_dir}"
189}
190
191# Pop a directory from the bash directory stack, or exit with a failure message.
192must_popd() {
193popd &> /dev/null || die "failed to popd"
194}
195
196# Run a command and die if it fails.
197#
198# Optional flags:
199# -v: print the command before running it.
200# -o: display the command output.
201# $@: The command to run.
202must_do() {
203local verbose=0
204local output="/dev/null"
205while true; do
206case ${1} in
207-v) verbose=1; shift;;
208-o) output="/dev/stdout"; shift;;
209*) break;;
210esac
211done
212
213[[ "${verbose}" -eq 1 ]] && echo "${@}"
214eval "${@}" >${output} || die "${1} failed"
215}
216
217# Ask the user a yes/no question.
218#
219# $1: The prompt to use
220# $_return: 0 if the user answered no; 1 if the user answered yes.
221ask_yes_no() {
222local prompt="${1}"
223while true; do
224read -r -p "${prompt} " response
225case "${response}" in
226[yY]|[yY][eE][sS]) _return=1; return;;
227[nN]|[nN][oO]) _return=0; return;;
228*);;
229esac
230echo "Please respond 'yes' or 'no'."
231echo
232done
233}
234
235# Wait for all the background jobs
236wait_jobs() {
237for job in `jobs -p`
238do
239wait $job
240done
241}
242
243# Build a docker image.
244#
245# $1: The docker build context
246# $2: The name of the image to build.
247ducker_build_image() {
248local docker_context="${1}"
249local image_name="${2}"
250
251# Use SECONDS, a builtin bash variable that gets incremented each second, to measure the docker
252# build duration.
253SECONDS=0
254
255must_pushd "${ducker_dir}"
256# Tip: if you are scratching your head for some dependency problems that are referring to an old code version
257# (for example java.lang.NoClassDefFoundError), add --no-cache flag to the build shall give you a clean start.
258must_do -v -o docker build --memory="${docker_build_memory_limit}" \
259--build-arg "ducker_creator=${user_name}" --build-arg "jdk_version=${jdk_version}" -t "${image_name}" \
260-f "${docker_context}/Dockerfile" -- "${docker_context}"
261docker_status=$?
262must_popd
263duration="${SECONDS}"
264if [[ ${docker_status} -ne 0 ]]; then
265die "** ERROR: Failed to build ${what} image after $((duration / 60))m $((duration % 60))s."
266fi
267
268# Save docker image id to the file. Then could use this file to find version of docker image built last time.
269# It could be useful if we don't confident about necessity of stoping the cluster.
270get_image_id "${image_name}" > "${ducker_dir}/build/image_${image_name}.build"
271
272echo "** Successfully built ${what} image in $((duration / 60))m $((duration % 60))s."
273}
274
275ducker_build() {
276require_commands docker
277
278local docker_context=
279while [[ $# -ge 1 ]]; do
280case "${1}" in
281-j|--jdk) set_once jdk_version "${2}" "the OpenJDK base image"; shift 2;;
282-c|--context) docker_context="${2}"; shift 2;;
283*) set_once image_name "${1}" "docker image name"; shift;;
284esac
285done
286
287[[ -n "${jdk_version}" ]] || jdk_version="${default_jdk}"
288[[ -n "${image_name}" ]] || image_name="${default_image_name}-${jdk_version/:/-}"
289[[ -n "${docker_context}" ]] || docker_context="${ducker_dir}"
290
291ducker_build_image "${docker_context}" "${image_name}"
292}
293
294docker_run() {
295local node=${1}
296local image_name=${2}
297local ports_option=${3}
298
299local expose_ports=""
300if [[ -n ${ports_option} ]]; then
301expose_ports="-P"
302for expose_port in ${ports_option//,/ }; do
303expose_ports="${expose_ports} --expose ${expose_port}"
304done
305fi
306
307# Invoke docker-run. We need privileged mode to be able to run iptables
308# and mount FUSE filesystems inside the container. We also need it to
309# run iptables inside the container.
310must_do -v docker run --privileged \
311-d \
312-t \
313-h "${node}" \
314--network ducknet ${expose_ports} \
315--memory=${docker_run_memory_limit} \
316--memory-swappiness=1 \
317--mount type=bind,source="${ignite_dir}",target=/opt/ignite-dev,consistency=delegated \
318$DOCKER_OPTIONS \
319--name "${node}" \
320-- "${image_name}"
321}
322
323setup_custom_ducktape() {
324local custom_ducktape="${1}"
325local image_name="${2}"
326
327[[ -f "${custom_ducktape}/ducktape/__init__.py" ]] || \
328die "You must supply a valid ducktape directory to --custom-ducktape"
329docker_run ducker01 "${image_name}"
330local running_container
331running_container=$(docker ps -f=network=ducknet -q)
332must_do -v -o docker cp "${custom_ducktape}" "${running_container}:/opt/ducktape"
333docker exec --user=root ducker01 bash -c 'set -x && cd /opt/ignite-dev/modules/ducktests/tests && sudo python ./setup.py develop install && cd /opt/ducktape && sudo python ./setup.py develop install'
334[[ $? -ne 0 ]] && die "failed to install the new ducktape."
335must_do -v -o docker commit ducker01 "${image_name}"
336must_do -v docker kill "${running_container}"
337must_do -v docker rm ducker01
338}
339
340ducker_up() {
341require_commands docker
342while [[ $# -ge 1 ]]; do
343case "${1}" in
344-C|--custom-ducktape) set_once custom_ducktape "${2}" "the custom ducktape directory"; shift 2;;
345-f|--force) force=1; shift;;
346-n|--num-nodes) set_once num_nodes "${2}" "number of nodes"; shift 2;;
347-e|--expose-ports) set_once expose_ports "${2}" "the ports to expose"; shift 2;;
348--subnet) set_once subnet "${2}" "subnet in CIDR format that represents a network segment"; shift 2;;
349*) set_once image_name "${1}" "docker image name"; shift;;
350esac
351done
352[[ -n "${num_nodes}" ]] || num_nodes="${default_num_nodes}"
353[[ -n "${image_name}" ]] || image_name="${default_image_name}-${default_jdk/:/-}"
354[[ "${num_nodes}" =~ ^-?[0-9]+$ ]] || \
355die "ducker_up: the number of nodes must be an integer."
356[[ "${num_nodes}" -gt 0 ]] || die "ducker_up: the number of nodes must be greater than 0."
357if [[ "${num_nodes}" -lt 2 ]]; then
358if [[ "${force}" -ne 1 ]]; then
359echo "ducker_up: It is recommended to run at least 2 nodes, since ducker01 is only \
360used to run ducktape itself. If you want to do it anyway, you can use --force to attempt to \
361use only ${num_nodes}."
362exit 1
363fi
364fi
365[[ "${subnet}" ]] && docker_network_options="--subnet ${subnet}"
366
367docker ps >/dev/null || die "ducker_up: failed to run docker. Please check that the daemon is started."
368
369local running_containers="$(docker ps -f=network=ducknet -q)"
370local num_running_containers=$(count ${running_containers})
371if [[ ${num_running_containers} -gt 0 ]]; then
372die "ducker_up: there are ${num_running_containers} ducker containers \
373running already. Use ducker down to bring down these containers before \
374attempting to start new ones."
375fi
376
377echo "ducker_up: Bringing up ${image_name} with ${num_nodes} nodes..."
378docker image inspect "${image_name}" &>/dev/null || \
379must_do -v -o docker pull "${image_name}"
380
381if docker network inspect ducknet &>/dev/null; then
382must_do -v docker network rm ducknet
383fi
384must_do -v docker network create "${docker_network_options}" ducknet
385if [[ -n "${custom_ducktape}" ]]; then
386setup_custom_ducktape "${custom_ducktape}" "${image_name}"
387fi
388for n in $(seq -f %02g 1 ${num_nodes}); do
389local node="ducker${n}"
390docker_run "${node}" "${image_name}" "${expose_ports}"
391done
392mkdir -p "${ducker_dir}/build"
393exec 3<> "${ducker_dir}/build/node_hosts"
394for n in $(seq -f %02g 1 ${num_nodes}); do
395local node="ducker${n}"
396docker exec --user=root "${node}" grep "${node}" /etc/hosts >&3
397[[ $? -ne 0 ]] && die "failed to find the /etc/hosts entry for ${node}"
398done
399exec 3>&-
400for n in $(seq -f %02g 1 ${num_nodes}); do
401local node="ducker${n}"
402docker exec --user=root "${node}" \
403bash -c "grep -v ${node} /opt/ignite-dev/modules/ducktests/tests/docker/build/node_hosts >> /etc/hosts"
404[[ $? -ne 0 ]] && die "failed to append to the /etc/hosts file on ${node}"
405done
406
407echo "ducker_up: added the latest entries to /etc/hosts on each node."
408generate_cluster_json_file "${num_nodes}" "${ducker_dir}/build/cluster.json"
409echo "ducker_up: successfully wrote ${ducker_dir}/build/cluster.json"
410
411# Save docker image id to the file. Then could use this file to find version of docker image that is running.
412# It could be useful if we don't confident about necessity of rebuilding image.
413get_image_id "${image_name}" > "${ducker_dir}/build/image_id.up"
414
415echo "** ducker_up: successfully brought up ${num_nodes} nodes."
416}
417
418# Generate the cluster.json file used by ducktape to identify cluster nodes.
419#
420# $1: The number of cluster nodes.
421# $2: The path to write the cluster.json file to.
422generate_cluster_json_file() {
423local num_nodes="${1}"
424local path="${2}"
425rm ${path}
426touch ${path}
427exec 3<> "${path}"
428cat<<EOF >&3
429{
430"_comment": [
431"Licensed to the Apache Software Foundation (ASF) under one or more",
432"contributor license agreements. See the NOTICE file distributed with",
433"this work for additional information regarding copyright ownership.",
434"The ASF licenses this file to You under the Apache License, Version 2.0",
435"(the \"License\"); you may not use this file except in compliance with",
436"the License. You may obtain a copy of the License at",
437"",
438"http://www.apache.org/licenses/LICENSE-2.0",
439"",
440"Unless required by applicable law or agreed to in writing, software",
441"distributed under the License is distributed on an \"AS IS\" BASIS,",
442"WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.",
443"See the License for the specific language governing permissions and",
444"limitations under the License."
445],
446"nodes": [
447EOF
448for n in $(seq 2 ${num_nodes}); do
449if [[ ${n} -eq ${num_nodes} ]]; then
450suffix=""
451else
452suffix=","
453fi
454local node=$(printf ducker%02d ${n})
455cat<<EOF >&3
456{
457"externally_routable_ip": "${node}",
458"ssh_config": {
459"host": "${node}",
460"hostname": "${node}",
461"identityfile": "/home/ducker/.ssh/id_rsa",
462"password": "",
463"port": 22,
464"user": "ducker"
465}
466}${suffix}
467EOF
468done
469cat<<EOF >&3
470]
471}
472EOF
473exec 3>&-
474}
475
476ducker_test() {
477require_commands docker
478docker inspect ducker01 &>/dev/null || \
479die "ducker_test: the ducker01 instance appears to be down. Did you run 'ducker up'?"
480[[ $# -lt 1 ]] && \
481die "ducker_test: you must supply at least one system test to run. Type --help for help."
482local args=""
483local ignite_test=0
484for arg in "${@}"; do
485local regex=".*\/ignitetest\/(.*)"
486if [[ $arg =~ $regex ]]; then
487local ignpath=${BASH_REMATCH[1]}
488args="${args} ./modules/ducktests/tests/ignitetest/${ignpath}"
489else
490args="${args} ${arg}"
491fi
492done
493must_pushd "${ignite_dir}"
494#(test mvn) && mvn package -DskipTests -Dmaven.javadoc.skip=true -Plgpl,-examples,-clean-libs,-release,-scala,-clientDocs
495must_popd
496if [[ -n "${DUCKTAPE_EXTRA_SETUP}" ]]; then
497echo "executing extra ducktape setup with '${DUCKTAPE_EXTRA_SETUP}'"
498local nodes=$(echo_running_container_names)
499for node in ${nodes}; do
500docker exec --user=root ${node} bash -c "${DUCKTAPE_EXTRA_SETUP}" &
501done
502wait_jobs
503[[ $? -ne 0 ]] && die "failed to execute extra ducktape setup."
504fi
505cmd="cd /opt/ignite-dev && ducktape --cluster-file /opt/ignite-dev/modules/ducktests/tests/docker/build/cluster.json $args"
506echo "docker exec ducker01 bash -c \"${cmd}\""
507exec docker exec --user=ducker ducker01 bash -c "${cmd}"
508}
509
510ducker_ssh() {
511require_commands docker
512[[ $# -eq 0 ]] && die "ducker_ssh: Please specify a container name to log into. \
513Currently active containers: $(echo_running_container_names)"
514local node_info="${1}"
515shift
516local guest_command="$*"
517local user_name="ducker"
518if [[ "${node_info}" =~ @ ]]; then
519user_name="${node_info%%@*}"
520local node_name="${node_info##*@}"
521else
522local node_name="${node_info}"
523fi
524local docker_flags=""
525if [[ -z "${guest_command}" ]]; then
526local docker_flags="${docker_flags} -t"
527local guest_command_prefix=""
528guest_command=bash
529else
530local guest_command_prefix="bash -c"
531fi
532if [[ "${node_name}" == "all" ]]; then
533local nodes=$(echo_running_container_names)
534[[ "${nodes}" == "(none)" ]] && die "ducker_ssh: can't locate any running ducker nodes."
535for node in ${nodes}; do
536docker exec --user=${user_name} -i ${docker_flags} "${node}" \
537${guest_command_prefix} "${guest_command}" || die "docker exec ${node} failed"
538done
539else
540docker inspect --type=container -- "${node_name}" &>/dev/null || \
541die "ducker_ssh: can't locate node ${node_name}. Currently running nodes: \
542$(echo_running_container_names)"
543exec docker exec --user=${user_name} -i ${docker_flags} "${node_name}" \
544${guest_command_prefix} "${guest_command}"
545fi
546}
547
548# Echo all the running Ducker container names, or (none) if there are no running Ducker containers.
549echo_running_container_names() {
550node_names="$(docker ps -f=network=ducknet -q --format '{{.Names}}' | sort)"
551if [[ -z "${node_names}" ]]; then
552echo "(none)"
553else
554echo ${node_names//$'\n'/ }
555fi
556}
557
558ducker_down() {
559require_commands docker
560local verbose=1
561local force_str=""
562while [[ $# -ge 1 ]]; do
563case "${1}" in
564-q|--quiet) verbose=0; shift;;
565-f|--force) force_str="-f"; shift;;
566*) die "ducker_down: unexpected command-line argument ${1}";;
567esac
568done
569local running_containers
570running_containers="$(docker ps -f=network=ducknet -q)"
571[[ $? -eq 0 ]] || die "ducker_down: docker command failed. Is the docker daemon running?"
572running_containers=${running_containers//$'\n'/ }
573local all_containers
574all_containers=$(docker ps -a -f=network=ducknet -q)
575all_containers=${all_containers//$'\n'/ }
576if [[ -z "${all_containers}" ]]; then
577maybe_echo "${verbose}" "No ducker containers found."
578return
579fi
580verbose_flag=""
581if [[ ${verbose} == 1 ]]; then
582verbose_flag="-v"
583fi
584if [[ -n "${running_containers}" ]]; then
585must_do ${verbose_flag} docker kill "${running_containers[@]}"
586fi
587must_do ${verbose_flag} docker rm ${force_str} "${all_containers}"
588must_do ${verbose_flag} -o rm -f -- "${ducker_dir}/build/node_hosts" "${ducker_dir}/build/cluster.json"
589if docker network inspect ducknet &>/dev/null; then
590must_do -v docker network rm ducknet
591fi
592rm "${ducker_dir}/build/image_id.up"
593maybe_echo "${verbose}" "ducker_down: removed $(count ${all_containers}) containers."
594}
595
596ducker_purge() {
597require_commands docker
598local force_str=""
599while [[ $# -ge 1 ]]; do
600case "${1}" in
601-f|--force) force_str="-f"; shift;;
602*) die "ducker_purge: unknown argument ${1}";;
603esac
604done
605echo "** ducker_purge: attempting to locate ducker images to purge"
606local images
607images=$(docker images -q -a -f label=ducker.creator)
608[[ $? -ne 0 ]] && die "docker images command failed"
609images=${images//$'\n'/ }
610declare -a purge_images=()
611if [[ -z "${images}" ]]; then
612echo "** ducker_purge: no images found to purge."
613exit 0
614fi
615echo "** ducker_purge: images to delete:"
616for image in ${images}; do
617echo -n "${image} "
618docker inspect --format='{{.Config.Labels}} {{.Created}}' --type=image "${image}"
619[[ $? -ne 0 ]] && die "docker inspect ${image} failed"
620done
621ask_yes_no "Delete these docker images? [y/n]"
622[[ "${_return}" -eq 0 ]] && exit 0
623must_do -v -o docker rmi ${force_str} ${images}
624}
625
626get_image_id() {
627require_commands docker
628local image_name="${1}"
629
630must_do -o docker image inspect --format "{{.Id}}" "${image_name}"
631}
632
633ducker_compare() {
634local cmd=""
635
636local verbose=1
637local force_str=""
638
639while [[ $# -ge 1 ]]; do
640case "${1}" in
641-q|--quiet) verbose=0; cmd="${cmd} ${1}"; shift;;
642-f|--force) force_str="-f"; cmd="${cmd} ${1}"; shift;;
643*) set_once image_name "${1}" "docker image name"; shift;;
644esac
645done
646
647[ -n "${image_name}" ] || image_name="${default_image_name}-${default_jdk/:/-}"
648
649cmp -s "${ducker_dir}/build/image_${image_name}.build" "${ducker_dir}/build/image_id.up"
650local ret="$?"
651
652if [[ $ret != "0" ]]; then
653echo "Docker image ${image_name} is outdated. Stop the cluster"
654ducker_down ${cmd}
655fi
656}
657
658# Parse command-line arguments
659[[ $# -lt 1 ]] && usage 0
660# Display the help text if -h or --help appears in the command line
661for arg in ${@}; do
662case "${arg}" in
663-h|--help) usage 0;;
664--) break;;
665*);;
666esac
667done
668action="${1}"
669shift
670case "${action}" in
671help) usage 0;;
672
673build|up|test|ssh|down|purge|compare)
674ducker_${action} "${@}"; exit 0;;
675
676*) echo "Unknown command '${action}'. Type '${script_path} --help' for usage information."
677exit 1;;
678esac
679