Kubernetes: using bash variable expansion in container entrypoint - bash

According to the [documentation][1] Kubernetes variables are expanded using the previous defined environment variables in the container using the syntax $(VAR_NAME). The variable can be used in the container's entrypoint.
For example:
env:
- name: MESSAGE
value: "hello world"
command: ["/bin/echo"]
args: ["$(MESSAGE)"]
Is this possible though to use bash expansion aka ${Var1:-${Var2}} inside the container's entrypoint for the kubernetes environment variables E.g.
env:
- name: Var1
value: "hello world"
- name: Var2
value: "no hello"
command: ['bash', '-c', "echo ${Var1:-$Var2}"]

Is this possible though to use bash expansion aka ${Var1:-${Var2}} inside the container's entrypoint ?
Yes, by using
command:
- /bin/bash
- "-c"
- "echo ${Var1:-${Var2}}"
but not otherwise -- kubernetes is not a wrapper for bash, it use the Linux exec system call to launch programs inside the container, and so the only way to get bash behavior is to launch bash
That's also why they chose $() syntax for their environment interpolation so it would be different from the ${} style that a shell would use, although this question comes up so much that one might wish they had not gone with $ anything to avoid further confusing folks

Related

Need input with passing commands in Kubernetes containers

What is the difference in below three declarations for passing command/arguments:
containers:
name: busybox
image: busybox
args:
-sleep
-"1000"
containers:
name: busybox
image: busybox
command: ["/bin/sh", "-c", "sleep 1000"]
containers:
name: busybox
image: busybox
args:
-sleep
-"1000"
A. Would these produce same result?
B. What is the preference or usage for each?
The YAML list definition are only a matter of taste, it's just a YAML syntax. This two examples are equivalent:
listOne:
- item1
- item2
listTwo: ['item1', 'item2']
And this syntax works for both args and command. Beside that args and command are slight different, as the documentation says:
If you do not supply command or args for a Container, the defaults
defined in the Docker image are used
If you supply a command but no args for a Container, only the supplied command is used. The default EntryPoint and the default Cmd defined in the Docker image are ignored.
If you supply only args for a Container, the default Entrypoint defined in the Docker image is run with the args that you supplied.
If you supply a command and args, the default Entrypoint and the default Cmd defined in the Docker image are ignored. Your command is run with your args.
Imagine a container like mysql, if you look it's Dockerfile you'll notice this:
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["mysqld"]
The entrypoint call a script that prepare everything the database needs, when finish, this script calls exec "$#" and the shell variable $# are everything defined in cmd.
So, on Kubernetes, if you want to pass arguments to mysqld you do something like:
image: mysql
args:
- mysqld
- --skip-grant-tables
# or args: ["mysqld", "--skip-grant-tables"]
This still executes the entrypoint but now, the value of $# is mysqld --skip-grant-tables.

Setting environment variable for docker container at runtime

I have a requirement to set the environment variable for docker container at runtime. The value can only be determined at runtime by combination two other variables which are available at runtime. In a simpler form, the following is not working.
docker run -ti -e host="name" -e port="123" centos:7 bash -c "export url=$host:$port; env"
returns the following where url is empty when the value of $host and $port is available to construct $url ?
...
host=name
url=:
port=123
...
You have to single quote the shell command to avoid your interactive shell expanding the variable before the docker shell sees them:
docker run -ti -e host="name" -e port="123" centos:7 bash -c 'export url=$host:$port; env'
With single quotes, your shell will pass export url=$host:$port; env to Docker.
With double quotes, your current shell will first dutifully expand the variables, and therefore just pass url=:; env

Scriptable args in docker-compose file

In my docker-compose file (docker-compose.yaml), I would like to set an argument based on a small shell script like this:
services:
backend:
[...]
build:
[...]
args:
PKG_NAME: $(dpkg -l <my_package>)
In the Dockerfile, I read this argument like this:
ARG PKG_NAME
First of all: I know that this approach is OS-dependent (requires dpkg), but for starters I would be happy to make it run on Debian. Also, it's fine it the value is an empty string.
However, docker-compose up throws this error:
ERROR: Invalid interpolation format for "build" option in service "backend": "$(dpkg -l <my_package>)"
Is there a way to dynamically specify an argument in the docker-compose file through a shell script (or another way)?
You can only use variable substitution as described in compose file documentation
You are trying to inject a shell construct and this is not supported.
The documentation has several examples on how to pass vars to compose file. In your case, you could:
export the var in your environment:
export MY_PACKAGE=$(dpkg -l <my_package>)
use that var in your compose file with default:
args:
PKG_NAME: "${MY_PACKAGE:-some_default_pkg}"

Unable to pass variables to run docker container [duplicate]

If I set an environment variable, say ENV ADDRESSEE=world, and I want to use it in the entry point script concatenated into a fixed string like:
ENTRYPOINT ["./greeting", "--message", "Hello, world!"]
with world being the value of the environment varible, how do I do it? I tried using "Hello, $ADDRESSEE" but that doesn't seem to work, as it takes the $ADDRESSEE literally.
You're using the exec form of ENTRYPOINT. Unlike the shell form, the exec form does not invoke a command shell. This means that normal shell processing does not happen. For example, ENTRYPOINT [ "echo", "$HOME" ] will not do variable substitution on $HOME. If you want shell processing then either use the shell form or execute a shell directly, for example: ENTRYPOINT [ "sh", "-c", "echo $HOME" ].
When using the exec form and executing a shell directly, as in the case for the shell form, it is the shell that is doing the environment variable expansion, not docker.(from Dockerfile reference)
In your case, I would use shell form
ENTRYPOINT ./greeting --message "Hello, $ADDRESSEE\!"
After much pain, and great assistance from #vitr et al above, i decided to try
standard bash substitution
shell form of ENTRYPOINT (great tip from above)
and that worked.
ENV LISTEN_PORT=""
ENTRYPOINT java -cp "app:app/lib/*" hello.Application --server.port=${LISTEN_PORT:-80}
e.g.
docker run --rm -p 8080:8080 -d --env LISTEN_PORT=8080 my-image
and
docker run --rm -p 8080:80 -d my-image
both set the port correctly in my container
Refs
see https://www.cyberciti.biz/tips/bash-shell-parameter-substitution-2.html
I tried to resolve with the suggested answer and still ran into some issues...
This was a solution to my problem:
ARG APP_EXE="AppName.exe"
ENV _EXE=${APP_EXE}
# Build a shell script because the ENTRYPOINT command doesn't like using ENV
RUN echo "#!/bin/bash \n mono ${_EXE}" > ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
# Run the generated shell script.
ENTRYPOINT ["./entrypoint.sh"]
Specifically targeting your problem:
RUN echo "#!/bin/bash \n ./greeting --message ${ADDRESSEE}" > ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]
I SOLVED THIS VERY SIMPLY!
IMPORTANT: The variable which you wish to use in the ENTRYPOINT MUST be ENV type (and not ARG type).
EXAMPLE #1:
ARG APP_NAME=app.jar # $APP_NAME can be ARG or ENV type.
ENV APP_PATH=app-directory/$APP_NAME # $APP_PATH must be ENV type.
ENTRYPOINT java -jar $APP_PATH
This will result with executing:
java -jar app-directory/app.jar
EXAMPLE #2 (YOUR QUESTION):
ARG ADDRESSEE="world" # $ADDRESSEE can be ARG or ENV type.
ENV MESSAGE="Hello, $ADDRESSEE!" # $MESSAGE must be ENV type.
ENTRYPOINT ./greeting --message $MESSAGE
This will result with executing:
./greeting --message Hello, world!
Please verify to be sure, whether you need quotation-marks "" when assigning string variables.
MY TIP: Use ENV instead of ARG whenever possible to avoid confusion on your part or the SHELL side.
For me, I wanted to store the name of the script in a variable and still use the exec form.
Note: Make sure, the variable you are trying to use is declared an environment variable either from the commandline or via the ENV directive.
Initially I did something like:
ENTRYPOINT [ "${BASE_FOLDER}/scripts/entrypoint.sh" ]
But obviously this didn't work because we are using the shell form and the first program listed needs to be an executable on the PATH. So to fix this, this is what I ended up doing:
ENTRYPOINT [ "/bin/bash", "-c", "exec ${BASE_FOLDER}/scripts/entrypoint.sh \"${#}\"", "--" ]
Note the double quotes are required
What this does is to allow us to take whatever extra args were passed to /bin/bash, and supply those same arguments to our script after the name has been resolved by bash.
man 7 bash
-- A -- signals the end of options and disables further
option processing. Any arguments after the -- are treated
as filenames and arguments. An argument of - is
equivalent to --.
In my case worked this way: (for Spring boot app in docker)
ENTRYPOINT java -DidMachine=${IDMACHINE} -jar my-app-name
and passing the params on docker run
docker run --env IDMACHINE=Idmachine -p 8383:8383 my-app-name
I solved the problem using a variation on "create a custom script" approach above. Like this:
FROM hairyhenderson/figlet
ENV GREETING="Hello"
RUN printf '#!/bin/sh\nfiglet -W \${GREETING} \$#\n' > /runme && chmod +x /runme
ENTRYPOINT ["/runme"]
CMD ["World"]
Run like
docker container run -it --rm -e GREETING="G'Day" dockerfornovices/figlet-greeter Alec
If someone wants to pass an ARG or ENV variable to exec form of ENTRYPOINT then a temp file created during image building process might be used.
In my case I had to start the app differently depending on whether the .NET app has been published as self-contained or not.
What I did is I created the temp file and I used its name in the if statement of my bash script.
Part of my dockerfile:
ARG SELF_CONTAINED=true #ENV SELF_CONTAINED=true also works
# File has to be used as a variable as it's impossible to pass variable do ENTRYPOINT using Exec form. File name allows to check whether app is self-contained
RUN touch ${SELF_CONTAINED}.txt
COPY run-dotnet-app.sh .
ENTRYPOINT ["./run-dotnet-app.sh", "MyApp" ]
run-dotnet-app.sh:
#!/bin/sh
FILENAME=$1
if [ -f "true.txt" ]; then
./"${FILENAME}"
else
dotnet "${FILENAME}".dll
fi
Here is what worked for me:
ENTRYPOINT [ "/bin/bash", "-c", "source ~/.bashrc && ./entrypoint.sh ${#}", "--" ]
Now you can supply whatever arguments to the docker run command and still read all environment variables.

"docker run" command to evaluate bash $variable inside the container

How can I run a command inside a docker container, using docker run, where bash variables are evaluated inside the container?
E.g.:
$ SOMEONE=host
$ docker run --env SOMEONE=busybox busybox echo "Hello $SOMEONE"
Hello host
How can I make it output Hello busybox?
To prevent the replacement from happening from the outer shell, one needs to use single quotes, not double.
To ensure that there is an inner shell that can do a replacement (echo doesn't have any such functionality itself!), we need to explicitly call sh -c; otherwise, Docker will just directly invoke execlp("echo", "echo", "$SOMEONE", NUL) inside the container, which doesn't actually do any substitution.
Thus:
docker run --env SOMEONE=busybox busybox sh -c 'echo "Hello $SOMEONE"'
Using docker run, where bash variables are evaluated inside
By far the easiest, non-cryptic approach is to write a bash function with all commands to be executed inside the container. Benefits:
Easy to write - no need to use special quote placement and escaping
Easy to debug - see what bash actually does inside the container
Easy to maintain - write readable scripts, not cryptic commands
Easy to write and maintain
Here's an example bash function that expands all variables inside a docker container.
-- (host) $ ./create-db.sh
#!/bin/bash
function main_inside_docker {
# all variables are expanded insider docker
DBNAME=${1:-testdb}
echo "creating database $DBNAME"
PATH=$MSSQL_PATH:$PATH
SQL="
create database $DBNAME;
select database_id, name, create_date from sys.databases;
"
sqlcmd -U SA -P $SA_PASSWORD -Q "$SQL"
}
# declare the function inside docker and run it there
CMD="$(declare -f main_inside_docker); main_inside_docker $#"
docker exec -it mssql bash -c "$CMD"
Essentially this declares the main_inside_docker function inside the container, then runs it with all arguments provided from the host invocation. All variables inside the function are expanded inside the docker container. The function just works the way one would expect.
Easy to debug
To debug the function, set "-x" as the first command in $CMD:
CMD="set -x; $(declare -f ...)"
When running it this way, it will print the bash trace from inside the container nicely:
(host) $ ./create-db.sh foodb
+ main_inside_docker
+ DBNAME=foodb
+ echo 'creating database foodb'
creating database testdb
...

Resources