Issues (1426)

bin/dbstack (1 issue)

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() {
80
    # @todo do proper min-version checking
81
82
    which docker >/dev/null 2>&1
83
    if [ $? -ne 0 ]; then
0 ignored issues
show
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
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