Run bash commands inside container with alias - linux

We have docker contianers, and i would like to run a bash command inside my container, through a script. Like so:
bin/run-in-container ls -la
where the script run-in-container looks something like this:
#!/usr/bin/env bash
docker compose exec container_name bash -ic "$#"
I cannot get my script to interpret all parameters, inside single quotes.
this interprets to
docker compose exec api bash -ic ls -la
but what I want to to interpret is
docker compose exec api bash -ic 'ls -la'
If i try to concatenate a string of single quotes and my parameters, it renders the character escaped:
#!/usr/bin/env bash
escaped_single_qoute="'"
docker compose exec container_name bash -ic $escaped_single_qoute "$#" $escaped_single_qoute
But this interprets into :
docker compose exec api bash -ic ''\''ls' '-la'\'''
Here is a MCVE: gist with Dockerfile and code
services:
node:
restart: unless-stopped
image: 888aaen/bash_stackoverflow:latest
volumes:
# Mount .bash_aliases file.
- "./.bash_aliases:/home/node/.bash_aliases"
script
#!/usr/bin/env bash
docker compose exec node bash -ic "$#"
UPDATE: not solved
KamilCuk came with a great solution, but this does not run with aliases inside the contianer.
docker compose exec container_name bash -ic '"$#"' _ "$#"
# usage example: ./script ls -la
# usage example: ./script sh -c "ls -la ; echo another command"
Let's say we have these aliases:
# ~/.bash_aliases
alias ll='ls -l'
alias la='ls -la'
I want to be able to run
bin/run-in-container ll
which is possible with
docker compose exec api bash -ic 'la src/'
but not with
bin/run-in-container la src/

If you want to pass a single command, just pass the command:
docker compose exec container_name "$#"
# usage example: ./script ls -la
# usage example: ./script sh -c "ls -la ; echo another command"
If you want to pass a single command with an (odd?) requirement in running the command inside bash interactive shell, you would forward the arguments and execute them inside the shell:
docker compose exec container_name bash -ic '"$#"' _ "$#"
# usage example: ./script ls -la
# usage example: ./script sh -c "ls -la ; echo another command"
If you want to pass a shell script, just like to eval, just concatenate arguments with space:
docker compose exec container_name bash -ic "$*"
# usage example: ./script ls -la
# usage example: ./script ls -la ';' echo another command
# usage example: ./script "$(printf "%q " sh -c "ls -la ; echo another command")"
Let's say we have these aliases:
That's all unorthodox. Aliases are for interactive shells, the 1000 years old advice is to use functions instead of aliases. .bash_aliases is a nonstandard (but common) file, that is not being sourced on bash startup unless explicitly mentioned in .bashrc file for interactive non-login shell. See https://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html .
If your intention is to provide ll command for non-interactive use, instead create an executable in /usr/local/bin named ll that will call ls -l "$#" in it.
Another common convention, is to put functions (not aliases! aliases are for interactive shells) inside /etc/profile.d and then run an login non-interactive shell. People are used to doing sh -l when needing environment over ssh, the convention is somewhat understood by users.

Related

Dockerfile set runtime ENV dinamically by sourcing a script

Basically, I need to keep the functionality of an ubuntu:18.04 image but with some environment variables set every time I execute a docker run or a docker exec this variables are dynamic, so I can't use the keyword ENV in the Dockerfile, I will need to use a script that should be sourced, for simplicity the file I will be using for this post is:
$ cat setenv.sh
#!/usr/bin/env bash
# Set some dynamic variables
export TEST="Hello World"
I have tried different approaches without success, here is my research:
Using an entrypoint
The files I used for this example:
$ cat entrypoint.sh
#!/usr/bin/env bash
echo "Setting environment"
. /setenv.sh
exec $#
$ cat Dockerfile
FROM ubuntu:18.04
COPY setenv.sh /
COPY entrypoint.sh /
ENTRYPOINT [ "/entrypoint.sh" ]
I built this Dockerfile the following command: docker build -f Dockerfile -t test_img .
This works fine except by two problems:
1. exec does not support double ampersand && nor pipes | nor escaping chars \
As I previously stated, I require my container to have the same functionality as the ubuntu image, for example, in ubuntu I can totally execute the following container:
$ docker run --rm ubuntu:18.04 bash -c "echo \"Hello World\" && ls | head -n1 "
Hello World
bin
But if I use the image I created:
$ docker run --rm test_img bash -c "echo \"Hello World\" && ls | head -n1"
Setting environment
It truncates the command every time it finds a quote (doesn't honor the escape character) a double ampersand or a pipe, here is an example of the commands in different order:
$ docker run --rm ubuntu:18.04 bash -c "ls | head -n1 && echo \"Hello World\""
bin
Hello World
$ docker run --rm test_img bash -c "ls | head -n1 && echo \"Hello World\""
Setting environment
bin
boot
dev
entrypoint.sh
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
setenv.sh
srv
sys
tmp
usr
var
In this case, the command truncates when finding the pipe |.
2. Entrypoint is only called for the parent shell.
If I run a ephemeral container I can see that my env variable is there:
$ docker run --rm test_img env | grep TEST
TEST=Hello World
But if I want a keep-alive container, the env var is not set:
$ docker create -ti --name=test test_img bash
e0e5278c46bdcf33195661fac5911326b701586e9a9c638f71a6e08021ee2f57
$ docker start test
test
$ docker exec test env | grep TEST
What is happening here is that the shell I create when running docker create is calling the entrypoint, but the shell I create when running docker exec is a different one.
If you login to the container you can see shells are different:
$ docker exec -ti test bash
root#e0e5278c46bd:/# ps -fe
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 15:21 pts/0 00:00:00 bash
root 15 0 0 15:29 pts/1 00:00:00 bash
root 29 15 0 15:29 pts/1 00:00:00 ps -fe
root#e0e5278c46bd:/# env | grep TEST
If instead of having an entrypoint script to set the environment variable TEST I had used the keyword ENV in my Dockerfile: ENV TEST "Hello World" this would set the variable in every shell created by the commands docker run and docker exec. Here is the example:
$ cat Dockerfile
FROM ubuntu:18.04
ENV TEST "Hello World"
$ docker build -f Dockerfile -t test_img .
Sending build context to Docker daemon 4.096kB
Step 1/2 : FROM ubuntu:18.04
---> 6526a1858e5d
Step 2/2 : ENV TEST "Hello World"
---> Using cache
---> eebe9952bb76
Successfully built eebe9952bb76
Successfully tagged test_img:latest
$ docker create -ti --name=test test_img bash
c1e508dae0f398a40c4c5534cf2811cdfe284a4f6601198f0ca97fdea100c376
$ docker start test
test
$ docker exec test env | grep TEST
TEST=Hello World
$ docker exec -ti test bash
root#c1e508dae0f3:/# env | grep TEST
TEST=Hello World
Sourcing in bashrc
I modify the Dockerfile to look like this, and built the image with the same build command:
$ cat Dockerfile
FROM ubuntu:18.04
COPY setenv.sh /
RUN echo ". /setenv.sh" >> /etc/bash.bashrc
The problem with this approach is the shell used to execute docker run, the bashrc file is not called, only on interactive bash shells, here is the output:
$ docker run --rm test_img echo $SHELL
/bin/bash
$ docker run --rm test_img env | grep TEST
$ docker run --rm test_img bash -c "env" | grep TEST
$ docker run --rm -ti test_img bash
root#1187568e1bec:/# env | grep TEST
TEST=Hello World
First I tried to add the setenv.sh to /etc/profile.d directory, but the problem with this is that /etc/profile is called only for login shells, and I will need to change the commands to explicitly use a login shell, in other words, instead of docker run test_img env I would need it to be docker run test_img bash -lc "env" (The -l is for login).
Create Dockerfile dinamically
This is the best solution so far, but is not the cleaner, I have to have a Dockerfile.pre file to create a container and save the generated variables to a file, then use this file to create a final Dockerfile and write all those ENV lines into the Dockerfile.
Combining two approaches
By using an entrypoint and sourcing in bashrc file I was able to get the variables set in all cases, the problem is the exec $# command that doesn't support full bash scripts. Is any way to modify my entrypoint script? or is there other approach for this problem?
you can create an enviroment file and just pass it to your container with the --env-file flag. This will make all the variables in the file available in the container.
ubuntu#vps-f116ed9f:~$ cat my_env_file
TEST=Hello World
ubuntu#vps-f116ed9f:~$ docker container run -it --rm --env-file my_env_file ubuntu bash -c "echo \$TEST"
Hello World
ubuntu#vps-f116ed9f:~$ docker container run -it --rm --env-file my_env_file ubuntu bash -c "echo \$TEST | wc -c"
12
here you can see i have used the latest ubuntu image, i pass my_env_file to it and then using the bash shell i print the value of this variable (Note i have to escape the $ other wise the shell will interpolate this before passing it to docker, this could be avoided by using single qoutes as the shell wont interpolate variables in single qoutes.)
I also dont see any issues using pipe or &&
ubuntu#vps-f116ed9f:~$ docker container run -it --rm --env-file my_env_file ubuntu bash -c 'ls | head -n1 && echo "$TEST"'
bin
Hello World
This also will persist in detached containers
ubuntu#vps-f116ed9f:~$ docker container run -itd --rm --name=c1 --env-file my_env_file ubuntu bash
3d7705f2f91f3f30c45e855778bd80f08a35616bbe822545c20d5a8886139693
ubuntu#vps-f116ed9f:~$ docker container exec c1 sh -c "ls | head -1 && echo \$TEST"
bin
Hello World

Executing `sh -c` in a bash script

I have a test.sh file which takes as a parameter a bash command, it does some logic, i.e. setting and checking some env vars, and then executes that input command.
#!/bin/bash
#Some other logic here
echo "Run command: $#"
eval "$#"
When I run it, here's the output
% ./test.sh echo "ok"
Run command: echo ok
ok
But the issue is, when I pass something like sh -c 'echo "ok"', I don't get the output.
% ./test.sh sh -c 'echo "ok"'
Run command: sh -c echo "ok"
%
So I tried changing eval with exec, tried to execute $# directly (without eval or exec), even tried to execute it and save the output to a variable, still no use.
Is there any way to run the passed command in this format and get the ourput?
Use case:
The script is used as an entrypoint for the docker container, it receives the parameters from docker CMD and executes those to run the container.
As a quickfix I can remove the sh -c and pass the command without it, but I want to make the script reusable and not to change the commands.
TL;DR:
This is a typical use case (perform some business logic in a Docker entrypoint script before running a compound command, given at command line) and the recommended last line of the script is:
exec "$#"
Details
To further explain this line, some remarks and hyperlinks:
As per the Bash user manual, exec is a POSIX shell builtin that replaces the shell [with the command supplied] without creating a new process.
As a result, using exec like this in a Docker entrypoint context is important because it ensures that the CMD program that is executed will still have PID 1 and can directly handle signals, including that of docker stop (see also that other SO answer: Speed up docker-compose shutdown).
The double quotes ("$#") are also important to avoid word splitting (namely, ensure that each positional argument is passed as is, even if it contains spaces). See e.g.:
#!/usr/bin/env bash
printargs () { for arg; do echo "$arg"; done; }
test0 () {
echo "test0:"
printargs $#
}
test1 () {
echo "test1:"
printargs "$#"
}
test0 /bin/sh -c 'echo "ok"'
echo
test1 /bin/sh -c 'echo "ok"'
test0:
/bin/sh
-c
echo
"ok"
test1:
/bin/sh
-c
echo "ok"
Finally eval is a powerful bash builtin that is (1) unneeded for your use case, (2) and actually not advised to use in general, in particular for security reasons. E.g., if the string argument of eval relies on some user-provided input… For details on this issue, see e.g. https://mywiki.wooledge.org/BashFAQ/048 (which recaps the few situations where one would like to use this builtin, typically, the command eval "$(ssh-agent -s)").

What kind of command is "sudo", "su", or "torify"

I know what they do. I was just wondering what kind of command are they. How can you make one using shell scripting.
For example, command like:
ignoreError ls /Home/
ignoreError mkdir /Home/
ignoreError cat
ignoreError randomcommand
Hope you get the idea
The way to do it in a shell script is with the "$#" construct.
"$#" expands to a quoted list of all of the arguments you passed to your shell script. $1 would be the command you want your shell script to run, and $2 $3 etc are the arguments to that command.
The only example I have is from cygwin. Cygwin does not have sudo, but I have this script that emulates it:
#!/usr/bin/bash
cygstart --action=runas "$#"
So when I run a command like
$ sudo ls -l
my sudo script does whatever it needs to do (cygstart --action=runas) and calls the ls command with the -l argument.
Try this script:
#!/bin/sh
"$#"
Call it, for example, run, make it runnable chmod u+x run, and try it:
$ run ls -l #or ./run ls -l
...
output of ls
...
The idea is that the script takes the parameters specified on the command line and use them as a (sub)command... Modify the script this way:
#!/bin/sh
echo "Trying to run $*"
"$#"
and you will see.

Option -l of exec shell command

Could you please clarify on the use of -l option of exec shell command. I didn't notice any difference when I ran exec ls | cat and exec -l ls | cat.
The -l option of exec adds a - at the beginning of the name of your command. For example:
exec -l diff | head
-diff: missing operand after '-diff'
-diff: Try '-diff --help' for more information.
Note the - everywhere before diff.
The point of all this? If you have a - before a command to start a shell it will act as a login shell. From man bash:
A login shell is one whose first character of argument zero is a -, or one started with the --login option.
Now, man exec states that:
If the -l option is supplied, the shell places a dash at the beginning of the zeroth argument passed to command. This is
what login(1) does.
So exec -l bash will run bash as a login shell. To test this, we can use the fact that a login bash executes the file ~/.bash_profile, so:
$ cat ~/.bash_profile
#!/bin/sh
printf "I am a login shell!\n"
If I start a login bash, the command printf "I am a login shell!\n" will be executed. Now to test with exec:
$ exec bash
$
Nothing is displayed, we are on a non-login shell.
$ exec -l bash
I am a login shell!
$
Here we have a login shell.

How to write a bash script which automate entering "docker container" and doing other things?

I want to implement an automatic bash script which enters a running docker container, and do some stuffs:
# cat docker.sh
#!/bin/bash -x
docker exec -it hammerdb_net8 bash
cd /data/oracle/tablespaces/
pwd
Executing the script on terminal:
# ./docker.sh
+ docker exec -it hammerdb_net8 bash
[root#npar1 /]#
The output shows only login the docker container, but won't do other operations.
Is there any method to automate entering docker container and doing other things?
You can use bash -c:
docker exec -it hammerdb_net8 bash -c 'cd /data/oracle/tablespaces/; pwd; ls'
For running a series of commands use here-doc in BASH:
docker exec -i hammerdb_net8 bash <<'EOF'
cd /data/oracle/tablespaces/
pwd
ls
EOF

Resources