Issues (1426)

bin/dbstack (6 issues)

1
#!/usr/bin/env bash
2
3
# Manage the whole set of containers without having to learn Docker
4
5
# @todo allow end user to enter pwd for root db accounts on build. If not interactive, generate a random one
6
# @todo check for ports conflicts before starting the web container
7
# @todo if there's no docker-compose onboard but there is curl or wget, download and install docker-compose
8
# @todo add a command to remove the built containers and their anon volumes ? eg. `docker-compose rm -s -v`
9
# @todo make SETUP_APP_ON_BOOT take effect also on start. Also: make it tri-valued: skip, force and null
10
11
# consts
12
BOOTSTRAP_OK_FILE=/var/run/bootstrap_ok
13
COMPOSE_DEFAULT_ENV_FILE=.env
14
WORKER_SERVICE=worker
15
# vars
16
COMPOSE_LOCAL_ENV_FILE=${COMPOSE_LOCAL_ENV_FILE:-.env.local}
17
AS_ROOT=false
18
BOOTSTRAP_TIMEOUT=300
19
CLEANUP_UNUSED_IMAGES=false
20
DOCKER_COMPOSE_CMD=
21
DOCKER_NO_CACHE=
22
PARALLEL_BUILD=
23
PULL_IMAGES=false
24
REBUILD=false
25
RECREATE=false
26
SILENT=false
27
SETUP_APP_ON_BOOT=true
28
SILENT=false
29
VERBOSITY=
30
WORKER_CONTAINER=
31
WORKER_USER=
32
33
help() {
34
    printf "Usage: dbstack [OPTIONS] COMMAND [OPTARGS]
35
36
Manages the Db3v4l Docker Stack
37
38
Commands:
39
    build               build or rebuild the complete set of containers and set up the app. Leaves the stack running
40
    cleanup CATEGORY    remove temporary data/logs/caches/etc... CATEGORY can be any of:
41
                        - databases       NB: this removes all your data! Better done when containers are stopped
42
                        - docker-images   removes only unused images. Can be quite beneficial to free up space
43
                        - docker-logs     NB: for this to work, you'll need to run this script as root
44
                        - logs            removes log files from the databases, webservers, symfony
45
                        - shared-data     removes every file in the ./shared folder
46
    images              list container images
47
    kill [\$svc]         kill containers
48
    logs [\$svc]         view output from containers
49
    monitor             starts an interactive console for monitoring containers, images, volumes
50
    pause [\$svc]        pause the containers
51
    ps [\$svc]           show the status of running containers
52
    setup               set up the app without rebuilding the containers first
53
    run                 execute a single command in the worker container
54
    shell               start a shell in the worker container as the application user
55
    services            list docker-compose services
56
    restart [\$svc]      restart the complete set of containers
57
    start [\$svc]        start the complete set of containers
58
    stop [\$svc]         stop the complete set of containers
59
    top [\$svc]          display the running container processes
60
    unpause [\$svc]      unpause the containers
61
62
Options:
63
    -c              clean up docker images which have become useless - when running 'build'
64
    -e ENV_FILE     use an alternative to .env.local as local config file (path relative to the docker folder).
65
                    You can also use the env var COMPOSE_LOCAL_ENV_FILE for the same purpose.
66
    -h              print help
67
    -n              do not set up the app - when running 'build'
68
    -p              build containers in parallel - when running 'build'
69
    -r              force containers to rebuild from scratch (this forces a full app set up as well) - when running 'build'
70
                    log in as root instead of the app user account - when running 'shell' and 'run'
71
    -s              force app set up (via resetting containers to clean-build status besides updating them if needed) - when running 'build'
72
    -u              update containers by pulling the base images - when running 'build'
73
    -v              verbose mode
74
    -w SECONDS      wait timeout for completion of app and container set up - when running 'build' and 'start'. Defaults to ${BOOTSTRAP_TIMEOUT}
75
    -z              avoid using docker cache - when running 'build -r'
76
"
77
}
78
79
check_requirements() {
0 ignored issues
show
The mentioned parser error was in this brace group.
Loading history...
80
    # @todo do proper min-version checking
81
82
    which docker >/dev/null 2>&1
83
    if [ $? -ne 0 ]; then
0 ignored issues
show
Did you forget the 'then' for this 'if'?
Loading history...
Couldn't parse this if expression.
Loading history...
Use semicolon or linefeed before 'then' (or quote to make it literal).
Loading history...
84
        printf "\n\e[31mPlease install docker & add it to \$PATH\e[0m\n\n" >&2
85
        exit 1
86
    fi
0 ignored issues
show
Expected 'then'.
Loading history...
Unexpected keyword/token. Fix any mentioned problems and try again.
Loading history...
87
88
    which docker-compose >/dev/null 2>&1
89
    if [ $? -ne 0 ]; then
90
        printf "\n\e[31mPlease install docker-compose & add it to \$PATH\e[0m\n\n" >&2
91
        exit 1
92
    fi
93
}
94
95
# @todo have this function looked at by a bash guru to validate it's not a brainf**t
96
#load_config_value() {
97
#    local VALUE=
98
#    if [ -f ${COMPOSE_LOCAL_ENV_FILE} ]; then
99
#        VALUE=$(grep "^${1}=" ${COMPOSE_LOCAL_ENV_FILE})
100
#    fi
101
#    if [ -z "${VALUE}" ]; then
102
#        VALUE=$(grep "^${1}=" ${COMPOSE_DEFAULT_ENV_FILE})
103
#    fi
104
#    VALUE=${VALUE/${1}=/}
105
#}
106
107
load_config() {
108
    source "${COMPOSE_DEFAULT_ENV_FILE}"
109
110
    # @todo check that COMPOSE_LOCAL_ENV_FILE does not override env vars that it should not, eg. COMPOSE_LOCAL_ENV_FILE, BOOTSTRAP_OK_FILE, ...
111
    if [ -f "${COMPOSE_LOCAL_ENV_FILE}" ]; then
112
        # these vars we have to export as otherwise they are not taken into account by docker-compose
113
        set -a
114
        source "${COMPOSE_LOCAL_ENV_FILE}"
115
        set +a
116
    fi
117
118
    # @todo run `docker-compose ps` to retrieve the WORKER_CONTAINER id instead of parsing the config
119
    if [ -z "${COMPOSE_PROJECT_NAME}" ]; then
120
        printf "\n\e[31mCan not find the name of the composer project name in ${COMPOSE_DEFAULT_ENV_FILE} / ${COMPOSE_LOCAL_ENV_FILE}\e[0m\n\n" >&2
121
        exit 1
122
    fi
123
    # @todo the value for WORKER_USER could be hardcoded instead of being variable...
124
    if [ -z "${CONTAINER_USER}" ]; then
125
        printf "\n\e[31mCan not find the name of the container user account in ${COMPOSE_DEFAULT_ENV_FILE} / ${COMPOSE_LOCAL_ENV_FILE}\e[0m\n\n" >&2
126
        exit 1
127
    fi
128
129
    WORKER_CONTAINER="${COMPOSE_PROJECT_NAME}_${WORKER_SERVICE}"
130
    WORKER_USER=${CONTAINER_USER}
131
}
132
133
setup_local_config() {
134
    local CURRENT_USER_UID
135
    local CURRENT_USER_GID
136
137
    CURRENT_USER_UID=$(id -u)
138
    CURRENT_USER_GID=$(id -g)
139
140
    if [ "${CONTAINER_USER_UID}" = "${CURRENT_USER_UID}" -a "${CONTAINER_USER_GID}" = "${CURRENT_USER_GID}" ]; then
141
        return
142
    fi
143
144
    if [ -f "${COMPOSE_LOCAL_ENV_FILE}" ]; then
145
        # @todo in case the file already exists and has incorrect values for these vars, replace them instead of skipping...
146
        printf "\n\e[31mWARNING: current user id and/or group id do not match the ones in config files\e[0m\n\n" >&2
147
        # Q: why not just use the current values ?
148
        #export CONTAINER_USER_UID=${CURRENT_USER_UID}
149
        #export CONTAINER_USER_GID=${CURRENT_USER_GID}
150
        return
151
    fi
152
153
    echo "[$(date)] Setting up the configuration file '${COMPOSE_LOCAL_ENV_FILE}'..."
154
155
    #CURRENT_USER_UID=$(id -u)
156
    #CURRENT_USER_GID=$(id -g)
157
    #CONTAINER_USER_UID=$(grep -F CONTAINER_USER_UID ${COMPOSE_DEFAULT_ENV_FILE} | sed 's/CONTAINER_USER_UID=//')
158
    #CONTAINER_USER_GID=$(grep -F CONTAINER_USER_GID ${COMPOSE_DEFAULT_ENV_FILE} | sed 's/CONTAINER_USER_GID=//')
159
160
    touch "${COMPOSE_LOCAL_ENV_FILE}"
161
162
    if [ "${CONTAINER_USER_UID}" != "${CURRENT_USER_UID}" ]; then
163
        echo "CONTAINER_USER_UID=${CURRENT_USER_UID}" >> "${COMPOSE_LOCAL_ENV_FILE}"
164
        export CONTAINER_USER_UID=${CURRENT_USER_UID}
165
    fi
166
    if [ "${CONTAINER_USER_GID}" != "${CURRENT_USER_GID}" ]; then
167
        echo "CONTAINER_USER_GID=${CURRENT_USER_GID}" >> "${COMPOSE_LOCAL_ENV_FILE}"
168
        export CONTAINER_USER_GID=${CURRENT_USER_GID}
169
    fi
170
171
    # @todo allow setting up: custom db root account pwd, sf env, etc...
172
}
173
174
create_compose_command() {
175
    local VENDORS
176
    DOCKER_COMPOSE_CMD='docker-compose -f docker-compose.yml'
177
    if [ -n "${COMPOSE_ONLY_VENDORS}" ]; then
178
        VENDORS=${COMPOSE_ONLY_VENDORS//,/ }
179
    else
180
        VENDORS=$(cd compose && ls -- *.yml | tr '\n' ' ')
181
        VENDORS=${VENDORS//.yml/}
182
    fi
183
    if [ -n "${COMPOSE_EXCEPT_VENDORS}" ]; then
184
        for COMPOSE_EXCEPT_VENDOR in ${COMPOSE_EXCEPT_VENDORS//,/ }; do
185
            # @bug what if a COMPOSE_EXCEPT_VENDOR is a substring of a VENDOR ?
186
            VENDORS=${VENDORS/$COMPOSE_EXCEPT_VENDOR/}
187
        done
188
    fi
189
    for DC_CONF_FILE in ${VENDORS}; do
190
        DOCKER_COMPOSE_CMD="${DOCKER_COMPOSE_CMD} -f compose/${DC_CONF_FILE}.yml"
191
    done
192
}
193
194
build() {
195
    local IMAGES
196
197
    if [ ${CLEANUP_UNUSED_IMAGES} = 'true' ]; then
198
        # for good measure, do a bit of hdd disk cleanup ;-)
199
        echo "[$(date)] Removing unused Docker images from disk..."
200
        docker rmi $(docker images | grep "<none>" | awk "{print \$3}")
201
    fi
202
203
    echo "[$(date)] Building and Starting all Containers..."
204
205
    ${DOCKER_COMPOSE_CMD} ${VERBOSITY} stop
206
    if [ ${REBUILD} = 'true' ]; then
207
        ${DOCKER_COMPOSE_CMD} ${VERBOSITY} rm -f
208
    fi
209
210
    if [ ${PULL_IMAGES} = 'true' ]; then
211
        echo "[$(date)] Pulling base Docker images..."
212
213
        for DOCKERFILE in $(find . -name Dockerfile); do
214
            IMAGE=$(fgrep -h 'FROM' "${DOCKERFILE}" | sed 's/FROM //g')
215
            if [[ "${IMAGE}" == *'${base_image_version}'* ]]; then
216
                # @todo resolve the `base_image_version` dockerfile arg by resolving the source env var - run eg. docker-compose config
217
                DEFAULT_BASE_IMAGE=$(fgrep -h 'ARG base_image_version=' "${DOCKERFILE}" | sed 's/ARG base_image_version=//g')
218
                IMAGE=${IMAGE/\$\{base_image_version\}/$DEFAULT_BASE_IMAGE}
219
220
                printf "\e[31mWARNING: pulling base image ${IMAGE} for container ${DOCKERFILE} which might have been overwritten via env var...\e[0m\n" >&2
221
            fi
222
223
            docker pull "${IMAGE}"
224
        done
225
    fi
226
227
    ${DOCKER_COMPOSE_CMD} ${VERBOSITY} build ${PARALLEL_BUILD} ${DOCKER_NO_CACHE}
228
229
    if [ ${SETUP_APP_ON_BOOT} = 'false' ]; then
230
        export COMPOSE_SETUP_APP_ON_BOOT=false
231
    fi
232
233
    if [ ${RECREATE} = 'true' ]; then
234
        ${DOCKER_COMPOSE_CMD} ${VERBOSITY} up -d --force-recreate
235
        RETCODE=$?
236
    else
237
        ${DOCKER_COMPOSE_CMD} ${VERBOSITY} up -d
238
        RETCODE=$?
239
    fi
240
    if [ ${RETCODE} -ne 0 ]; then
241
        exit ${RETCODE}
242
    fi
243
244
    wait_for_bootstrap all
245
    RETCODE=$?
246
247
    if [ ${CLEANUP_UNUSED_IMAGES} = 'true' ]; then
248
        echo "[$(date)] Removing unused Docker images from disk, again..."
249
        docker rmi $(docker images | grep "<none>" | awk "{print \$3}")
250
    fi
251
252
    echo "[$(date)] Build finished"
253
254
    exit ${RETCODE}
255
}
256
257
258
# @todo loop over all args
259
cleanup() {
260
    case "${1}" in
261
        databases)
262
            if [ ${SILENT} != true ]; then
263
                echo "Do you really want to delete all database data?"
264
                select yn in "Yes" "No"; do
265
                    case $yn in
266
                        Yes ) break ;;
267
                        No ) exit 1 ;;
268
                    esac
269
                done
270
            fi
271
272
            find ./data/ -type f ! -name .gitkeep -delete
273
            # leftover sockets happen...
274
            find ./data/ -type s -delete
275
            find ./data/ -type d -empty -delete
276
        ;;
277
        docker-images)
278
            # @todo this gives a warning when no images are found to delete
279
            docker rmi $(docker images | grep "<none>" | awk "{print \$3}")
280
        ;;
281
        docker-logs)
282
            for CONTAINER in $(${DOCKER_COMPOSE_CMD} ps -q)
283
            do
284
                LOGFILE=$(docker inspect --format='{{.LogPath}}' ${CONTAINER})
285
                if [ -n "${LOGFILE}" ]; then
286
                    echo "" > "${LOGFILE}"
287
                fi
288
            done
289
        ;;
290
        logs)
291
            find ./logs/ -type f ! -name .gitkeep -delete
292
            find ../app/var/log/ -type f ! -name .gitkeep -delete
293
        ;;
294
        shared-data)
295
            if [ ${SILENT} != true ]; then
296
                echo "Do you really want to delete all data in the 'shared' folder?"
297
                select yn in "Yes" "No"; do
298
                    case $yn in
299
                        Yes ) break ;;
300
                        No ) exit 1 ;;
301
                    esac
302
                done
303
            fi
304
305
            find ../shared/ -type f ! -name .gitkeep -delete
306
        ;;
307
        symfony-cache)
308
            find ../app/var/cache/ -type f ! -name .gitkeep -delete
309
        ;;
310
        *)
311
            printf "\n\e[31mERROR: unknown cleanup target: ${1}\e[0m\n\n" >&2
312
            help
313
            exit 1
314
        ;;
315
    esac
316
}
317
318
setup_app() {
319
    echo "[$(date)] Starting the Worker container..."
320
321
    # avoid automatic app setup being triggered here
322
    export COMPOSE_SETUP_APP_ON_BOOT=false
323
324
    ${DOCKER_COMPOSE_CMD} ${VERBOSITY} up -d ${WORKER_SERVICE}
325
    RETCODE=$?
326
    if [ ${RETCODE} -ne 0 ]; then
327
        exit ${RETCODE}
328
    fi
329
330
    wait_for_bootstrap worker
331
    RETCODE=$?
332
    if [ ${RETCODE} -ne 0 ]; then
333
        exit ${RETCODE}
334
    fi
335
336
    echo "[$(date)] Setting up the app (from inside the Worker container)..."
337
    docker exec ${WORKER_CONTAINER} su - "${WORKER_USER}" -c "cd /home/${WORKER_USER}/app && composer install"
338
    echo "[$(date)] Setup finished"
339
}
340
341
# Wait until containers have fully booted
342
wait_for_bootstrap() {
343
344
    if [ ${BOOTSTRAP_TIMEOUT} -le 0 ]; then
345
        return 0
346
    fi
347
348
    case "${1}" in
349
        admin)
350
            BOOTSTRAP_CONTAINERS=adminer
351
        ;;
352
        all)
353
            # q: check all services or only the running ones?
354
            #BOOTSTRAP_CONTAINERS=$(${DOCKER_COMPOSE_CMD} config --services)
355
            BOOTSTRAP_CONTAINERS=$(${DOCKER_COMPOSE_CMD} ps --services | sort | tr '\n' ' ')
356
        ;;
357
        app)
358
            BOOTSTRAP_CONTAINERS='worker web adminer'
359
        ;;
360
        #web)
361
        #    BOOTSTRAP_CONTAINERS=web
362
        #;;
363
        #worker)
364
        #    BOOTSTRAP_CONTAINERS=worker
365
        #;;
366
        *)
367
            #printf "\n\e[31mERROR: unknown booting container: ${1}\e[0m\n\n" >&2
368
            #help
369
            #exit 1
370
            # @todo add check that this service is actually defined
371
            BOOTSTRAP_CONTAINERS=${1}
372
        ;;
373
    esac
374
375
    echo "[$(date)] Waiting for containers bootstrap to finish..."
376
377
    # @todo speed this up...
378
    #       - maybe go back to generating and checking files mounted on the host?
379
    #       - find a way to run commands in parallel while collecting their output/exit-code?
380
    #         see eg. https://www.golinuxcloud.com/run-shell-scripts-in-parallel-collect-exit-status-process/
381
    #         or https://gist.github.com/mjambon/79adfc5cf6b11252e78b75df50793f24
382
    local BOOTSTRAP_OK
383
    i=0
384
    while [ $i -le "${BOOTSTRAP_TIMEOUT}" ]; do
385
        sleep 1
386
        BOOTSTRAP_OK=''
387
        for BS_CONTAINER in ${BOOTSTRAP_CONTAINERS}; do
388
            printf "Waiting for ${BS_CONTAINER} ... "
389
            ${DOCKER_COMPOSE_CMD} exec ${BS_CONTAINER} cat ${BOOTSTRAP_OK_FILE} >/dev/null 2>/dev/null
390
            RETCODE=$?
391
            if [ ${RETCODE} -eq 0 ]; then
392
                printf "\e[32mdone\e[0m\n"
393
                BOOTSTRAP_OK="${BOOTSTRAP_OK} ${BS_CONTAINER}"
394
            else
395
                echo;
396
            fi
397
        done
398
        if [ -n "${BOOTSTRAP_OK}" ]; then
399
            for BS_CONTAINER in ${BOOTSTRAP_OK}; do
400
                BOOTSTRAP_CONTAINERS=${BOOTSTRAP_CONTAINERS//${BS_CONTAINER}/}
401
            done
402
            if [ -z  "${BOOTSTRAP_CONTAINERS// /}" ]; then
403
                break
404
            fi
405
        fi
406
        i=$(( i + 1 ))
407
    done
408
    if [ $i -gt 0 ]; then echo; fi
409
410
    if [ -n "${BOOTSTRAP_CONTAINERS// /}" ]; then
411
        printf "\n\e[31mBootstrap process did not finish within ${BOOTSTRAP_TIMEOUT} seconds\e[0m\n\n" >&2
412
        return 1
413
    fi
414
415
    return 0
416
}
417
418
### Begin live code
419
420
# @todo move to a function
421
while getopts ":ce:hnprsuvw:z" opt
422
do
423
    case $opt in
424
        c)
425
            CLEANUP_UNUSED_IMAGES=true
426
        ;;
427
        e)
428
            COMPOSE_LOCAL_ENV_FILE=${OPTARG}
429
        ;;
430
        h)
431
            help
432
            exit 0
433
        ;;
434
        n)
435
            SETUP_APP_ON_BOOT=false
436
        ;;
437
        p)
438
            PARALLEL_BUILD=--parallel
439
        ;;
440
        r)
441
            AS_ROOT=true
442
            REBUILD=true
443
        ;;
444
        s)
445
            RECREATE=true
446
        ;;
447
        u)
448
            PULL_IMAGES=true
449
        ;;
450
        v)
451
            VERBOSITY=--verbose
452
        ;;
453
        w)
454
            BOOTSTRAP_TIMEOUT=${OPTARG}
455
        ;;
456
        z)
457
            DOCKER_NO_CACHE=--no-cache
458
        ;;
459
        \?)
460
            printf "\n\e[31mERROR: unknown option -${OPTARG}\e[0m\n\n" >&2
461
            help
462
            exit 1
463
        ;;
464
    esac
465
done
466
shift $((OPTIND-1))
467
468
COMMAND=$1
469
470
check_requirements
471
472
cd "$(dirname -- ${BASH_SOURCE[0]})/../docker"
473
474
load_config
475
476
setup_local_config
477
478
create_compose_command
479
480
case "${COMMAND}" in
481
    build)
482
        build
483
    ;;
484
485
    cleanup)
486
        # @todo allow to pass many cleanup targets in one go
487
        cleanup "${2}"
488
    ;;
489
490
    config)
491
        ${DOCKER_COMPOSE_CMD} ${VERBOSITY} config
492
    ;;
493
494
    dbconsole)
495
        # @deprecated - left in for courtesy
496
        shift
497
        # scary line ? found it at https://stackoverflow.com/questions/12343227/escaping-bash-function-arguments-for-use-by-su-c
498
        docker exec -ti "${WORKER_CONTAINER}" su - "${WORKER_USER}" -c '"$0" "$@"' -- "/usr/bin/php" "app/bin/dbconsole" "$@"
499
    ;;
500
501
    images)
502
        ${DOCKER_COMPOSE_CMD} ${VERBOSITY} images ${2}
503
    ;;
504
505
    kill)
506
        ${DOCKER_COMPOSE_CMD} ${VERBOSITY} kill ${2}
507
    ;;
508
509
    logs)
510
        ${DOCKER_COMPOSE_CMD} ${VERBOSITY} logs ${2}
511
    ;;
512
513
    monitor)
514
        docker exec -ti db3v4l_lazydocker lazydocker
515
    ;;
516
517
    pause)
518
        ${DOCKER_COMPOSE_CMD} ${VERBOSITY} pause ${2}
519
    ;;
520
521
    ps)
522
        ${DOCKER_COMPOSE_CMD} ${VERBOSITY} ps ${2}
523
    ;;
524
525
    restart)
526
        ${DOCKER_COMPOSE_CMD} ${VERBOSITY} stop ${2}
527
        ${DOCKER_COMPOSE_CMD} ${VERBOSITY} up -d ${2}
528
        RETCODE=$?
529
        if [ ${RETCODE} -ne 0 ]; then
530
            exit ${RETCODE}
531
        fi
532
        if [ -z "${2}" ]; then
533
            wait_for_bootstrap all
534
            exit $?
535
        else
536
            wait_for_bootstrap ${2}
537
            exit $?
538
        fi
539
    ;;
540
541
    run)
542
        shift
543
        if [ ${AS_ROOT} = true ]; then
544
            docker exec -ti "${WORKER_CONTAINER}" "$@"
545
        else
546
            # @todo should we try to start from the 'app' dir ?
547
            # q: which one is better? test with a command with spaces in options values, and with a composite command such as cd here && do that
548
            docker exec -ti "${WORKER_CONTAINER}" sudo -iu "${WORKER_USER}" -- "$@"
549
            #docker exec -ti "${WORKER_CONTAINER}" su - "${WORKER_USER}" -c '"$0" "$@"' -- "$@"
550
        fi
551
    ;;
552
553
    setup)
554
        setup_app
555
    ;;
556
557
    services)
558
        ${DOCKER_COMPOSE_CMD} config --services | sort
559
    ;;
560
561
    shell)
562
        if [ ${AS_ROOT} = true ]; then
563
            docker exec -ti "${WORKER_CONTAINER}" bash
564
        else
565
            docker exec -ti "${WORKER_CONTAINER}" sudo -iu "${WORKER_USER}"
566
            #docker exec -ti "${WORKER_CONTAINER}" su - "${WORKER_USER}"
567
        fi
568
    ;;
569
570
    start)
571
        ${DOCKER_COMPOSE_CMD} ${VERBOSITY} up -d ${2}
572
        RETCODE=$?
573
        if [ ${RETCODE} -ne 0 ]; then
574
            exit ${RETCODE}
575
        fi
576
        if [ -z "${2}" ]; then
577
            wait_for_bootstrap all
578
            exit $?
579
        else
580
            wait_for_bootstrap ${2}
581
            exit $?
582
        fi
583
    ;;
584
585
    stop)
586
        ${DOCKER_COMPOSE_CMD} ${VERBOSITY} stop ${2}
587
    ;;
588
589
    top)
590
        ${DOCKER_COMPOSE_CMD} ${VERBOSITY} top ${2}
591
    ;;
592
593
    unpause)
594
        ${DOCKER_COMPOSE_CMD} ${VERBOSITY} unpause ${2}
595
    ;;
596
597
    # achieved by running `build -u`... could be expanded to also do a git pull?
598
    #update)
599
    #;;
600
601
    *)
602
        printf "\n\e[31mERROR: unknown command '${COMMAND}'\e[0m\n\n" >&2
603
        help
604
        exit 1
605
    ;;
606
esac
607