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
89 changes: 89 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,92 @@ jobs:
- name: Run tests
run: |
cargo test

integration:
name: Integration (Docker + runc)
if: github.event.pull_request.draft == false
runs-on: ubuntu-24.04 # current challenge server target
Comment thread
hi-liang marked this conversation as resolved.
needs: ci
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # v1.94.1
with:
toolchain: stable
- name: Set up Rust caching
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- name: Record Docker and runc versions
run: |
docker version
docker info --format '{{json .Runtimes}}' || true
runc --version || true
- name: Build oci-interceptor
run: cargo build --release
- name: Install binary
run: sudo install -m 0755 target/release/oci-interceptor /usr/local/bin/oci-interceptor
- name: Create debug output directory
run: |
sudo mkdir -p /tmp/oi-interceptor-debug
sudo chmod 0777 /tmp/oi-interceptor-debug
- name: Configure Docker daemon
run: |
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"runtimes": {
"oi-default": {
"path": "/usr/local/bin/oci-interceptor"
},
"oi-ro-net": {
"path": "/usr/local/bin/oci-interceptor",
"runtimeArgs": ["--oi-readonly-networking-mounts"]
},
"oi-env-foo": {
"path": "/usr/local/bin/oci-interceptor",
"runtimeArgs": ["--oi-env", "FOO=bar"]
},
"oi-env-force-foo": {
"path": "/usr/local/bin/oci-interceptor",
"runtimeArgs": ["--oi-env-force", "FOO=forced"]
},
"oi-debug": {
"path": "/usr/local/bin/oci-interceptor",
"runtimeArgs": [
"--oi-write-debug-output",
"--oi-debug-output-dir", "/tmp/oi-interceptor-debug"
]
},
"oi-debug-ro-net": {
"path": "/usr/local/bin/oci-interceptor",
"runtimeArgs": [
"--oi-readonly-networking-mounts",
"--oi-write-debug-output",
"--oi-debug-output-dir", "/tmp/oi-interceptor-debug"
]
}
}
}
EOF
sudo systemctl restart docker
# Wait for the daemon to be responsive again before kicking off tests.
for i in $(seq 1 30); do
if docker info > /dev/null 2>&1; then
echo "Docker daemon is ready"; break
fi
sleep 1
done
docker info --format '{{json .Runtimes}}'
- name: Pre-pull test image
run: docker pull alpine:3.20
- name: Run integration tests
env:
OCI_INTERCEPTOR_INTEGRATION: "1"
run: cargo test --test integration -- --test-threads=1 --nocapture
- name: Upload debug output on failure
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: oci-interceptor-debug-output
path: /tmp/oi-interceptor-debug/
if-no-files-found: ignore
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,19 @@ Alternatively, use `--oi-env-force <NAME=VALUE>` to force an certain value even
- Solution for https://stackoverflow.com/questions/33775075/how-to-set-default-docker-environment-variables
- Solution for https://stackoverflow.com/questions/50644143/dockerd-set-default-environment-variable-for-all-containers

## Testing

Unit tests run with `cargo test`. End-to-end integration tests live in `tests/integration.rs` and exercise the wrapper through a real Docker daemon. They are gated by the `OCI_INTERCEPTOR_INTEGRATION` environment variable so the default `cargo test` invocation stays portable.

To run the integration tests locally on a Linux host with Docker:

1. Build and install the binary: `cargo build --release && sudo install -m 0755 target/release/oci-interceptor /usr/local/bin/oci-interceptor`
2. Configure `/etc/docker/daemon.json` with the named runtimes listed in the module docs of [tests/integration.rs](tests/integration.rs). The CI workflow (`.github/workflows/CI.yml`, `integration` job) is the canonical reference for the required daemon.json.
3. `sudo systemctl restart docker`
4. `OCI_INTERCEPTOR_INTEGRATION=1 cargo test --test integration -- --test-threads=1`

CI runs the same suite on every push and pull request, which provides a regression check against the Docker and runc versions shipped on `ubuntu-latest` runners.

### Debug output

Specify the `--oi-write-debug-output` flag to write original, parsed, and modified container configs to the directory specified as `--oi-debug-output-dir` (default `/var/log/oci-interceptor`).
Expand Down
39 changes: 39 additions & 0 deletions src/env_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,42 @@ pub(crate) fn modify_env_vars(spec: &mut Spec, vars: Vec<EnvVarOverride>) {
spec.set_process(Some(new_process));
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parses_simple_key_value() {
let v = parse_env_var("FOO=bar").unwrap();
assert_eq!(v.name, "FOO");
assert_eq!(v.value, "bar");
}

#[test]
fn value_may_contain_equals_signs() {
let v = parse_env_var("FOO=a=b=c").unwrap();
assert_eq!(v.name, "FOO");
assert_eq!(v.value, "a=b=c");
}

#[test]
fn missing_equals_is_rejected() {
assert!(parse_env_var("FOO").is_err());
}

#[test]
fn empty_name_is_rejected() {
assert!(parse_env_var("=bar").is_err());
}

#[test]
fn empty_value_is_rejected() {
assert!(parse_env_var("FOO=").is_err());
}

#[test]
fn empty_string_is_rejected() {
assert!(parse_env_var("").is_err());
}
}
52 changes: 52 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ fn main() -> Result<()> {
}
if !env_var_overrides.is_empty() {
modify_env_vars(&mut spec, env_var_overrides);
spec_modified = true;
}

// Write the updated config back out to disk
Expand Down Expand Up @@ -207,3 +208,54 @@ fn call_oci_runtime(runtime_path: &str, options: Vec<String>) -> Result<i32> {
None => Ok(-1), // child process was killed by a signal
}
}

#[cfg(test)]
mod tests {
use super::*;

fn opts(args: &[&str]) -> Vec<String> {
args.iter().map(|s| s.to_string()).collect()
}

#[test]
fn finds_bundle_short_flag_space_separated() {
let mut o = opts(&["create", "-b", "/tmp/bundle", "cid"]);
assert_eq!(get_bundle_path(&mut o), Some(PathBuf::from("/tmp/bundle")));
}

#[test]
fn finds_bundle_short_flag_with_equals() {
let mut o = opts(&["create", "-b=/tmp/bundle", "cid"]);
assert_eq!(get_bundle_path(&mut o), Some(PathBuf::from("/tmp/bundle")));
}

#[test]
fn finds_bundle_long_flag_space_separated() {
let mut o = opts(&["create", "--bundle", "/tmp/bundle", "cid"]);
assert_eq!(get_bundle_path(&mut o), Some(PathBuf::from("/tmp/bundle")));
}

#[test]
fn finds_bundle_long_flag_with_equals() {
let mut o = opts(&["create", "--bundle=/tmp/bundle", "cid"]);
assert_eq!(get_bundle_path(&mut o), Some(PathBuf::from("/tmp/bundle")));
}

#[test]
fn returns_none_when_no_bundle_flag_present() {
let mut o = opts(&["start", "cid"]);
assert_eq!(get_bundle_path(&mut o), None);
}

#[test]
fn returns_none_for_empty_options() {
let mut o: Vec<String> = Vec::new();
assert_eq!(get_bundle_path(&mut o), None);
}

#[test]
fn returns_none_when_short_flag_has_no_following_arg() {
let mut o = opts(&["create", "-b"]);
assert_eq!(get_bundle_path(&mut o), None);
}
}
44 changes: 44 additions & 0 deletions tests/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//! CLI smoke tests for the oci-interceptor binary.
//!
//! These exercise clap-handled flags that short-circuit before any runtime call,
//! so they do not require Docker or runc and always run as part of `cargo test`.

use std::process::Command;

const BIN: &str = env!("CARGO_BIN_EXE_oci-interceptor");

#[test]
fn version_flag_prints_version() {
let out = Command::new(BIN)
.arg("--oi-version")
.output()
.expect("failed to invoke oci-interceptor");
assert!(
out.status.success(),
"--oi-version exited non-zero: {:?}",
out.status
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains(env!("CARGO_PKG_VERSION")),
"version output missing crate version: {stdout}"
);
}

#[test]
fn help_flag_prints_usage() {
let out = Command::new(BIN)
.arg("--oi-help")
.output()
.expect("failed to invoke oci-interceptor");
assert!(
out.status.success(),
"--oi-help exited non-zero: {:?}",
out.status
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("--oi-runtime-path"),
"help output missing expected flag, got: {stdout}"
);
}
Loading