Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,20 @@ initContainers:
value: postgres
```

### How do I run initium as a sidecar container?

Use the `--sidecar` global flag to keep the process alive after tasks complete:

```yaml
containers:
- name: initium-sidecar
image: ghcr.io/kitstream/initium:latest
restartPolicy: Always
args: ["--sidecar", "wait-for", "--target", "tcp://postgres:5432"]
```

The process sleeps indefinitely after success. On failure it exits with code `1` immediately.

### How do I get JSON logs?

Add the `--json` global flag:
Expand Down
33 changes: 30 additions & 3 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,12 +364,39 @@ initContainers:

## Global Flags

| Flag | Default | Env Var | Description |
| -------- | ------- | -------------- | -------------------------------- |
| `--json` | `false` | `INITIUM_JSON` | Enable JSON-formatted log output |
| Flag | Default | Env Var | Description |
| ----------- | ------- | ------------------ | ------------------------------------------------------------ |
| `--json` | `false` | `INITIUM_JSON` | Enable JSON-formatted log output |
| `--sidecar` | `false` | `INITIUM_SIDECAR` | Keep process alive after task completion (sidecar containers) |

All flags can be set via environment variables. Flag values take precedence over environment variables. Boolean env vars accept `true`/`false`, `1`/`0`, `yes`/`no`. The `INITIUM_TARGET` env var accepts comma-separated values for multiple targets.

### Sidecar mode

When running initium as a Kubernetes sidecar container (rather than an init container), use `--sidecar` to keep the process alive after tasks complete. Without this flag, the process exits on success, which causes Kubernetes to restart the sidecar container in a loop.

```bash
# Via flag
initium --sidecar wait-for --target tcp://postgres:5432

# Via environment variable
INITIUM_SIDECAR=true initium seed --spec /seeds/seed.yaml
```

**Behavior:**

- On **success**: logs completion, then sleeps indefinitely
- On **failure**: exits with code `1` immediately (does not sleep)

```yaml
# Kubernetes sidecar example (requires K8s 1.29+)
containers:
- name: initium-sidecar
image: ghcr.io/kitstream/initium:latest
restartPolicy: Always
args: ["--sidecar", "wait-for", "--target", "tcp://postgres:5432"]
```

**Duration format:** All time parameters (`--timeout`, `--initial-delay`, `--max-delay`) accept values with optional time unit suffixes: `ms` (milliseconds), `s` (seconds), `m` (minutes), `h` (hours). Decimal values are supported (e.g. `1.5m`, `2.7s`). Multiple units can be combined (e.g. `1m30s`, `2s700ms`, `18h36m4s200ms`). Bare numbers without a unit are treated as seconds. Examples: `30s`, `5m`, `1h`, `500ms`, `1m30s`, `120` (= 120 seconds).

## Exit Codes
Expand Down
2 changes: 1 addition & 1 deletion src/duration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ mod tests {
);
assert_eq!(
parse_duration("18h36m4s200ms").unwrap(),
Duration::from_millis(18 * 3600_000 + 36 * 60_000 + 4_000 + 200)
Duration::from_millis(18 * 3_600_000 + 36 * 60_000 + 4_000 + 200)
);
assert_eq!(parse_duration("1h30m").unwrap(), Duration::from_secs(5400));
assert_eq!(
Expand Down
18 changes: 18 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ struct Cli {
)]
json: bool,

#[arg(
long,
global = true,
env = "INITIUM_SIDECAR",
help = "Keep process alive after task completion (for sidecar containers)"
)]
sidecar: bool,

#[command(subcommand)]
command: Commands,
}
Expand Down Expand Up @@ -360,4 +368,14 @@ fn main() {
log.error(&e, &[]);
std::process::exit(1);
}

if cli.sidecar {
log.info(
"tasks completed, entering sidecar mode (sleeping indefinitely)",
&[],
);
loop {
std::thread::sleep(std::time::Duration::from_secs(3600));
}
}
}
2 changes: 1 addition & 1 deletion src/safety.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ mod tests {
#[test]
fn test_traversal_rejected() {
let dir = TempDir::new().unwrap();
let traversal = ["..", "..", "..", "tmp", "x"].join(&std::path::MAIN_SEPARATOR.to_string());
let traversal = ["..", "..", "..", "tmp", "x"].join(std::path::MAIN_SEPARATOR_STR);
let result = validate_file_path(dir.path().to_str().unwrap(), &traversal);
assert!(result.is_err());
}
Expand Down
120 changes: 120 additions & 0 deletions tests/env_var_flags.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::process::Command;
use std::time::{Duration, Instant};

fn initium_bin() -> String {
env!("CARGO_BIN_EXE_initium").to_string()
Expand Down Expand Up @@ -239,3 +240,122 @@ fn test_env_var_false_boolean_not_set() {
stderr
);
}

#[test]
fn test_sidecar_flag_on_failure_exits_immediately() {
// --sidecar should NOT keep the process alive when the subcommand fails
let start = Instant::now();
let output = Command::new(initium_bin())
.args([
"--sidecar",
"wait-for",
"--target",
"tcp://localhost:1",
"--timeout",
"1s",
"--max-attempts",
"1",
])
.output()
.unwrap();
let elapsed = start.elapsed();
assert!(
!output.status.success(),
"expected failure exit code with --sidecar on error"
);
// Should exit quickly (well under 10s), not sleep
assert!(
elapsed < Duration::from_secs(10),
"sidecar should not sleep on failure, took {:?}",
elapsed
);
}

#[test]
fn test_sidecar_flag_on_success_sleeps() {
// --sidecar on a successful command should keep the process alive.
// We use `exec -- true` which succeeds immediately, then verify the
// process is still running after a short delay.
let mut child = Command::new(initium_bin())
.args(["--sidecar", "exec", "--", "true"])
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();

// Wait briefly to give the process time to complete the subcommand
std::thread::sleep(Duration::from_secs(2));

// The process should still be running (sidecar sleep)
let status = child.try_wait().unwrap();
assert!(
status.is_none(),
"expected sidecar process to still be running, but it exited: {:?}",
status
);

// Clean up: kill the process
child.kill().unwrap();
child.wait().unwrap();
}

#[test]
fn test_sidecar_env_var() {
// INITIUM_SIDECAR=true should enable sidecar mode via env var
let mut child = Command::new(initium_bin())
.args(["exec", "--", "true"])
.env("INITIUM_SIDECAR", "true")
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();

std::thread::sleep(Duration::from_secs(2));

let status = child.try_wait().unwrap();
assert!(
status.is_none(),
"expected sidecar process to still be running via env var, but it exited: {:?}",
status
);

child.kill().unwrap();
child.wait().unwrap();
}

#[test]
fn test_sidecar_logs_message_on_success() {
// --sidecar should log "sidecar mode" message on success
let mut child = Command::new(initium_bin())
.args(["--sidecar", "exec", "--", "true"])
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();

std::thread::sleep(Duration::from_secs(2));

// Kill and collect stderr
child.kill().unwrap();
let output = child.wait_with_output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("sidecar mode"),
"expected sidecar mode log message, got: {}",
stderr
);
}

#[test]
fn test_without_sidecar_exits_on_success() {
// Without --sidecar, a successful command should exit immediately
let start = Instant::now();
let output = Command::new(initium_bin())
.args(["exec", "--", "true"])
.output()
.unwrap();
let elapsed = start.elapsed();
assert!(output.status.success(), "exec true should succeed");
assert!(
elapsed < Duration::from_secs(5),
"without --sidecar, process should exit immediately, took {:?}",
elapsed
);
}
2 changes: 1 addition & 1 deletion tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ fn initium_bin() -> String {
}

fn integration_enabled() -> bool {
std::env::var("INTEGRATION").map_or(false, |v| v == "1")
std::env::var("INTEGRATION").is_ok_and(|v| v == "1")
}

fn input_dir() -> String {
Expand Down