Skip to content

Commit

Permalink
Merge pull request #68 from ryantate13/master
Browse files Browse the repository at this point in the history
add support for from scratch and distroless images
  • Loading branch information
ufoscout authored Mar 21, 2023
2 parents 69de418 + 8069b37 commit 92e75a5
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 18 deletions.
72 changes: 70 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
[package]
name = "wait"
version = "2.9.1"
version = "2.10.0"
authors = ["ufoscout <[email protected]>"]
edition = "2018"

[dependencies]
port_check = "0.1"
log = "0.4"
env_logger = { version = "0.10", default-features = false }
exec = "0.3.1"
shell-words = "1.1.0"

[dev-dependencies]
atomic-counter = "1.0"
Expand Down
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ For example, your application "MySuperApp" uses MongoDB, Postgres and MySql (wow
FROM alpine

## Add the wait script to the image
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.9.0/wait /wait
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.10.0/wait /wait
RUN chmod +x /wait

## Add your application to the docker image
Expand Down Expand Up @@ -75,9 +75,23 @@ command: sh -c "/wait && /MySuperApp.sh"

This is discussed further [here](https://stackoverflow.com/questions/30063907/using-docker-compose-how-to-execute-multiple-commands) and [here](https://github.com/docker/compose/issues/2033).

Do note the recommended way of using `wait` is with the shell operator `&&`, which implies the requirement of a shell. This introduces a requirement for Docker use where bases images like [scratch](https://hub.docker.com/_/scratch) not offering a shell cannot be used.
## Usage in images that do not have a shell

Instead the recommendation for base Docker images are ones offering a shell like [alpine](https://hub.docker.com/_/alpine), [debian](https://hub.docker.com/_/debian) etc. and if you want to aim for _minimalism_, evaluate something like: [busybox](https://hub.docker.com/_/busybox)
When using [distroless](https://github.com/GoogleContainerTools/distroless) or building images [`FROM scratch`](https://docs.docker.com/develop/develop-images/baseimages/#create-a-simple-parent-image-using-scratch), it is common to not have `sh` available. In this case, it is necessary to [specify the command for wait to run explicitly](#additional-configuration-options). The invoked command will be invoked with any arguments configured for it and will completely replace the `wait` process in your container via a syscall to [`exec`](https://man7.org/linux/man-pages/man3/exec.3.html). Because there is no shell to expand arguments in this case, `wait` must be the `ENTRYPOINT` for the container and has to be specified in [the exec form](https://docs.docker.com/engine/reference/builder/#exec-form-entrypoint-example). Note that because there is no shell to perform expansion, arguments like `*` must be interpreted by the program that receives them.

```dockerfile
FROM golang
RUN wget -o /wait https://github.com/ufoscout/docker-compose-wait/releases/download/2.10.0/wait
COPY myApp /app
WORKDIR /app
RUN go build -o /myApp -ldflags '-s -w -extldflags -static' ./...
FROM scratch
COPY --from=0 /wait /wait
COPY --from=0 /myApp /myApp
ENV WAIT_COMMAND="/myApp arg1 argN..."
ENTRYPOINT ["/wait"]
```

## Additional configuration options

Expand All @@ -86,6 +100,7 @@ The behaviour of the wait utility can be configured with the following environme
- _WAIT_LOGGER_LEVEL_ : the output logger level. Valid values are: _debug_, _info_, _error_, _off_. the default is _debug_.
- _WAIT_HOSTS_: comma-separated list of pairs host:port for which you want to wait.
- _WAIT_PATHS_: comma-separated list of paths (i.e. files or directories) on the local filesystem for which you want to wait until they exist.
- _WAIT_COMMAND_: command and arguments to run once waiting completes. The invoked command will completely replace the `wait` process. The default is none.
- _WAIT_TIMEOUT_: max number of seconds to wait for all the hosts/paths to be available before failure. The default is 30 seconds.
- _WAIT_HOST_CONNECT_TIMEOUT_: The timeout of a single TCP connection to a remote host before attempting a new connection. The default is 5 seconds.
- _WAIT_BEFORE_: number of seconds to wait (sleep) before start checking for the hosts/paths availability
Expand Down
104 changes: 92 additions & 12 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
use crate::env_reader::env_var_exists;
use env_reader::env_var_exists;
use log::*;
use std::path::Path;
use std::time::Duration;
use std::option::Option;

pub mod env_reader;
pub mod sleeper;

pub struct Command {
pub program: String,
pub argv: Vec<String>,
}

pub struct Config {
pub hosts: String,
pub paths: String,
pub command: Option<Command>,
pub global_timeout: u64,
pub tcp_connection_timeout: u64,
pub wait_before: u64,
Expand All @@ -19,7 +26,7 @@ pub struct Config {
const LINE_SEPARATOR: &str = "--------------------------------------------------------";

pub fn wait(
sleep: &mut dyn crate::sleeper::Sleeper,
sleep: &mut dyn sleeper::Sleeper,
config: &Config,
on_timeout: &mut dyn FnMut(),
) {
Expand All @@ -37,6 +44,12 @@ pub fn wait(
" - TCP connection timeout before retry: {} seconds ",
config.tcp_connection_timeout
);
if config.command.is_some() {
debug!(
" - Command to run once ready: {}",
env_reader::env_var("WAIT_COMMAND", "".to_string())
);
}
debug!(
" - Sleeping time before checking for hosts/paths availability: {} seconds",
config.wait_before
Expand Down Expand Up @@ -116,21 +129,38 @@ pub fn wait(

info!("docker-compose-wait - Everything's fine, the application can now start!");
info!("{}", LINE_SEPARATOR);

if let Some(command) = &config.command {
let err = exec::Command::new(&command.program).args(&command.argv).exec();
panic!("{}", err);
}
}

pub fn parse_command<S: Into<String>>(raw_cmd: S) -> Result<Option<Command>, shell_words::ParseError> {
let s = raw_cmd.into();
let t = s.trim();
if t.len() == 0 {
return Ok(None)
}
let argv = shell_words::split(&t)?;
Ok(Some(Command { program: argv[0].clone(), argv }))
}

pub fn config_from_env() -> Config {
Config {
hosts: crate::env_reader::env_var("WAIT_HOSTS", "".to_string()),
paths: crate::env_reader::env_var("WAIT_PATHS", "".to_string()),
hosts: env_reader::env_var("WAIT_HOSTS", "".to_string()),
paths: env_reader::env_var("WAIT_PATHS", "".to_string()),
command: parse_command(env_reader::env_var("WAIT_COMMAND", "".to_string()))
.expect("failed to parse command value from environment"),
global_timeout: to_int(&legacy_or_new("WAIT_HOSTS_TIMEOUT", "WAIT_TIMEOUT", ""), 30),
tcp_connection_timeout: to_int(
&crate::env_reader::env_var("WAIT_HOST_CONNECT_TIMEOUT", "".to_string()),
&env_reader::env_var("WAIT_HOST_CONNECT_TIMEOUT", "".to_string()),
5,
),
wait_before: to_int(&legacy_or_new("WAIT_BEFORE_HOSTS", "WAIT_BEFORE", ""), 0),
wait_after: to_int(&legacy_or_new("WAIT_AFTER_HOSTS", "WAIT_AFTER", ""), 0),
wait_sleep_interval: to_int(
&crate::env_reader::env_var("WAIT_SLEEP_INTERVAL", "".to_string()),
&env_reader::env_var("WAIT_SLEEP_INTERVAL", "".to_string()),
1,
),
}
Expand All @@ -143,9 +173,9 @@ fn legacy_or_new(legacy_var_name: &str, var_name: &str, default: &str) -> String
"Environment variable [{}] is deprecated. Use [{}] instead.",
legacy_var_name, var_name
);
temp_value = crate::env_reader::env_var(legacy_var_name, temp_value);
temp_value = env_reader::env_var(legacy_var_name, temp_value);
}
temp_value = crate::env_reader::env_var(var_name, temp_value);
temp_value = env_reader::env_var(var_name, temp_value);
temp_value
}

Expand All @@ -158,7 +188,6 @@ fn to_int(number: &str, default: u64) -> u64 {

#[cfg(test)]
mod test {

use super::*;
use lazy_static::*;
use std::env;
Expand Down Expand Up @@ -195,7 +224,7 @@ mod test {
#[test]
fn config_should_use_default_values() {
let _guard = TEST_MUTEX.lock().unwrap();
set_env("", "", "10o", "10", "", "abc");
set_env("", "", "10o", "10", "", "abc", "");
let config = config_from_env();
assert_eq!("".to_string(), config.hosts);
assert_eq!(30, config.global_timeout);
Expand All @@ -207,7 +236,7 @@ mod test {
#[test]
fn should_get_config_values_from_env() {
let _guard = TEST_MUTEX.lock().unwrap();
set_env("localhost:1234", "20", "2", "3", "4", "23");
set_env("localhost:1234", "20", "2", "3", "4", "23", "");
let config = config_from_env();
assert_eq!("localhost:1234".to_string(), config.hosts);
assert_eq!(20, config.global_timeout);
Expand All @@ -220,7 +249,7 @@ mod test {
#[test]
fn should_get_default_config_values() {
let _guard = TEST_MUTEX.lock().unwrap();
set_env("localhost:1234", "", "", "", "", "");
set_env("localhost:1234", "", "", "", "", "", "");
let config = config_from_env();
assert_eq!("localhost:1234".to_string(), config.hosts);
assert_eq!(30, config.global_timeout);
Expand All @@ -230,19 +259,70 @@ mod test {
assert_eq!(1, config.wait_sleep_interval);
}

#[test]
#[should_panic]
fn should_panic_when_given_an_invalid_command(){
let _guard = TEST_MUTEX.lock().unwrap();
set_env("", "", "", "", "", "", "a 'b");
config_from_env();
}

fn set_env(
hosts: &str,
timeout: &str,
before: &str,
after: &str,
sleep: &str,
tcp_timeout: &str,
command: &str,
) {
env::set_var("WAIT_BEFORE_HOSTS", before.to_string());
env::set_var("WAIT_AFTER_HOSTS", after.to_string());
env::set_var("WAIT_HOSTS_TIMEOUT", timeout.to_string());
env::set_var("WAIT_HOST_CONNECT_TIMEOUT", tcp_timeout.to_string());
env::set_var("WAIT_HOSTS", hosts.to_string());
env::set_var("WAIT_SLEEP_INTERVAL", sleep.to_string());
env::set_var("WAIT_COMMAND", command.to_string());
}

#[test]
fn parse_command_fails_when_command_is_invalid() {
assert!(parse_command(" intentionally 'invalid").is_err())
}

#[test]
fn parse_command_returns_none_when_command_is_empty() {
for c in vec!["", " \t\n\r\n"] {
let p = parse_command(c.to_string()).unwrap();
assert!(p.is_none());
}
}

#[test]
fn parse_command_handles_commands_without_args() {
let p = parse_command("ls".to_string()).unwrap().unwrap();
assert_eq!("ls", p.program);
assert_eq!(vec!["ls"], p.argv);
}

#[test]
fn parse_command_handles_commands_with_args() {
let p = parse_command("ls -al".to_string()).unwrap().unwrap();
assert_eq!("ls", p.program);
assert_eq!(vec!["ls", "-al"], p.argv);
}

#[test]
fn parse_command_discards_leading_and_trailing_whitespace() {
let p = parse_command(" hello world ".to_string()).unwrap().unwrap();
assert_eq!("hello", p.program);
assert_eq!(vec!["hello", "world"], p.argv);
}

#[test]
fn parse_command_strips_shell_quotes() {
let p = parse_command(" find . -type \"f\" -name '*.rs' ".to_string()).unwrap().unwrap();
assert_eq!("find", p.program);
assert_eq!(vec!["find", ".", "-type", "f", "-name", "*.rs"], p.argv);
}
}
12 changes: 12 additions & 0 deletions test.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM alpine:3.16
WORKDIR /tmp
RUN apk add build-base
RUN printf 'int main(){ exit(getpid()-1); }' > /tmp/is_pid_1.c \
&& gcc -w -static -o /is_pid_1 /tmp/is_pid_1.c

FROM scratch
COPY --from=0 /is_pid_1 /is_pid_1
COPY ./target/x86_64-unknown-linux-musl/release/wait /wait
ENV WAIT_LOGGER_LEVEL=off
ENV WAIT_COMMAND=/is_pid_1
ENTRYPOINT ["/wait"]
2 changes: 2 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/bin/bash

docker build -t wait:test -f test.Dockerfile . && docker run --rm wait:test

export WAIT_HOSTS=localhost:4748
#export WAIT_PATHS=./target/one
export WAIT_TIMEOUT=10
Expand Down
Loading

0 comments on commit 92e75a5

Please sign in to comment.