This is for a cheap VPS I use for personal projects. I like having a message of the day displaying system info on login; I wanted to add the status of my docker containers to it, to see if all are running and whatnot.
The scripts I found online for this displayed all the containers, but since I run mailcow for my email and it has 10+ containers in that single docker-compose project, the MOTD was much too long for my liking. So I decided to write my own although I've never written anything in Bash before this.
My script lists Docker containers and their status, but containers that are part of a docker-compose project are condensed into one as if they were a single container with the name of the compose project. If a container of a docker-compose project isn't running then the status of the "condensed container" is changed to "partial", and the container that isn't running is listed with its full name.
Screenshot (highlighted with a box is the output of the script)
My concerns about this are:
- I don't want to miss out on relevant info (other than the amount of time a container has been up for; I don't care about that), I'm not too familiar with docker and possible status a container could have and whether my way of doing
...!= "Up"
to check the status is good enough. - When a compose project has all its containers stopped/exited (any status that isn't "Up") I'd like to "condense" it again, since at the moment if that's the case then it will display partial, even though all its containers are not running, then it also list all of its containers. I don't know how I'd achieve this without running multiple chained
docker ps
commands that would slow down the script considerably. For now I'm content that a compose project having all its containers being down isn't something frequent. - I'm not too happy with the way I order the containers. I use two arrays put what I want near the top in one, and then what I want near the bottom in the second. I then concat the arrays. Not sure if this matters or if there's a cleaner way of doing it.
- Any unforeseen effects, or any way this doesn't work how I intended (outlined above) that is caused by my relative unfamiliarity with Docker or complete unfamiliarity with Bash scripts. And lastly and super minor, almost not worth mentioning: my choice of wording for saying "partial" when not all compose project's containers are running. Couldn't think of any better word, not sure if there is one.
#!/bin/bash
COLUMNS=2
# colors
green="\e[1;32m"
yellow="\e[1;33m"
red="\e[1;31m"
blue="\e[36;1m"
undim="\e[0m"
mapfile -t containers \
< <(docker ps -a --format='{{.Label "com.docker.compose.project"}},{{.Names}},{{.Status}}' \
| sort -r -k3 -t "," \
| awk -F '[ ,]' -v OFS=',' '{ print 1,ドル2,ドル3ドル }')
declare -A upper_containers # To later concat with the ones I want to display first at the top
declare -A lower_containers
for i in "${!containers[@]}"; do
IFS="," read proj_name name status <<< ${containers[i]}
if [[ -n $proj_name ]]; then # is docker-compose
color=$green;
if [[ "$status" != "Up" ]]; then
lower_containers[$name]="${name}:,${red}${status,,}${undim},";
color=$yellow;
status="partial";
fi
upper_containers[$proj_name]="${blue}${proj_name}:,${color}${status,,}${undim},"
else # not docker-compose
if [[ "$status" != "Up" ]]; then
lower_containers[$name]="${name}:,${red}${status,,}${undim},";
else
upper_containers[$name]="${name}:,${green}${status,,}${undim},";
fi
fi
done;
i=1
out=""
containers=("${upper_containers[@]}" "${lower_containers[@]}")
for el in "${containers[@]}"; do
out+=$el
if [ $(($i % $COLUMNS)) -eq 0 ]; then
out+="\n"
fi
i=$(($i+1))
done;
out+="\n"
printf "\ndocker status:\n"
printf "$out" | column -ts $',' | sed -e 's/^/ /'
printf "\n\n"
2 Answers 2
Addressing your concerns
- I don't want to miss out on relevant info (other than the amount of time a container has been up for; I don't care about that), I'm not too familiar with docker and possible status a container could have and whether my way of doing ...!= "Up" to check the status is good enough.
I don't know docker compose very well either, but I have some ideas here:
- When it's not docker compose, the status value will be always printed. So when "Up" is no longer good enough, you will see it in the output, and then you can take appropriate action.
- When it's docker compose, the status value will be printed for
$name
, but not for$proj_name
(where you print "partial" instead). This seems ok too, because again, the original value gets printed, so the logic in the previous point works for this one too.
- When a compose project has all its containers stopped/exited (any status that isn't "Up") I'd like to "condense" it again, since at the moment if that's the case then it will display partial, even though all its containers are not running, then it also list all of its containers. I don't know how I'd achieve this without running multiple chained docker ps commands that would slow down the script considerably. For now I'm content that a compose project having all its containers being down isn't something frequent.
You can do with a single docker ps command, storing data about projects, to process in a second pass. The data you would store:
- A regular array of unique project names
- An associative array
any_container_up
to track for each project if any of its containers was up - An associative array
any_container_down
to track for each project if any of its containers was down
You could populate these data structures in the first pass over the output of docker ps.
Next, you could loop over the project names,
and decide based on any_container_up[$proj_name]
and any_container_down[$proj_name]
the correct status for the project itself:
- if
any_container_up[$proj_name]
andany_container_down[$proj_name]
-> partial: some containers are up, others are down - else if
any_container_up[$proj_name]
-> all containers are up - else -> all containers are down
- I'm not too happy with the way I order the containers. I use two arrays put what I want near the top in one, and then what I want near the bottom in the second. I then concat the arrays. Not sure if this matters or if there's a cleaner way of doing it.
As written, the last few containers up and the first few containers down may appear on the same line. So for each line I would have to scan horizontally, which is a mental burden. It seems to me that the different statuses are significant enough that they would deserve to be listed with a clean break in between. I would even split to 3 groups: up, partial, down.
- Any unforeseen effects, or any way this doesn't work how I intended (outlined above) that is caused by my relative unfamiliarity with Docker or complete unfamiliarity with Bash scripts. And lastly and super minor, almost not worth mentioning: my choice of wording for saying "partial" when not all compose project's containers are running. Couldn't think of any better word, not sure if there is one.
The script looks pretty good to me, I don't see major causes for concern.
As for the wording of "partial", it felt natural to me, and I'm very sensitive to naming.
And on that note... I didn't like the names upper_containers
and lower_containers
. My first assumption was that "upper" and "lower" refers to uppercasing and lowercasing names, that these are data structures for formatting. (Then of course I saw that's not the case, I'm just sharing my initial impressions.) I would rename these to containers_up
and containers_down
.
Looping in Bash
Instead of:
for i in "${!containers[@]}"; do IFS="," read proj_name name status <<< ${containers[i]}
It would be simpler this way:
for container in "${containers[@]}"; do
IFS="," read proj_name name status <<< ${container}
Instead of:
containers=("${upper_containers[@]}" "${lower_containers[@]}") for el in "${containers[@]}"; do
You could write:
for container in "${upper_containers[@]}" "${lower_containers[@]}"; do
Using arithmetic evaluation
Instead of this:
if [ $(($i % $COLUMNS)) -eq 0 ]; then out+="\n" fi i=$(($i+1))
It would be simpler to use arithmetic evaluation:
if (( i % COLUMNS == 0 )); then
out+="\n"
fi
((++i))
For more info about arithmetic evaluation, see the ARITHMETIC EVALUATION section in man bash
.
Odd semicolons
Several lines end with unnecessary ;
that can be simply removed.
-
1\$\begingroup\$ What a brilliant and comple response, thank you! I didn't even think to ask about the loops, but i did feel like they were clunky. Regarding the semi-colons is not using them a convention? Or is it because I was inconsistent in my usage (which I hadn't noticed) \$\endgroup\$DavidPH– DavidPH2021年11月01日 16:50:37 +00:00Commented Nov 1, 2021 at 16:50
-
\$\begingroup\$ @DavidPH Your loop is find, I didn't think it's clunky. My suggestion on it is rather cosmetic. The semi-colons in Bash are to separate commands. Since the newline already has that effect, a semi-colon is completely unnecessary there, therefore it shouldn't be there. \$\endgroup\$janos– janos2021年11月01日 16:57:40 +00:00Commented Nov 1, 2021 at 16:57
-
1\$\begingroup\$ Yeah by clunky I meant as in I never liked the way they looked, but didn't consider there being alternatives. Also thanks for pointing out, didn't notice I hadn't upvoted that months ago. \$\endgroup\$DavidPH– DavidPH2021年11月01日 21:33:33 +00:00Commented Nov 1, 2021 at 21:33
# colors green="\e[1;32m" yellow="\e[1;33m" red="\e[1;31m" blue="\e[36;1m"
You don't know what kind of terminal (if indeed a terminal) that output will be going to, so don't hard-code these terminal-specific codes.
Instead, use tput
which will generate the right strings for terminals that support them:
green=$(tput setaf 2)
yellow=$(tput setaf 3)
red=$(tput setaf 1)
blue=$(tput setaf 4)
Don't set COLUMNS
yourself globally for the script - normally Bash sets this automatically to the terminal's correct number of columns, and setting it to 2 will certainly confuse many applications.
It looks like you meant to create a variable for your own use - don't use all-caps for your own variables, to avoid collisions like this with environment variables that affect child processes.
-
\$\begingroup\$ Ah thanks, was wondering why the colors were a bit different on my laptop and pc, didn't think much of it. When you say "if indeed a terminal", since the script is located in /etc/update-motd.d/ along my other MOTD messages is it not guaranteed to be a terminal? Also if am I fine with making a differently named variable / just lower case columns if I want always 2? \$\endgroup\$DavidPH– DavidPH2021年06月04日 11:00:09 +00:00Commented Jun 4, 2021 at 11:00
-
\$\begingroup\$ Yes, use a lower-case name so that you're not setting
COLUMNS
which already has a meaning. \$\endgroup\$Toby Speight– Toby Speight2021年06月04日 11:44:19 +00:00Commented Jun 4, 2021 at 11:44