Skip to content

Commit 3935d76

Browse files
feat: add --sidecar global flag to keep process alive after task completion (#25) (#26)
When running as a Kubernetes sidecar container, the process needs to stay alive after completing its tasks. The new --sidecar flag (env: INITIUM_SIDECAR) causes the process to sleep indefinitely on success, while still exiting immediately with code 1 on failure. Also fixes pre-existing clippy warnings for newer Rust toolchain. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c9f7ef4 commit 3935d76

File tree

7 files changed

+185
-6
lines changed

7 files changed

+185
-6
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,20 @@ initContainers:
207207
value: postgres
208208
```
209209

210+
### How do I run initium as a sidecar container?
211+
212+
Use the `--sidecar` global flag to keep the process alive after tasks complete:
213+
214+
```yaml
215+
containers:
216+
- name: initium-sidecar
217+
image: ghcr.io/kitstream/initium:latest
218+
restartPolicy: Always
219+
args: ["--sidecar", "wait-for", "--target", "tcp://postgres:5432"]
220+
```
221+
222+
The process sleeps indefinitely after success. On failure it exits with code `1` immediately.
223+
210224
### How do I get JSON logs?
211225

212226
Add the `--json` global flag:

docs/usage.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -364,12 +364,39 @@ initContainers:
364364
365365
## Global Flags
366366
367-
| Flag | Default | Env Var | Description |
368-
| -------- | ------- | -------------- | -------------------------------- |
369-
| `--json` | `false` | `INITIUM_JSON` | Enable JSON-formatted log output |
367+
| Flag | Default | Env Var | Description |
368+
| ----------- | ------- | ------------------ | ------------------------------------------------------------ |
369+
| `--json` | `false` | `INITIUM_JSON` | Enable JSON-formatted log output |
370+
| `--sidecar` | `false` | `INITIUM_SIDECAR` | Keep process alive after task completion (sidecar containers) |
370371

371372
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.
372373

374+
### Sidecar mode
375+
376+
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.
377+
378+
```bash
379+
# Via flag
380+
initium --sidecar wait-for --target tcp://postgres:5432
381+
382+
# Via environment variable
383+
INITIUM_SIDECAR=true initium seed --spec /seeds/seed.yaml
384+
```
385+
386+
**Behavior:**
387+
388+
- On **success**: logs completion, then sleeps indefinitely
389+
- On **failure**: exits with code `1` immediately (does not sleep)
390+
391+
```yaml
392+
# Kubernetes sidecar example (requires K8s 1.29+)
393+
containers:
394+
- name: initium-sidecar
395+
image: ghcr.io/kitstream/initium:latest
396+
restartPolicy: Always
397+
args: ["--sidecar", "wait-for", "--target", "tcp://postgres:5432"]
398+
```
399+
373400
**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).
374401

375402
## Exit Codes

src/duration.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ mod tests {
197197
);
198198
assert_eq!(
199199
parse_duration("18h36m4s200ms").unwrap(),
200-
Duration::from_millis(18 * 3600_000 + 36 * 60_000 + 4_000 + 200)
200+
Duration::from_millis(18 * 3_600_000 + 36 * 60_000 + 4_000 + 200)
201201
);
202202
assert_eq!(parse_duration("1h30m").unwrap(), Duration::from_secs(5400));
203203
assert_eq!(

src/main.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ struct Cli {
2727
)]
2828
json: bool,
2929

30+
#[arg(
31+
long,
32+
global = true,
33+
env = "INITIUM_SIDECAR",
34+
help = "Keep process alive after task completion (for sidecar containers)"
35+
)]
36+
sidecar: bool,
37+
3038
#[command(subcommand)]
3139
command: Commands,
3240
}
@@ -360,4 +368,14 @@ fn main() {
360368
log.error(&e, &[]);
361369
std::process::exit(1);
362370
}
371+
372+
if cli.sidecar {
373+
log.info(
374+
"tasks completed, entering sidecar mode (sleeping indefinitely)",
375+
&[],
376+
);
377+
loop {
378+
std::thread::sleep(std::time::Duration::from_secs(3600));
379+
}
380+
}
363381
}

src/safety.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ mod tests {
5555
#[test]
5656
fn test_traversal_rejected() {
5757
let dir = TempDir::new().unwrap();
58-
let traversal = ["..", "..", "..", "tmp", "x"].join(&std::path::MAIN_SEPARATOR.to_string());
58+
let traversal = ["..", "..", "..", "tmp", "x"].join(std::path::MAIN_SEPARATOR_STR);
5959
let result = validate_file_path(dir.path().to_str().unwrap(), &traversal);
6060
assert!(result.is_err());
6161
}

tests/env_var_flags.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::process::Command;
2+
use std::time::{Duration, Instant};
23

34
fn initium_bin() -> String {
45
env!("CARGO_BIN_EXE_initium").to_string()
@@ -239,3 +240,122 @@ fn test_env_var_false_boolean_not_set() {
239240
stderr
240241
);
241242
}
243+
244+
#[test]
245+
fn test_sidecar_flag_on_failure_exits_immediately() {
246+
// --sidecar should NOT keep the process alive when the subcommand fails
247+
let start = Instant::now();
248+
let output = Command::new(initium_bin())
249+
.args([
250+
"--sidecar",
251+
"wait-for",
252+
"--target",
253+
"tcp://localhost:1",
254+
"--timeout",
255+
"1s",
256+
"--max-attempts",
257+
"1",
258+
])
259+
.output()
260+
.unwrap();
261+
let elapsed = start.elapsed();
262+
assert!(
263+
!output.status.success(),
264+
"expected failure exit code with --sidecar on error"
265+
);
266+
// Should exit quickly (well under 10s), not sleep
267+
assert!(
268+
elapsed < Duration::from_secs(10),
269+
"sidecar should not sleep on failure, took {:?}",
270+
elapsed
271+
);
272+
}
273+
274+
#[test]
275+
fn test_sidecar_flag_on_success_sleeps() {
276+
// --sidecar on a successful command should keep the process alive.
277+
// We use `exec -- true` which succeeds immediately, then verify the
278+
// process is still running after a short delay.
279+
let mut child = Command::new(initium_bin())
280+
.args(["--sidecar", "exec", "--", "true"])
281+
.stderr(std::process::Stdio::piped())
282+
.spawn()
283+
.unwrap();
284+
285+
// Wait briefly to give the process time to complete the subcommand
286+
std::thread::sleep(Duration::from_secs(2));
287+
288+
// The process should still be running (sidecar sleep)
289+
let status = child.try_wait().unwrap();
290+
assert!(
291+
status.is_none(),
292+
"expected sidecar process to still be running, but it exited: {:?}",
293+
status
294+
);
295+
296+
// Clean up: kill the process
297+
child.kill().unwrap();
298+
child.wait().unwrap();
299+
}
300+
301+
#[test]
302+
fn test_sidecar_env_var() {
303+
// INITIUM_SIDECAR=true should enable sidecar mode via env var
304+
let mut child = Command::new(initium_bin())
305+
.args(["exec", "--", "true"])
306+
.env("INITIUM_SIDECAR", "true")
307+
.stderr(std::process::Stdio::piped())
308+
.spawn()
309+
.unwrap();
310+
311+
std::thread::sleep(Duration::from_secs(2));
312+
313+
let status = child.try_wait().unwrap();
314+
assert!(
315+
status.is_none(),
316+
"expected sidecar process to still be running via env var, but it exited: {:?}",
317+
status
318+
);
319+
320+
child.kill().unwrap();
321+
child.wait().unwrap();
322+
}
323+
324+
#[test]
325+
fn test_sidecar_logs_message_on_success() {
326+
// --sidecar should log "sidecar mode" message on success
327+
let mut child = Command::new(initium_bin())
328+
.args(["--sidecar", "exec", "--", "true"])
329+
.stderr(std::process::Stdio::piped())
330+
.spawn()
331+
.unwrap();
332+
333+
std::thread::sleep(Duration::from_secs(2));
334+
335+
// Kill and collect stderr
336+
child.kill().unwrap();
337+
let output = child.wait_with_output().unwrap();
338+
let stderr = String::from_utf8_lossy(&output.stderr);
339+
assert!(
340+
stderr.contains("sidecar mode"),
341+
"expected sidecar mode log message, got: {}",
342+
stderr
343+
);
344+
}
345+
346+
#[test]
347+
fn test_without_sidecar_exits_on_success() {
348+
// Without --sidecar, a successful command should exit immediately
349+
let start = Instant::now();
350+
let output = Command::new(initium_bin())
351+
.args(["exec", "--", "true"])
352+
.output()
353+
.unwrap();
354+
let elapsed = start.elapsed();
355+
assert!(output.status.success(), "exec true should succeed");
356+
assert!(
357+
elapsed < Duration::from_secs(5),
358+
"without --sidecar, process should exit immediately, took {:?}",
359+
elapsed
360+
);
361+
}

tests/integration_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ fn initium_bin() -> String {
1515
}
1616

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

2121
fn input_dir() -> String {

0 commit comments

Comments
 (0)