ufoscout / docker-compose-wait

A simple script to wait for other docker images to be started while using docker-compose (or Kubernetes or docker stack or whatever)

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support for docker images that don't have a shell

mapx opened this issue · comments

It's possible that a docker image is based on SCRATCH, and contains nothing but the executable. In that case, the command part of this image should be somthing like:

command: ["/the_executable", "--foo", "bar"]

There won't be && available as well without a shell. Could such a thin image have the favor of wait?

Hi @mapx
honestly, I don't know how to make it work without the && shell support.
My only advice would be to try a minimal docker image like the Busybox one instead of scratch.

Do you have any proposal about it?

@mapx
I am going to close this as there are no actions I can take. The documentation will be updated thanks to #28

When a command chain begins with wait and no strings like &&, the rest part of this command becomes env::args().

If this arguments list is not empty, we know wait might be running with such paramenters. At the end of wait process, it could run the paramenters as a command.

The first item in env::args() would be the "main" command, and the rest be its parameters.

If env::args() is empty, wait exits like what it is now.

Could it work?

Hi @mapx

I am not so knowledgeable on Rust and I am primarily just here to learn.

If I understand this correctly, what you propose is, to let wait handle the scheduling instead of the shell. Eliminating the use of the shell? but keeping backwards compatibility due to the handling of && in Rust.

Meaning wait on shell-less base images, like scratch could be supported, but also supported on base images with shells, with && usage however being optional (at the users convenience).

I report here a proposal of @SamMousa:


It would be nice to support syntax that uses exec instead of a subshell.

The syntax for usage would be simple:

entrypoint: [
    "wait",
    "--",
    "myApp"
]

That way no shell is needed and signal handling becomes easier for the app since there's no longer a shell in between.


I think that what you all are proposing could effectively work; anyway, I don't know how it should be implemented.
When docker starts the wait executable, it assigns a PID to it (first doubt, is it always PID 1? is it the same if there's no shell?); based on your proposal, the wait executable itself should launch the application executable; so, my question is, how?
If it launches a child process, then what happens when the wait process ends? Is the child process killed too? If not, what is the PID of the second process?
Are there issues with docker signal handling if the surviving process does not have PID 1?

commented

There as a system call called exec that loads a new process that replaces the current process.
I think it's this: https://docs.rs/exec/0.3.1/exec/

FYI, I do not know Rust.

Hi @mapx

I am not so knowledgeable on Rust and I am primarily just here to learn.

If I understand this correctly, what you propose is, to let wait handle the scheduling instead of the shell. Eliminating the use of the shell? but keeping backwards compatibility due to the handling of && in Rust.

Meaning wait on shell-less base images, like scratch could be supported, but also supported on base images with shells, with && usage however being optional (at the users convenience).

A parent process which calls wait desides how many parameters wait gets. If the parent process is a shell, it understands &&, cuts off the right part starting from &&, and passes the left part to wait. If the parent process does not know about &&, it passes everything to wait.

In the latter case, it's the user's responsibility not to put && in the middle, unless she/he wants that.

After some investigation, I think it could effectively work as you propose. Anyway, I don't have enough knowledge to implement it in a way that I can guarantee it works. In fact, there are too low-level details that I miss and I would not be able to provide support where needed.
So, I keep the task open with a "PR welcome" tag in case someone is willing to attempt a fix; but, honestly, considering that the BusyBox docker image size is only 750Kb, I don't think it is worth investing much time in this feature.

I work around this issue by copying the busybox sh into the container at a less-common location (to reduce potental exploits). this works in google's distroless images. here's an example multi-stage Dockerfile;

FROM busybox:1.31.1-uclibc as builder
RUN wget -O /wait https://github.com/ufoscout/docker-compose-wait/releases/download/2.7.3/wait &&\
    chmod +x /wait 

...

FROM gcr.io/distroless/java:11 as base
COPY --from=builder /bin/sh /sh
USER nonroot
COPY --from=builder --chown=nonroot:nonroot /wait /wait
...
ENTRYPOINT ["/sh", "-c", "/wait && java -jar application.jar"]

I made it with alpine os image, and alpine itself had /bin/bash.
Also the wait shell script I downloaded and added it to the current source file folder.

I made it with alpine os image, and alpine itself had /bin/bash.
Also the wait shell script I downloaded and added it to the current source file folder.

@jimmyyem Could you share your code solution?

FYI guys, I have a PR up here that fixes this issue. I ran into the issue recently using distroless and did some research and got a fix in place. One thing that I wanted to prevent was having multiple processes running in the container and wait basically having to act like an init system. I've prevented that by making a syscall out to exec so the wait process gets completely replaced by the configured command, whether that's tini or another static binary. For configuration I added a single new env var, WAIT_COMMAND. The arguments to the command to invoke get properly parsed like they would if they were being processed by a shell using the shell_words crate so any command to run should be copy/pasteable from CMD to WAIT_COMMAND. Hope you have a chance to get 👀 on it @ufoscout and let me know your thoughts

SamMuosa proposal (/wait -- command arg1 arg2...), by using exec and replacing the current process, seems the most straightforward. https://github.com/Eficode/wait-for does the same.