Get exit code from docker entrypoint command - bash

I have a docker container that runs a script via the entrypoint directive. The container closes after the entrypoint script is finished. I need to get the exit code from the script in order to do some logging if the script fails. Right now I'm thinking of something like this
docker run container/myContainer:latest
if [ $? != 0 ];
then
do some stuff
fi
Is this proper way to achieve this? Specifically, will this be the exit code of docker run or of my entrypoint script?

Yes, the docker container run exit code is the exit code from your entrypoint/cmd:
$ docker container run busybox /bin/sh -c "exit 5"
$ echo $?
5
You may also inspect the state of an exited container:
$ docker container inspect --format '{{.State.ExitCode}}' \
$(docker container ls -lq)
5

Checking the value of $? is not needed if you just want to act upon the exit status of the previous command.
if docker run container/myContainer:latest; then
do_stuff
fi
The above example will run/execute do_stuff if the exit status of docker run is zero which is a success.
You can add an else and elif clause in that
Or if you want to negate the exit status of the command.
if ! docker run container/myContainer:latest; then
do_stuff
fi
The above example will run do_stuff if the exit status of docker run is anything but zero, e.g. 1 and going up, since the ! negates.
If the command has some output and if does not have a silent/quite flag/option you can redirect it to /dev/null
if docker run container/myContainer:latest >/dev/null; then
do_stuff
fi
Should not output anything to stdout
see help test | grep -- '^[[:blank:]]*!'
In some cases if some output is still showing then that might be stderr which you can silent with >/dev/null 2>&1 instead of just >/dev/null

Related

bash run command without exiting on error and tell me its exit code

From a bash script I want to run a command which might fail, store its exit code in a variable, and run a subsequent command regardless of that exit code.
Examples of what I'm trying to avoid:
Using set:
set +e # disable exit on error (it was explicitly enabled earlier)
docker exec $CONTAINER_NAME npm test
test_exit_code=$? # remember exit code of previous command
set -e # enable exit on error
echo "copying unit test result file to host"
docker cp $CONTAINER_NAME:/home/test/test-results.xml .
exit $test_exit_code
Using if:
if docker exec $CONTAINER_NAME npm test ; then
test_exit_code=$?
else
test_exit_code=$?
fi
echo "copying unit test result file to host"
docker cp $CONTAINER_NAME:/home/test/test-results.xml .
exit $test_exit_code
Is there a semantically straightforward way to tell bash "run command without exiting on error, and tell me its exit code"?
The best alternative I have is still confusing and requires comments to explain to subsequent developers (it's just a terser if/else):
docker exec $CONTAINER_NAME npm test && test_exit_code=$? || test_exit_code=$?
echo "copying unit test result file to host"
docker cp $CONTAINER_NAME:/home/test/test-results.xml .
exit $test_exit_code
I believe you could just use the || operator? Which is equivalent to an "if − else" command.
Would the following address your use case? (otherwise feel free to comment!)
set -e # implied in a CI context
exit_status=0
docker exec "$CONTAINER_NAME" npm test || exit_status=$?
docker cp "$CONTAINER_NAME:/home/test/test-results.xml" .
exit "$exit_status"
or more briefly:
set -e # implied in a CI context
docker exec "$CONTAINER_NAME" npm test || exit_status=$?
docker cp "$CONTAINER_NAME:/home/test/test-results.xml" .
exit "${exit_status:-0}"
As an aside, if you are not interested in this exit status code, you can also do something like this:
set -e # implied in a CI context
docker exec "$CONTAINER_NAME" npm test || :
docker cp "$CONTAINER_NAME:/home/test/test-results.xml" .
For more details on the || : tip, see e.g. this answer on Unix-&-Linux SE:
Which is more idiomatic in a bash script: || true or || :?
Very simply save the return-code if command failed:
#!/usr/bin/env sh
# Implied by CI
set -e
# Initialise exit return code
rc=0
# Run command or save its error return code if it fail
docker exec "$CONTAINER_NAME" npm test || rc="$?"
printf '%s\n' "copying unit test result file to host"
# Run other command regardless if first one failed
docker cp "$CONTAINER_NAME:/home/test/test-results.xml" .
# Exit with the return code of the first command
exit "$rc"
You could use a kind of try catch, to get the exit code and use a simple switch case to run another commands depending on the error exit code:
(
exit 2
#here your command which might fail
)
exit_code=$?
case "$exit_code" in
0) echo "Success execution"
#do something
;;
1) echo "Error type 1"
#do something
;;
2) echo "Error type 2"
#do something
;;
*) echo "Unknown error type: $exit_code"
;;
esac

Linux script run with run-this-one doesn't work with docker

I'm experiencing an issue in which I run a command in a cronjob and want to make sure that it's not already being executed. I achieve that running as run-one [command] (man-page).
If I want to cancel the already running command and force the new command to run, I run as run-this-one [command].
At least this is what I expected, but if the command runs a docker container, the other process seems to be terminated (but isn't), the terminal shows Terminated, but continues to show the command output that is running in the container (but the commands after the container ends running are not executed). In this case, the command that runs run-this-one is not executed (not expected).
Example:
/path/to/file.sh
#!/bin/bash
set -eou pipefail
echo "sleep started..." >&2
docker run --rm alpine /bin/sh -c 'echo "sleep started inside..." && sleep 5 && echo "sleep ended inside..."'
echo "sleep ended..." >&2
If I run in a terminal window sudo run-one /path/to/file.sh, and then run in another terminal (before the previous command ends running) the command sudo run-one /path/to/file.sh, this command is not executed, as expected, and that command ends succesfully.
Terminal1:
user#host:/path$ sudo run-one /path/to/file.sh
sleep started...
sleep started inside...
sleep ended inside...
sleep ended...
user#host:/path$
Terminal2:
user#host:/path$ sudo run-one /path/to/file.sh
user#host:/path$
But if I run in a terminal window sudo run-one /path/to/file.sh, and then run in another terminal (before the previous command ends running) the command sudo run-this-one /path/to/file.sh, this command is not executed, which is not expected, and that command shows in the terminal Terminated, with the terminal showing user#host:/path$, but the output in the container still shows (the command is still running in the container created in the 1st terminal).
Terminal1:
user#host:/path$ sudo run-one /path/to/file.sh
sleep started...
sleep started inside...
Terminated
user#host:/path$ sleep ended inside...
# terminal doesn't show new input from the keyboard, but I can run commands after
Terminal2:
user#host:/path$ sudo run-this-one /path/to/file.sh
user#host:/path$
It works if the file is changed to:
/path/to/file.sh
#!/bin/bash
set -eou pipefail
echo "sleep started..." >&2
sleep 5
echo "sleep ended..." >&2
The above script file with docker was just an example, in my case it's different, but the problem is the same, and occurs independently of running the container with or without -it.
Someone knows why this is happening? Is there a (not very complex and not very hackish) solution to this problem? I've executed the above commands in Ubuntu 20.04 inside a VirtualBox machine (with vagrant).
Update (2021-07-15)
Based on #ErikMD comment and #DannyB answer, I put a trap and a cleanup function to remove the container, as can be seen in the script below:
/path/to/test
#!/bin/bash
set -eou pipefail
trap 'echo "[error] ${BASH_SOURCE[0]}:$LINENO" >&2; exit 3;' ERR
RED='\033[0;31m'
NC='\033[0m' # No Color
function error {
msg="$(date '+%F %T') - ${BASH_SOURCE[0]}:${BASH_LINENO[0]}: ${*}"
>&2 echo -e "${RED}${msg}${NC}"
exit 2
}
file="${BASH_SOURCE[0]}"
command="${1:-}"
if [ -z "$command" ]; then
error "[error] no command entered"
fi
shift;
case "$command" in
"cmd1")
function cleanup {
echo "cleaning $command..."
sudo docker rm --force "test-container"
}
trap 'cleanup; exit 4;' ERR
args=( "$file" "cmd:unique" )
echo "$command: run-one ${args[*]}" >&2
run-one "${args[#]}"
;;
"cmd2")
function cleanup {
echo "cleaning $command..."
sudo docker rm --force "test-container"
}
trap 'cleanup; exit 4;' ERR
args=( "$file" "cmd:unique" )
echo "$command: run-this-one ${args[*]}" >&2
run-this-one "${args[#]}"
;;
"cmd:unique")
"$file" "cmd:container"
;;
"cmd:container")
echo "sleep started..." >&2
sudo docker run --rm --name "test-container" alpine \
/bin/sh -c 'echo "sleep started inside..." && sleep 5 && echo "sleep ended inside..."'
echo "sleep ended..." >&2
;;
*)
echo -e "${RED}[error] invalid command: $command${NC}"
exit 1
;;
esac
If I run /path/to/test cmd1 (run-one) and /path/to/test cmd2 (run-this-one) in another terminal, it works as expected (the cmd1 process is stopped and removes the container, and the cmd2 process runs successfully).
If I run /path/to/test cmd2 in 2 terminals, it also works as expected (the 1st cmd2 process is stopped and removes the container, and the 2nd cmd2 process runs successfully).
But not so good: in the 2 cases above, sometimes the 2nd process stops with an error before the 1st removes the container (this can occur intermittently, probably due to a race condition).
And it gets worse: if I run /path/to/test cmd1 in 2 terminals, both commands fail, although the 1st cmd1 should run successfully (it fails because the 2nd cmd1 removes the container in the cleanup).
I tried to put the cleanup in the cmd:unique command instead (removing from the other 2 places), so as to call only by the single process running, to avoid the problem above, but weirdly the cleanup is not called there, even if the trap is also defined there.
Just to simplify your question, I would use this command to reproduce the problem:
run-one docker run --rm -it alpine sleep 10
As can be seen - either with run-one and run-this-one - the behavior is definitely not the desired one.
Since the command creates a process managed by docker, I suspect that the run-one set of tools is not the right tool for the job, since docker containers should not be killed with pkill, but rather with docker kill.
One relatively easy solution, is to embrace the way docker wants you to kill containers, and create your short run-one scripts that handle docker properly.
run-one-docker.sh
#!/usr/bin/env bash
if [[ "$#" -lt 2 ]]; then
echo "Usage: ./run-one-docker.sh NAME COMMAND"
echo "Example: ./run-one-docker.sh temp alpine sleep 10"
exit 1
fi
name="$1"
command=("${#:2}")
container_is_running() {
[ "$( docker container inspect -f '{{.State.Running}}' "$1" 2> /dev/null)" == "true" ]
}
if container_is_running "$name"; then
echo "$name is already running, aborting"
exit 1
else
docker run --rm -it --name "$name" "${command[#]}"
fi
run-this-one-docker.sh
#!/usr/bin/env bash
if [[ "$#" -lt 2 ]]; then
echo "Usage: ./run-this-one-docker.sh NAME COMMAND"
echo "Example: ./run-this-one-docker.sh temp alpine sleep 10"
exit 1
fi
name="$1"
command=("${#:2}")
container_is_running() {
[ "$( docker container inspect -f '{{.State.Running}}' "$1" 2> /dev/null)" == "true" ]
}
if container_is_running "$name"; then
echo "killing old $name"
docker kill "$name" > /dev/null
fi
docker run --rm -it --name "$name" "${command[#]}"

exec as a pipeline component

For our application running inside a container it is preferable that it receives a SIGTERM when the container is being (gracefully) shutdown. At the same time, we want it's output to go to a log file.
In the startscript of our docker container, we had therefore been using bash's exec similar to this
exec command someParam >> stdout.log
That worked just fine, command replaced the shell that had been the container's root process and would receive the SIGTERM.
Since the application tends to log a lot, we decided to add log rotation by using Apache's rotatelogs tool, i.e.
exec command | rotatelogs -n 10 stdout.log 10M
Alas, it seems that by using the pipe, exec can no longer have command replace the shell. When looking at the processes in the running container with pstree -p, it now looks like this
mycontainer#/#pstree -p
start.sh(1)-+-command(118)
`-rotatelogs(119)
So bash remains the root process, and does not pass the SIGTERM on to command.
Before stumbling upon exec, I had found an approach that installs a signal handler into the bash script, which would then itself send a SIGTERM to the command process using kill. However, this became really convoluted, getting the PID was also not always straightforward, and I would like to preserve the convenience of exec when it comes to signal handling and get piping for log rotation.
Any idea how to accomplish this?
Perhaps you want
exec sh -c 'command | rotatelogs -n 10 stdout.log 10M'
I was able to get around this by using process substitution. For your specific case the following may work.
exec command > >(rotatelogs -n 10 stdout.log 10M)
To reproduce the scenario I built this simple Dockerfile
FROM perl
SHELL ["/bin/bash", "-c"]
# The following will gracefully terminate upon docker stop
CMD exec perl -e '$SIG{TERM} = sub { $|++; print "Caught a sigterm!\n"; sleep(5); die "is the end!" }; sleep(30);' 2>&1 > >(tee /my_log)
# The following won't gracefully terminate upon docker stop
#CMD exec perl -e '$SIG{TERM} = sub { $|++; print "Caught a sigterm!\n"; sleep(5); die "is the end!" }; sleep(30);' 2>&1 | tee /my_log
Build docker build -f Dockerfile.meu -t test .
Run docker run --name test --rm -ti test
Stop it docker stop test
Output:
Caught a sigterm!
is the end! at -e line 1.

Starting multiple services using shell script in Dockerfile

I am creating a Dockerfile to install and start the WebLogic 12c services using startup scripts at "docker run" command. I am passing the shell script in the CMD instruction which executes the startWeblogic.sh and startNodeManager.sh script. But when I logged in to the container, it has started only the first script startWeblogic.sh and not even started the second script which is obvious from the docker logs.
The same script executed inside the container manually and it is starting both the services. What is the right instruction for running the script to start multiple processes in a container and not to exit the container?
What am I missing in this script and in the dockerfile? I know that container can run only one process, but in a dirty way, how to start multiple services for an application like WebLogic which has a nameserver, node manager, managed server and creating managed domains and machines. The managed server can only be started when WebLogic nameserver is running.
Script: startscript.sh
#!/bin/bash
# Start the first process
/u01/app/oracle/product/wls122100/domains/verdomain/bin/startWebLogic.sh -D
status=$?
if [ $status -ne 0 ]; then
echo "Failed to start my_first_process: $status"
exit $status
fi
# Start the second process
/u01/app/oracle/product/wls122100/domains/verdomain/bin/startNodeManager.sh -D
status=$?
if [ $status -ne 0 ]; then
echo "Failed to start my_second_process: $status"
exit $status
fi
while sleep 60; do
ps aux |grep "Name=adminserver" |grep -q -v grep
PROCESS_1_STATUS=$?
ps aux |grep node |grep -q -v grep
PROCESS_2_STATUS=$?
# If the greps above find anything, they exit with 0 status
# If they are not both 0, then something is wrong
if [ $PROCESS_1_STATUS -ne 0 -o $PROCESS_2_STATUS -ne 0 ]; then
echo "One of the processes has already exited."
exit 1
fi
done
Truncated the dockerfile.
RUN unzip $WLS_PKG
RUN $JAVA_HOME/bin/java -Xmx1024m -jar /u01/app/oracle/$WLS_JAR -silent -responseFile /u01/app/oracle/wls.rsp -invPtrLoc /u01/app/oracle/oraInst.loc > install.log
RUN rm -f $WLS_PKG
RUN . $WLS_HOME/server/bin/setWLSEnv.sh && java weblogic.version
RUN java weblogic.WLST -skipWLSModuleScanning create_basedomain.py
WORKDIR /u01/app/oracle
CMD ./startscript.sh
docker build and run commands:
docker build -f Dockerfile-weblogic --tag="weblogic12c:startweb" /var/dprojects
docker rund -d -it weblogic12c:startweb
docker exec -it 6313c4caccd3 bash
Please use supervisord for running multiple services in a docker container. It will make the whole process more robust and reliable.
Run supervisord -n as your CMD command and configure all your services in /etc/supervisord.conf.
Sample conf would look like:
[program:WebLogic]
command=/u01/app/oracle/product/wls122100/domains/verdomain/bin/startWebLogic.sh -D
stderr_logfile = /var/log/supervisord/WebLogic-stderr.log
stdout_logfile = /var/log/supervisord/WebLogic-stdout.log
autorestart=unexpected
[program:NodeManager]
command=/u01/app/oracle/product/wls122100/domains/verdomain/bin/startNodeManager.sh -D
stderr_logfile = /var/log/supervisord/NodeManager-stderr.log
stdout_logfile = /var/log/supervisord/NodeManager-stdout.log
autorestart=unexpected
It will handle all the things you are trying to do with a shell script.
Hope it helps!

TravisCI build succeeds even when tests fail

This is where I am running my tests in travis.yml:
# Run tests
script:
# Test application in Docker container
- ./tools/docker-test.sh
The shell script docker-test.sh looks like this:
#!/usr/bin/env bash
githash="$(git rev-parse --short HEAD)"
echo "-------------------------------------------------"
echo "| Running unit tests |"
echo "-------------------------------------------------"
docker create -it --name test eu.gcr.io/test/test:$githash
docker start test
docker exec test /bin/sh -c "go test ./..."
docker stop test
docker rm -fv test
The TravisCI build is a success even if the tests fail.
How can I get TravisCI to know if test have failed or not? I don't know if this is a problem with errors not being propagated from Docker, errors not being propagated from the shell script or TravisCI not knowing when go tests succeed or fail.
Your script is exiting with the status code of the last command docker rm -fv test.
You need to capture the status code of the test's, then clean up docker, then exit.
This code example is from a slightly different question over here but it's the same solution.
#!/usr/bin/env bash
set -e
# Set a default return code
RC=2
# Cleanup
function cleanup {
echo "Removing container"
docker stop test || true
docker rm -f test || true
exit $RC
}
trap cleanup EXIT
# Test steps
docker create -it --name test path
docker start test
docker exec test /bin/sh -c "go test ./..."
RC=$?

Resources