Karsten Morks
Karsten Morks

Reputation: 185

Run an arbitrary command in a docker container that runs on a remote host after sourcing some environment variables from another command

To show what I am trying to do, this is part of the bash script I have so far:

COMMAND="${@:1}"
CONTAINER_DOCKER_NAME=this-value-is-computed-prior
MY_IP=this-ip-is-computed-prior

ssh user@$MY_IP -t 'bash -c "docker exec -it $( docker ps -a -q -f name='$CONTAINER_DOCKER_NAME' | head -n 1 ) /bin/sh -c "eval $(echo export FOO=$BAR) && $COMMAND""'

So let's break down the long command:
I am ssh-ing into a host where I run a bash which fetches the correct container with docker ps and then I do docker exec to run a shell in the container to load some environment variables that my $COMMAND needs to work. Important to note is that $BAR should be the value of the BAR variable inside the container.
So thats what I'm trying to accomplish in theory. However when running this no matter how I set the braces, quotes or escape characters - I always run into problems, either the shell syntax is not correct or it does not run the correct command (especially when the command has multiple arguments) or it loads $BAR value from my local desktop or the remote host but not from the container.

Is this even possible at all with a single shell one-liner?

Upvotes: 0

Views: 690

Answers (2)

Karsten Morks
Karsten Morks

Reputation: 185

thanks to larsks great explanation I got it working, my final one-liner is:

ssh -i $ECS_SSH_KEY ec2-user@$EC2_IP -t "bash -c \"docker exec -it \$( docker ps -a -q -f name=$CONTAINER_DOCKER_NAME | head -n 1 ) /bin/sh -c \\\"eval \\\\\\\$(AWS_ENV_PATH=/\\\\\\\$ENVIRONMENT /bin/aws-env) && $COMMAND\\\"\""

so basically you wrap everything in double quotes and then also use double quotes inside of it because we need some variables ,like $DOCKER_CONTAINER_NAME from the host. to escape the quotes and the $ sign you use \ .
but because we have multiple levels of shells (host, server, container) we also need to use multiple levels of escaping. so the first level is just \$ which will protect that the variable (or the shell command, like docker ps) is not run on the host but on the server.
then the next level of escaping is 7 times \ . every \ escapes the character to the right so in the end it is \\\$ on the second level (server) and \$ on the third level (container). this ensures that the variable is evaluated in the container not on the server. same principle with the double quotes. Everything between \" is run on the second level and everything between \\\" is run on the third level.

Upvotes: 0

larsks
larsks

Reputation: 312858

I think we can simplify your command quite a bit.

First, there's no need to use eval here, and you don't need the && operator, either:

/bin/sh -c "eval $(echo export FOO=$BAR) && $COMMAND"

Instead:

/bin/sh -c "FOO=$BAR $COMMAND"

That sets the environment variable FOO for the duration of $COMMAND.

Next, you don't need this complex docker ps expression:

docker ps -a -q -f name="$CONTAINER_DOCKER_NAME"

Docker container names are unique. If you have a container name stored in $CONTAINER_DOCKER_NAME, you can just run:

docker exec -it $CONTAINER_DOCKER_NAME ...

This simplifies the docker command down to:

docker exec -it $CONTAINER_DOCKER_NAME \
  /bin/sh -c "FOO=\$BAR $COMMAND"

Note how we're escaping the $ in $BAR there, because we want that interpreted inside the container, rather than by our current shell. Now we just need to arrange to run this via ssh. There are a couple of solutions to that. We can just make sure to protect everything on the command line against the extra level of shell expansion, like this:

ssh user@$MY_IP "docker exec -it $CONTAINER_DOCKER_NAME \
  /bin/sh -c \"FOO=\\\$BAR $COMMAND\""

We need to wrap the entire command in double quotes, which means we need to escape any quotes inside the command (we can't use single quotes because we actually want to expand the variable $CONTAINER_DOCKER_NAME locally). We're going to lose one level of \ expansion, so our \$BAR becomes \\\$BAR.

If your command isn't interactive, you can make this a little less hairy by piping the script to bash rather than including it on the command line, like this:

ssh user@$MY_IP docker exec -i $CONTAINER_DOCKER_NAME /bin/sh <<EOF
  FOO=\$BAR $COMMAND
EOF

That simplifies the quoting and escaping necessary to get things passed through to the container shell.

Upvotes: 3

Related Questions