diff --git a/.github/workflows/branch-checks.yml b/.github/workflows/branch-checks.yml index a62d442bc9..f702c06c95 100644 --- a/.github/workflows/branch-checks.yml +++ b/.github/workflows/branch-checks.yml @@ -233,8 +233,9 @@ jobs: cargo fmt --manifest-path tests/testkit/Cargo.toml --all -- --check cargo fmt --manifest-path tests/tmachine/Cargo.toml --all -- --check cargo fmt --manifest-path tests/runner/Cargo.toml --all -- --check - cargo fmt --manifest-path tests/suites/conformance/Cargo.toml -p conformance -- --check - cargo fmt --manifest-path tests/suites/features/Cargo.toml -p keycloak-provider-refresh -- --check + cargo fmt --manifest-path tests/suites/conformance/Cargo.toml --all -- --check + cargo fmt --manifest-path tests/suites/drivers/Cargo.toml --all -- --check + cargo fmt --manifest-path tests/suites/features/Cargo.toml --all -- --check cargo fmt --manifest-path tests/support/Cargo.toml --all -- --check cargo-lockfiles: @@ -297,8 +298,9 @@ jobs: cargo clippy --locked --manifest-path tests/testkit/Cargo.toml --all-targets -- -D warnings cargo clippy --locked --manifest-path tests/tmachine/Cargo.toml --all-targets -- -D warnings cargo clippy --locked --manifest-path tests/runner/Cargo.toml --all-targets -- -D warnings - cargo clippy --locked --manifest-path tests/suites/conformance/Cargo.toml -p conformance --all-targets -- -D warnings - cargo clippy --locked --manifest-path tests/suites/features/Cargo.toml -p keycloak-provider-refresh --all-targets -- -D warnings + cargo clippy --locked --manifest-path tests/suites/conformance/Cargo.toml --workspace --all-targets -- -D warnings + cargo clippy --locked --manifest-path tests/suites/drivers/Cargo.toml --workspace --all-targets -- -D warnings + cargo clippy --locked --manifest-path tests/suites/features/Cargo.toml --workspace --all-targets -- -D warnings cargo clippy --locked --manifest-path tests/support/Cargo.toml --all-targets -- -D warnings rust-test: @@ -389,6 +391,7 @@ jobs: cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/testkit/Cargo.toml cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/tmachine/Cargo.toml cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/runner/Cargo.toml + cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/support/Cargo.toml rust-postgres-test: name: Rust PostgreSQL tests diff --git a/.github/workflows/branch-e2e.yml b/.github/workflows/branch-e2e.yml index 3647f3ee02..cc558fb4e2 100644 --- a/.github/workflows/branch-e2e.yml +++ b/.github/workflows/branch-e2e.yml @@ -255,65 +255,55 @@ jobs: secrets: CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} - # Run feature-specific integration tests: + # Run the testkit feature suites (tests/suites/features) on tmachine. The + # environments and suites are listed in e2e-suites.yml. feature-specific-integration: needs: prepare-integration permissions: actions: read contents: read packages: read - uses: ./.github/workflows/integration-runner.yml + uses: ./.github/workflows/e2e-suites.yml with: - category: feature-specific + family: features source-sha: ${{ needs.prepare-integration.outputs.source_sha }} integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} - test-matrix: >- - [ - {"environment":"fedora-podman-rootful","installer":"rpm","testsuite":"provider-refresh"}, - {"environment":"fedora-podman-rootless","installer":"rpm","testsuite":"provider-refresh"}, - {"environment":"ubuntu-docker-rootful","installer":"deb","testsuite":"oci-image"}, - {"environment":"fedora-podman-rootful","installer":"rpm","testsuite":"oci-image"}, - {"environment":"fedora-podman-rootless","installer":"rpm","testsuite":"oci-image"} - ] + secrets: + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} - # Run driver-specific integration tests: - # These suites require the binary installer's CLI path, local HTTP gateway, - # and system service configuration for their Podman user-namespace fixtures. + # Run the testkit driver suites (tests/suites/drivers) on tmachine. The + # environments and suites are listed in e2e-suites.yml. driver-specific-integration: needs: prepare-integration permissions: actions: read contents: read packages: read - uses: ./.github/workflows/integration-runner.yml + uses: ./.github/workflows/e2e-suites.yml with: - category: driver-specific + family: drivers source-sha: ${{ needs.prepare-integration.outputs.source_sha }} integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} - test-matrix: >- - [ - {"environment":"fedora-podman-rootful","installer":"binaries","testsuite":"driver-podman"}, - {"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"e2e-podman"}, - {"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"driver-podman"} - ] + secrets: + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} - # Run the testkit feature suites in tests/suites/ on tmachine. The - # environments and suites are listed in e2e-suites.yml; the conformance rows - # run in conformance-integration. - e2e-suites: - needs: [pr_metadata, prepare-integration] - if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' + # Run the Podman e2e suite on tmachine. It needs the binary installer's CLI + # path and local HTTP gateway. + podman-e2e-integration: + needs: prepare-integration permissions: actions: read contents: read packages: read - uses: ./.github/workflows/e2e-suites.yml + uses: ./.github/workflows/integration-runner.yml with: - family: features + category: driver-specific source-sha: ${{ needs.prepare-integration.outputs.source_sha }} integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} - secrets: - CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + test-matrix: >- + [ + {"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"e2e-podman"} + ] docker-e2e: needs: [pr_metadata, build-binaries, build-images] @@ -573,9 +563,9 @@ jobs: needs: - pr_metadata - conformance-integration - - e2e-suites - feature-specific-integration - driver-specific-integration + - podman-e2e-integration - docker-e2e - podman-e2e - podman-additional-ca-e2e diff --git a/.github/workflows/e2e-suites.yml b/.github/workflows/e2e-suites.yml index 5e47052d91..9c66b3e5f8 100644 --- a/.github/workflows/e2e-suites.yml +++ b/.github/workflows/e2e-suites.yml @@ -62,14 +62,27 @@ jobs: # Only amd64 for now: CI has no arm64 runner with KVM for tmachine, # and the caller only builds amd64 inputs. The gateway is the name the # installer registers with the CLI: `openshell` for the packages, - # `tmachine` for the k3s installers. + # `tmachine` for the k3s and binaries installers. rows='[ {"environment":"ubuntu-docker-rootful","installer":"deb","family":"conformance","package":"conformance","gateway":"openshell","runner":"ubuntu-24.04"}, {"environment":"fedora-podman-rootful","installer":"rpm","family":"conformance","package":"conformance","gateway":"openshell","runner":"ubuntu-24.04"}, {"environment":"fedora-podman-rootless","installer":"rpm","family":"conformance","package":"conformance","gateway":"openshell","runner":"ubuntu-24.04"}, {"environment":"ubuntu-k3s","installer":"k3s","family":"conformance","package":"conformance","gateway":"tmachine","runner":"ubuntu-24.04"}, {"environment":"ubuntu-k3s","installer":"k3s-ha","family":"conformance","package":"conformance","gateway":"tmachine","runner":"ubuntu-24.04"}, - {"environment":"ubuntu-docker-rootful-keycloak","installer":"deb","family":"features","package":"keycloak-provider-refresh","gateway":"openshell","runner":"ubuntu-24.04"} + {"environment":"ubuntu-docker-rootful-keycloak","installer":"deb","family":"features","package":"keycloak-provider-refresh","gateway":"openshell","runner":"ubuntu-24.04"}, + {"environment":"ubuntu-docker-rootful","installer":"deb","family":"features","package":"oci-image","gateway":"openshell","runner":"ubuntu-24.04"}, + {"environment":"fedora-podman-rootful","installer":"rpm","family":"features","package":"oci-image","gateway":"openshell","runner":"ubuntu-24.04"}, + {"environment":"fedora-podman-rootless","installer":"rpm","family":"features","package":"oci-image","gateway":"openshell","runner":"ubuntu-24.04"}, + {"environment":"fedora-podman-rootful-keycloak","installer":"rpm","family":"features","package":"keycloak-provider-refresh","gateway":"openshell","runner":"ubuntu-24.04"}, + {"environment":"fedora-podman-rootless-keycloak","installer":"rpm","family":"features","package":"keycloak-provider-refresh","gateway":"openshell","runner":"ubuntu-24.04"}, + {"environment":"fedora-podman-rootful-userns-default","installer":"binaries","family":"drivers","package":"podman","gateway":"tmachine","runner":"ubuntu-24.04"}, + {"environment":"fedora-podman-rootful-userns-auto","installer":"binaries","family":"drivers","package":"podman","gateway":"tmachine","runner":"ubuntu-24.04"}, + {"environment":"fedora-podman-rootful-userns-keep-id","installer":"binaries","family":"drivers","package":"podman","gateway":"tmachine","runner":"ubuntu-24.04"}, + {"environment":"fedora-podman-rootful-userns-private","installer":"binaries","family":"drivers","package":"podman","gateway":"tmachine","runner":"ubuntu-24.04"}, + {"environment":"fedora-podman-rootless-userns-default","installer":"binaries","family":"drivers","package":"podman","gateway":"tmachine","runner":"ubuntu-24.04"}, + {"environment":"fedora-podman-rootless-userns-auto","installer":"binaries","family":"drivers","package":"podman","gateway":"tmachine","runner":"ubuntu-24.04"}, + {"environment":"fedora-podman-rootless-userns-keep-id","installer":"binaries","family":"drivers","package":"podman","gateway":"tmachine","runner":"ubuntu-24.04"}, + {"environment":"fedora-podman-rootless-userns-private","installer":"binaries","family":"drivers","package":"podman","gateway":"tmachine","runner":"ubuntu-24.04"} ]' matrix=$(jq -c --arg family "${FAMILY}" \ '{include: map(select($family == "" or .family == $family))}' <<< "$rows") diff --git a/.github/workflows/integration-runner.yml b/.github/workflows/integration-runner.yml index 586c22be30..906a60e0cb 100644 --- a/.github/workflows/integration-runner.yml +++ b/.github/workflows/integration-runner.yml @@ -24,8 +24,7 @@ on: type: string default: >- [ - {"environment":"fedora-podman-rootful","installer":"rpm","testsuite":"provider-refresh"}, - {"environment":"fedora-podman-rootless","installer":"rpm","testsuite":"provider-refresh"} + {"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"e2e-podman"} ] permissions: diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index bfc5847c4e..5efe0bd4e1 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -20,15 +20,14 @@ on: description: Integration-test category, used for the job name and concurrency key required: true type: string - default: feature-specific + default: driver-specific test-matrix: description: JSON array of environment, installer, and testsuite combinations required: true type: string default: >- [ - {"environment":"fedora-podman-rootful","installer":"rpm","testsuite":"provider-refresh"}, - {"environment":"fedora-podman-rootless","installer":"rpm","testsuite":"provider-refresh"} + {"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"e2e-podman"} ] permissions: diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index eae71894e5..ebf8c7e12e 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -132,19 +132,13 @@ jobs: actions: read contents: read packages: read - uses: ./.github/workflows/integration-runner.yml + uses: ./.github/workflows/e2e-suites.yml with: - category: feature-specific + family: features source-sha: ${{ needs.prepare-integration.outputs.source_sha }} integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} - test-matrix: >- - [ - {"environment":"fedora-podman-rootful","installer":"rpm","testsuite":"provider-refresh"}, - {"environment":"fedora-podman-rootless","installer":"rpm","testsuite":"provider-refresh"}, - {"environment":"ubuntu-docker-rootful","installer":"deb","testsuite":"oci-image"}, - {"environment":"fedora-podman-rootful","installer":"rpm","testsuite":"oci-image"}, - {"environment":"fedora-podman-rootless","installer":"rpm","testsuite":"oci-image"} - ] + secrets: + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} docker-e2e: needs: [build-binaries, build-images] diff --git a/.github/workflows/release-qualification.yml b/.github/workflows/release-qualification.yml index 5538a363b2..ac834bec8d 100644 --- a/.github/workflows/release-qualification.yml +++ b/.github/workflows/release-qualification.yml @@ -85,19 +85,13 @@ jobs: actions: read contents: read packages: read - uses: ./.github/workflows/integration-runner.yml + uses: ./.github/workflows/e2e-suites.yml with: - category: feature-specific + family: features source-sha: ${{ needs.prepare-integration.outputs.source_sha }} integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} - test-matrix: >- - [ - {"environment":"fedora-podman-rootful","installer":"rpm","testsuite":"provider-refresh"}, - {"environment":"fedora-podman-rootless","installer":"rpm","testsuite":"provider-refresh"}, - {"environment":"ubuntu-docker-rootful","installer":"deb","testsuite":"oci-image"}, - {"environment":"fedora-podman-rootful","installer":"rpm","testsuite":"oci-image"}, - {"environment":"fedora-podman-rootless","installer":"rpm","testsuite":"oci-image"} - ] + secrets: + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} docker-e2e: permissions: diff --git a/AGENTS.md b/AGENTS.md index 4170ae3df0..a8be3102a1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,6 @@ Do not rely on this file for a full inventory. The detailed public and contribut | Path | Components | Purpose | |------|-----------|---------| | `crates/openshell-cli/` | CLI binary | User-facing command-line interface | -| `crates/openshell-conformance/` | CLI conformance library | Reusable driver-agnostic scenarios and command runner | | `crates/openshell-server/` | Gateway server | Control-plane API, sandbox lifecycle, auth boundary | | `crates/openshell-sandbox/` | Sandbox runtime | Capability-free workload launcher, process identity, and seccomp-mediated I/O | | `crates/openshell-supervisor/` | Supervisor runtime | Gateway session, policy evaluation, credentials, and upstream networking | diff --git a/CI.md b/CI.md index 6fe38b9a8b..52d8bcc8bf 100644 --- a/CI.md +++ b/CI.md @@ -165,10 +165,13 @@ selected. DEB and RPM packages build only when integration qualification is sele the DEB on Ubuntu with Docker and installs the CLI and gateway RPMs on Fedora with rootful and rootless Podman. These lanes run conformance using the matching runtime images. Release Dev, Release, and Pre-release use the same package installers. -Fedora provider-refresh tests also use RPMs. The Podman driver-specific suites -retain the binary installer because their fixtures configure its system service, -local HTTP gateway, and CLI path. The manual Integration Tests workflow defaults -to the package installers and downloads the packages selected by its matrix. +The feature suites (`tests/suites/features`) run the same way, on the +package installers. The Podman driver suite (`tests/suites/drivers`) and the +`e2e-podman` lane retain the binary installer because they configure its system +service, local HTTP gateway, and CLI path. The driver suite and `e2e-podman` run +only in `Branch E2E Checks`. The manual Integration Tests workflow runs the +remaining tmachine testsuites, such as `e2e-podman`, and downloads the packages +selected by its matrix. Three opt-in labels enable the long-running E2E suites: diff --git a/Cargo.lock b/Cargo.lock index 7f0c1753f1..1c07a4abf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4413,17 +4413,6 @@ dependencies = [ "url", ] -[[package]] -name = "openshell-conformance" -version = "0.0.0" -dependencies = [ - "rand 0.9.4", - "serde", - "serde_json", - "tempfile", - "tokio", -] - [[package]] name = "openshell-core" version = "0.0.0" diff --git a/TESTING.md b/TESTING.md index f914f254a3..f7a7b09d50 100644 --- a/TESTING.md +++ b/TESTING.md @@ -245,9 +245,10 @@ A flaky or broken test is marked `#[ignore = ""]`, which takes it out of every environment until it's fixed. **CI.** `.github/workflows/e2e-suites.yml` lists the environments and suites as -an explicit matrix and runs the runner once per row. Branch E2E calls it when -the `test:e2e` label is set, and a failure fails the Core E2E result. To run -a suite in another environment on CI, add a row. +an explicit matrix and runs the runner once per row. Branch E2E calls it once +per family when the `test:e2e` label is set, and a failure fails the Core E2E +result. Release Dev and Release Qualification run the conformance and feature +families. To run a suite in another environment on CI, add a row. ### Adding a feature suite @@ -261,11 +262,9 @@ suite, with a name that says what the suite tests. 2. Write the tests in `tests/*.rs` as `#[testkit::test]` functions. 3. Re-lock the workspace: `cargo metadata --manifest-path tests/suites/features/Cargo.toml`, then - `uv run tasks/scripts/check_cargo_lockfile_alignment.py --fix`. -4. Add `cargo fmt` and `cargo clippy` lines for the package (`-p `) to - `tasks/rust.toml` and to the Format and Lint steps in - `.github/workflows/branch-checks.yml`. -5. Add a row for the suite to the matrix in `.github/workflows/e2e-suites.yml`. + `uv run tasks/scripts/check_cargo_lockfile_alignment.py --fix`. `cargo fmt` + and `cargo clippy` already cover every package in the workspace. +4. Add a row for the suite to the matrix in `.github/workflows/e2e-suites.yml`. If the feature only needs the gateway, run the suite in an existing environment such as `ubuntu-docker-rootful`, and skip the rest of this @@ -287,8 +286,8 @@ service. values never appear in a runner command. Values are taken literally, without quoting. - **Fixture.** Put the service's fixture in a support crate under - `tests/support/`, so any suite can depend on it, or reuse the - existing one. It defines the variable names as constants and a + `tests/support/`, named `-support`, so any suite can + depend on it, or reuse the existing one. It defines the variable names as constants and a `Context::new(cx)` that reads them, waits for the service, and wraps the testkit `Context`. Each test calls it on its first line. Nextest runs each test in its own process, so the fixture builds its @@ -511,7 +510,6 @@ Run the portable subset in a disposable rootless Podman guest: ```shell nix run .#build-artifacts nix run .#tmachine -- test fedora-podman-rootless binaries e2e-podman -nix run .#tmachine -- test fedora-podman-rootless binaries driver-podman ``` Print the exact tmachine archive selection as a shell `PODMAN_CI_TESTS` array: @@ -522,22 +520,34 @@ nix run .#generate-podman-e2e-ci-tests The `e2e-podman` testsuite runs a nextest archive built with the corresponding Rust feature and preloads its Python workload image into the rootless Podman -store. The separate `driver-podman` testsuite compares OpenShell and direct -Podman user-namespace mappings for the default, `auto`, `keep-id`, and private -profiles. The E2E archive excludes binaries that still depend on wrapper-owned +store. The E2E archive excludes binaries that still depend on wrapper-owned gateway controls, SPIFFE fixtures, the caller driver-config setting, or other -guest setup. The `driver-podman` suite replaces the removed `podman_userns` E2E -binary. `tests/tmachine/artifacts.nix` keeps the follow-up exclusions explicit and uses +guest setup. `tests/tmachine/artifacts.nix` keeps the follow-up exclusions explicit and uses the same filter for the generated inventory, so excluded binaries cannot appear as false passes or silently re-enter the archive. -The `oci-image` feature testsuite (`tests/suites/features/oci-image`) checks -OCI image identity and working-directory behavior shared by the Docker and -Podman drivers against installed deb packages on Ubuntu and rpm packages on -Fedora. CI runs it on Docker rootful, Podman rootful, and Podman rootless guests: +Driver suites live in `tests/suites/drivers/`, with the package named +after the driver. The Podman driver suite (`tests/suites/drivers/podman`) +compares OpenShell and direct Podman user-namespace mappings. +Each `fedora-podman-{rootful,rootless}-userns-` environment configures +the gateway with one mode (`default`, `auto`, `keep-id` or `private`); only the +`binaries` installer applies it: ```shell -nix run .#tmachine -- test ubuntu-docker-rootful deb oci-image +nix run .#openshell-test-runner -- \ + --environment fedora-podman-rootless-userns-keep-id --installer binaries \ + --gateway tmachine --family drivers --package podman +``` + +The `oci-image` feature suite (`tests/suites/features/oci-image`) checks OCI +image identity and working-directory behavior shared by the Docker and Podman +drivers against installed deb packages on Ubuntu and rpm packages on Fedora. +CI runs it on Docker rootful, Podman rootful, and Podman rootless guests: + +```shell +nix run .#openshell-test-runner -- \ + --environment ubuntu-docker-rootful --installer deb \ + --family features --package oci-image ``` The shared suite also runs in the existing ARM64 Docker E2E workflow for diff --git a/crates/openshell-conformance/Cargo.toml b/crates/openshell-conformance/Cargo.toml deleted file mode 100644 index fbe777a7aa..0000000000 --- a/crates/openshell-conformance/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -[package] -name = "openshell-conformance" -description = "Reusable OpenShell CLI conformance scenarios and runner" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -rand.workspace = true -serde.workspace = true -serde_json.workspace = true -tempfile = "3" -tokio.workspace = true - -[lints] -workspace = true diff --git a/crates/openshell-conformance/src/executor.rs b/crates/openshell-conformance/src/executor.rs deleted file mode 100644 index 5c5e5844c9..0000000000 --- a/crates/openshell-conformance/src/executor.rs +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Process execution boundary for the CLI conformance runner. - -use std::future::Future; -use std::path::PathBuf; -use std::pin::Pin; -use std::process::{Output, Stdio}; -use std::time::Duration; - -use tokio::time::timeout; - -pub type CliExecution<'a> = - Pin> + Send + 'a>>; - -pub trait CliExecutor: Send + Sync { - fn execute( - &self, - args: Vec, - environment: Vec<(String, String)>, - command_timeout: Duration, - ) -> CliExecution<'_>; -} - -pub enum CliExecutionError { - Spawn(std::io::Error), - Timeout, -} - -pub struct ProcessCli { - binary: PathBuf, -} - -impl ProcessCli { - pub fn new(binary: PathBuf) -> Self { - Self { binary } - } -} - -impl CliExecutor for ProcessCli { - fn execute( - &self, - args: Vec, - environment: Vec<(String, String)>, - command_timeout: Duration, - ) -> CliExecution<'_> { - Box::pin(async move { - let mut process = tokio::process::Command::new(&self.binary); - process - .args(&args) - .envs(environment) - .env("NO_COLOR", "1") - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .kill_on_drop(true); - - timeout(command_timeout, process.output()) - .await - .map_err(|_| CliExecutionError::Timeout)? - .map_err(CliExecutionError::Spawn) - }) - } -} diff --git a/crates/openshell-conformance/src/lib.rs b/crates/openshell-conformance/src/lib.rs deleted file mode 100644 index 4d1fcc1308..0000000000 --- a/crates/openshell-conformance/src/lib.rs +++ /dev/null @@ -1,1068 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Reusable support for portable `OpenShell` CLI conformance scenarios. - -pub mod executor; -mod scenarios; - -use std::collections::BTreeSet; -use std::error::Error; -use std::fmt; -use std::future::Future; -use std::path::PathBuf; -use std::pin::Pin; -use std::process::ExitStatus; -use std::sync::Arc; -use std::time::{Duration, Instant}; - -use rand::Rng; -use serde::Deserialize; -use serde::de::DeserializeOwned; -use tokio::time::sleep; - -use self::executor::{CliExecutionError, CliExecutor, ProcessCli}; - -pub use scenarios::{ - FILE_TRANSFER_GIT_FILTERING_SCENARIO, FILE_TRANSFER_PATH_SAFETY_SCENARIO, - FILE_TRANSFER_ROUND_TRIP_SCENARIO, MECHANISTIC_PROPOSAL_SCENARIO, - NEW_HOSTNAME_PROPOSAL_SCENARIO, POLICY_LOCAL_SCENARIO, SANDBOX_LIFECYCLE_SCENARIO, - SMOKE_SCENARIO, -}; - -/// A portable conformance scenario exercised by the Cargo test suite. -#[derive(Debug)] -pub struct Scenario { - pub name: &'static str, - run: for<'a> fn(&'a mut OpenShellRunner) -> ScenarioFuture<'a>, -} - -pub type ScenarioFuture<'a> = Pin> + Send + 'a>>; - -impl Scenario { - pub async fn run(&self, runner: &mut OpenShellRunner) -> Result<(), String> { - (self.run)(runner).await - } -} - -const CLEANUP_TIMEOUT: Duration = Duration::from_mins(2); -pub const STATUS_TIMEOUT: Duration = Duration::from_secs(30); -const GATEWAY_STATUS_ATTEMPT_TIMEOUT: Duration = Duration::from_secs(10); -const GATEWAY_STATUS_INTERVAL: Duration = Duration::from_secs(2); - -/// Generate the suite-owned identifier used in resource names and diagnostics. -fn generate_run_id() -> String { - const ALPHABET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz"; - let mut rng = rand::rng(); - (0..10) - .map(|_| ALPHABET[rng.random_range(0..ALPHABET.len())] as char) - .collect() -} - -/// The completed outcome of one `OpenShell` CLI process. -#[derive(Debug)] -pub struct CommandResult { - run_id: String, - scenario: String, - step: String, - expectation: String, - command: String, - status: ExitStatus, - elapsed: Duration, - stdout: String, - stderr: String, -} - -impl CommandResult { - pub fn success(&self) -> bool { - self.status.success() - } - - pub fn exit_code(&self) -> Option { - self.status.code() - } - - pub fn stdout(&self) -> &str { - &self.stdout - } - - pub fn stderr(&self) -> &str { - &self.stderr - } - - pub fn elapsed(&self) -> Duration { - self.elapsed - } - - pub fn json(&self) -> Result { - serde_json::from_str(&self.stdout).map_err(|source| RunnerError::InvalidJson { - context: self.context(), - source, - stdout: self.stdout.clone(), - stderr: self.stderr.clone(), - }) - } - - pub fn require_success(&self) -> Result<(), String> { - if self.success() { - return Ok(()); - } - Err(self.failure_diagnostic(&self.expectation)) - } - - pub fn failure_diagnostic(&self, expectation: &str) -> String { - format!( - "{}\nexpected: {expectation}\nactual: exit {} after {:.1?}\ncommand: {}\nstdout:\n{}\nstderr:\n{}", - self.context(), - exit_description(self.status), - self.elapsed, - self.command, - self.stdout, - self.stderr, - ) - } - - fn context(&self) -> String { - format!("[run {}][{}/{}]", self.run_id, self.scenario, self.step) - } -} - -/// Failures in reusable runner mechanics rather than scenario assertions. -#[derive(Debug)] -pub enum RunnerError { - BinaryUnavailable(String), - Spawn { - context: String, - command: String, - source: std::io::Error, - }, - Timeout { - context: String, - command: String, - timeout: Duration, - }, - InvalidJson { - context: String, - source: serde_json::Error, - stdout: String, - stderr: String, - }, - PollTimeout { - context: String, - timeout: Duration, - last_observation: String, - }, - ObservationFailed { - context: String, - diagnostic: String, - }, -} - -impl fmt::Display for RunnerError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::BinaryUnavailable(message) => write!(f, "{message}"), - Self::Spawn { - context, - command, - source, - } => write!( - f, - "{context} failed to spawn command: {source}\ncommand: {command}" - ), - Self::Timeout { - context, - command, - timeout, - } => write!( - f, - "{context} timed out after {timeout:.1?}\ncommand: {command}" - ), - Self::InvalidJson { - context, - source, - stdout, - stderr, - } => write!( - f, - "{context} returned invalid JSON: {source}\nstdout:\n{stdout}\nstderr:\n{stderr}" - ), - Self::PollTimeout { - context, - timeout, - last_observation, - } => write!( - f, - "{context} did not satisfy the observation within {timeout:.1?}\nlast observation:\n{last_observation}" - ), - Self::ObservationFailed { - context, - diagnostic, - } => write!(f, "{context} observation failed early:\n{diagnostic}"), - } - } -} - -impl Error for RunnerError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - Self::Spawn { source, .. } => Some(source), - Self::InvalidJson { source, .. } => Some(source), - _ => None, - } - } -} - -/// Result of one read-only polling observation. -pub enum Poll { - Ready(T), - Pending(String), - Failed(String), -} - -#[derive(Debug, Deserialize)] -struct StatusOutput { - gateway: Option, - server: Option, - status: String, - version: Option, - authentication: Option, -} - -#[derive(Debug, Deserialize)] -struct AuthenticationOutput { - status: String, -} - -/// Runs `OpenShell` commands for one conformance scenario and owns its cleanup. -pub struct OpenShellRunner { - cli: Arc, - run_id: String, - scenario: String, - known_sandboxes: BTreeSet, - known_providers: BTreeSet, - known_provider_profiles: BTreeSet, - finished: bool, -} - -/// A runner command with diagnostic context but no timeout yet. -pub struct CommandStep<'a> { - runner: &'a OpenShellRunner, - step: String, - description: Option, -} - -/// A fully configured runner command ready to execute. -pub struct OpenShellCommand<'a> { - runner: &'a OpenShellRunner, - step: String, - description: String, - timeout: Duration, -} - -impl OpenShellRunner { - /// Uses an explicit `openshell` binary rather than resolving it on `PATH`. - pub fn with_binary(binary: PathBuf, scenario: &str) -> Result { - if !binary.is_file() { - return Err(RunnerError::BinaryUnavailable(format!( - "OpenShell CLI binary not found at {}", - binary.display() - ))); - } - Ok(Self::with_executor( - Arc::new(ProcessCli::new(binary)), - scenario, - )) - } - - /// Uses the candidate CLI selected by the archive test runner. - /// - /// Archive-based tests set `OPENSHELL_BIN` to the candidate artifact - /// installed in the guest. Requiring it here prevents a test from silently - /// resolving a different `openshell` binary from `PATH`. - pub fn from_env(scenario: &str) -> Result { - let binary = std::env::var_os("OPENSHELL_BIN") - .map(PathBuf::from) - .ok_or_else(|| { - RunnerError::BinaryUnavailable( - "OPENSHELL_BIN must name the candidate openshell CLI".to_string(), - ) - })?; - Self::with_binary(binary, scenario) - } - - /// Creates a runner with an injected executor. This is useful for harness tests. - pub fn with_executor(cli: Arc, scenario: &str) -> Self { - Self { - cli, - run_id: generate_run_id(), - scenario: scenario.to_string(), - known_sandboxes: BTreeSet::new(), - known_providers: BTreeSet::new(), - known_provider_profiles: BTreeSet::new(), - finished: false, - } - } - - pub fn id(&self) -> &str { - &self.run_id - } - - pub fn step(&self, step: impl Into) -> CommandStep<'_> { - CommandStep { - runner: self, - step: step.into(), - description: None, - } - } - - /// Confirm that the configured gateway is reachable before a scenario runs. - pub async fn check_gateway_status(&mut self) -> Result<(), String> { - let status = self - .poll_until( - "preflight", - STATUS_TIMEOUT, - GATEWAY_STATUS_INTERVAL, - async |runner| match runner - .step("status/json") - .description("machine-readable gateway status succeeds") - .with_timeout(GATEWAY_STATUS_ATTEMPT_TIMEOUT) - .run(&["status", "--output", "json"]) - .await - { - Ok(result) if !result.success() => Poll::Pending( - result.failure_diagnostic("gateway status command succeeds"), - ), - Ok(result) => match result.json::() { - Ok(status) if status.status == "connected" => Poll::Ready(status), - Ok(status) if status.status == "not_configured" => Poll::Failed( - "no active gateway; register and select a gateway before running conformance" - .to_string(), - ), - Ok(status) => Poll::Pending(format!( - "gateway status is {:?}; expected \"connected\"\nstderr:\n{}", - status.status, - result.stderr() - )), - Err(error) => Poll::Failed(error.to_string()), - }, - Err(error) => Poll::Pending(error.to_string()), - }, - ) - .await - .map_err(|error| error.to_string())?; - - eprintln!( - "gateway preflight connected: gateway={}, server={}, version={}, authentication={}", - status.gateway.as_deref().unwrap_or("unknown"), - status.server.as_deref().unwrap_or("unknown"), - status.version.as_deref().unwrap_or("unknown"), - status - .authentication - .as_ref() - .map_or("unknown", |authentication| authentication.status.as_str()), - ); - Ok(()) - } - - /// Register a sandbox name for cleanup. - /// - /// Call this before running the command that may create the sandbox. The - /// scenario remains responsible for constructing and running that command. - pub fn track_sandbox(&mut self, name: &str) { - self.known_sandboxes.insert(name.to_string()); - } - - /// Stop tracking a sandbox after the scenario confirms it is absent. - pub fn forget_sandbox(&mut self, name: &str) { - self.known_sandboxes.remove(name); - } - - /// Register a provider name for cleanup. - pub fn track_provider(&mut self, name: &str) { - self.known_providers.insert(name.to_string()); - } - - /// Register a provider profile ID for cleanup. - pub fn track_provider_profile(&mut self, id: &str) { - self.known_provider_profiles.insert(id.to_string()); - } - - pub async fn poll_until( - &mut self, - step: &str, - poll_timeout: Duration, - interval: Duration, - mut observe: F, - ) -> Result - where - F: AsyncFnMut(&mut Self) -> Poll, - { - let started = Instant::now(); - let context = self.context(step); - - loop { - match observe(self).await { - Poll::Ready(value) => return Ok(value), - Poll::Pending(diagnostic) => { - if started.elapsed() >= poll_timeout { - return Err(RunnerError::PollTimeout { - context, - timeout: poll_timeout, - last_observation: diagnostic, - }); - } - } - Poll::Failed(diagnostic) => { - return Err(RunnerError::ObservationFailed { - context, - diagnostic, - }); - } - } - sleep(interval).await; - } - } - - pub async fn finish(mut self, scenario_result: Result<(), String>) -> Result<(), String> { - let cleanup_result = self.cleanup().await; - self.finished = true; - combine_results(scenario_result, cleanup_result) - } - - async fn run_strings( - &self, - step: &str, - expectation: &str, - args: Vec, - environment: Vec<(String, String)>, - command_timeout: Duration, - ) -> Result { - let context = self.context(step); - let command = sanitized_command(&args); - eprintln!("{context} running: {command}"); - - let started = Instant::now(); - let output = self - .cli - .execute(args, environment, command_timeout) - .await - .map_err(|error| match error { - CliExecutionError::Timeout => RunnerError::Timeout { - context: context.clone(), - command: command.clone(), - timeout: command_timeout, - }, - CliExecutionError::Spawn(source) => RunnerError::Spawn { - context: context.clone(), - command: command.clone(), - source, - }, - })?; - let elapsed = started.elapsed(); - eprintln!( - "{context} completed in {:.1?}: exit {}", - elapsed, - exit_description(output.status) - ); - - Ok(CommandResult { - run_id: self.run_id.clone(), - scenario: self.scenario.clone(), - step: step.to_string(), - expectation: expectation.to_string(), - command, - status: output.status, - elapsed, - stdout: String::from_utf8_lossy(&output.stdout).into_owned(), - stderr: String::from_utf8_lossy(&output.stderr).into_owned(), - }) - } - - async fn cleanup(&self) -> Result<(), String> { - if self.known_sandboxes.is_empty() - && self.known_providers.is_empty() - && self.known_provider_profiles.is_empty() - { - return Ok(()); - } - - let cleanup_started = Instant::now(); - let mut failures = Vec::new(); - for name in self.known_sandboxes.clone() { - let remaining = remaining_cleanup_time(cleanup_started); - if remaining.is_zero() { - failures.push(format!( - "{} cleanup budget expired before deleting sandbox '{name}'", - self.context("cleanup/delete") - )); - break; - } - match self - .step("cleanup/delete") - .description(format!("sandbox '{name}' is deleted or already absent")) - .with_timeout(remaining) - .run(&["sandbox", "delete", &name]) - .await - { - Ok(result) if result.success() || output_reports_not_found(&result) => {} - Ok(result) => { - failures.push(result.failure_diagnostic(&format!( - "sandbox '{name}' is deleted or already absent" - ))); - } - Err(error) => failures.push(error.to_string()), - } - } - - for name in self.known_providers.clone() { - let remaining = remaining_cleanup_time(cleanup_started); - if remaining.is_zero() { - failures.push(format!( - "{} cleanup budget expired before deleting provider '{name}'", - self.context("cleanup/delete-provider") - )); - break; - } - match self - .step("cleanup/delete-provider") - .description(format!("provider '{name}' is deleted or already absent")) - .with_timeout(remaining) - .run(&["provider", "delete", &name]) - .await - { - Ok(result) if result.success() || output_reports_not_found(&result) => {} - Ok(result) => failures.push(result.failure_diagnostic(&format!( - "provider '{name}' is deleted or already absent" - ))), - Err(error) => failures.push(error.to_string()), - } - } - - for id in self.known_provider_profiles.clone() { - let remaining = remaining_cleanup_time(cleanup_started); - if remaining.is_zero() { - failures.push(format!( - "{} cleanup budget expired before deleting provider profile '{id}'", - self.context("cleanup/delete-provider-profile") - )); - break; - } - match self - .step("cleanup/delete-provider-profile") - .description(format!( - "provider profile '{id}' is deleted or already absent" - )) - .with_timeout(remaining) - .run(&["provider", "profile", "delete", &id]) - .await - { - Ok(result) if result.success() || output_reports_not_found(&result) => {} - Ok(result) => failures.push(result.failure_diagnostic(&format!( - "provider profile '{id}' is deleted or already absent" - ))), - Err(error) => failures.push(error.to_string()), - } - } - - if failures.is_empty() { - Ok(()) - } else { - Err(failures.join("\n\n")) - } - } - - fn context(&self, step: &str) -> String { - format!("[run {}][{}/{}]", self.run_id, self.scenario, step) - } -} - -impl<'a> CommandStep<'a> { - #[must_use] - pub fn description(mut self, description: impl Into) -> Self { - self.description = Some(description.into()); - self - } - - pub fn with_timeout(self, timeout: Duration) -> OpenShellCommand<'a> { - let description = self - .description - .unwrap_or_else(|| format!("step '{}' succeeds", self.step)); - OpenShellCommand { - runner: self.runner, - step: self.step, - description, - timeout, - } - } -} - -impl OpenShellCommand<'_> { - pub async fn run(&self, args: &[&str]) -> Result { - self.run_with_env(args, &[]).await - } - - /// Run a command with environment values that are excluded from diagnostics. - pub async fn run_with_env( - &self, - args: &[&str], - environment: &[(&str, &str)], - ) -> Result { - self.runner - .run_strings( - &self.step, - &self.description, - args.iter().map(|arg| (*arg).to_string()).collect(), - environment - .iter() - .map(|(name, value)| ((*name).to_string(), (*value).to_string())) - .collect(), - self.timeout, - ) - .await - } -} - -impl Drop for OpenShellRunner { - fn drop(&mut self) { - if self.finished { - return; - } - let resources = if self.known_sandboxes.is_empty() { - "none explicitly tracked".to_string() - } else { - self.known_sandboxes - .iter() - .cloned() - .collect::>() - .join(", ") - }; - eprintln!( - "[run {}][{}] WARNING: runner dropped without finish(); known sandboxes: {}", - self.run_id, self.scenario, resources - ); - } -} - -fn combine_results( - scenario_result: Result<(), String>, - cleanup_result: Result<(), String>, -) -> Result<(), String> { - match (scenario_result, cleanup_result) { - (Ok(()), Ok(())) => Ok(()), - (Err(functional), Ok(())) => Err(functional), - (Ok(()), Err(cleanup)) => Err(format!("cleanup failed:\n{cleanup}")), - (Err(functional), Err(cleanup)) => Err(format!( - "{functional}\n\nsecondary cleanup failure:\n{cleanup}" - )), - } -} - -fn remaining_cleanup_time(started: Instant) -> Duration { - CLEANUP_TIMEOUT.saturating_sub(started.elapsed()) -} - -fn output_reports_not_found(result: &CommandResult) -> bool { - result.stderr().to_ascii_lowercase().contains("not found") - || result.stdout().to_ascii_lowercase().contains("not found") -} - -fn exit_description(status: ExitStatus) -> String { - status.code().map_or_else( - || "terminated by signal".to_string(), - |code| code.to_string(), - ) -} - -fn sanitized_command(args: &[String]) -> String { - let mut redact_next = false; - let rendered = args.iter().map(|arg| { - let rendered = if redact_next { - redact_next = false; - "".to_string() - } else if sensitive_flag(arg) { - redact_next = !arg.contains('='); - arg.split_once('=') - .map_or_else(|| arg.clone(), |(flag, _)| format!("{flag}=")) - } else { - arg.clone() - }; - shell_escape(&rendered) - }); - std::iter::once("openshell".to_string()) - .chain(rendered) - .collect::>() - .join(" ") -} - -fn sensitive_flag(arg: &str) -> bool { - let flag = arg.split_once('=').map_or(arg, |(flag, _)| flag); - matches!( - flag, - "--credential" - | "--credentials" - | "--env" - | "--header" - | "--material" - | "--password" - | "--secret" - | "--token" - ) || flag.contains("client-secret") - || flag.contains("private-key") - || flag.ends_with("-token") -} - -fn shell_escape(arg: &str) -> String { - if arg - .chars() - .all(|character| character.is_ascii_alphanumeric() || "-_./:@=,".contains(character)) - { - return arg.to_string(); - } - format!("'{}'", arg.replace('\'', "'\\''")) -} - -#[cfg(all(test, unix))] -mod tests { - use std::collections::VecDeque; - use std::os::unix::process::ExitStatusExt; - use std::process::Output; - use std::sync::Mutex; - - use serde::Deserialize; - - use super::executor::CliExecution; - use super::*; - - struct MockCli { - state: Mutex, - } - - struct MockCliState { - responses: VecDeque, - invocations: Vec>, - } - - enum MockResponse { - Output { - exit_code: i32, - stdout: String, - stderr: String, - }, - Timeout, - } - - impl MockResponse { - fn output(exit_code: i32, stdout: &str, stderr: &str) -> Self { - Self::Output { - exit_code, - stdout: stdout.to_string(), - stderr: stderr.to_string(), - } - } - - fn success(stdout: &str) -> Self { - Self::output(0, stdout, "") - } - } - - impl MockCli { - fn new(responses: Vec) -> Self { - Self { - state: Mutex::new(MockCliState { - responses: responses.into(), - invocations: Vec::new(), - }), - } - } - - fn invocations(&self) -> Vec> { - self.state - .lock() - .expect("lock mock CLI state") - .invocations - .clone() - } - } - - impl CliExecutor for MockCli { - fn execute( - &self, - args: Vec, - _environment: Vec<(String, String)>, - _command_timeout: Duration, - ) -> CliExecution<'_> { - let response = { - let mut state = self.state.lock().expect("lock mock CLI state"); - state.invocations.push(args); - state - .responses - .pop_front() - .expect("mock CLI received an unexpected invocation") - }; - - Box::pin(async move { - match response { - MockResponse::Output { - exit_code, - stdout, - stderr, - } => Ok(Output { - status: ExitStatus::from_raw(exit_code << 8), - stdout: stdout.into_bytes(), - stderr: stderr.into_bytes(), - }), - MockResponse::Timeout => Err(CliExecutionError::Timeout), - } - }) - } - } - - fn test_runner(responses: Vec) -> (OpenShellRunner, Arc) { - let cli = Arc::new(MockCli::new(responses)); - let runner = OpenShellRunner::with_executor(cli.clone(), "smoke"); - (runner, cli) - } - - #[test] - fn with_binary_rejects_a_missing_file() { - let directory = tempfile::tempdir().expect("create missing CLI directory"); - let Err(error) = OpenShellRunner::with_binary(directory.path().join("missing"), "smoke") - else { - panic!("missing CLI should be rejected"); - }; - - assert!(matches!(error, RunnerError::BinaryUnavailable(_))); - } - - #[test] - fn exposes_generated_identity() { - let (mut runner, _cli) = test_runner(Vec::new()); - - assert_eq!(runner.id().len(), 10); - runner.finished = true; - } - - #[tokio::test] - async fn captures_streams_and_nonzero_exit_as_result() { - let (mut runner, _cli) = test_runner(vec![MockResponse::output(7, "stdout", "stderr")]); - - let result = runner - .step("capture") - .description("sandbox list succeeds") - .with_timeout(Duration::from_secs(1)) - .run(&["sandbox", "list"]) - .await - .expect("completed nonzero exit is a result"); - - assert_eq!(result.exit_code(), Some(7)); - assert_eq!(result.stdout(), "stdout"); - assert_eq!(result.stderr(), "stderr"); - assert!( - result - .require_success() - .expect_err("nonzero result should fail its expectation") - .contains("expected: sandbox list succeeds") - ); - runner.finished = true; - } - - #[tokio::test] - async fn reports_timeout_as_typed_error() { - let (mut runner, _cli) = test_runner(vec![MockResponse::Timeout]); - - let error = runner - .step("timeout") - .with_timeout(Duration::from_millis(10)) - .run(&["status"]) - .await - .expect_err("command should time out"); - - assert!(matches!(error, RunnerError::Timeout { .. })); - runner.finished = true; - } - - #[derive(Debug, Deserialize, PartialEq, Eq)] - struct JsonFixture { - status: String, - } - - #[tokio::test] - async fn deserializes_stdout_json_without_stderr() { - let (mut runner, _cli) = test_runner(vec![MockResponse::output( - 0, - "{\"status\":\"connected\"}", - "diagnostic", - )]); - - let result = runner - .step("json") - .with_timeout(Duration::from_secs(1)) - .run(&["status"]) - .await - .expect("run fake CLI"); - - assert_eq!( - result.json::().expect("parse JSON"), - JsonFixture { - status: "connected".to_string() - } - ); - runner.finished = true; - } - - #[tokio::test] - async fn accepts_connected_gateway_status() { - let (mut runner, _cli) = - test_runner(vec![MockResponse::success("{\"status\":\"connected\"}")]); - - runner - .check_gateway_status() - .await - .expect("connected gateway should satisfy preflight"); - - runner.finished = true; - } - - #[tokio::test] - async fn polling_returns_ready_value() { - let (mut runner, _cli) = test_runner(Vec::new()); - let mut attempts = 0; - - let value = runner - .poll_until( - "poll", - Duration::from_secs(1), - Duration::from_millis(1), - async |_runner| { - attempts += 1; - if attempts == 2 { - Poll::Ready("ready") - } else { - Poll::Pending("not ready".to_string()) - } - }, - ) - .await - .expect("poll should become ready"); - - assert_eq!(value, "ready"); - runner.finished = true; - } - - #[tokio::test] - async fn polling_timeout_preserves_last_observation() { - let (mut runner, _cli) = test_runner(Vec::new()); - - let error = runner - .poll_until( - "poll", - Duration::from_millis(1), - Duration::from_millis(1), - async |_runner| Poll::<()>::Pending("still pending".to_string()), - ) - .await - .expect_err("poll should time out"); - - assert!(matches!( - error, - RunnerError::PollTimeout { - last_observation, - .. - } if last_observation == "still pending" - )); - runner.finished = true; - } - - #[tokio::test] - async fn finish_skips_cleanup_commands_without_registered_resources() { - let (runner, cli) = test_runner(Vec::new()); - - let error = runner - .finish(Err("preflight failed".to_string())) - .await - .expect_err("functional failure should be preserved"); - - assert_eq!(error, "preflight failed"); - assert!(cli.invocations().is_empty()); - } - - #[test] - fn tracks_sandbox_for_cleanup() { - let (mut runner, _cli) = test_runner(Vec::new()); - - runner.track_sandbox("ct-0123456789-01"); - - assert!(runner.known_sandboxes.contains("ct-0123456789-01")); - runner.finished = true; - } - - #[test] - fn forgets_exact_name_cleanup() { - let (mut runner, _cli) = test_runner(Vec::new()); - - runner.track_sandbox("ct-0123456789-01"); - runner.forget_sandbox("ct-0123456789-01"); - - assert!(runner.known_sandboxes.is_empty()); - runner.finished = true; - } - - #[tokio::test] - async fn finish_deletes_explicitly_tracked_resources() { - let (mut runner, cli) = test_runner(vec![MockResponse::success("")]); - runner.track_sandbox("ct-0123456789-01"); - - runner - .finish(Ok(())) - .await - .expect("owned resource cleanup should succeed"); - - assert_eq!( - cli.invocations(), - vec![vec![ - "sandbox".to_string(), - "delete".to_string(), - "ct-0123456789-01".to_string(), - ]] - ); - } - - #[test] - fn functional_failure_remains_primary_when_cleanup_also_fails() { - let error = combine_results( - Err("functional failure".to_string()), - Err("cleanup failure".to_string()), - ) - .expect_err("combined result should fail"); - - assert!(error.starts_with("functional failure")); - assert!(error.contains("secondary cleanup failure:\ncleanup failure")); - } - - #[test] - fn generated_run_id_is_portable_and_compact() { - let run_id = generate_run_id(); - assert_eq!(run_id.len(), 10); - assert!( - run_id - .chars() - .all(|character| character.is_ascii_lowercase() || character.is_ascii_digit()) - ); - } - - #[test] - fn sanitizes_sensitive_cli_arguments() { - let command = sanitized_command(&[ - "provider".to_string(), - "create".to_string(), - "--credential".to_string(), - "TOKEN=secret".to_string(), - "--client-secret=hunter2".to_string(), - ]); - - assert!(!command.contains("TOKEN=secret")); - assert!(!command.contains("hunter2")); - assert!(command.contains("")); - } -} diff --git a/crates/openshell-conformance/src/scenarios/file_transfer.rs b/crates/openshell-conformance/src/scenarios/file_transfer.rs deleted file mode 100644 index 0af908008c..0000000000 --- a/crates/openshell-conformance/src/scenarios/file_transfer.rs +++ /dev/null @@ -1,764 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Portable CLI file-transfer conformance scenario. - -use std::fs; -use std::path::Path; -use std::process::Stdio; -use std::time::Duration; - -use tokio::process::Command; - -use crate::{CommandResult, OpenShellRunner, Scenario, ScenarioFuture}; - -const CREATE_TIMEOUT: Duration = Duration::from_mins(10); -const COMMAND_TIMEOUT: Duration = Duration::from_mins(2); -const TRANSFER_TIMEOUT: Duration = Duration::from_mins(5); -const LARGE_FILE_SIZE: usize = 512 * 1024; - -/// Certify basic file and directory upload and download behavior. -pub const FILE_TRANSFER_ROUND_TRIP_SCENARIO: Scenario = Scenario { - name: "file-transfer/round-trip", - run: run_round_trip, -}; - -/// Certify Git-aware upload filtering and explicit unfiltered uploads. -pub const FILE_TRANSFER_GIT_FILTERING_SCENARIO: Scenario = Scenario { - name: "file-transfer/git-filtering", - run: run_git_filtering, -}; - -/// Certify file-transfer workspace boundary and filename safety behavior. -pub const FILE_TRANSFER_PATH_SAFETY_SCENARIO: Scenario = Scenario { - name: "file-transfer/path-safety", - run: run_path_safety, -}; - -fn run_round_trip(runner: &mut OpenShellRunner) -> ScenarioFuture<'_> { - Box::pin(async move { - let (sandbox_name, remote_root, local) = prepare_sandbox(runner, "round-trip").await?; - upload_existing_directories(runner, &sandbox_name, &remote_root, local.path()).await?; - round_trip(runner, &sandbox_name, &remote_root, local.path()).await?; - download_file(runner, &sandbox_name, &remote_root, local.path()).await?; - download_directory(runner, &sandbox_name, &remote_root, local.path()).await?; - delete_sandbox(runner, &sandbox_name).await - }) -} - -fn run_git_filtering(runner: &mut OpenShellRunner) -> ScenarioFuture<'_> { - Box::pin(async move { - let (sandbox_name, remote_root, local) = prepare_sandbox(runner, "git-filtering").await?; - gitignore_filtering(runner, &sandbox_name, &remote_root, local.path()).await?; - single_file_from_git_repo(runner, &sandbox_name, &remote_root, local.path()).await?; - gitignored_directory_requires_override(runner, &sandbox_name, &remote_root, local.path()) - .await?; - delete_sandbox(runner, &sandbox_name).await - }) -} - -fn run_path_safety(runner: &mut OpenShellRunner) -> ScenarioFuture<'_> { - Box::pin(async move { - let (sandbox_name, remote_root, local) = prepare_sandbox(runner, "path-safety").await?; - reject_workspace_escape(runner, &sandbox_name, &remote_root, local.path()).await?; - download_dash_leading_name(runner, &sandbox_name, &remote_root, local.path()).await?; - delete_sandbox(runner, &sandbox_name).await - }) -} - -async fn prepare_sandbox( - runner: &mut OpenShellRunner, - group: &str, -) -> Result<(String, String, tempfile::TempDir), String> { - let suffix = match group { - "round-trip" => "fr", - "git-filtering" => "fg", - "path-safety" => "fs", - _ => return Err(format!("unknown file-transfer group {group:?}")), - }; - let sandbox_name = format!("ct-{}-{suffix}", runner.id()); - let remote_root = format!("/sandbox/file-transfer-{}-{suffix}", runner.id()); - let local = - tempfile::tempdir().map_err(|error| format!("create temporary directory: {error}"))?; - - runner.track_sandbox(&sandbox_name); - let create = runner - .step(format!("{group}/create")) - .description(format!("sandbox '{sandbox_name}' is created")) - .with_timeout(CREATE_TIMEOUT) - .run(&["sandbox", "create", "--name", &sandbox_name, "--detach"]) - .await - .map_err(|error| error.to_string())?; - create.require_success()?; - - exec( - runner, - &sandbox_name, - &format!("{group}/prepare"), - &format!("mkdir -p '{remote_root}'"), - ) - .await?; - - Ok((sandbox_name, remote_root, local)) -} - -async fn delete_sandbox(runner: &mut OpenShellRunner, sandbox_name: &str) -> Result<(), String> { - let delete = runner - .step("delete") - .description(format!("sandbox '{sandbox_name}' is deleted")) - .with_timeout(COMMAND_TIMEOUT) - .run(&["sandbox", "delete", sandbox_name]) - .await - .map_err(|error| error.to_string())?; - delete.require_success()?; - runner.forget_sandbox(sandbox_name); - Ok(()) -} - -/// Check remote destination types, not just successful transfer status: tar -/// can report success after replacing an empty directory with a regular file. -async fn upload_existing_directories( - runner: &OpenShellRunner, - sandbox: &str, - remote_root: &str, - local_root: &Path, -) -> Result<(), String> { - let source = local_root.join("config.json"); - fs::write(&source, "upload-destination-payload") - .map_err(fs_error("write destination fixture"))?; - let root = format!("{remote_root}/upload destinations"); - exec( - runner, - sandbox, - "upload-destinations/prepare", - &format!( - "mkdir -p '{root}/empty' '{root}/populated' '{root}/target' '{root}/slash' && \ - printf keep > '{root}/populated/keep.txt' && \ - ln -s target '{root}/link'" - ), - ) - .await?; - - for directory in ["empty", "populated", "link", "slash/"] { - let destination = format!("{root}/{directory}"); - upload( - runner, - sandbox, - &format!("upload-destinations/{directory}/upload"), - &source, - &destination, - true, - ) - .await?; - exec( - runner, - sandbox, - &format!("upload-destinations/{directory}/verify"), - &format!( - "test -d '{destination}' && \ - test \"$(cat '{destination}/config.json')\" = upload-destination-payload && \ - test \"$(cat '{root}/populated/keep.txt')\" = keep && \ - test -L '{root}/link'" - ), - ) - .await?; - } - - // A missing destination remains an intentional rename; uploading again - // must replace that file rather than treating it as a directory. - let renamed = format!("{root}/renamed.json"); - for pass in ["rename", "overwrite"] { - upload( - runner, - sandbox, - &format!("upload-destinations/{pass}/upload"), - &source, - &renamed, - true, - ) - .await?; - exec( - runner, - sandbox, - &format!("upload-destinations/{pass}/verify"), - &format!( - "test -f '{renamed}' && test \"$(cat '{renamed}')\" = upload-destination-payload" - ), - ) - .await?; - } - Ok(()) -} - -async fn round_trip( - runner: &OpenShellRunner, - sandbox: &str, - remote_root: &str, - local_root: &Path, -) -> Result<(), String> { - let source = local_root.join("roundtrip-upload"); - fs::create_dir_all(source.join("subdir")).map_err(fs_error("create round-trip source"))?; - fs::write(source.join("greeting.txt"), "hello-from-local") - .map_err(fs_error("write greeting.txt"))?; - fs::write(source.join("subdir/nested.txt"), "nested-content") - .map_err(fs_error("write nested.txt"))?; - - let large = (0u8..=250) - .cycle() - .take(LARGE_FILE_SIZE) - .collect::>(); - fs::write(source.join("large.bin"), &large).map_err(fs_error("write large.bin"))?; - - let remote = format!("{remote_root}/roundtrip"); - let result = - upload_result(runner, sandbox, "roundtrip/upload", &source, &remote, false).await?; - result.require_success()?; - if !result.stderr().contains("outside a Git work tree") - || !result.stderr().contains(".gitignore rules are not applied") - { - return Err( - result.failure_diagnostic("upload outside Git warns that filtering is disabled") - ); - } - - let destination = local_root.join("roundtrip-download"); - fs::create_dir(&destination).map_err(fs_error("create round-trip destination"))?; - let remote_source = format!("{remote}/roundtrip-upload"); - download( - runner, - sandbox, - "roundtrip/download", - &remote_source, - &destination, - ) - .await?; - - require_text( - &destination.join("greeting.txt"), - "hello-from-local", - "round-trip greeting", - )?; - require_text( - &destination.join("subdir/nested.txt"), - "nested-content", - "round-trip nested file", - )?; - let actual = fs::read(destination.join("large.bin")).map_err(fs_error("read large.bin"))?; - if actual != large { - return Err(format!( - "large file changed during round trip: expected {} bytes, received {} bytes", - large.len(), - actual.len() - )); - } - - let single = local_root.join("single.txt"); - fs::write(&single, "single-file-payload").map_err(fs_error("write single.txt"))?; - let remote_single = format!("{remote_root}/single.txt"); - upload( - runner, - sandbox, - "single/upload", - &single, - &remote_single, - false, - ) - .await?; - let single_destination = local_root.join("single-download"); - fs::create_dir(&single_destination).map_err(fs_error("create single-file destination"))?; - download( - runner, - sandbox, - "single/download", - &remote_single, - &single_destination, - ) - .await?; - require_text( - &single_destination.join("single.txt"), - "single-file-payload", - "single-file round trip", - ) -} - -async fn gitignore_filtering( - runner: &OpenShellRunner, - sandbox: &str, - remote_root: &str, - local_root: &Path, -) -> Result<(), String> { - let repository = local_root.join("filter-repo"); - fs::create_dir(&repository).map_err(fs_error("create filter repository"))?; - git_init(&repository).await?; - fs::write(repository.join(".gitignore"), "*.log\nbuild/\n") - .map_err(fs_error("write filter .gitignore"))?; - fs::write(repository.join("tracked.txt"), "i-am-tracked") - .map_err(fs_error("write tracked.txt"))?; - fs::write(repository.join("ignored.log"), "i-should-be-filtered") - .map_err(fs_error("write ignored.log"))?; - fs::create_dir(repository.join("build")).map_err(fs_error("create ignored build directory"))?; - fs::write(repository.join("build/output.bin"), "build-artifact") - .map_err(fs_error("write ignored build artifact"))?; - git(&repository, &["add", "."]).await?; - - let remote = format!("{remote_root}/filtered"); - upload( - runner, - sandbox, - "gitignore/upload", - &repository, - &remote, - false, - ) - .await?; - - let destination = local_root.join("filter-download"); - fs::create_dir(&destination).map_err(fs_error("create filter destination"))?; - download(runner, sandbox, "gitignore/download", &remote, &destination).await?; - let uploaded = destination.join("filter-repo"); - require_text( - &uploaded.join("tracked.txt"), - "i-am-tracked", - "Git-aware tracked file", - )?; - require_exists(&uploaded.join(".gitignore"), "uploaded .gitignore")?; - require_absent(&uploaded.join("ignored.log"), "Git-ignored file")?; - require_absent(&uploaded.join("build"), "Git-ignored directory") -} - -async fn single_file_from_git_repo( - runner: &OpenShellRunner, - sandbox: &str, - remote_root: &str, - local_root: &Path, -) -> Result<(), String> { - let repository = local_root.join("single-repo"); - fs::create_dir_all(repository.join("nested")) - .map_err(fs_error("create single-file repository"))?; - git_init(&repository).await?; - fs::write(repository.join(".gitignore"), "*.log\n") - .map_err(fs_error("write single-file .gitignore"))?; - fs::write( - repository.join("nested/config.txt"), - "single-file-from-repo", - ) - .map_err(fs_error("write repository config.txt"))?; - fs::write(repository.join("tracked.txt"), "should-not-upload") - .map_err(fs_error("write unrelated tracked.txt"))?; - fs::write(repository.join("ignored.log"), "ignored") - .map_err(fs_error("write repository ignored.log"))?; - - let remote = format!("{remote_root}/single-from-repo"); - upload( - runner, - sandbox, - "single-from-repo/upload", - &repository.join("nested/config.txt"), - &remote, - false, - ) - .await?; - let destination = local_root.join("single-repo-download"); - fs::create_dir(&destination).map_err(fs_error("create single-repo destination"))?; - download( - runner, - sandbox, - "single-from-repo/download", - &remote, - &destination, - ) - .await?; - require_text( - &destination.join("config.txt"), - "single-file-from-repo", - "single file selected from repository", - )?; - require_absent( - &destination.join("tracked.txt"), - "unselected repository file", - )?; - require_absent(&destination.join("ignored.log"), "ignored repository file") -} - -async fn download_file( - runner: &OpenShellRunner, - sandbox: &str, - remote_root: &str, - local_root: &Path, -) -> Result<(), String> { - let remote = format!("{remote_root}/download-file.txt"); - exec( - runner, - sandbox, - "download-file/seed", - &format!("printf greeting-payload > '{remote}'"), - ) - .await?; - let destination = local_root.join("download-file"); - fs::create_dir(&destination).map_err(fs_error("create file-download destination"))?; - download( - runner, - sandbox, - "download-file/download", - &remote, - &destination, - ) - .await?; - require_text( - &destination.join("download-file.txt"), - "greeting-payload", - "downloaded file", - ) -} - -async fn download_directory( - runner: &OpenShellRunner, - sandbox: &str, - remote_root: &str, - local_root: &Path, -) -> Result<(), String> { - let remote = format!("{remote_root}/tree"); - exec( - runner, - sandbox, - "download-directory/seed", - &format!( - "mkdir -p '{remote}/sub' && printf top-level > '{remote}/root.txt' && printf nested > '{remote}/sub/child.txt'" - ), - ) - .await?; - let destination = local_root.join("download-directory"); - fs::create_dir(&destination).map_err(fs_error("create directory-download destination"))?; - download( - runner, - sandbox, - "download-directory/download", - &remote, - &destination, - ) - .await?; - require_text( - &destination.join("root.txt"), - "top-level", - "downloaded directory root file", - )?; - require_text( - &destination.join("sub/child.txt"), - "nested", - "downloaded directory nested file", - ) -} - -async fn reject_workspace_escape( - runner: &OpenShellRunner, - sandbox: &str, - remote_root: &str, - local_root: &Path, -) -> Result<(), String> { - let etc_link = format!("{remote_root}/etc-link"); - let passwd_link = format!("{remote_root}/passwd-link"); - exec( - runner, - sandbox, - "workspace-escape/seed", - &format!("ln -s /etc '{etc_link}' && ln -s /etc/passwd '{passwd_link}'"), - ) - .await?; - let destination = local_root.join("workspace-escape"); - fs::create_dir(&destination).map_err(fs_error("create workspace-escape destination"))?; - - for (step, source) in [ - ("directory-link", etc_link.clone()), - ("file-link", passwd_link), - ("linked-component", format!("{etc_link}/passwd")), - ] { - let result = download_result( - runner, - sandbox, - &format!("workspace-escape/{step}"), - &source, - &destination, - ) - .await?; - if result.success() { - return Err(format!( - "download unexpectedly accepted sandbox path {source:?} that resolves outside the workspace" - )); - } - let diagnostic = format!("{}\n{}", result.stdout(), result.stderr()); - if !diagnostic.contains("resolves to") - || !diagnostic.contains("outside the") - || !diagnostic.contains("sandbox workspace") - { - return Err(result.failure_diagnostic( - "download is rejected because the resolved source is outside the sandbox workspace", - )); - } - } - require_absent(&destination.join("passwd"), "escaped passwd file")?; - require_absent(&destination.join("etc-link"), "escaped /etc directory") -} - -async fn download_dash_leading_name( - runner: &OpenShellRunner, - sandbox: &str, - remote_root: &str, - local_root: &Path, -) -> Result<(), String> { - let remote = format!("{remote_root}/--checkpoint-action=evil"); - exec( - runner, - sandbox, - "dash-leading/seed", - &format!("printf dash-payload > '{remote}'"), - ) - .await?; - let destination = local_root.join("dash-leading"); - fs::create_dir(&destination).map_err(fs_error("create dash-leading destination"))?; - download( - runner, - sandbox, - "dash-leading/download", - &remote, - &destination, - ) - .await?; - require_text( - &destination.join("--checkpoint-action=evil"), - "dash-payload", - "dash-leading file", - ) -} - -async fn gitignored_directory_requires_override( - runner: &OpenShellRunner, - sandbox: &str, - remote_root: &str, - local_root: &Path, -) -> Result<(), String> { - let remote_seed = format!("{remote_root}/runs/test.json"); - exec( - runner, - sandbox, - "gitignored-override/seed", - &format!("mkdir -p '{remote_root}/runs' && printf downloaded-payload > '{remote_seed}'"), - ) - .await?; - - let repository = local_root.join("override-repo"); - fs::create_dir(&repository).map_err(fs_error("create override repository"))?; - git_init(&repository).await?; - fs::write(repository.join(".gitignore"), "runs/\n") - .map_err(fs_error("write override .gitignore"))?; - let runs = repository.join("runs"); - fs::create_dir(&runs).map_err(fs_error("create ignored runs directory"))?; - download( - runner, - sandbox, - "gitignored-override/download-seed", - &remote_seed, - &runs, - ) - .await?; - require_exists(&runs.join("test.json"), "downloaded ignored file")?; - - let remote = format!("{remote_root}/reuploaded"); - let rejected = upload_result( - runner, - sandbox, - "gitignored-override/reject-upload", - &runs, - &remote, - false, - ) - .await?; - let output = format!("{}\n{}", rejected.stdout(), rejected.stderr()); - if rejected.success() - || !output.contains("filtering selected no files") - || !output.contains("--no-git-ignore") - { - return Err(rejected.failure_diagnostic( - "upload rejects an empty Git selection and explains the explicit override", - )); - } - exec( - runner, - sandbox, - "gitignored-override/no-transfer", - &format!("test ! -e '{remote}'"), - ) - .await?; - upload( - runner, - sandbox, - "gitignored-override/upload", - &runs, - &remote, - true, - ) - .await?; - - let destination = local_root.join("override-download"); - fs::create_dir(&destination).map_err(fs_error("create override destination"))?; - download( - runner, - sandbox, - "gitignored-override/download", - &remote, - &destination, - ) - .await?; - require_text( - &destination.join("runs/test.json"), - "downloaded-payload", - "re-uploaded Git-ignored file", - ) -} - -async fn upload( - runner: &OpenShellRunner, - sandbox: &str, - step: &str, - source: &Path, - destination: &str, - no_git_ignore: bool, -) -> Result<(), String> { - let result = upload_result(runner, sandbox, step, source, destination, no_git_ignore).await?; - result.require_success() -} - -async fn upload_result( - runner: &OpenShellRunner, - sandbox: &str, - step: &str, - source: &Path, - destination: &str, - no_git_ignore: bool, -) -> Result { - let source = source - .to_str() - .ok_or_else(|| format!("local upload path is not UTF-8: {}", source.display()))?; - let mut args = vec!["sandbox", "upload", sandbox, source, destination]; - if no_git_ignore { - args.push("--no-git-ignore"); - } - runner - .step(step) - .description(format!("upload {source:?} to {destination:?}")) - .with_timeout(TRANSFER_TIMEOUT) - .run(&args) - .await - .map_err(|error| error.to_string()) -} - -async fn download( - runner: &OpenShellRunner, - sandbox: &str, - step: &str, - source: &str, - destination: &Path, -) -> Result<(), String> { - let result = download_result(runner, sandbox, step, source, destination).await?; - result.require_success() -} - -async fn download_result( - runner: &OpenShellRunner, - sandbox: &str, - step: &str, - source: &str, - destination: &Path, -) -> Result { - let destination = destination.to_str().ok_or_else(|| { - format!( - "local download path is not UTF-8: {}", - destination.display() - ) - })?; - runner - .step(step) - .description(format!( - "download {source:?} to {destination:?} has the expected disposition" - )) - .with_timeout(TRANSFER_TIMEOUT) - .run(&["sandbox", "download", sandbox, source, destination]) - .await - .map_err(|error| error.to_string()) -} - -async fn exec( - runner: &OpenShellRunner, - sandbox: &str, - step: &str, - script: &str, -) -> Result<(), String> { - let result = runner - .step(step) - .description(format!("sandbox fixture setup for {step} succeeds")) - .with_timeout(COMMAND_TIMEOUT) - .run(&[ - "sandbox", "exec", "--name", sandbox, "--no-tty", "--", "sh", "-c", script, - ]) - .await - .map_err(|error| error.to_string())?; - result.require_success() -} - -async fn git_init(repository: &Path) -> Result<(), String> { - git(repository, &["init", "--quiet"]).await -} - -async fn git(repository: &Path, args: &[&str]) -> Result<(), String> { - let output = Command::new("git") - .args(args) - .current_dir(repository) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .await - .map_err(|error| format!("failed to run git in {}: {error}", repository.display()))?; - if output.status.success() { - return Ok(()); - } - Err(format!( - "git {} failed in {} with status {}\nstdout:\n{}\nstderr:\n{}", - args.join(" "), - repository.display(), - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - )) -} - -fn require_text(path: &Path, expected: &str, label: &str) -> Result<(), String> { - let actual = fs::read_to_string(path) - .map_err(|error| format!("read {label} at {}: {error}", path.display()))?; - if actual == expected { - Ok(()) - } else { - Err(format!( - "{label} content mismatch at {}: expected {expected:?}, received {actual:?}", - path.display() - )) - } -} - -fn require_exists(path: &Path, label: &str) -> Result<(), String> { - if path.exists() { - Ok(()) - } else { - Err(format!("{label} does not exist at {}", path.display())) - } -} - -fn require_absent(path: &Path, label: &str) -> Result<(), String> { - if path.exists() { - Err(format!("{label} unexpectedly exists at {}", path.display())) - } else { - Ok(()) - } -} - -fn fs_error(context: &'static str) -> impl FnOnce(std::io::Error) -> String { - move |error| format!("{context}: {error}") -} diff --git a/crates/openshell-conformance/src/scenarios/mod.rs b/crates/openshell-conformance/src/scenarios/mod.rs deleted file mode 100644 index 72a90925f0..0000000000 --- a/crates/openshell-conformance/src/scenarios/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Portable conformance scenarios exercised by the Cargo test suite. - -mod file_transfer; -mod policy_behavior; -mod sandbox_lifecycle; -mod smoke; - -pub use file_transfer::{ - FILE_TRANSFER_GIT_FILTERING_SCENARIO, FILE_TRANSFER_PATH_SAFETY_SCENARIO, - FILE_TRANSFER_ROUND_TRIP_SCENARIO, -}; -pub use policy_behavior::{ - MECHANISTIC_PROPOSAL_SCENARIO, NEW_HOSTNAME_PROPOSAL_SCENARIO, POLICY_LOCAL_SCENARIO, -}; -pub use sandbox_lifecycle::SANDBOX_LIFECYCLE_SCENARIO; -pub use smoke::SMOKE_SCENARIO; diff --git a/crates/openshell-conformance/src/scenarios/policy_behavior.rs b/crates/openshell-conformance/src/scenarios/policy_behavior.rs deleted file mode 100644 index 386b8e0f0b..0000000000 --- a/crates/openshell-conformance/src/scenarios/policy_behavior.rs +++ /dev/null @@ -1,579 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Portable policy-local and mechanistic proposal checks. - -use std::io::Write as _; -use std::time::{Duration, Instant}; - -use serde::Deserialize; -use serde_json::Value; -use tempfile::NamedTempFile; -use tokio::time::sleep; - -use crate::{OpenShellRunner, Scenario, ScenarioFuture}; - -const CREATE_TIMEOUT: Duration = Duration::from_mins(10); -const COMMAND_TIMEOUT: Duration = Duration::from_secs(45); -const READY_TIMEOUT: Duration = Duration::from_mins(4); -const POLL_INTERVAL: Duration = Duration::from_secs(2); -const PROPOSAL_TIMEOUT: Duration = Duration::from_secs(90); - -#[derive(Deserialize)] -struct SandboxState { - name: String, - phase: String, -} - -pub const POLICY_LOCAL_SCENARIO: Scenario = Scenario { - name: "policy-local", - run: run_policy_local, -}; - -pub const MECHANISTIC_PROPOSAL_SCENARIO: Scenario = Scenario { - name: "mechanistic-proposal", - run: run_mechanistic_proposal, -}; - -pub const NEW_HOSTNAME_PROPOSAL_SCENARIO: Scenario = Scenario { - name: "new-hostname-proposal", - run: run_new_hostname_proposal, -}; - -const EMPTY_NETWORK_POLICY: &[u8] = br"version: 1 -filesystem_policy: - include_workdir: true - read_only: [/usr, /bin, /lib, /lib64, /proc, /dev/urandom, /app, /etc, /var/log] - read_write: [/sandbox, /tmp, /dev/null] -landlock: { compatibility: best_effort } -network_policies: {} -"; - -fn run_policy_local(runner: &mut OpenShellRunner) -> ScenarioFuture<'_> { - Box::pin(async move { - let name = format!("ct-{}-pl", runner.id()); - create_sandbox(runner, &name, None).await?; - enable_proposals(runner, &name).await?; - let binary = sandbox_bash_path(runner, &name).await?; - - let started = Instant::now(); - let readiness_path = format!("/v1/proposals/ct-{}-readiness", runner.id()); - loop { - match request_policy_local(runner, &name, "/v1/policy/current").await { - Ok(response) - if response["format"] == "yaml" - && response["policy_yaml"] - .as_str() - .is_some_and(|yaml| yaml.contains("version: 1")) => - { - // The current-policy route is local; proposal submission also - // needs the supervisor's workspace and gateway lookup session. - match request_policy_local_http(runner, &name, "GET", &readiness_path, "", 404) - .await - { - Ok(lookup) if lookup["error"] == "chunk_not_found" => break, - Ok(lookup) => { - return Err(format!( - "policy.local proposal lookup returned an invalid readiness response: {lookup}" - )); - } - Err(error) if started.elapsed() >= READY_TIMEOUT => return Err(error), - Err(_) => {} - } - } - Ok(response) => { - return Err(format!( - "policy.local returned an invalid current policy: {response}" - )); - } - Err(error) => { - if started.elapsed() >= READY_TIMEOUT { - return Err(error); - } - } - } - sleep(POLL_INTERVAL).await; - } - let denials = request_policy_local(runner, &name, "/v1/denials?last=1").await?; - if !denials["denials"].is_array() || !denials["log_available"].is_boolean() { - return Err(format!( - "policy.local returned an invalid denials response: {denials}" - )); - } - - let rule_name = format!("conformance_local_{}", runner.id()); - let payload = serde_json::json!({ - "intent_summary": "Allow Bash to read the conformance path on example.invalid.", - "operations": [{ - "addRule": { - "ruleName": &rule_name, - "rule": { - "name": &rule_name, - "endpoints": [{ - "host": "example.invalid", - "port": 443, - "protocol": "rest", - "enforcement": "enforce", - "rules": [{"allow": {"method": "GET", "path": "/conformance"}}] - }], - "binaries": [{"path": &binary}] - } - } - }] - }) - .to_string(); - let submitted = - request_policy_local_http(runner, &name, "POST", "/v1/proposals", &payload, 202) - .await?; - let chunk_id = submitted["accepted_chunk_ids"] - .as_array() - .filter(|ids| ids.len() == 1) - .and_then(|ids| ids[0].as_str()) - .filter(|id| !id.is_empty()) - .ok_or_else(|| format!("policy.local did not accept one proposal: {submitted}"))?; - if submitted["status"] != "submitted" - || submitted["accepted_chunks"] != 1 - || submitted["rejected_chunks"] != 0 - { - return Err(format!("policy.local did not submit one rule: {submitted}")); - } - - let state = - request_policy_local(runner, &name, &format!("/v1/proposals/{chunk_id}")).await?; - if state["chunk_id"] != chunk_id - || state["rule_name"] != rule_name - || state["binary"] != binary - || !matches!(state["status"].as_str(), Some("pending" | "approved")) - { - return Err(format!("policy.local returned the wrong proposal: {state}")); - } - - let review = runner - .step("reviewer-inbox") - .description("the requested rule is visible to the reviewer") - .with_timeout(COMMAND_TIMEOUT) - .run(&["rule", "get", &name]) - .await - .map_err(|error| error.to_string())?; - review.require_success()?; - if !review.stdout().contains(&format!("Chunk: {chunk_id}")) - || !review.stdout().contains(&format!("Rule: {rule_name}")) - { - return Err(review.failure_diagnostic("the submitted rule is in the reviewer inbox")); - } - Ok(()) - }) -} - -async fn sandbox_bash_path(runner: &OpenShellRunner, name: &str) -> Result { - let result = runner - .step("bash-binary") - .description("the sandbox's Bash executable has a canonical path") - .with_timeout(COMMAND_TIMEOUT) - .run(&[ - "sandbox", - "exec", - "--name", - name, - "--no-tty", - "--", - "bash", - "-c", - "printf '%s\\n' \"$(readlink -f /proc/$$/exe)\"", - ]) - .await - .map_err(|error| error.to_string())?; - result.require_success()?; - let binary = result.stdout().trim(); - if !binary.starts_with('/') - || binary.contains('\n') - || binary.rsplit('/').next() != Some("bash") - { - return Err(result.failure_diagnostic("one absolute Bash executable path ending in /bash")); - } - Ok(binary.to_string()) -} - -async fn enable_proposals(runner: &OpenShellRunner, name: &str) -> Result<(), String> { - let set = runner - .step("enable-policy-advisor") - .description("sandbox-scoped policy advisor setting is enabled") - .with_timeout(COMMAND_TIMEOUT) - .run(&[ - "settings", - "set", - name, - "--key", - "agent_policy_proposals_enabled", - "--value", - "true", - ]) - .await - .map_err(|error| error.to_string())?; - set.require_success()?; - - let settings = runner - .step("effective-settings") - .description("policy advisor setting is effectively enabled") - .with_timeout(COMMAND_TIMEOUT) - .run(&["settings", "get", name, "--json"]) - .await - .map_err(|error| error.to_string())?; - settings.require_success()?; - let value: Value = settings.json().map_err(|error| error.to_string())?; - if value["settings"]["agent_policy_proposals_enabled"]["value"] != "true" { - return Err(settings.failure_diagnostic( - "effective agent_policy_proposals_enabled is true; check for a global override", - )); - } - Ok(()) -} - -async fn request_policy_local( - runner: &OpenShellRunner, - sandbox: &str, - path: &str, -) -> Result { - request_policy_local_http(runner, sandbox, "GET", path, "", 200).await -} - -async fn request_policy_local_http( - runner: &OpenShellRunner, - sandbox: &str, - method: &str, - path: &str, - body: &str, - expected_status: u16, -) -> Result { - let script = "method=$1; path=$2; body=$3; exec 3<>/dev/tcp/policy.local/80 || exit 1; printf '%s %s HTTP/1.1\\r\\nHost: policy.local\\r\\nContent-Type: application/json\\r\\nContent-Length: %s\\r\\nConnection: close\\r\\n\\r\\n' \"$method\" \"$path\" \"${#body}\" >&3; printf '%s' \"$body\" >&3; cat <&3"; - let result = runner - .step(format!("policy-local-{method}{path}")) - .description(format!( - "{method} http://policy.local{path} succeeds from the sandbox" - )) - .with_timeout(COMMAND_TIMEOUT) - .run(&[ - "sandbox", - "exec", - "--name", - sandbox, - "--no-tty", - "--", - "bash", - "-c", - script, - "policy-local-http", - method, - path, - body, - ]) - .await - .map_err(|error| error.to_string())?; - result.require_success()?; - let (headers, body) = result.stdout().split_once("\r\n\r\n").ok_or_else(|| { - result.failure_diagnostic("a complete HTTP response with headers and JSON body") - })?; - if !headers.starts_with(&format!("HTTP/1.1 {expected_status} ")) - || !headers.lines().any(|line| { - line.to_ascii_lowercase() - .starts_with("content-type: application/json") - }) - { - return Err( - result.failure_diagnostic(&format!("HTTP {expected_status} with JSON Content-Type")) - ); - } - serde_json::from_str(body) - .map_err(|error| result.failure_diagnostic(&format!("valid JSON body: {error}"))) -} - -fn run_mechanistic_proposal(runner: &mut OpenShellRunner) -> ScenarioFuture<'_> { - Box::pin(async move { - let mut policy = NamedTempFile::new().map_err(|error| error.to_string())?; - policy - .write_all(EMPTY_NETWORK_POLICY) - .map_err(|error| error.to_string())?; - let policy_path = policy - .path() - .to_str() - .ok_or("temporary policy path is not UTF-8")?; - let name = format!("ct-{}-mp", runner.id()); - create_sandbox(runner, &name, Some(policy_path)).await?; - enable_proposals(runner, &name).await?; - - let effective = runner - .step("effective-policy") - .description("sandbox has no network allow rules before the probe") - .with_timeout(COMMAND_TIMEOUT) - .run(&["policy", "get", &name, "--full", "--output", "json"]) - .await - .map_err(|error| error.to_string())?; - effective.require_success()?; - let value: Value = effective.json().map_err(|error| error.to_string())?; - let network_rules = &value["policy"]["network_policies"]; - if !network_rules.is_null() - && !network_rules - .as_object() - .is_some_and(serde_json::Map::is_empty) - { - return Err(effective.failure_diagnostic("effective network_policies is empty")); - } - - let probe = runner - .step("denied-tcp-open") - .description("one Bash TCP open to 1.1.1.1:443 is denied by policy") - .with_timeout(COMMAND_TIMEOUT) - .run(&[ - "sandbox", - "exec", - "--name", - &name, - "--no-tty", - "--", - "bash", - "-c", - "printf 'BINARY=%s\\n' \"$(readlink -f /proc/$$/exe)\"; if exec 3<>/dev/tcp/1.1.1.1/443; then echo UNEXPECTED_ALLOWED; exit 1; else echo DENIED; fi", - ]) - .await - .map_err(|error| error.to_string())?; - probe.require_success()?; - let binary = probe - .stdout() - .lines() - .find_map(|line| line.strip_prefix("BINARY=")) - .filter(|binary| binary.starts_with('/') && binary.rsplit('/').next() == Some("bash")) - .ok_or_else(|| probe.failure_diagnostic("canonical Bash executable path is reported"))? - .to_string(); - if !probe.stdout().lines().any(|line| line == "DENIED") { - return Err(probe.failure_diagnostic("TCP open is denied before any upstream dial")); - } - - await_mechanistic_draft( - runner, - &name, - &ExpectedDraft { - rule: "allow_1_1_1_1_443", - endpoint: "1.1.1.1:443", - binary: &binary, - }, - probe.stderr(), - ) - .await - }) -} - -fn run_new_hostname_proposal(runner: &mut OpenShellRunner) -> ScenarioFuture<'_> { - Box::pin(async move { - let mut policy = NamedTempFile::new().map_err(|error| error.to_string())?; - policy - .write_all(EMPTY_NETWORK_POLICY) - .map_err(|error| error.to_string())?; - let policy_path = policy - .path() - .to_str() - .ok_or("temporary policy path is not UTF-8")?; - let name = format!("ct-{}-nh", runner.id()); - create_sandbox(runner, &name, Some(policy_path)).await?; - let binary = sandbox_bash_path(runner, &name).await?; - - let probe = runner - .step("denied-new-hostname") - .description("Bash cannot connect to pypi.org:80 before approval") - .with_timeout(COMMAND_TIMEOUT) - .run(&[ - "sandbox", - "exec", - "--name", - &name, - "--no-tty", - "--", - "bash", - "-c", - "if exec 3<>/dev/tcp/pypi.org/80; then echo UNEXPECTED_ALLOWED; exit 1; else echo DENIED; fi", - ]) - .await - .map_err(|error| error.to_string())?; - probe.require_success()?; - if !probe.stdout().lines().any(|line| line == "DENIED") { - return Err(probe.failure_diagnostic("new hostname stays denied before approval")); - } - - await_mechanistic_draft( - runner, - &name, - &ExpectedDraft { - rule: "allow_pypi_org_80", - endpoint: "pypi.org:80", - binary: &binary, - }, - probe.stderr(), - ) - .await - }) -} - -/// The single L4 mechanistic draft expected for one denied endpoint. -struct ExpectedDraft<'a> { - rule: &'a str, - endpoint: &'a str, - binary: &'a str, -} - -/// Poll the reviewer inbox until a draft appears, tolerating transient read -/// failures, then require that it is the single expected draft. -async fn await_mechanistic_draft( - runner: &OpenShellRunner, - sandbox: &str, - expected: &ExpectedDraft<'_>, - probe_stderr: &str, -) -> Result<(), String> { - let started = Instant::now(); - loop { - let draft = runner - .step("mechanistic-draft") - .description("a single scoped mechanistic draft appears") - .with_timeout(COMMAND_TIMEOUT) - .run(&["rule", "get", sandbox]) - .await - .map_err(|error| error.to_string())?; - if !draft.success() { - if started.elapsed() >= PROPOSAL_TIMEOUT { - return Err(draft.failure_diagnostic("the reviewer inbox is readable")); - } - sleep(POLL_INTERVAL).await; - continue; - } - if !draft.stdout().contains("Chunk:") { - if started.elapsed() >= PROPOSAL_TIMEOUT { - return Err(draft.failure_diagnostic(&format!( - "one mechanistic draft for {} and {}; probe stderr:\n{probe_stderr}", - expected.endpoint, expected.binary - ))); - } - sleep(POLL_INTERVAL).await; - continue; - } - return assert_mechanistic_draft(draft.stdout(), expected) - .map_err(|error| draft.failure_diagnostic(&error)); - } -} - -fn assert_mechanistic_draft(output: &str, expected: &ExpectedDraft<'_>) -> Result<(), String> { - let fields = output.lines().map(str::trim).collect::>(); - let field = |name: &str| { - fields - .iter() - .find_map(|line| line.strip_prefix(name).map(str::trim)) - }; - let endpoints = format!("{} [L4]", expected.endpoint); - if fields - .iter() - .filter(|line| line.starts_with("Chunk:")) - .count() - != 1 - || !matches!(field("Status:"), Some("pending" | "approved")) - || field("Rule:") != Some(expected.rule) - || field("Binary:") != Some(expected.binary) - || field("Binaries:") != Some(expected.binary) - || field("Endpoints:") != Some(endpoints.as_str()) - || !field("Rationale:").is_some_and(|value| value.contains(expected.endpoint)) - { - return Err(format!( - "expected one pending or approved L4 mechanistic draft scoped to {} and {}", - expected.binary, expected.endpoint - )); - } - Ok(()) -} - -async fn create_sandbox( - runner: &mut OpenShellRunner, - name: &str, - policy_path: Option<&str>, -) -> Result<(), String> { - runner.track_sandbox(name); - let mut args = vec![ - "sandbox", - "create", - "--name", - name, - "--detach", - "--no-tty", - "--no-auto-providers", - ]; - if let Some(path) = policy_path { - args.extend(["--policy", path]); - } - args.extend(["--", "sh", "-c", "exec sleep infinity"]); - let create = runner - .step("create") - .description(format!("sandbox '{name}' is created")) - .with_timeout(CREATE_TIMEOUT) - .run(&args) - .await - .map_err(|error| error.to_string())?; - create.require_success()?; - - let started = Instant::now(); - loop { - let get = runner - .step("ready") - .description(format!("sandbox '{name}' reaches Ready")) - .with_timeout(COMMAND_TIMEOUT) - .run(&["sandbox", "get", name, "--output", "json"]) - .await - .map_err(|error| error.to_string())?; - if get.success() { - let state: SandboxState = get.json().map_err(|error| error.to_string())?; - if state.name != name { - return Err(get.failure_diagnostic("sandbox get returns the created name")); - } - if state.phase == "Ready" { - return Ok(()); - } - if state.phase == "Failed" { - return Err(get.failure_diagnostic("sandbox reaches Ready instead of Failed")); - } - } - if started.elapsed() >= READY_TIMEOUT { - return Err(get.failure_diagnostic("sandbox reaches Ready before timeout")); - } - sleep(POLL_INTERVAL).await; - } -} - -#[cfg(test)] -mod tests { - use super::{ExpectedDraft, assert_mechanistic_draft}; - - const EXPECTED: ExpectedDraft<'static> = ExpectedDraft { - rule: "allow_pypi_org_80", - endpoint: "pypi.org:80", - binary: "/usr/bin/bash", - }; - - fn draft(binary: &str) -> String { - format!( - "Chunk: id\nStatus: pending\nRule: allow_pypi_org_80\nBinary: {binary}\nRationale: Allow {binary} to connect to pypi.org:80 (HTTP).\nEndpoints: pypi.org:80 [L4]\nBinaries: {binary}\n" - ) - } - - #[test] - fn draft_assertion_accepts_a_hostname_scoped_draft() { - assert!(assert_mechanistic_draft(&draft("/usr/bin/bash"), &EXPECTED).is_ok()); - } - - #[test] - fn draft_assertion_rejects_unrelated_binary() { - assert!(assert_mechanistic_draft(&draft("/usr/bin/sh"), &EXPECTED).is_err()); - } - - #[test] - fn draft_assertion_rejects_fields_spread_across_drafts() { - let drafts = format!( - "{}Chunk: other\nStatus: pending\nRule: allow_1_1_1_1_443\n", - draft("/usr/bin/bash") - ); - assert!(assert_mechanistic_draft(&drafts, &EXPECTED).is_err()); - } -} diff --git a/crates/openshell-conformance/src/scenarios/sandbox_lifecycle.rs b/crates/openshell-conformance/src/scenarios/sandbox_lifecycle.rs deleted file mode 100644 index a0b1c0016d..0000000000 --- a/crates/openshell-conformance/src/scenarios/sandbox_lifecycle.rs +++ /dev/null @@ -1,347 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Portable sandbox lifecycle conformance scenarios. - -use std::collections::HashSet; -use std::time::{Duration, Instant}; - -use serde::Deserialize; - -use crate::{OpenShellRunner, Poll, Scenario, ScenarioFuture}; - -const CREATE_TIMEOUT: Duration = Duration::from_mins(10); -const COMMAND_TIMEOUT: Duration = Duration::from_mins(2); -const TRANSITION_TIMEOUT: Duration = Duration::from_mins(4); -const TRANSITION_INTERVAL: Duration = Duration::from_secs(2); - -#[derive(Debug, Deserialize)] -struct SandboxState { - id: String, - name: String, - phase: String, -} - -#[derive(Debug, Deserialize)] -struct SandboxListPage { - sandboxes: Vec, - next_page_token: String, -} - -/// Certify sandbox stop, start, and deletion lifecycle behavior. -pub const SANDBOX_LIFECYCLE_SCENARIO: Scenario = Scenario { - name: "sandbox-lifecycle", - run: run_sandbox_lifecycle, -}; - -fn run_sandbox_lifecycle(runner: &mut OpenShellRunner) -> ScenarioFuture<'_> { - Box::pin(async move { - stop_start_preserves_workspace(runner).await?; - stopped_can_be_deleted(runner).await - }) -} - -async fn stop_start_preserves_workspace(runner: &mut OpenShellRunner) -> Result<(), String> { - let sandbox_name = format!("ct-{}-ss", runner.id()); - let sentinel = format!("openshell-stop-start-{}", runner.id()); - let sentinel_path = "/sandbox/.openshell-stop-start-sentinel"; - let run_count_path = "/sandbox/.openshell-main-run-count"; - let main = format!( - "count=0; test ! -f '{run_count_path}' || count=$(cat '{run_count_path}'); \ - count=$((count + 1)); printf '%s\\n' \"$count\" > '{run_count_path}'; \ - exec sleep infinity" - ); - - create_running_sandbox(runner, &sandbox_name, &main, "stop-start").await?; - exec_expect_exact( - runner, - &sandbox_name, - "write-sentinel", - &[ - "sh", - "-lc", - &format!("printf '%s\\n' '{sentinel}' > '{sentinel_path}' && sync"), - ], - "", - ) - .await?; - - run_lifecycle_command(runner, "stop", &sandbox_name, "stop-start/stop").await?; - wait_for_phase(runner, &sandbox_name, "Stopped", "stop-start/stopped").await?; - - let stopped_exec = runner - .step("stop-start/exec-while-stopped") - .description(format!( - "sandbox '{sandbox_name}' rejects exec while stopped" - )) - .with_timeout(COMMAND_TIMEOUT) - .run(&[ - "sandbox", - "exec", - "--name", - &sandbox_name, - "--no-tty", - "--", - "cat", - sentinel_path, - ]) - .await - .map_err(|error| error.to_string())?; - if stopped_exec.success() { - return Err( - stopped_exec.failure_diagnostic("sandbox exec fails while the sandbox is stopped") - ); - } - - run_lifecycle_command(runner, "start", &sandbox_name, "stop-start/start").await?; - wait_for_phase(runner, &sandbox_name, "Ready", "stop-start/restarted").await?; - - exec_expect_exact( - runner, - &sandbox_name, - "read-sentinel", - &["cat", sentinel_path], - &format!("{sentinel}\n"), - ) - .await?; - exec_expect_exact( - runner, - &sandbox_name, - "read-main-run-count", - &["cat", run_count_path], - "2\n", - ) - .await -} - -async fn stopped_can_be_deleted(runner: &mut OpenShellRunner) -> Result<(), String> { - let sandbox_name = format!("ct-{}-sd", runner.id()); - create_running_sandbox( - runner, - &sandbox_name, - "exec sleep infinity", - "stopped-delete", - ) - .await?; - - run_lifecycle_command(runner, "stop", &sandbox_name, "stopped-delete/stop").await?; - let sandbox = - wait_for_phase(runner, &sandbox_name, "Stopped", "stopped-delete/stopped").await?; - run_lifecycle_command(runner, "delete", &sandbox_name, "stopped-delete/delete").await?; - wait_for_absence(runner, &sandbox.id, &sandbox_name, "stopped-delete/deleted").await?; - runner.forget_sandbox(&sandbox_name); - Ok(()) -} - -async fn create_running_sandbox( - runner: &mut OpenShellRunner, - sandbox_name: &str, - main: &str, - step: &str, -) -> Result<(), String> { - runner.track_sandbox(sandbox_name); - let create = runner - .step(format!("{step}/create")) - .description(format!("sandbox '{sandbox_name}' is created")) - .with_timeout(CREATE_TIMEOUT) - .run(&[ - "sandbox", - "create", - "--name", - sandbox_name, - "--detach", - "--no-tty", - "--", - "sh", - "-lc", - main, - ]) - .await - .map_err(|error| error.to_string())?; - create.require_success()?; - wait_for_phase(runner, sandbox_name, "Ready", &format!("{step}/ready")) - .await - .map(|_| ()) -} - -async fn run_lifecycle_command( - runner: &OpenShellRunner, - operation: &str, - sandbox_name: &str, - step: &str, -) -> Result<(), String> { - let result = runner - .step(step) - .description(format!("sandbox '{sandbox_name}' {operation} succeeds")) - .with_timeout(COMMAND_TIMEOUT) - .run(&["sandbox", operation, sandbox_name]) - .await - .map_err(|error| error.to_string())?; - result.require_success() -} - -async fn exec_expect_exact( - runner: &OpenShellRunner, - sandbox_name: &str, - step: &str, - command: &[&str], - expected_stdout: &str, -) -> Result<(), String> { - let mut args = vec!["sandbox", "exec", "--name", sandbox_name, "--no-tty", "--"]; - args.extend_from_slice(command); - let result = runner - .step(format!("stop-start/{step}")) - .description(format!("sandbox '{sandbox_name}' exec {step} succeeds")) - .with_timeout(COMMAND_TIMEOUT) - .run(&args) - .await - .map_err(|error| error.to_string())?; - result.require_success()?; - if result.stdout() == expected_stdout { - Ok(()) - } else { - Err(result.failure_diagnostic(&format!("stdout is exactly {expected_stdout:?}"))) - } -} - -async fn wait_for_phase( - runner: &mut OpenShellRunner, - sandbox_name: &str, - expected_phase: &str, - step: &str, -) -> Result { - let sandbox_name = sandbox_name.to_string(); - let expected_phase = expected_phase.to_string(); - let step = step.to_string(); - let poll_step = step.clone(); - runner - .poll_until( - &poll_step, - TRANSITION_TIMEOUT, - TRANSITION_INTERVAL, - async move |runner| { - let result = runner - .step(format!("{step}/get")) - .description(format!( - "sandbox '{sandbox_name}' reaches phase {expected_phase}" - )) - .with_timeout(COMMAND_TIMEOUT) - .run(&["sandbox", "get", &sandbox_name, "--output", "json"]) - .await; - match result { - Ok(result) if !result.success() => { - Poll::Pending(result.failure_diagnostic(&format!( - "sandbox '{sandbox_name}' can be retrieved" - ))) - } - Ok(result) => match result.json::() { - Ok(state) if state.name != sandbox_name => Poll::Failed(format!( - "sandbox get returned {:?}; expected '{sandbox_name}'", - state.name - )), - Ok(state) if state.phase == expected_phase => Poll::Ready(state), - Ok(state) => Poll::Pending(format!( - "sandbox '{sandbox_name}' phase is {:?}; expected {expected_phase:?}", - state.phase - )), - Err(error) => Poll::Failed(error.to_string()), - }, - Err(error) => Poll::Pending(error.to_string()), - } - }, - ) - .await - .map_err(|error| error.to_string()) -} - -async fn wait_for_absence( - runner: &mut OpenShellRunner, - sandbox_id: &str, - sandbox_name: &str, - step: &str, -) -> Result<(), String> { - let sandbox_id = sandbox_id.to_string(); - let sandbox_name = sandbox_name.to_string(); - let step = step.to_string(); - let poll_step = step.clone(); - runner - .poll_until( - &poll_step, - TRANSITION_TIMEOUT, - TRANSITION_INTERVAL, - async move |runner| match sandbox_is_listed(runner, &sandbox_id, &sandbox_name, &step) - .await - { - Ok(false) => Poll::Ready(()), - Ok(true) => Poll::Pending(format!( - "sandbox '{sandbox_name}' with ID '{sandbox_id}' is still listed" - )), - Err(error) => Poll::Pending(error), - }, - ) - .await - .map_err(|error| error.to_string()) -} - -async fn sandbox_is_listed( - runner: &OpenShellRunner, - sandbox_id: &str, - sandbox_name: &str, - step: &str, -) -> Result { - let deadline = Instant::now() + COMMAND_TIMEOUT; - let mut seen_page_tokens = HashSet::new(); - let mut page_token = String::new(); - let mut page = 0u32; - loop { - let remaining = deadline.saturating_duration_since(Instant::now()); - if remaining.is_zero() { - return Err(format!( - "sandbox list observation for '{sandbox_name}' exceeded its {COMMAND_TIMEOUT:?} deadline" - )); - } - - let result = runner - .step(format!("{step}/list/{page}")) - .description(format!( - "sandbox list confirms whether '{sandbox_name}' with ID '{sandbox_id}' still exists" - )) - .with_timeout(remaining) - .run(&[ - "sandbox", - "list", - "--page-size", - "1000", - "--page-token", - &page_token, - "--output", - "json", - ]) - .await - .map_err(|error| error.to_string())?; - result.require_success()?; - - let response = result - .json::() - .map_err(|error| error.to_string())?; - if response - .sandboxes - .iter() - .any(|sandbox| sandbox.id == sandbox_id) - { - return Ok(true); - } - if response.next_page_token.is_empty() { - return Ok(false); - } - if !seen_page_tokens.insert(response.next_page_token.clone()) { - return Err(format!( - "sandbox list returned a repeated page token while looking for '{sandbox_name}'" - )); - } - page_token = response.next_page_token; - page = page - .checked_add(1) - .ok_or_else(|| "sandbox list page counter overflowed".to_string())?; - } -} diff --git a/crates/openshell-conformance/src/scenarios/smoke.rs b/crates/openshell-conformance/src/scenarios/smoke.rs deleted file mode 100644 index 0f627b7fa2..0000000000 --- a/crates/openshell-conformance/src/scenarios/smoke.rs +++ /dev/null @@ -1,205 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Portable phase-1 CLI conformance scenario. - -use std::time::{Duration, Instant}; - -use crate::{OpenShellRunner, STATUS_TIMEOUT, Scenario, ScenarioFuture}; -use serde::Deserialize; -use tokio::time::sleep; - -const CREATE_TIMEOUT: Duration = Duration::from_mins(10); -const LIST_ATTEMPT_TIMEOUT: Duration = Duration::from_secs(10); -const LIST_PAGE_SIZE: u32 = 1_000; -const EXEC_TIMEOUT: Duration = Duration::from_mins(2); -const DELETE_TIMEOUT: Duration = Duration::from_mins(2); -const DELETE_POLL_INTERVAL: Duration = Duration::from_secs(1); - -#[derive(Debug, Deserialize)] -struct SandboxListEntry { - name: String, - phase: String, -} - -#[derive(Debug, Deserialize)] -struct SandboxListPage { - sandboxes: Vec, - next_page_token: String, -} - -/// Certify status -> create -> list Ready -> exec -> delete -> list empty. -pub const SMOKE_SCENARIO: Scenario = Scenario { - name: "smoke", - run: run_smoke, -}; - -fn run_smoke(runner: &mut OpenShellRunner) -> ScenarioFuture<'_> { - Box::pin(async move { run_smoke_inner(runner).await }) -} - -async fn run_smoke_inner(runner: &mut OpenShellRunner) -> Result<(), String> { - let status = runner - .step("status") - .description("openshell status succeeds") - .with_timeout(STATUS_TIMEOUT) - .run(&["status"]) - .await - .map_err(|error| error.to_string())?; - status.require_success()?; - - let sandbox_name = format!("ct-{}-01", runner.id()); - runner.track_sandbox(&sandbox_name); - let create = runner - .step("create") - .description("sandbox creation succeeds") - .with_timeout(CREATE_TIMEOUT) - .run(&["sandbox", "create", "--name", &sandbox_name, "--detach"]) - .await - .map_err(|error| error.to_string())?; - create.require_success()?; - - let get = runner - .step("get-ready") - .description(format!("sandbox '{sandbox_name}' can be retrieved")) - .with_timeout(LIST_ATTEMPT_TIMEOUT) - .run(&["sandbox", "get", &sandbox_name, "--output", "json"]) - .await - .map_err(|error| error.to_string())?; - get.require_success()?; - - let sandbox = get - .json::() - .map_err(|error| error.to_string())?; - if sandbox.name != sandbox_name { - return Err(format!( - "sandbox get returned {:?}; expected sandbox '{sandbox_name}'", - sandbox.name - )); - } - if sandbox.phase != "Ready" { - return Err(format!( - "sandbox '{sandbox_name}' is in phase {:?}; expected Ready", - sandbox.phase - )); - } - - check_sandbox_listed(runner, &sandbox_name).await?; - - let marker = format!("openshell-conformance-{}", runner.id()); - let exec = runner - .step("exec") - .description("sandbox exec exits successfully") - .with_timeout(EXEC_TIMEOUT) - .run(&[ - "sandbox", - "exec", - "--name", - &sandbox_name, - "--no-tty", - "--", - "echo", - &marker, - ]) - .await - .map_err(|error| error.to_string())?; - exec.require_success()?; - let expected_stdout = format!("{marker}\n"); - if exec.stdout() != expected_stdout { - return Err(exec.failure_diagnostic(&format!("stdout is exactly {expected_stdout:?}"))); - } - - let delete = runner - .step("delete") - .description("sandbox deletion succeeds") - .with_timeout(DELETE_TIMEOUT) - .run(&["sandbox", "delete", &sandbox_name]) - .await - .map_err(|error| error.to_string())?; - delete.require_success()?; - - check_empty_list(runner, &sandbox_name).await?; - runner.forget_sandbox(&sandbox_name); - Ok(()) -} - -async fn check_sandbox_listed(runner: &OpenShellRunner, sandbox_name: &str) -> Result<(), String> { - if find_sandbox(runner, sandbox_name, "list-visible") - .await? - .is_some() - { - return Ok(()); - } - Err(format!( - "sandbox '{sandbox_name}' does not appear in sandbox list" - )) -} - -async fn check_empty_list(runner: &OpenShellRunner, sandbox_name: &str) -> Result<(), String> { - let started = Instant::now(); - - loop { - match find_sandbox(runner, sandbox_name, "list-empty/query").await? { - None => return Ok(()), - Some(sandbox) if started.elapsed() >= DELETE_TIMEOUT => { - return Err(format!( - "sandbox '{sandbox_name}' remains listed in phase {:?} after {DELETE_TIMEOUT:.1?}", - sandbox.phase - )); - } - Some(_) => sleep(DELETE_POLL_INTERVAL).await, - } - } -} - -async fn find_sandbox( - runner: &OpenShellRunner, - sandbox_name: &str, - step: &str, -) -> Result, String> { - let mut page_token = String::new(); - let mut page = 0u32; - - loop { - let page_size = LIST_PAGE_SIZE.to_string(); - let result = runner - .step(format!("{step}/{page}")) - .description(format!("sandbox list page {page} succeeds")) - .with_timeout(LIST_ATTEMPT_TIMEOUT) - .run(&[ - "sandbox", - "list", - "--page-size", - &page_size, - "--page-token", - &page_token, - "--output", - "json", - ]) - .await - .map_err(|error| error.to_string())?; - result.require_success()?; - - let response = result - .json::() - .map_err(|error| error.to_string())?; - if let Some(sandbox) = response - .sandboxes - .iter() - .find(|sandbox| sandbox.name == sandbox_name) - { - return Ok(Some(SandboxListEntry { - name: sandbox.name.clone(), - phase: sandbox.phase.clone(), - })); - } - if response.next_page_token.is_empty() { - return Ok(None); - } - - page_token = response.next_page_token; - page = page - .checked_add(1) - .ok_or_else(|| "sandbox list page counter overflowed".to_string())?; - } -} diff --git a/tasks/rust.toml b/tasks/rust.toml index e8d1c69f4f..e67e34bf1a 100644 --- a/tasks/rust.toml +++ b/tasks/rust.toml @@ -39,8 +39,9 @@ run = [ "cargo clippy --manifest-path tests/testkit/Cargo.toml --all-targets -- -D warnings", "cargo clippy --manifest-path tests/tmachine/Cargo.toml --all-targets -- -D warnings", "cargo clippy --manifest-path tests/runner/Cargo.toml --all-targets -- -D warnings", - "cargo clippy --manifest-path tests/suites/conformance/Cargo.toml -p conformance --all-targets -- -D warnings", - "cargo clippy --manifest-path tests/suites/features/Cargo.toml -p keycloak-provider-refresh --all-targets -- -D warnings", + "cargo clippy --manifest-path tests/suites/conformance/Cargo.toml --workspace --all-targets -- -D warnings", + "cargo clippy --manifest-path tests/suites/drivers/Cargo.toml --workspace --all-targets -- -D warnings", + "cargo clippy --manifest-path tests/suites/features/Cargo.toml --workspace --all-targets -- -D warnings", "cargo clippy --manifest-path tests/support/Cargo.toml --all-targets -- -D warnings", ] run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 lint native" @@ -57,8 +58,9 @@ run = [ "cargo fmt --manifest-path tests/testkit/Cargo.toml --all", "cargo fmt --manifest-path tests/tmachine/Cargo.toml --all", "cargo fmt --manifest-path tests/runner/Cargo.toml --all", - "cargo fmt --manifest-path tests/suites/conformance/Cargo.toml -p conformance", - "cargo fmt --manifest-path tests/suites/features/Cargo.toml -p keycloak-provider-refresh", + "cargo fmt --manifest-path tests/suites/conformance/Cargo.toml --all", + "cargo fmt --manifest-path tests/suites/drivers/Cargo.toml --all", + "cargo fmt --manifest-path tests/suites/features/Cargo.toml --all", "cargo fmt --manifest-path tests/support/Cargo.toml --all", ] hide = true @@ -74,8 +76,9 @@ run = [ "cargo fmt --manifest-path tests/testkit/Cargo.toml --all -- --check", "cargo fmt --manifest-path tests/tmachine/Cargo.toml --all -- --check", "cargo fmt --manifest-path tests/runner/Cargo.toml --all -- --check", - "cargo fmt --manifest-path tests/suites/conformance/Cargo.toml -p conformance -- --check", - "cargo fmt --manifest-path tests/suites/features/Cargo.toml -p keycloak-provider-refresh -- --check", + "cargo fmt --manifest-path tests/suites/conformance/Cargo.toml --all -- --check", + "cargo fmt --manifest-path tests/suites/drivers/Cargo.toml --all -- --check", + "cargo fmt --manifest-path tests/suites/features/Cargo.toml --all -- --check", "cargo fmt --manifest-path tests/support/Cargo.toml --all -- --check", ] hide = true diff --git a/tasks/scripts/check_cargo_lockfile_alignment.py b/tasks/scripts/check_cargo_lockfile_alignment.py index 520c7c4719..fb851641cc 100644 --- a/tasks/scripts/check_cargo_lockfile_alignment.py +++ b/tasks/scripts/check_cargo_lockfile_alignment.py @@ -38,6 +38,7 @@ "tests/support", "tests/testkit", "tests/suites/conformance", + "tests/suites/drivers", "tests/suites/features", ) diff --git a/tasks/test.toml b/tasks/test.toml index 558d7a2401..549e3dd8d6 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -127,6 +127,7 @@ run = [ "cargo nextest run --config-file .config/nextest.toml --manifest-path tests/testkit/Cargo.toml", "cargo nextest run --config-file .config/nextest.toml --manifest-path tests/tmachine/Cargo.toml", "cargo nextest run --config-file .config/nextest.toml --manifest-path tests/runner/Cargo.toml", + "cargo nextest run --config-file .config/nextest.toml --manifest-path tests/support/Cargo.toml", ] run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 test-precommit native" hide = true @@ -161,11 +162,11 @@ run = "e2e/rust/e2e-docker.sh" ["e2e:oci-image"] description = "Run the shared OCI image feature suite against a Docker-backed gateway" -run = "OPENSHELL_TEST_CONTAINER_ENGINE=docker e2e/with-docker-gateway.sh cargo test --locked --manifest-path tests/suites/features/Cargo.toml -p openshell-test-feature-oci-image -- --test-threads 1 --nocapture" +run = "OPENSHELL_TEST_CONTAINER_ENGINE=docker e2e/with-docker-gateway.sh cargo test --locked --manifest-path tests/suites/features/Cargo.toml -p oci-image -- --test-threads 1 --nocapture" ["e2e:oci-image:podman"] description = "Run the shared OCI image feature suite against a Podman-backed gateway" -run = "OPENSHELL_TEST_CONTAINER_ENGINE=podman e2e/with-podman-gateway.sh cargo test --locked --manifest-path tests/suites/features/Cargo.toml -p openshell-test-feature-oci-image -- --test-threads 1 --nocapture" +run = "OPENSHELL_TEST_CONTAINER_ENGINE=podman e2e/with-podman-gateway.sh cargo test --locked --manifest-path tests/suites/features/Cargo.toml -p oci-image -- --test-threads 1 --nocapture" ["e2e:testkit"] description = "Run the testkit suites in tests/suites/conformance against a Docker-backed gateway" diff --git a/tests/suites/drivers/Cargo.lock b/tests/suites/drivers/Cargo.lock index 5654347dad..452ee563cf 100644 --- a/tests/suites/drivers/Cargo.lock +++ b/tests/suites/drivers/Cargo.lock @@ -2,11 +2,330 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" @@ -14,421 +333,3949 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "container-engine-support" +version = "0.0.0" +dependencies = [ + "openshell-testkit", + "tempfile", + "tokio", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe3e9d21d39e6f39ff46e03d5b621424f88ee23d8a1672c6bfc9ccc7f2da4f6a" +dependencies = [ + "cmov", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "enum-ordinalize" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64 0.22.1", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1 0.10.6", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.2", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "hostname" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" +dependencies = [ + "cfg-if", + "libc", + "windows-link", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-http-proxy" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd1d471ea2f65ba45eddb1d6ab7d58ac2671d2d4ac14da5c6516ae1d97e1327a" +dependencies = [ + "bytes", + "futures-util", + "headers", + "http", + "hyper", + "hyper-rustls", + "hyper-util", + "pin-project-lite", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "jsonpath-rust" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c00ae348f9f8fd2d09f82a98ca381c60df9e0820d8d79fce43e649b4dc3128b" +dependencies = [ + "pest", + "pest_derive", + "regex", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "jsonwebtoken" +version = "10.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +dependencies = [ + "aws-lc-rs", + "base64 0.22.1", + "getrandom 0.2.17", + "js-sys", + "pem 3.0.6", + "serde", + "serde_json", + "signature", + "simple_asn1", +] + +[[package]] +name = "k8s-openapi" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c75b990324f09bef15e791606b7b7a296d02fc88a344f6eba9390970a870ad5" +dependencies = [ + "base64 0.22.1", + "chrono", + "serde", + "serde-value", + "serde_json", +] + +[[package]] +name = "kube" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4eb20010536b48abe97fec37d23d43069bcbe9686adcf9932202327bc5ca6e" +dependencies = [ + "k8s-openapi", + "kube-client", + "kube-core", + "kube-derive", + "kube-runtime", +] + +[[package]] +name = "kube-client" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc2ed952042df20d15ac2fe9614d0ec14b6118eab89633985d4b36e688dccf1" +dependencies = [ + "base64 0.22.1", + "bytes", + "chrono", + "either", + "futures", + "home", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-http-proxy", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "jsonpath-rust", + "k8s-openapi", + "kube-core", + "pem 3.0.6", + "rustls", + "secrecy", + "serde", + "serde_json", + "serde_yaml", + "thiserror 2.0.20", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing", +] + +[[package]] +name = "kube-core" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff0d0793db58e70ca6d689489183816cb3aa481673e7433dc618cf7e8007c675" +dependencies = [ + "chrono", + "form_urlencoded", + "http", + "json-patch", + "k8s-openapi", + "schemars", + "serde", + "serde-value", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "kube-derive" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c562f58dc9f7ca5feac8a6ee5850ca221edd6f04ce0dd2ee873202a88cd494c9" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.117", +] + +[[package]] +name = "kube-runtime" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f34cfab9b4bd8633062e0e85edb81df23cb09f159f2e31c60b069ae826ffdc" +dependencies = [ + "ahash", + "async-broadcast", + "async-stream", + "async-trait", + "backon", + "educe", + "futures", + "hashbrown 0.15.5", + "hostname", + "json-patch", + "k8s-openapi", + "kube-client", + "parking_lot", + "pin-project", + "serde", + "serde_json", + "thiserror 2.0.20", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.2", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noyalib" +version = "0.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "962838abbd8dd2cd3844d2208dc502458bba6985f8bf1dc21761b30260fcbd7f" +dependencies = [ + "hashbrown 0.17.0", + "indexmap", + "libm", + "memchr", + "rustc-hash", + "serde_core", + "smallvec", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.22.1", + "chrono", + "getrandom 0.2.17", + "http", + "rand 0.8.6", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2 0.10.9", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openshell-bootstrap" +version = "0.0.0" +dependencies = [ + "bytes", + "miette", + "openshell-core", + "openshell-crypto", + "rcgen", + "serde", + "serde_json", + "tempfile", + "tokio", + "tracing", +] + +[[package]] +name = "openshell-core" +version = "0.0.0" +dependencies = [ + "async-trait", + "base64 0.22.1", + "chrono", + "glob", + "ipnet", + "libc", + "miette", + "nix", + "openshell-crypto", + "openshell-extension-core", + "openshell-policy-schema", + "prost", + "prost-types", + "protoc-bin-vendored", + "reqwest", + "rustix", + "rustls", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror 2.0.20", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tonic-prost-build", + "tonic-types", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "openshell-crypto" +version = "0.0.0" +dependencies = [ + "aws-lc-rs", + "jsonwebtoken", + "kube", + "pem 3.0.6", + "rcgen", + "rustls", + "serde", + "sqlx", + "thiserror 2.0.20", + "tonic", + "x509-parser", +] + +[[package]] +name = "openshell-extension-core" +version = "0.0.0" +dependencies = [ + "hyper-util", + "serde", + "thiserror 2.0.20", + "tokio", + "tonic", + "tower", +] + +[[package]] +name = "openshell-policy-schema" +version = "0.0.0" +dependencies = [ + "miette", + "noyalib", + "serde", + "serde_json", + "serde_path_to_error", + "thiserror 2.0.20", +] + +[[package]] +name = "openshell-sdk" +version = "0.0.0" +dependencies = [ + "async-stream", + "async-trait", + "futures", + "hyper", + "hyper-util", + "miette", + "oauth2", + "openshell-core", + "openshell-crypto", + "reqwest", + "rustls", + "serde", + "thiserror 2.0.20", + "tokio", + "tokio-rustls", + "tokio-tungstenite", + "tonic", + "tower", + "tracing", +] + +[[package]] +name = "openshell-testkit" +version = "0.0.0" +dependencies = [ + "futures", + "openshell-bootstrap", + "openshell-core", + "openshell-sdk", + "openshell-testkit-macros", + "rand 0.9.4", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "openshell-testkit-macros" +version = "0.0.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" +dependencies = [ + "base64 0.23.1", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2 0.10.9", +] + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "podman" +version = "0.0.0" +dependencies = [ + "container-engine-support", + "openshell-testkit", + "podman-support", +] + +[[package]] +name = "podman-support" +version = "0.0.0" +dependencies = [ + "container-engine-support", + "openshell-testkit", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn 2.0.117", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" + +[[package]] +name = "pulldown-cmark" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db2be46835f013dd3f4fa7f5f7402c537787b54ebb6fe2b94bbdd7e2669bb7b" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90" +dependencies = [ + "pulldown-cmark", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rcgen" +version = "0.14.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8" +dependencies = [ + "aws-lc-rs", + "pem 4.0.0", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64 0.22.1", + "bytes", + "cfg-if", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap", + "log", + "memchr", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.20", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.117", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.117", + "thiserror 2.0.20", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags", + "byteorder", + "bytes", + "crc", + "digest 0.11.2", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array", + "log", + "percent-encoding", + "serde", + "sha1 0.11.0", + "sha2 0.11.0", + "sqlx-core", + "thiserror 2.0.20", + "tracing", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "rand 0.10.2", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.20", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +dependencies = [ + "atoi", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror 2.0.20", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "slab", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "rustls-native-certs", + "socket2", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tonic-prost" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.117", + "tempfile", + "tonic-build", +] + +[[package]] +name = "tonic-types" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8" +dependencies = [ + "prost", + "prost-types", + "tonic", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project-lite", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "base64 0.22.1", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "mime", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.4", + "rustls", + "rustls-pki-types", + "sha1 0.10.6", + "thiserror 2.0.20", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "errno" -version = "0.3.14" +name = "want" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "libc", - "windows-sys", + "try-lock", ] [[package]] -name = "fastrand" -version = "2.5.0" +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "getrandom" -version = "0.3.4" +name = "wasip2" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", + "wit-bindgen 0.57.1", ] [[package]] -name = "itoa" -version = "1.0.18" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] [[package]] -name = "libc" -version = "0.2.189" +name = "wasm-bindgen" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] [[package]] -name = "linux-raw-sys" -version = "0.12.1" +name = "wasm-bindgen-futures" +version = "0.4.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] -name = "lock_api" -version = "0.4.14" +name = "wasm-bindgen-macro" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ - "scopeguard", + "quote", + "wasm-bindgen-macro-support", ] [[package]] -name = "memchr" -version = "2.8.3" +name = "wasm-bindgen-macro-support" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] [[package]] -name = "mio" -version = "1.2.3" +name = "wasm-bindgen-shared" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ - "libc", - "wasi", - "windows-sys", + "unicode-ident", ] [[package]] -name = "once_cell" -version = "1.21.4" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] [[package]] -name = "openshell-conformance" -version = "0.0.0" +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ - "rand", - "serde", - "serde_json", - "tempfile", - "tokio", + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", ] [[package]] -name = "openshell-test-suite-podman" -version = "0.0.0" +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "openshell-conformance", - "serde", - "tokio", + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", ] [[package]] -name = "parking_lot" -version = "0.12.5" +name = "web-sys" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" dependencies = [ - "lock_api", - "parking_lot_core", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "parking_lot_core" -version = "0.9.12" +name = "webpki-roots" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "1c1c8e20c8a38fc7d65abfe74eb7280e4aa667e8eec8bc84996d2874810748d9" dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", + "rustls-pki-types", ] [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "whoami" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "windows-core" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "zerocopy", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] -name = "proc-macro2" -version = "1.0.107" +name = "windows-implement" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ - "unicode-ident", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "quote" -version = "1.0.47" +name = "windows-interface" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "r-efi" -version = "5.3.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "rand" -version = "0.9.5" +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "rand_chacha", - "rand_core", + "windows-link", ] [[package]] -name = "rand_chacha" -version = "0.9.0" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "ppv-lite86", - "rand_core", + "windows-link", ] [[package]] -name = "rand_core" -version = "0.9.5" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "getrandom", + "windows-targets", ] [[package]] -name = "redox_syscall" -version = "0.5.18" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "bitflags", + "windows-link", ] [[package]] -name = "rustix" -version = "1.1.5" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "windows_aarch64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "serde" -version = "1.0.229" +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "serde_core", - "serde_derive", + "wit-bindgen-rust-macro", ] [[package]] -name = "serde_core" -version = "1.0.229" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ - "serde_derive", + "anyhow", + "heck", + "wit-parser", ] [[package]] -name = "serde_derive" -version = "1.0.229" +name = "wit-bindgen-rust" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" dependencies = [ + "anyhow", + "prettyplease", "proc-macro2", "quote", - "syn 3.0.5", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] -name = "serde_json" -version = "1.0.151" +name = "wit-component" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ - "itoa", - "memchr", + "anyhow", + "bitflags", + "indexmap", + "log", "serde", - "serde_core", - "zmij", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", ] [[package]] -name = "signal-hook-registry" -version = "1.4.8" +name = "wit-parser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ - "errno", - "libc", + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", ] [[package]] -name = "smallvec" -version = "1.16.1" +name = "writeable" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] -name = "socket2" -version = "0.6.5" +name = "x509-parser" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ - "libc", - "windows-sys", + "asn1-rs", + "aws-lc-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.20", + "time", ] [[package]] -name = "syn" -version = "2.0.119" +name = "yasna" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "bit-vec", + "time", ] [[package]] -name = "syn" -version = "3.0.5" +name = "yoke" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "stable_deref_trait", + "yoke-derive", + "zerofrom", ] [[package]] -name = "tempfile" -version = "3.27.0" +name = "yoke-derive" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ - "fastrand", - "getrandom", - "once_cell", - "rustix", - "windows-sys", + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", ] [[package]] -name = "tokio" -version = "1.53.3" +name = "zerocopy" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys", + "zerocopy-derive", ] [[package]] -name = "tokio-macros" -version = "2.7.2" +name = "zerocopy-derive" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 3.0.5", + "syn 2.0.117", ] [[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "zerofrom" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] [[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" +name = "zerofrom-derive" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ - "wit-bindgen", + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", ] [[package]] -name = "windows-link" -version = "0.2.1" +name = "zeroize" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] -name = "windows-sys" -version = "0.61.2" +name = "zerotrie" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ - "windows-link", + "displaydoc", + "yoke", + "zerofrom", ] [[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "zerocopy" -version = "0.8.57" +name = "zerovec" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ - "zerocopy-derive", + "yoke", + "zerofrom", + "zerovec-derive", ] [[package]] -name = "zerocopy-derive" -version = "0.8.57" +name = "zerovec-derive" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.117", ] [[package]] diff --git a/tests/suites/drivers/podman/Cargo.toml b/tests/suites/drivers/podman/Cargo.toml index 1a06b69e8d..9588a1d857 100644 --- a/tests/suites/drivers/podman/Cargo.toml +++ b/tests/suites/drivers/podman/Cargo.toml @@ -2,14 +2,16 @@ # SPDX-License-Identifier: Apache-2.0 [package] -name = "openshell-test-suite-podman" +name = "podman" +description = "Drivers suite: Podman driver behavior, such as user namespaces" version = "0.0.0" edition = "2024" rust-version = "1.94" license = "Apache-2.0" repository = "https://github.com/NVIDIA/OpenShell" +publish = false [dependencies] -openshell-conformance = { path = "../../../../crates/openshell-conformance" } -serde = { version = "1", features = ["derive"] } -tokio = { version = "1.43", features = ["macros", "rt"] } +container-engine-support = { path = "../../../support/container-engine" } +openshell-testkit = { path = "../../../testkit/testkit" } +podman-support = { path = "../../../support/podman" } diff --git a/tests/suites/drivers/podman/fixtures/userns-auto.toml b/tests/suites/drivers/podman/fixtures/userns-auto.toml deleted file mode 100644 index 3701222828..0000000000 --- a/tests/suites/drivers/podman/fixtures/userns-auto.toml +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -userns = "auto" diff --git a/tests/suites/drivers/podman/fixtures/userns-keep-id.toml b/tests/suites/drivers/podman/fixtures/userns-keep-id.toml deleted file mode 100644 index c52cdb528f..0000000000 --- a/tests/suites/drivers/podman/fixtures/userns-keep-id.toml +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -userns = "keep-id" diff --git a/tests/suites/drivers/podman/fixtures/userns-private.toml b/tests/suites/drivers/podman/fixtures/userns-private.toml deleted file mode 100644 index 48a7968d13..0000000000 --- a/tests/suites/drivers/podman/fixtures/userns-private.toml +++ /dev/null @@ -1,6 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -userns = "private" -uidmap = ["0:0:1", "1:1:65535"] -gidmap = ["0:0:1", "1:1:65535"] diff --git a/tests/suites/drivers/podman/tests/default_userns.rs b/tests/suites/drivers/podman/tests/default_userns.rs deleted file mode 100644 index 95cb8c814f..0000000000 --- a/tests/suites/drivers/podman/tests/default_userns.rs +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Podman-driver user-namespace integration tests. - -mod support; - -use openshell_conformance::OpenShellRunner; -use std::fs; -use std::path::PathBuf; -use std::time::Duration; - -use support::assert_podman_gateway; - -const SANDBOX_TIMEOUT: Duration = Duration::from_secs(300); -const PODMAN_TEST_INPUT_DIR_ENV: &str = "OPENSHELL_TEST_INPUT_DIR"; -const PODMAN_TEST_IMAGE_ENV: &str = "OPENSHELL_PODMAN_TEST_IMAGE"; - -const WORKSPACE_AND_UID_MAP_PROBE: &str = r#"set -eu -workload_owner="$(id -u):$(id -g)" -workspace_owner="$(stat -c '%u:%g' /sandbox)" -printf 'workload-owner=%s\nworkspace-owner=%s\n' "$workload_owner" "$workspace_owner" -test "$(id -u)" -ne 0 -test "$workspace_owner" = "$workload_owner" -probe=$(mktemp /sandbox/userns-probe.XXXXXX) -printf 'workspace probe\n' > "$probe" -rm "$probe" -echo podman-userns-workspace-ok -cat /proc/self/uid_map -"#; - -/// Verify that the gateway's user-namespace configuration matches Podman's -/// direct behavior for the same profile and preserves workspace access. -/// -/// The test runs a short-lived sandbox command and compares its user-namespace -/// mapping with the direct-Podman reference stored at -/// `OPENSHELL_TEST_INPUT_DIR/reference-uid-map`. The tmachine pre-test -/// playbook creates that reference in the same gateway-user context. This deliberately -/// avoids baking a particular Podman mapping into OpenShell's test contract. -/// The workload also verifies that the managed workspace is owned by its -/// non-root UID/GID and that it can create, write, and remove a file there. -#[tokio::test] -async fn configured_userns_matches_podman_reference() { - let mut runner = - OpenShellRunner::from_env("podman-userns").expect("candidate openshell CLI is available"); - let result = async { - runner.check_gateway_status().await?; - assert_podman_gateway(&runner).await?; - - let test_input_dir = std::env::var_os(PODMAN_TEST_INPUT_DIR_ENV) - .map(PathBuf::from) - .ok_or_else(|| format!("{PODMAN_TEST_INPUT_DIR_ENV} must name the Podman test-input directory"))?; - let expected_path = test_input_dir.join("reference-uid-map"); - let expected_uid_map = fs::read_to_string(&expected_path).map_err(|error| { - format!( - "could not read Podman reference UID map {}: {error}", - expected_path.display() - ) - })?; - let expected_uid_map = normalize_uid_map(&expected_uid_map).ok_or_else(|| { - format!( - "Podman reference UID map {} contains no mappings", - expected_path.display() - ) - })?; - - let workload_image = std::env::var(PODMAN_TEST_IMAGE_ENV) - .ok() - .filter(|image| !image.trim().is_empty()); - let sandbox_name = format!("pu-{}", runner.id()); - runner.track_sandbox(&sandbox_name); - let mut create_args = vec!["sandbox", "create", "--name", &sandbox_name]; - if let Some(image) = workload_image.as_deref() { - create_args.extend(["--from", image]); - } - create_args.extend(["--no-tty", "--", "sh", "-c", WORKSPACE_AND_UID_MAP_PROBE]); - let run = runner - .step("userns/workspace-and-uid-map") - .description("sandbox can write to its owned workspace and exposes its UID map") - .with_timeout(SANDBOX_TIMEOUT) - .run(&create_args) - .await - .map_err(|error| error.to_string())?; - run.require_success()?; - if !run.stdout().contains("podman-userns-workspace-ok") { - return Err(run.failure_diagnostic("non-root workload owns and can write to /sandbox")); - } - let sandbox_uid_map = normalize_uid_map(run.stdout()).ok_or_else(|| { - run.failure_diagnostic("sandbox returns a non-empty UID map") - })?; - if sandbox_uid_map != expected_uid_map { - return Err(format!( - "sandbox UID map differs from the direct Podman reference:\nexpected:\n{expected_uid_map}\nactual:\n{sandbox_uid_map}" - )); - } - Ok(()) - } - .await; - - if let Err(error) = runner.finish(result).await { - panic!("Podman userns test failed:\n{error}"); - } -} - -fn normalize_uid_map(value: &str) -> Option { - let mut mappings: Vec<(u64, u64, u64)> = Vec::new(); - for line in value.lines() { - let fields = line - .split_whitespace() - .map(str::parse::) - .collect::, _>>(); - let Ok(fields) = fields else { continue }; - let [inside, outside, length] = fields.as_slice() else { - continue; - }; - if let Some(previous) = mappings.last_mut() - && previous.0.checked_add(previous.2) == Some(*inside) - && previous.1.checked_add(previous.2) == Some(*outside) - { - previous.2 += *length; - } else { - mappings.push((*inside, *outside, *length)); - } - } - (!mappings.is_empty()).then(|| { - mappings - .iter() - .map(|(inside, outside, length)| format!("{inside} {outside} {length}")) - .collect::>() - .join("\n") - }) -} - -#[test] -fn adjacent_uid_ranges_match_a_combined_mapping() { - assert_eq!( - normalize_uid_map("0 0 1\n1 1 65535\n"), - normalize_uid_map("0 0 65536\n") - ); -} diff --git a/tests/suites/drivers/podman/tests/support/mod.rs b/tests/suites/drivers/podman/tests/support/mod.rs deleted file mode 100644 index 5bde19b0c1..0000000000 --- a/tests/suites/drivers/podman/tests/support/mod.rs +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -use openshell_conformance::OpenShellRunner; -use serde::Deserialize; -use std::time::Duration; - -#[derive(Debug, Deserialize)] -struct GatewayInfo { - status: String, - compute_drivers: Vec, -} - -#[derive(Debug, Deserialize)] -struct ComputeDriver { - name: String, - capabilities: ComputeDriverCapabilities, -} - -#[derive(Debug, Deserialize)] -struct ComputeDriverCapabilities { - driver_name: String, -} - -/// Require the target gateway to use Podman as its only compute driver. -/// -/// This interrogates the running gateway rather than accepting a runner -/// environment variable. A driver-specific suite must fail, rather than skip, -/// when it is pointed at the wrong gateway. -pub async fn assert_podman_gateway(runner: &OpenShellRunner) -> Result<(), String> { - let result = runner - .step("preflight/driver-podman") - .description("gateway reports Podman as its only compute driver") - .with_timeout(Duration::from_secs(10)) - .run(&["gateway", "info", "--output", "json"]) - .await - .map_err(|error| format!("could not query gateway compute drivers: {error}"))?; - result.require_success()?; - - let info = result - .json::() - .map_err(|error| format!("gateway returned invalid driver information: {error}"))?; - if info.status != "healthy" { - return Err(format!( - "Podman test suite requires a healthy gateway; gateway status is {:?}", - info.status - )); - } - - let driver_names = info - .compute_drivers - .iter() - .map(|driver| driver.name.as_str()) - .collect::>(); - let podman_only = matches!(info.compute_drivers.as_slice(), [driver] - if driver.name == "podman" && driver.capabilities.driver_name == "podman"); - if !podman_only { - return Err(format!( - "Podman test suite requires exactly one Podman compute driver; gateway reported {driver_names:?}" - )); - } - - Ok(()) -} diff --git a/tests/suites/drivers/podman/tests/userns.rs b/tests/suites/drivers/podman/tests/userns.rs new file mode 100644 index 0000000000..5dff4a8a44 --- /dev/null +++ b/tests/suites/drivers/podman/tests/userns.rs @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Podman driver user namespaces. +//! +//! The environment configures the gateway with one user-namespace mode and +//! publishes it (see the `podman-support` crate). The test compares a +//! sandbox's UID map with the one Podman itself gives a container in the same +//! mode, so it doesn't bake a particular Podman mapping into the test. + +use container_engine_support::Engine; +use podman_support::{Userns, normalize_uid_map, reference_uid_map}; +use testkit::{Context, Error, ResultExt as _}; + +const WORKSPACE_OK: &str = "podman-userns-workspace-ok"; +const WORKSPACE_AND_UID_MAP_PROBE: &str = r#"set -eu +workload_owner="$(id -u):$(id -g)" +workspace_owner="$(stat -c '%u:%g' /sandbox)" +printf 'workload-owner=%s\nworkspace-owner=%s\n' "$workload_owner" "$workspace_owner" +test "$(id -u)" -ne 0 +test "$workspace_owner" = "$workload_owner" +probe=$(mktemp /sandbox/userns-probe.XXXXXX) +printf 'workspace probe\n' > "$probe" +rm "$probe" +echo podman-userns-workspace-ok +cat /proc/self/uid_map +"#; + +/// A sandbox gets the UID map Podman gives a container in the gateway's +/// user-namespace mode, and its non-root workload owns and can write to the +/// managed `/sandbox` workspace. +#[testkit::test] +async fn configured_userns_matches_podman_reference(cx: &mut Context) -> testkit::Result { + cx.expect_compute_driver("podman").await?; + let userns = Userns::from_env()?; + let engine = Engine::from_env()?; + let image = testkit::base_image(); + let reference = reference_uid_map(&engine, userns, &image).await?; + + let name = cx.name("pu"); + cx.track(&name); + let run = cx + .cli([ + "sandbox", + "create", + "--name", + &name, + "--from", + &image, + "--no-tty", + "--", + "sh", + "-c", + WORKSPACE_AND_UID_MAP_PROBE, + ]) + .await? + .success() + .context("sandbox can write to its owned workspace and prints its UID map")?; + testkit::ensure!( + run.stdout.contains(WORKSPACE_OK), + "the non-root workload does not own or cannot write to /sandbox:\n{}", + run.stdout + ); + let sandbox = normalize_uid_map(&run.stdout).ok_or_else(|| { + Error::msg(format!( + "the sandbox printed no UID mappings:\n{}", + run.stdout + )) + })?; + testkit::ensure!( + sandbox == reference, + "the sandbox UID map differs from Podman's {userns:?} reference:\nexpected:\n{reference}\nactual:\n{sandbox}" + ); + Ok(()) +} diff --git a/tests/suites/features/Cargo.lock b/tests/suites/features/Cargo.lock index 1206d952e4..4362c9cacd 100644 --- a/tests/suites/features/Cargo.lock +++ b/tests/suites/features/Cargo.lock @@ -315,49 +315,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "bollard" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" -dependencies = [ - "base64 0.22.1", - "bollard-stubs", - "bytes", - "futures-core", - "futures-util", - "hex", - "http", - "http-body-util", - "hyper", - "hyper-named-pipe", - "hyper-util", - "hyperlocal", - "log", - "pin-project-lite", - "serde", - "serde_derive", - "serde_json", - "serde_urlencoded", - "thiserror 2.0.20", - "tokio", - "tokio-util", - "tower-service", - "url", - "winapi", -] - -[[package]] -name = "bollard-stubs" -version = "1.52.1-rc.29.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0a8ca8799131c1837d1282c3f81f31e76ceb0ce426e04a7fe1ccee3287c066" -dependencies = [ - "serde", - "serde_json", - "serde_repr", -] - [[package]] name = "bumpalo" version = "3.20.2" @@ -449,6 +406,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "container-engine-support" +version = "0.0.0" +dependencies = [ + "openshell-testkit", + "tempfile", + "tokio", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -498,15 +464,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -770,16 +727,6 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "flume" version = "0.12.0" @@ -1221,21 +1168,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "hyper-named-pipe" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" -dependencies = [ - "hex", - "hyper", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", - "winapi", -] - [[package]] name = "hyper-rustls" version = "0.27.9" @@ -1289,21 +1221,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "hyperlocal" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" -dependencies = [ - "hex", - "http-body-util", - "hyper", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - [[package]] name = "iana-time-zone" version = "0.1.65" @@ -1579,21 +1496,21 @@ dependencies = [ ] [[package]] -name = "keycloak" +name = "keycloak-provider-refresh" version = "0.0.0" dependencies = [ + "keycloak-support", "openshell-testkit", - "serde_json", - "tokio", + "tempfile", ] [[package]] -name = "keycloak-provider-refresh" +name = "keycloak-support" version = "0.0.0" dependencies = [ - "keycloak", "openshell-testkit", - "tempfile", + "serde_json", + "tokio", ] [[package]] @@ -1768,15 +1685,6 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - [[package]] name = "matchit" version = "0.8.4" @@ -1799,16 +1707,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "metrics" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" -dependencies = [ - "ahash", - "portable-atomic", -] - [[package]] name = "miette" version = "7.6.0" @@ -1858,7 +1756,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", - "simd-adler32", ] [[package]] @@ -1915,15 +1812,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "num-bigint" version = "0.4.8" @@ -1987,6 +1875,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "oci-image" +version = "0.0.0" +dependencies = [ + "container-engine-support", + "openshell-testkit", + "serde_json", + "tempfile", +] + [[package]] name = "oid-registry" version = "0.8.1" @@ -2018,17 +1916,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "openshell-conformance" -version = "0.0.0" -dependencies = [ - "rand 0.9.4", - "serde", - "serde_json", - "tempfile", - "tokio", -] - [[package]] name = "openshell-core" version = "0.0.0" @@ -2082,41 +1969,6 @@ dependencies = [ "x509-parser", ] -[[package]] -name = "openshell-e2e" -version = "0.1.0" -dependencies = [ - "base64 0.22.1", - "bollard", - "bytes", - "futures-util", - "hex", - "http-body-util", - "hyper", - "hyper-util", - "jsonwebtoken", - "nix", - "noyalib", - "openshell-crypto", - "openshell-otel-test-support", - "opentelemetry-proto", - "prost", - "rand 0.9.4", - "rustls", - "serde", - "serde_json", - "sha1 0.10.6", - "sha2 0.10.9", - "tempfile", - "tokio", - "tokio-rustls", - "tokio-stream", - "tonic", - "tonic-prost", - "tower", - "url", -] - [[package]] name = "openshell-extension-core" version = "0.0.0" @@ -2129,73 +1981,6 @@ dependencies = [ "tower", ] -[[package]] -name = "openshell-ocsf" -version = "0.0.0" -dependencies = [ - "chrono", - "serde", - "serde_json", - "serde_repr", - "tracing", - "tracing-subscriber", - "uuid", -] - -[[package]] -name = "openshell-otel" -version = "0.0.0" -dependencies = [ - "async-trait", - "bytes", - "futures", - "http", - "http-body-util", - "hyper-rustls", - "hyper-util", - "metrics", - "openshell-crypto", - "openshell-ocsf", - "opentelemetry", - "opentelemetry-http", - "opentelemetry-otlp", - "opentelemetry_sdk", - "rustls", - "rustls-native-certs", - "serde_json", - "thiserror 2.0.20", - "tokio", - "tonic", - "tower-http", - "tracing", - "tracing-opentelemetry", - "tracing-subscriber", - "zeroize", -] - -[[package]] -name = "openshell-otel-test-support" -version = "0.0.0" -dependencies = [ - "bytes", - "http", - "http-body-util", - "hyper", - "hyper-util", - "openshell-crypto", - "openshell-otel", - "opentelemetry-proto", - "prost", - "rcgen", - "rustls", - "tokio", - "tokio-rustls", - "tokio-stream", - "tonic", - "tracing", - "tracing-subscriber", -] - [[package]] name = "openshell-policy-schema" version = "0.0.0" @@ -2245,27 +2030,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "openshell-test-feature-oci-image" -version = "0.0.0" -dependencies = [ - "openshell-conformance", - "serde_json", - "tempfile", - "tokio", -] - -[[package]] -name = "openshell-test-feature-provider-refresh-keycloak" -version = "0.0.0" -dependencies = [ - "openshell-conformance", - "openshell-e2e", - "serde_json", - "tempfile", - "tokio", -] - [[package]] name = "openshell-testkit" version = "0.0.0" @@ -2296,83 +2060,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" -[[package]] -name = "opentelemetry" -version = "0.33.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c1f1a6ac9c875a4941e4c7c085b61e8f4daa326de03b4701768c3491a6ad9ee" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", - "thiserror 2.0.20", - "tracing", -] - -[[package]] -name = "opentelemetry-http" -version = "0.33.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302249bd8ee804789faaa7b5b2cbf79bc61cb864714079c644e3828af5b7e811" -dependencies = [ - "async-trait", - "bytes", - "http", - "opentelemetry", -] - -[[package]] -name = "opentelemetry-otlp" -version = "0.33.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0756af24841e367128ef31c831999937b8b5c5197d47914590d8f4e2e88c2e1a" -dependencies = [ - "flate2", - "http", - "httpdate", - "opentelemetry", - "opentelemetry-http", - "opentelemetry-proto", - "opentelemetry_sdk", - "prost", - "thiserror 2.0.20", - "tokio", - "tonic", - "tonic-types", -] - -[[package]] -name = "opentelemetry-proto" -version = "0.33.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d576f5dddc3299341bcd85a04be17b2564844c0c9563214f02d85f2524d1afae" -dependencies = [ - "opentelemetry", - "opentelemetry_sdk", - "prost", - "tonic", - "tonic-prost", -] - -[[package]] -name = "opentelemetry_sdk" -version = "0.33.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372a912db2974d1990207006fd0e3aa0ef3fd4ad3be43c65cfaadf2206866061" -dependencies = [ - "futures-channel", - "futures-executor", - "futures-util", - "opentelemetry", - "percent-encoding", - "portable-atomic", - "rand 0.9.4", - "thiserror 2.0.20", - "tokio", - "tokio-stream", -] - [[package]] name = "ordered-float" version = "2.10.1" @@ -2529,12 +2216,6 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - [[package]] name = "potential_utf" version = "0.1.5" @@ -3163,17 +2844,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3243,15 +2913,6 @@ dependencies = [ "digest 0.11.2", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "shlex" version = "1.3.0" @@ -3277,12 +2938,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - [[package]] name = "simple_asn1" version = "0.6.4" @@ -3666,15 +3321,6 @@ dependencies = [ "syn 3.0.5", ] -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - [[package]] name = "time" version = "0.3.47" @@ -3821,7 +3467,6 @@ dependencies = [ "axum", "base64 0.22.1", "bytes", - "flate2", "h2", "http", "http-body", @@ -3931,7 +3576,6 @@ dependencies = [ "tower-layer", "tower-service", "tracing", - "uuid", ] [[package]] @@ -3976,64 +3620,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-opentelemetry" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a904802a1b902f43638b677ff2a650847e3b4404101b6c586d648e8c1e3e8fe" -dependencies = [ - "js-sys", - "opentelemetry", - "tracing", - "tracing-core", - "tracing-log", - "tracing-subscriber", - "web-time", -] - -[[package]] -name = "tracing-serde" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" -dependencies = [ - "serde", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "serde", - "serde_json", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", - "tracing-serde", ] [[package]] @@ -4184,12 +3770,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - [[package]] name = "vcpkg" version = "0.2.15" @@ -4334,16 +3914,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "webpki-roots" version = "1.0.10" @@ -4359,28 +3929,6 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-core" version = "0.62.2" @@ -4719,20 +4267,6 @@ name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] [[package]] name = "zerotrie" diff --git a/tests/suites/features/Cargo.toml b/tests/suites/features/Cargo.toml index a9a0448c3e..5912e9176f 100644 --- a/tests/suites/features/Cargo.toml +++ b/tests/suites/features/Cargo.toml @@ -3,4 +3,4 @@ [workspace] resolver = "2" -members = ["keycloak-provider-refresh", "kubernetes-updates", "oci-image", "provider-refresh/keycloak"] +members = ["keycloak-provider-refresh", "kubernetes-updates", "oci-image"] diff --git a/tests/suites/features/keycloak-provider-refresh/Cargo.toml b/tests/suites/features/keycloak-provider-refresh/Cargo.toml index 08ce2237ba..e326fef5cb 100644 --- a/tests/suites/features/keycloak-provider-refresh/Cargo.toml +++ b/tests/suites/features/keycloak-provider-refresh/Cargo.toml @@ -12,6 +12,6 @@ repository = "https://github.com/NVIDIA/OpenShell" publish = false [dependencies] -keycloak = { path = "../../../support/keycloak" } +keycloak-support = { path = "../../../support/keycloak" } openshell-testkit = { path = "../../../testkit/testkit" } tempfile = "3" diff --git a/tests/suites/features/keycloak-provider-refresh/tests/provider_refresh.rs b/tests/suites/features/keycloak-provider-refresh/tests/provider_refresh.rs index fb57396901..e245a3aadb 100644 --- a/tests/suites/features/keycloak-provider-refresh/tests/provider_refresh.rs +++ b/tests/suites/features/keycloak-provider-refresh/tests/provider_refresh.rs @@ -4,13 +4,11 @@ //! Provider OAuth refresh against Keycloak. //! //! Keycloak is only the provider's token issuer here; the CLI still reaches -//! the gateway the usual way. Ported from -//! `tests/suites/features/provider-refresh/keycloak`, which tmachine's -//! `provider-refresh` testsuite runs until the required lanes switch. +//! the gateway the usual way. use std::io::Write as _; -use keycloak::KeycloakContext; +use keycloak_support::KeycloakContext; use testkit::{CliOutput, Context, Error, ResultExt as _}; const CREDENTIAL_KEY: &str = "KEYCLOAK_ACCESS_TOKEN"; @@ -137,7 +135,7 @@ fn combined(output: &CliOutput) -> String { fn write_profile( profile_id: &str, - keycloak: &keycloak::Keycloak, + keycloak: &keycloak_support::Keycloak, ) -> testkit::Result { let issuer_token_url = keycloak.token_endpoint(); let profile = format!( diff --git a/tests/suites/features/oci-image/Cargo.toml b/tests/suites/features/oci-image/Cargo.toml index 9ec58feccd..b966d2c38d 100644 --- a/tests/suites/features/oci-image/Cargo.toml +++ b/tests/suites/features/oci-image/Cargo.toml @@ -2,12 +2,17 @@ # SPDX-License-Identifier: Apache-2.0 [package] -name = "openshell-test-feature-oci-image" +name = "oci-image" +description = "Features suite: OCI image identity and workspace on the Docker and Podman drivers" version = "0.0.0" edition = "2024" +rust-version = "1.94" +license = "Apache-2.0" +repository = "https://github.com/NVIDIA/OpenShell" +publish = false [dependencies] -openshell-conformance = { path = "../../../../crates/openshell-conformance" } +container-engine-support = { path = "../../../support/container-engine" } +openshell-testkit = { path = "../../../testkit/testkit" } serde_json = "1" tempfile = "3" -tokio = { version = "1.43", features = ["macros", "rt"] } diff --git a/tests/suites/features/oci-image/tests/oci_image.rs b/tests/suites/features/oci-image/tests/oci_image.rs index a145782e4b..3d39cf3943 100644 --- a/tests/suites/features/oci-image/tests/oci_image.rs +++ b/tests/suites/features/oci-image/tests/oci_image.rs @@ -3,25 +3,18 @@ //! OCI image behavior shared by the Docker and Podman compute drivers. //! -//! Each test builds a small image with the container engine that backs the -//! target gateway, creates a sandbox from it through the candidate CLI, and -//! checks the process identity, privileges, workspace, and image content seen -//! by both the sandbox main process and `sandbox exec`. One scenario also -//! inspects the containers the driver created, through the same engine. -//! -//! `OPENSHELL_BIN` names the candidate CLI. `OPENSHELL_TEST_CONTAINER_ENGINE` -//! is the command for the gateway's container engine and image store, such as -//! `docker`, `podman`, or `sudo -n podman`. - -use std::process::Command; -use std::time::Duration; +//! Each test builds a small image on `testkit::base_image()` with the +//! gateway's container engine (see the `container-engine-support` crate), creates a +//! sandbox from it through the CLI, and checks the process identity, +//! privileges, workspace, and image content seen by both the sandbox main +//! process and `sandbox exec`. Some tests also inspect the containers the +//! driver created, through the same engine. + +use container_engine_support::{Engine, Image}; +use serde_json::Value; +use testkit::sdk::DeleteOptions; +use testkit::{Context, DELETE_TIMEOUT, Error, Result, ResultExt as _}; -use openshell_conformance::OpenShellRunner; - -const BASE_IMAGE: &str = "nvcr.io/nvidia/base/ubuntu:24.04"; -const ENGINE_ENV: &str = "OPENSHELL_TEST_CONTAINER_ENGINE"; -const CREATE_TIMEOUT: Duration = Duration::from_mins(10); -const COMMAND_TIMEOUT: Duration = Duration::from_mins(2); /// Sandbox condition reason for an image `WORKDIR` the sandbox identity /// cannot use. const WORKSPACE_VALIDATION_FAILED: &str = "WorkspaceValidationFailed"; @@ -39,36 +32,36 @@ landlock: network_policies: {} "; -/// A named image user that owns its custom `WORKDIR`. Existing image content -/// keeps its ownership. -#[tokio::test] -async fn custom_workdir_with_named_user() { - run( - "oci-image/custom-workdir-named-user", - async |runner, engine| { - let image = engine.build_image( +/// A named image user owns its custom `WORKDIR`, existing image content keeps +/// its ownership, and upload and download resolve paths in that workspace. +#[testkit::test] +async fn custom_workdir_with_named_user(cx: &mut Context) -> testkit::Result { + run(cx, async |oci| { + let image = oci + .build_image( "named-workdir", &format!( - "FROM {BASE_IMAGE} + "FROM {} RUN groupadd -g 1235 appstaff && useradd -m -u 1234 -g appstaff app WORKDIR /workspace/project RUN printf root-owned > root-owned.txt && chown app:appstaff . && \ printf custom-image-e2e-marker > /etc/oci-image-marker USER app -" +", + oci.base_image ), - )?; - let checks = format!( - "{} test \"$(stat -c %u:%g .)\" = 1234:1235; \ + ) + .await?; + let checks = format!( + "{} test \"$(stat -c %u:%g .)\" = 1234:1235; \ test \"$(cat /etc/oci-image-marker)\" = custom-image-e2e-marker; \ test \"$(stat -c %u:%g /etc/oci-image-marker)\" = 0:0;", - workspace_checks("1234:1235", "/workspace/project", true) - ); - let sandbox = create_sandbox(runner, "nu", &image, None, &checks).await?; - file_transfer_uses_workspace(runner, &sandbox).await - }, - ) - .await; + workspace_checks("1234:1235", "/workspace/project", true) + ); + let sandbox = oci.create_sandbox("nu", &image, None, &checks).await?; + oci.file_transfer_uses_workspace(&sandbox).await + }) + .await } /// A numeric image user without passwd entries can reach a custom `WORKDIR` @@ -76,67 +69,75 @@ USER app /// `process`, so the image `USER` is the only source of the sandbox identity. /// The driver's containers launch the inspected image and keep the /// supervisor separate from the workload. -#[tokio::test] -async fn custom_workdir_with_numeric_user_and_private_parents() { - run( - "oci-image/custom-workdir-numeric-user", - async |runner, engine| { - let image = engine.build_image( +#[testkit::test] +async fn custom_workdir_with_numeric_user_and_private_parents(cx: &mut Context) -> testkit::Result { + run(cx, async |oci| { + let image = oci + .build_image( "numeric-workdir", &format!( - "FROM {BASE_IMAGE} + "FROM {} RUN mkdir -p /home/app/project && \\ chown 2345:2346 /home/app /home/app/project && \\ chmod 0700 /home/app /home/app/project WORKDIR /home/app/project RUN printf root-owned > root-owned.txt USER 2345:2346 -" +", + oci.base_image ), - )?; - let policy = PolicyFile::new(IMAGE_IDENTITY_POLICY)?; - let checks = workspace_checks("2345:2346", "/home/app/project", true); - let sandbox = - create_sandbox(runner, "pu", &image, Some(policy.path()?), &checks).await?; - container_checks(engine, &sandbox, &image, "2345:2346") - }, - ) - .await; + ) + .await?; + let policy = PolicyFile::new(IMAGE_IDENTITY_POLICY)?; + let checks = workspace_checks("2345:2346", "/home/app/project", true); + let sandbox = oci + .create_sandbox("pu", &image, Some(policy.path()?), &checks) + .await?; + oci.container_checks(&sandbox, &image, "2345:2346").await + }) + .await } /// An image without `USER`, with no policy identity, runs as the numeric /// default `1000:1000`. The base image has an account with UID 1000 in other /// groups, such as `sudo`, and the default identity does not get them. -#[tokio::test] -async fn userless_image_uses_default_identity() { - run("oci-image/userless-default", async |runner, engine| { - let image = engine.build_image("userless", &format!("FROM {BASE_IMAGE}\n"))?; +#[testkit::test] +async fn userless_image_uses_default_identity(cx: &mut Context) -> testkit::Result { + run(cx, async |oci| { + let image = oci + .build_image("userless", &format!("FROM {}\n", oci.base_image)) + .await?; let policy = PolicyFile::new(IMAGE_IDENTITY_POLICY)?; let checks = format!( "{} test \"$(id -G)\" = 1000;", workspace_checks("1000:1000", "/sandbox", false) ); - let sandbox = create_sandbox(runner, "ud", &image, Some(policy.path()?), &checks).await?; - container_checks(engine, &sandbox, &image, "1000:1000") + let sandbox = oci + .create_sandbox("ud", &image, Some(policy.path()?), &checks) + .await?; + oci.container_checks(&sandbox, &image, "1000:1000").await }) - .await; + .await } /// A policy `run_as_user` and `run_as_group` replace an image's `USER root`. /// The image has no `WORKDIR`, so the managed `/sandbox` workspace is /// writable by the policy identity and only identity resolution is tested. -#[tokio::test] -async fn policy_identity_overrides_root_image_user() { - run("oci-image/policy-over-root", async |runner, engine| { - let image = engine.build_image( - "root-user", - &format!( - "FROM {BASE_IMAGE} +#[testkit::test] +async fn policy_identity_overrides_root_image_user(cx: &mut Context) -> testkit::Result { + run(cx, async |oci| { + let image = oci + .build_image( + "root-user", + &format!( + "FROM {} RUN groupadd -g 4235 appstaff && useradd -m -u 4234 -g appstaff app USER root -" - ), - )?; +", + oci.base_image + ), + ) + .await?; let policy = PolicyFile::new(&format!( "{IMAGE_IDENTITY_POLICY} process: @@ -148,59 +149,60 @@ process: "{} test \"$(id -G)\" = 4235;", workspace_checks("4234:4235", "/sandbox", false) ); - let sandbox = create_sandbox(runner, "pr", &image, Some(policy.path()?), &checks).await?; - container_checks(engine, &sandbox, &image, "4234:4235") + let sandbox = oci + .create_sandbox("pr", &image, Some(policy.path()?), &checks) + .await?; + oci.container_checks(&sandbox, &image, "4234:4235").await }) - .await; + .await } /// An image without a `WORKDIR` uses the managed `/sandbox` workspace, owned /// by the image user, even when the image does not contain `/sandbox`. -#[tokio::test] -async fn default_workdir_uses_managed_workspace() { - run("oci-image/default-workdir", async |runner, engine| { - let image = engine.build_image( - "default-workdir", - &format!("FROM {BASE_IMAGE}\nUSER 2345:2346\n"), - )?; +#[testkit::test] +async fn default_workdir_uses_managed_workspace(cx: &mut Context) -> testkit::Result { + run(cx, async |oci| { + let image = oci + .build_image( + "default-workdir", + &format!("FROM {}\nUSER 2345:2346\n", oci.base_image), + ) + .await?; let checks = workspace_checks("2345:2346", "/sandbox", false); - create_sandbox(runner, "dw", &image, None, &checks) + oci.create_sandbox("dw", &image, None, &checks) .await .map(drop) }) - .await; + .await } /// OpenShell rejects a custom `WORKDIR` that the image user cannot write /// instead of granting the user new access to it, and reports that reason /// rather than a generic startup failure. -#[tokio::test] -async fn unwritable_custom_workdir_is_rejected() { - run("oci-image/unwritable-workdir", async |runner, engine| { +#[testkit::test] +async fn unwritable_custom_workdir_is_rejected(cx: &mut Context) -> testkit::Result { + run(cx, async |oci| { // First prove that this user, workspace and command can run. Only the // directory owner differs between the control and rejected image. for (suffix, owner, writable) in [("wc", "3234:3235", true), ("uw", "0:0", false)] { - let image = engine.build_image( - suffix, - &format!( - "FROM {BASE_IMAGE} + let image = oci + .build_image( + suffix, + &format!( + "FROM {} RUN groupadd -g 3235 appstaff && useradd -m -u 3234 -g appstaff app WORKDIR /workspace/project RUN chown {owner} . USER app -" - ), - )?; - let name = track_sandbox(runner, suffix); - let create = runner - .step(format!("{suffix}/create")) - .description(if writable { - "writable control runs the same command successfully" - } else { - "sandbox creation fails before the command runs" - }) - .with_timeout(CREATE_TIMEOUT) - .run(&[ +", + oci.base_image + ), + ) + .await?; + let name = oci.track(suffix); + let create = oci + .cx + .cli([ "sandbox", "create", "--name", @@ -213,84 +215,321 @@ USER app "-c", "echo workspace-access-marker", ]) - .await - .map_err(|error| error.to_string())?; + .await?; if writable { - create.require_success()?; - if !create.stdout().contains("workspace-access-marker") { - return Err( - create.failure_diagnostic("writable control executes the command marker") - ); - } - continue; - } - let printed = - |text: &str| create.stdout().contains(text) || create.stderr().contains(text); - if create.success() || printed("workspace-access-marker") { - return Err( - create.failure_diagnostic("sandbox creation fails before the command runs") + let create = create + .success() + .context("writable control runs the same command successfully")?; + testkit::ensure!( + create.stdout.contains("workspace-access-marker"), + "writable control did not print the command marker:\n{}", + create.stdout ); + continue; } + let printed = create.stdout.contains("workspace-access-marker") + || create.stderr.contains("workspace-access-marker"); + testkit::ensure!( + create.exit_code != 0 && !printed, + "sandbox creation did not fail before the command ran: `{}` exited with {}:\n{}{}", + create.command, + create.exit_code, + create.stdout, + create.stderr + ); // Require the condition reason, so another provisioning failure // cannot pass as a workspace rejection. - let sandbox = runner - .step("uw/get") - .description("rejected sandbox reports the workspace validation reason") - .with_timeout(COMMAND_TIMEOUT) - .run(&["sandbox", "get", &name, "--output", "json"]) - .await - .map_err(|error| error.to_string())?; - sandbox.require_success()?; - let reasons: Vec = sandbox - .json::() - .map_err(|error| error.to_string())?["conditions"] + let sandbox: Value = oci + .cx + .cli(["sandbox", "get", &name, "--output", "json"]) + .await? + .success()? + .json()?; + let reasons: Vec<&str> = sandbox["conditions"] .as_array() .into_iter() .flatten() - .filter_map(|condition| condition["reason"].as_str().map(str::to_string)) + .filter_map(|condition| condition["reason"].as_str()) .collect(); - if !reasons - .iter() - .any(|reason| reason == WORKSPACE_VALIDATION_FAILED) - { - return Err(sandbox.failure_diagnostic(&format!( - "sandbox condition reason is {WORKSPACE_VALIDATION_FAILED}, got {reasons:?}" - ))); - } + testkit::ensure!( + reasons.contains(&WORKSPACE_VALIDATION_FAILED), + "rejected sandbox condition reasons are {reasons:?}; expected {WORKSPACE_VALIDATION_FAILED}" + ); } Ok(()) }) - .await; + .await } -async fn run( - scenario: &str, - test: impl AsyncFnOnce(&mut OpenShellRunner, &mut Engine) -> Result<(), String>, -) { - let mut runner = - OpenShellRunner::from_env(scenario).expect("candidate openshell CLI is available"); - let mut engine = Engine::from_env(); - let result = async { - runner.check_gateway_status().await?; - test(&mut runner, &mut engine).await - } - .await; - // Delete sandboxes before their images. Non-forced image removal must - // report remaining users rather than deleting workloads behind the - // gateway's back. Keep functional and sandbox failures first. - let mut errors: Vec<_> = runner.finish(result).await.err().into_iter().collect(); - errors.extend(engine.remove_images()); - if !errors.is_empty() { - panic!("{scenario} failed:\n{}", errors.join("\n")); +/// What a test uses: the context, the gateway's container engine, and the +/// sandboxes it created through the CLI. +struct Oci<'a> { + cx: &'a Context, + engine: Engine, + base_image: String, + sandboxes: Vec, +} + +/// Run `test`, then delete its sandboxes and the images it built. +async fn run(cx: &Context, test: impl AsyncFnOnce(&mut Oci<'_>) -> Result) -> Result { + let mut oci = Oci { + cx, + engine: Engine::from_env()?, + base_image: testkit::base_image(), + sandboxes: Vec::new(), + }; + let result = test(&mut oci).await; + // Delete the sandboxes before their images, since image removal isn't + // forced; testkit's own sandbox cleanup runs too late for that. A panic + // skips this and leaks the images, but their tags are unique to the run. + let mut failures = oci.delete_sandboxes().await; + failures.extend(oci.engine.remove_images().await); + match (result, failures.is_empty()) { + (result, true) => result, + (Ok(()), false) => Err(Error::msg(format!( + "cleanup failed: {}", + failures.join("; ") + ))), + (Err(error), false) => Err(Error::msg(format!( + "{error}\ncleanup also failed: {}", + failures.join("; ") + ))), } } -/// Name a sandbox for this scenario and register it for cleanup. Sandbox -/// names are limited to 19 characters on some drivers. -fn track_sandbox(runner: &mut OpenShellRunner, short: &str) -> String { - let name = format!("oi-{}-{short}", runner.id()); - runner.track_sandbox(&name); - name +impl Oci<'_> { + /// Build `containerfile` with a tag unique to this run. + async fn build_image(&mut self, name: &str, containerfile: &str) -> Result { + let tag = format!("localhost/openshell-test-oci-{name}:{}", self.cx.run_id()); + self.engine.build_image(&tag, containerfile).await + } + + /// Name a sandbox for this test and register it for cleanup. Sandbox + /// names are limited to 19 characters on some drivers, so keep `short` + /// to a few characters. + fn track(&mut self, short: &str) -> String { + let name = self.cx.name(short); + self.cx.track(&name); + self.sandboxes.push(name.clone()); + name + } + + /// Delete the sandboxes this test created and wait until they are gone. + async fn delete_sandboxes(&self) -> Vec { + let client = self.cx.client(); + let mut failures = Vec::new(); + for name in &self.sandboxes { + let deleted = async { + client + .delete_sandbox( + name, + DeleteOptions { + allow_missing: true, + }, + ) + .await?; + client.wait_deleted(name, DELETE_TIMEOUT, None).await + }; + if let Err(error) = deleted.await { + failures.push(format!("delete sandbox {name}: {}", Error::from(error))); + } + } + failures + } + + /// Create a detached sandbox whose main process runs `checks` and writes + /// to the workspace, then run the same checks and a write through + /// `sandbox exec`. + async fn create_sandbox( + &mut self, + short: &str, + image: &Image, + policy: Option<&str>, + checks: &str, + ) -> Result { + let name = self.track(short); + let main = format!( + "(set -eu; {checks} touch main-write) >/tmp/oci-main.log 2>&1; \ + echo $? >/tmp/oci-main.status; exec sleep infinity" + ); + let mut args = vec!["sandbox", "create", "--name", &name, "--from", &image.tag]; + if let Some(policy) = policy { + args.extend(["--policy", policy]); + } + args.extend(["--detach", "--", "sh", "-c", &main]); + self.cx + .cli(args) + .await? + .success() + .context("sandbox starts from the test image")?; + + let script = format!( + "set -eu; i=0; \ + while [ ! -f /tmp/oci-main.status ]; do \ + i=$((i + 1)); [ \"$i\" -le 60 ] || {{ echo main process checks did not finish >&2; exit 1; }}; \ + sleep 1; \ + done; \ + if [ \"$(cat /tmp/oci-main.status)\" != 0 ]; then \ + echo main process checks failed: >&2; cat /tmp/oci-main.log >&2; exit 1; \ + fi; \ + test -f main-write; {checks} touch exec-write" + ); + self.exec(&name, &script) + .await + .context("main process and exec children see the image workspace")?; + Ok(name) + } + + /// Upload and download default to paths relative to the image workspace. + async fn file_transfer_uses_workspace(&self, sandbox: &str) -> Result { + let local = tempfile::tempdir() + .map_err(|error| Error::from_error(&error)) + .context("create temp dir")?; + let upload = local.path().join("oci-transfer.txt"); + write(&upload, "oci-transfer-ok")?; + self.cx + .cli([ + "sandbox", + "upload", + sandbox, + utf8(&upload)?, + "--no-git-ignore", + ]) + .await? + .success() + .context("upload without a destination writes to the workspace")?; + self.exec( + sandbox, + "test \"$(cat oci-transfer.txt)\" = oci-transfer-ok", + ) + .await + .context("uploaded file is in the workspace")?; + + let download = local.path().join("downloaded.txt"); + self.cx + .cli([ + "sandbox", + "download", + sandbox, + "oci-transfer.txt", + utf8(&download)?, + ]) + .await? + .success() + .context("download resolves relative paths in the workspace")?; + let downloaded = std::fs::read_to_string(&download) + .map_err(|error| Error::from_error(&error)) + .context("read download")?; + testkit::ensure!( + downloaded == "oci-transfer-ok", + "downloaded file has unexpected content: {downloaded:?}" + ); + + // A directory upload merges into an existing workspace directory. + let merge = local.path().join("merge-upload"); + std::fs::create_dir(&merge) + .map_err(|error| Error::from_error(&error)) + .context("create merge dir")?; + write(&merge.join("conflict.txt"), "local-conflict")?; + write(&merge.join("added.txt"), "local-added")?; + self.exec( + sandbox, + "mkdir merge-upload && printf remote-conflict > merge-upload/conflict.txt \ + && printf remote-preserved > merge-upload/unrelated.txt", + ) + .await + .context("seed an existing workspace directory")?; + self.cx + .cli([ + "sandbox", + "upload", + sandbox, + utf8(&merge)?, + "--no-git-ignore", + ]) + .await? + .success() + .context("directory upload merges into the existing directory")?; + self.exec( + sandbox, + "test \"$(cat merge-upload/conflict.txt)\" = local-conflict \ + && test \"$(cat merge-upload/added.txt)\" = local-added \ + && test \"$(cat merge-upload/unrelated.txt)\" = remote-preserved", + ) + .await + .context("upload overwrites conflicts and keeps unrelated files") + } + + /// Check the containers the driver created for `sandbox`. The workload + /// runs the image ID captured after the fixture build, before sandbox + /// creation, and starts directly as the sandbox `identity` without a + /// network or the supervisor's credentials. The supervisor runs in a + /// separate non-root container. + async fn container_checks(&self, sandbox: &str, image: &Image, identity: &str) -> Result { + let engine = &self.engine; + let workload = engine.container_for_role(sandbox, "sandbox").await?; + let supervisor = engine.container_for_role(sandbox, "supervisor").await?; + testkit::ensure!( + workload != supervisor, + "{sandbox} has no separate supervisor container" + ); + let launched = engine.inspect_container(&workload, "{{.Image}}").await?; + let digest = |id: &str| id.trim_start_matches("sha256:").to_string(); + testkit::ensure!( + digest(&launched) == digest(&image.id), + "workload launched image {launched}, expected the built image {}", + image.id + ); + let user = engine + .inspect_container(&workload, "{{.Config.User}}") + .await?; + testkit::ensure!( + user == identity, + "workload starts as {user:?}, expected {identity}" + ); + let network = engine + .inspect_container(&workload, "{{.HostConfig.NetworkMode}}") + .await?; + testkit::ensure!( + network == "none", + "workload network mode is {network:?}, expected none" + ); + let mounts = engine + .inspect_container( + &workload, + "{{range .Mounts}}{{println .Destination}}{{end}}", + ) + .await?; + if let Some(mount) = mounts.lines().find(|mount| { + mount.starts_with("/etc/openshell/tls") || mount.starts_with("/.openshell/supervisor") + }) { + return Err(Error::msg(format!( + "workload mounts supervisor credentials at {mount}" + ))); + } + let supervisor_user = engine + .inspect_container(&supervisor, "{{.Config.User}}") + .await?; + testkit::ensure!( + !matches!( + supervisor_user.split(':').next(), + None | Some("" | "0" | "root") + ), + "supervisor runs as root ({supervisor_user:?})" + ); + Ok(()) + } + + /// Run a shell script in `sandbox` through `sandbox exec`. + async fn exec(&self, sandbox: &str, script: &str) -> Result { + self.cx + .cli([ + "sandbox", "exec", "--name", sandbox, "--no-tty", "--", "sh", "-c", script, + ]) + .await? + .success() + .map(drop) + } } /// Shell checks for the identity, working directory, and `HOME` of a sandbox @@ -324,349 +563,30 @@ fn workspace_checks(identity: &str, workspace: &str, image_file: bool) -> String checks } -/// Create a detached sandbox whose main process runs `checks` and writes to -/// the workspace, then run the same checks and a write through `sandbox exec`. -async fn create_sandbox( - runner: &mut OpenShellRunner, - short: &str, - image: &TestImage, - policy: Option<&str>, - checks: &str, -) -> Result { - let name = track_sandbox(runner, short); - let main = format!( - "(set -eu; {checks} touch main-write) >/tmp/oci-main.log 2>&1; \ - echo $? >/tmp/oci-main.status; exec sleep infinity" - ); - let mut args = vec!["sandbox", "create", "--name", &name, "--from", &image.tag]; - if let Some(policy) = policy { - args.extend(["--policy", policy]); - } - args.extend(["--detach", "--", "sh", "-c", &main]); - run_ok( - runner, - format!("{short}/create"), - "sandbox starts from the test image", - CREATE_TIMEOUT, - &args, - ) - .await?; - - let script = format!( - "set -eu; i=0; \ - while [ ! -f /tmp/oci-main.status ]; do \ - i=$((i + 1)); [ \"$i\" -le 60 ] || {{ echo main process checks did not finish >&2; exit 1; }}; \ - sleep 1; \ - done; \ - if [ \"$(cat /tmp/oci-main.status)\" != 0 ]; then \ - echo main process checks failed: >&2; cat /tmp/oci-main.log >&2; exit 1; \ - fi; \ - test -f main-write; {checks} touch exec-write" - ); - exec( - runner, - &name, - format!("{short}/exec"), - "main process and exec children see the image workspace", - &script, - ) - .await?; - Ok(name) -} - -/// Upload and download default to paths relative to the image workspace. -async fn file_transfer_uses_workspace( - runner: &OpenShellRunner, - sandbox: &str, -) -> Result<(), String> { - let local = tempfile::tempdir().map_err(|error| format!("create temp dir: {error}"))?; - let upload = local.path().join("oci-transfer.txt"); - std::fs::write(&upload, "oci-transfer-ok").map_err(|error| format!("write upload: {error}"))?; - let upload = upload.to_str().ok_or("upload path is not UTF-8")?; - run_ok( - runner, - "named/upload", - "upload without a destination writes to the workspace", - COMMAND_TIMEOUT, - &["sandbox", "upload", sandbox, upload, "--no-git-ignore"], - ) - .await?; - exec( - runner, - sandbox, - "named/uploaded", - "uploaded file is in the workspace", - "test \"$(cat oci-transfer.txt)\" = oci-transfer-ok", - ) - .await?; - - let download = local.path().join("downloaded.txt"); - let download_path = download.to_str().ok_or("download path is not UTF-8")?; - run_ok( - runner, - "named/download", - "download resolves relative paths in the workspace", - COMMAND_TIMEOUT, - &[ - "sandbox", - "download", - sandbox, - "oci-transfer.txt", - download_path, - ], - ) - .await?; - let downloaded = - std::fs::read_to_string(&download).map_err(|error| format!("read download: {error}"))?; - if downloaded != "oci-transfer-ok" { - return Err(format!( - "downloaded file has unexpected content: {downloaded:?}" - )); - } - - // A directory upload merges into an existing workspace directory. - let merge = local.path().join("merge-upload"); - std::fs::create_dir(&merge).map_err(|error| format!("create merge dir: {error}"))?; - std::fs::write(merge.join("conflict.txt"), "local-conflict") - .and_then(|()| std::fs::write(merge.join("added.txt"), "local-added")) - .map_err(|error| format!("write merge files: {error}"))?; - let merge = merge.to_str().ok_or("merge path is not UTF-8")?; - exec( - runner, - sandbox, - "named/merge-seed", - "seed an existing workspace directory", - "mkdir merge-upload && printf remote-conflict > merge-upload/conflict.txt \ - && printf remote-preserved > merge-upload/unrelated.txt", - ) - .await?; - run_ok( - runner, - "named/merge-upload", - "directory upload merges into the existing directory", - COMMAND_TIMEOUT, - &["sandbox", "upload", sandbox, merge, "--no-git-ignore"], - ) - .await?; - exec( - runner, - sandbox, - "named/merged", - "upload overwrites conflicts and keeps unrelated files", - "test \"$(cat merge-upload/conflict.txt)\" = local-conflict \ - && test \"$(cat merge-upload/added.txt)\" = local-added \ - && test \"$(cat merge-upload/unrelated.txt)\" = remote-preserved", - ) - .await -} - -/// Check the containers the driver created for `sandbox`. The workload runs -/// the image ID captured after the fixture build, before sandbox creation, and -/// starts directly as the sandbox `identity` without a network or the -/// supervisor's credentials. The supervisor runs in a separate non-root -/// container. -fn container_checks( - engine: &Engine, - sandbox: &str, - image: &TestImage, - identity: &str, -) -> Result<(), String> { - let workload = engine.container_for_role(sandbox, "sandbox")?; - let supervisor = engine.container_for_role(sandbox, "supervisor")?; - if workload == supervisor { - return Err(format!("{sandbox} has no separate supervisor container")); - } - let launched = engine.inspect_container(&workload, "{{.Image}}")?; - let digest = |id: &str| id.trim_start_matches("sha256:").to_string(); - if digest(&launched) != digest(&image.id) { - return Err(format!( - "workload launched image {launched}, expected the built image {}", - image.id - )); - } - let user = engine.inspect_container(&workload, "{{.Config.User}}")?; - if user != identity { - return Err(format!("workload starts as {user:?}, expected {identity}")); - } - let network = engine.inspect_container(&workload, "{{.HostConfig.NetworkMode}}")?; - if network != "none" { - return Err(format!( - "workload network mode is {network:?}, expected none" - )); - } - let mounts = engine.inspect_container( - &workload, - "{{range .Mounts}}{{println .Destination}}{{end}}", - )?; - if let Some(mount) = mounts.lines().find(|mount| { - mount.starts_with("/etc/openshell/tls") || mount.starts_with("/.openshell/supervisor") - }) { - return Err(format!("workload mounts supervisor credentials at {mount}")); - } - let supervisor_user = engine.inspect_container(&supervisor, "{{.Config.User}}")?; - if matches!( - supervisor_user.split(':').next(), - None | Some("" | "0" | "root") - ) { - return Err(format!("supervisor runs as root ({supervisor_user:?})")); - } - Ok(()) -} - -/// Run a CLI step that must succeed. -async fn run_ok( - runner: &OpenShellRunner, - step: impl Into, - description: &str, - timeout: Duration, - args: &[&str], -) -> Result<(), String> { - runner - .step(step) - .description(description) - .with_timeout(timeout) - .run(args) - .await - .map_err(|error| error.to_string())? - .require_success() -} - -/// Run a shell script in `sandbox` through `sandbox exec`. -async fn exec( - runner: &OpenShellRunner, - sandbox: &str, - step: impl Into, - description: &str, - script: &str, -) -> Result<(), String> { - run_ok( - runner, - step, - description, - COMMAND_TIMEOUT, - &[ - "sandbox", "exec", "--name", sandbox, "--no-tty", "--", "sh", "-c", script, - ], - ) - .await -} - /// A sandbox policy written to a temporary file for `sandbox create --policy`. struct PolicyFile(tempfile::NamedTempFile); impl PolicyFile { - fn new(contents: &str) -> Result { + fn new(contents: &str) -> Result { let file = tempfile::NamedTempFile::new() - .map_err(|error| format!("create policy file: {error}"))?; - std::fs::write(file.path(), contents) - .map_err(|error| format!("write policy file: {error}"))?; + .map_err(|error| Error::from_error(&error)) + .context("create policy file")?; + write(file.path(), contents)?; Ok(Self(file)) } - fn path(&self) -> Result<&str, String> { - self.0 - .path() - .to_str() - .ok_or_else(|| "policy path is not UTF-8".to_string()) + fn path(&self) -> Result<&str> { + utf8(self.0.path()) } } -/// A built fixture's mutable tag and image ID captured before sandbox creation. -struct TestImage { - tag: String, - id: String, -} - -/// The gateway's container engine. Images it builds for a scenario are -/// removed by `run` after sandbox cleanup. -struct Engine { - command: Vec, - images: Vec, +fn write(path: &std::path::Path, contents: &str) -> Result { + std::fs::write(path, contents) + .map_err(|error| Error::from_error(&error)) + .context(format!("write {}", path.display())) } -impl Engine { - fn from_env() -> Self { - let command: Vec = std::env::var(ENGINE_ENV) - .unwrap_or_else(|_| panic!("{ENGINE_ENV} must name the gateway's container engine")) - .split_whitespace() - .map(str::to_string) - .collect(); - assert!(!command.is_empty(), "{ENGINE_ENV} is empty"); - Self { - command, - images: Vec::new(), - } - } - - /// Build `containerfile` and capture its image ID before sandbox creation. - fn build_image(&mut self, name: &str, containerfile: &str) -> Result { - let context = tempfile::tempdir().map_err(|error| format!("create context: {error}"))?; - let file = context.path().join("Containerfile"); - std::fs::write(&file, containerfile) - .map_err(|error| format!("write Containerfile: {error}"))?; - let tag = format!("localhost/openshell-test-oci-{name}:{}", std::process::id()); - self.output(&[ - "build", - "--file", - file.to_str().ok_or("Containerfile path is not UTF-8")?, - "--tag", - &tag, - context.path().to_str().ok_or("context path is not UTF-8")?, - ])?; - // A failed build leaves no tag to remove. - self.images.push(tag.clone()); - let id = self.output(&["image", "inspect", "--format", "{{.Id}}", &tag])?; - Ok(TestImage { tag, id }) - } - - /// Remove every built image, returning one error per failed removal. - fn remove_images(&self) -> Vec { - self.images - .iter() - .filter_map(|tag| self.output(&["image", "rm", tag]).err()) - .map(|error| format!("image cleanup failed: {error}")) - .collect() - } - - /// The one container with isolation `role` for `sandbox`. Docker and - /// Podman label sandbox containers the same way. - fn container_for_role(&self, sandbox: &str, role: &str) -> Result { - let ids = self.output(&[ - "ps", - "-aq", - "--filter", - &format!("label=openshell.ai/sandbox-name={sandbox}"), - "--filter", - &format!("label=openshell.ai/isolation-role={role}"), - ])?; - match ids.split_whitespace().collect::>().as_slice() { - [id] => Ok((*id).to_string()), - ids => Err(format!( - "expected one {role} container for {sandbox}, found {ids:?}" - )), - } - } - - fn inspect_container(&self, id: &str, format: &str) -> Result { - self.output(&["container", "inspect", "--format", format, id]) - } - - /// Run the engine and return its trimmed standard output. - fn output(&self, args: &[&str]) -> Result { - let command = format!("{} {}", self.command.join(" "), args.join(" ")); - let output = Command::new(&self.command[0]) - .args(&self.command[1..]) - .args(args) - .output() - .map_err(|error| format!("failed to run {command}: {error}"))?; - if !output.status.success() { - return Err(format!( - "{command} failed ({}):\n{}{}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } - Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) - } +fn utf8(path: &std::path::Path) -> Result<&str> { + path.to_str() + .ok_or_else(|| Error::msg(format!("{} is not UTF-8", path.display()))) } diff --git a/tests/suites/features/provider-refresh/keycloak/Cargo.toml b/tests/suites/features/provider-refresh/keycloak/Cargo.toml deleted file mode 100644 index f42a1c8bd7..0000000000 --- a/tests/suites/features/provider-refresh/keycloak/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -[package] -name = "openshell-test-feature-provider-refresh-keycloak" -version = "0.0.0" -edition = "2024" - -[dependencies] -openshell-conformance = { path = "../../../../../crates/openshell-conformance" } -openshell-e2e = { path = "../../../../../e2e/rust" } -serde_json = "1" -tempfile = "3" -tokio = { version = "1.43", features = ["macros", "process", "io-util", "rt"] } diff --git a/tests/suites/features/provider-refresh/keycloak/tests/provider_refresh.rs b/tests/suites/features/provider-refresh/keycloak/tests/provider_refresh.rs deleted file mode 100644 index 5bc12cf8bb..0000000000 --- a/tests/suites/features/provider-refresh/keycloak/tests/provider_refresh.rs +++ /dev/null @@ -1,340 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#![cfg(target_os = "linux")] - -//! Provider OAuth refresh recovery against Keycloak. -//! -//! OpenShell itself uses the local gateway's mTLS authentication. Keycloak is -//! only the provider token issuer: the test refreshes a valid grant, revokes -//! its Keycloak session, and verifies that the gateway reports the next -//! refresh as requiring user reauthorization. - -use std::io::Write as _; -use std::process::{Output, Stdio}; - -use openshell_e2e::harness::binary::openshell_cmd; -use serde_json::Value; -use tempfile::{Builder as TempFileBuilder, NamedTempFile}; -use tokio::io::AsyncWriteExt as _; -use tokio::process::Command; - -const PROVIDER_NAME: &str = "e2e-keycloak-refresh"; -const PROFILE_ID: &str = "e2e-keycloak-refresh"; -const CREDENTIAL_KEY: &str = "KEYCLOAK_ACCESS_TOKEN"; - -fn combined_output(output: &Output) -> String { - format!( - "{}{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ) -} - -async fn run_cli(args: &[&str], env: &[(&str, &str)]) -> Result { - openshell_cmd() - .args(args) - .env("NO_COLOR", "1") - .envs(env.iter().copied()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .await - .map_err(|error| format!("run openshell command: {error}")) -} - -async fn run_cli_success(args: &[&str], env: &[(&str, &str)]) -> Result { - let output = run_cli(args, env).await?; - let combined = combined_output(&output); - if !output.status.success() { - return Err(format!( - "openshell command failed (exit {:?}):\n{combined}", - output.status.code() - )); - } - Ok(combined) -} - -async fn acquire_keycloak_grant( - issuer: &str, - username: &str, - password: &str, -) -> Result<(String, String), String> { - let token_endpoint = format!("{issuer}/protocol/openid-connect/token"); - let username_form = format!("username={username}"); - let password_form = format!("password={password}"); - let output = Command::new("curl") - .args([ - "--fail", - "--silent", - "--show-error", - "--request", - "POST", - &token_endpoint, - "--data-urlencode", - "grant_type=password", - "--data-urlencode", - "client_id=openshell-cli", - "--data-urlencode", - &username_form, - "--data-urlencode", - &password_form, - "--data-urlencode", - "scope=openid", - ]) - .output() - .await - .map_err(|error| format!("request Keycloak grant: {error}"))?; - if !output.status.success() { - return Err(format!( - "Keycloak grant request failed (exit {:?}): {}", - output.status.code(), - String::from_utf8_lossy(&output.stderr) - )); - } - - let response: Value = serde_json::from_slice(&output.stdout) - .map_err(|error| format!("decode Keycloak grant response: {error}"))?; - let access_token = response - .get("access_token") - .and_then(Value::as_str) - .filter(|value| !value.is_empty()) - .ok_or_else(|| "Keycloak grant response omitted access_token".to_string())?; - let refresh_token = response - .get("refresh_token") - .and_then(Value::as_str) - .filter(|value| !value.is_empty()) - .ok_or_else(|| "Keycloak grant response omitted refresh_token".to_string())?; - Ok((access_token.to_string(), refresh_token.to_string())) -} - -async fn revoke_keycloak_grant(issuer: &str, refresh_token: &str) -> Result<(), String> { - let logout_endpoint = format!("{issuer}/protocol/openid-connect/logout"); - let mut child = Command::new("curl") - .args([ - "--fail", - "--silent", - "--show-error", - "--output", - "/dev/null", - "--request", - "POST", - &logout_endpoint, - "--data-urlencode", - "client_id=openshell-cli", - "--data-urlencode", - "refresh_token@-", - ]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .map_err(|error| format!("start Keycloak logout request: {error}"))?; - child - .stdin - .take() - .ok_or_else(|| "Keycloak logout stdin was not piped".to_string())? - .write_all(refresh_token.as_bytes()) - .await - .map_err(|error| format!("write Keycloak logout request: {error}"))?; - let output = child - .wait_with_output() - .await - .map_err(|error| format!("wait for Keycloak logout request: {error}"))?; - if !output.status.success() { - return Err(format!( - "Keycloak logout failed (exit {:?}): {}", - output.status.code(), - String::from_utf8_lossy(&output.stderr) - )); - } - Ok(()) -} - -fn write_profile(issuer: &str) -> Result { - let mut file = TempFileBuilder::new() - .suffix(".yaml") - .tempfile() - .map_err(|error| format!("create provider profile: {error}"))?; - let profile = format!( - r#"id: {PROFILE_ID} -display_name: Keycloak provider refresh E2E -category: other -credentials: - - name: access_token - env_vars: [{CREDENTIAL_KEY}] - required: true - auth_style: bearer - header_name: authorization - refresh: - strategy: oauth2_refresh_token - token_url: {issuer}/protocol/openid-connect/token - scopes: [openid] - refresh_before_seconds: 60 - max_lifetime_seconds: 3600 - material: - - name: client_id - required: true - - name: refresh_token - required: true - secret: true -endpoints: - - host: keycloak.test.invalid - port: 443 - protocol: rest - access: read-only - enforcement: enforce -binaries: - - /usr/bin/curl -"# - ); - file.write_all(profile.as_bytes()) - .map_err(|error| format!("write provider profile: {error}"))?; - file.flush() - .map_err(|error| format!("flush provider profile: {error}"))?; - Ok(file) -} - -async fn delete_provider_resources() { - let _ = run_cli(&["provider", "delete", PROVIDER_NAME], &[]).await; - let _ = run_cli(&["profile", "delete", PROFILE_ID], &[]).await; -} - -#[tokio::test] -async fn revoked_refresh_grant_requires_user_reauthorization() -> Result<(), String> { - let issuer = std::env::var("OPENSHELL_E2E_OIDC_ISSUER") - .map_err(|_| "OPENSHELL_E2E_OIDC_ISSUER is required".to_string())?; - let username = std::env::var("OPENSHELL_E2E_OIDC_USERNAME") - .map_err(|_| "OPENSHELL_E2E_OIDC_USERNAME is required".to_string())?; - let password = std::env::var("OPENSHELL_E2E_OIDC_PASSWORD") - .map_err(|_| "OPENSHELL_E2E_OIDC_PASSWORD is required".to_string())?; - let (access_token, refresh_token) = - acquire_keycloak_grant(&issuer, &username, &password).await?; - let profile = write_profile(&issuer)?; - let profile_path = profile.path().to_string_lossy().into_owned(); - - delete_provider_resources().await; - let result = async { - run_cli_success( - &["profile", "import", "--file", &profile_path], - &[], - ) - .await?; - run_cli_success( - &[ - "provider", - "create", - "--name", - PROVIDER_NAME, - "--type", - PROFILE_ID, - "--credential", - CREDENTIAL_KEY, - ], - &[(CREDENTIAL_KEY, &access_token)], - ) - .await?; - run_cli_success( - &[ - "provider", - "refresh", - "configure", - PROVIDER_NAME, - "--credential-key", - CREDENTIAL_KEY, - "--strategy", - "oauth2-refresh-token", - "--material", - "client_id=openshell-cli", - "--secret-material-env", - "refresh_token=KEYCLOAK_REFRESH_TOKEN", - ], - &[("KEYCLOAK_REFRESH_TOKEN", &refresh_token)], - ) - .await?; - - run_cli_success( - &[ - "provider", - "refresh", - "rotate", - PROVIDER_NAME, - "--credential-key", - CREDENTIAL_KEY, - ], - &[], - ) - .await?; - let valid_status = run_cli_success( - &[ - "provider", - "refresh", - "status", - PROVIDER_NAME, - "--credential-key", - CREDENTIAL_KEY, - ], - &[], - ) - .await?; - if !valid_status.contains("refreshed") { - return Err(format!( - "valid Keycloak refresh did not reach refreshed state:\n{valid_status}" - )); - } - - revoke_keycloak_grant(&issuer, &refresh_token).await?; - let failed_rotation = run_cli( - &[ - "provider", - "refresh", - "rotate", - PROVIDER_NAME, - "--credential-key", - CREDENTIAL_KEY, - ], - &[], - ) - .await?; - let failed_rotation_output = combined_output(&failed_rotation); - if failed_rotation.status.success() || !failed_rotation_output.contains("invalid_grant") { - return Err(format!( - "revoked Keycloak refresh did not fail with invalid_grant:\n{failed_rotation_output}" - )); - } - - let revoked_status = run_cli_success( - &[ - "provider", - "refresh", - "status", - PROVIDER_NAME, - "--credential-key", - CREDENTIAL_KEY, - ], - &[], - ) - .await?; - for expected in [ - "reauthorization_required", - "reauthorize", - "oauth_invalid_grant", - ] { - if !revoked_status.contains(expected) { - return Err(format!( - "revoked refresh status omitted {expected}:\n{revoked_status}" - )); - } - } - if revoked_status.contains("292278994") { - return Err(format!( - "parked refresh rendered the i64::MAX scheduling sentinel as a date:\n{revoked_status}" - )); - } - Ok(()) - } - .await; - - delete_provider_resources().await; - result -} diff --git a/tests/support/Cargo.lock b/tests/support/Cargo.lock index a4ba9ea16c..a7e717b011 100644 --- a/tests/support/Cargo.lock +++ b/tests/support/Cargo.lock @@ -406,6 +406,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "container-engine-support" +version = "0.0.0" +dependencies = [ + "openshell-testkit", + "tempfile", + "tokio", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -1487,7 +1496,7 @@ dependencies = [ ] [[package]] -name = "keycloak" +name = "keycloak-support" version = "0.0.0" dependencies = [ "openshell-testkit", @@ -2176,6 +2185,14 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "podman-support" +version = "0.0.0" +dependencies = [ + "container-engine-support", + "openshell-testkit", +] + [[package]] name = "potential_utf" version = "0.1.5" diff --git a/tests/support/Cargo.toml b/tests/support/Cargo.toml index 51cafce449..f56ee275f5 100644 --- a/tests/support/Cargo.toml +++ b/tests/support/Cargo.toml @@ -5,4 +5,4 @@ # environment runs. Suites depend on them by path. [workspace] resolver = "2" -members = ["keycloak"] +members = ["container-engine", "keycloak", "podman"] diff --git a/tests/support/container-engine/Cargo.toml b/tests/support/container-engine/Cargo.toml new file mode 100644 index 0000000000..739e0986ca --- /dev/null +++ b/tests/support/container-engine/Cargo.toml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "container-engine-support" +description = "Access to the container engine behind a Docker or Podman gateway, for test suites" +version = "0.0.0" +edition = "2024" +rust-version = "1.94" +license = "Apache-2.0" +repository = "https://github.com/NVIDIA/OpenShell" +publish = false + +[dependencies] +openshell-testkit = { path = "../../testkit/testkit" } +tempfile = "3" +tokio = { version = "1.43", features = ["process"] } diff --git a/tests/support/container-engine/src/lib.rs b/tests/support/container-engine/src/lib.rs new file mode 100644 index 0000000000..ad80c771f1 --- /dev/null +++ b/tests/support/container-engine/src/lib.rs @@ -0,0 +1,183 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! The container engine behind a Docker or Podman gateway. +//! +//! Some suites check what the driver did in the engine: they build images the +//! gateway then runs, or inspect the containers it created. The environment +//! names the command that reaches the gateway's engine and image store in +//! `OPENSHELL_TEST_CONTAINER_ENGINE`: +//! +//! | Gateway | Value | +//! |---|---| +//! | Docker | `docker` | +//! | Rootless Podman, running as the test user | `podman` | +//! | Rootful Podman | `sudo -n podman` | +//! +//! The value is split on whitespace, so it can't hold arguments with spaces. +//! Under `openshell-test-runner`, the tmachine environment's setup publishes +//! it. A missing variable fails the test; it never skips it. + +use std::path::Path; + +use testkit::{Error, Result, ResultExt as _}; +use tokio::process::Command; + +pub const ENGINE_ENV: &str = "OPENSHELL_TEST_CONTAINER_ENGINE"; +/// The label the Docker and Podman drivers put on a sandbox's containers. +pub const SANDBOX_NAME_LABEL: &str = "openshell.ai/sandbox-name"; +/// The label that tells a sandbox's workload (`sandbox`) and `supervisor` +/// containers apart. +pub const ISOLATION_ROLE_LABEL: &str = "openshell.ai/isolation-role"; + +/// The gateway's container engine, and the images a test built with it. +#[derive(Debug)] +pub struct Engine { + command: Vec, + images: Vec, +} + +/// An image built for a test: its tag and the image ID it had when built. +#[derive(Clone, Debug)] +pub struct Image { + pub tag: String, + pub id: String, +} + +impl Engine { + /// Read [`ENGINE_ENV`]. + pub fn from_env() -> Result { + let value = std::env::var(ENGINE_ENV).unwrap_or_default(); + Self::new(value.split_whitespace()) + } + + /// An engine run as `command`, e.g. `["sudo", "-n", "podman"]`. + pub fn new(command: I) -> Result + where + I: IntoIterator, + S: Into, + { + let command: Vec = command.into_iter().map(Into::into).collect(); + testkit::ensure!( + !command.is_empty(), + "{ENGINE_ENV} is not set; this suite needs the gateway's container engine, such as docker, podman or `sudo -n podman` (see the container-engine crate docs)" + ); + Ok(Self { + command, + images: Vec::new(), + }) + } + + /// Build `containerfile` as `tag` and record the image for + /// [`Engine::remove_images`]. The build context is empty. + pub async fn build_image(&mut self, tag: &str, containerfile: &str) -> Result { + let context = tempfile::tempdir() + .map_err(|error| Error::from_error(&error)) + .context("create build context")?; + let file = context.path().join("Containerfile"); + std::fs::write(&file, containerfile) + .map_err(|error| Error::from_error(&error)) + .context("write Containerfile")?; + self.output([ + "build", + "--file", + utf8(&file)?, + "--tag", + tag, + utf8(context.path())?, + ]) + .await?; + // A failed build leaves no tag to remove. + self.images.push(tag.to_string()); + let id = self + .output(["image", "inspect", "--format", "{{.Id}}", tag]) + .await?; + Ok(Image { + tag: tag.to_string(), + id, + }) + } + + /// Remove every image built with [`Engine::build_image`], returning one + /// error per failed removal. Removal isn't forced: delete the sandboxes + /// that use an image first, so an image still in use is reported rather + /// than pulled from under the gateway. + pub async fn remove_images(&mut self) -> Vec { + let mut errors = Vec::new(); + for tag in std::mem::take(&mut self.images) { + if let Err(error) = self.output(["image", "rm", tag.as_str()]).await { + errors.push(format!("remove image {tag}: {error}")); + } + } + errors + } + + /// The one container with isolation `role` (`sandbox` or `supervisor`) + /// that the driver created for `sandbox`. + pub async fn container_for_role(&self, sandbox: &str, role: &str) -> Result { + let ids = self + .output([ + "ps", + "--all", + "--quiet", + "--filter", + &format!("label={SANDBOX_NAME_LABEL}={sandbox}"), + "--filter", + &format!("label={ISOLATION_ROLE_LABEL}={role}"), + ]) + .await?; + match ids.split_whitespace().collect::>().as_slice() { + [id] => Ok((*id).to_string()), + ids => Err(Error::msg(format!( + "expected one {role} container for sandbox {sandbox}, found {ids:?}" + ))), + } + } + + /// `container inspect --format format` for one container. + pub async fn inspect_container(&self, id: &str, format: &str) -> Result { + self.output(["container", "inspect", "--format", format, id]) + .await + } + + /// Run the engine with `args` and return its trimmed standard output. + /// A non-zero exit is an error that includes the engine's output. + pub async fn output(&self, args: I) -> Result + where + I: IntoIterator, + S: AsRef, + { + let args: Vec = args + .into_iter() + .map(|arg| arg.as_ref().to_string()) + .collect(); + let command = self + .command + .iter() + .chain(&args) + .map(String::as_str) + .collect::>() + .join(" "); + let output = Command::new(&self.command[0]) + .args(&self.command[1..]) + .args(&args) + .kill_on_drop(true) + .output() + .await + .map_err(|error| Error::from_error(&error)) + .context(format!("run {command}"))?; + testkit::ensure!( + output.status.success(), + "{command} failed ({}):\n{}{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) + } +} + +fn utf8(path: &Path) -> Result<&str> { + path.to_str() + .ok_or_else(|| Error::msg(format!("{} is not UTF-8", path.display()))) +} diff --git a/tests/support/keycloak/Cargo.toml b/tests/support/keycloak/Cargo.toml index 11fc5e12b1..c91cf2bc10 100644 --- a/tests/support/keycloak/Cargo.toml +++ b/tests/support/keycloak/Cargo.toml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 [package] -name = "keycloak" +name = "keycloak-support" description = "Keycloak fixture for test suites that need an OIDC issuer" version = "0.0.0" edition = "2024" diff --git a/tests/support/keycloak/src/lib.rs b/tests/support/keycloak/src/lib.rs index 76a4e9269f..9edcab716c 100644 --- a/tests/support/keycloak/src/lib.rs +++ b/tests/support/keycloak/src/lib.rs @@ -26,7 +26,7 @@ //! ```ignore //! #[testkit::test] //! async fn example(cx: &mut testkit::Context) -> testkit::Result { -//! let kc = keycloak::KeycloakContext::new(cx).await?; +//! let kc = keycloak_support::KeycloakContext::new(cx).await?; //! let grant = kc.keycloak().password_grant().await?; //! kc.cli(["status"]).await?.success()?; // Context methods still work //! Ok(()) diff --git a/tests/support/podman/Cargo.toml b/tests/support/podman/Cargo.toml new file mode 100644 index 0000000000..660541fed1 --- /dev/null +++ b/tests/support/podman/Cargo.toml @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "podman-support" +description = "Podman helpers for driver suites, such as the user-namespace reference" +version = "0.0.0" +edition = "2024" +rust-version = "1.94" +license = "Apache-2.0" +repository = "https://github.com/NVIDIA/OpenShell" +publish = false + +[dependencies] +container-engine-support = { path = "../container-engine" } +openshell-testkit = { path = "../../testkit/testkit" } diff --git a/tests/support/podman/src/lib.rs b/tests/support/podman/src/lib.rs new file mode 100644 index 0000000000..fd10e13e68 --- /dev/null +++ b/tests/support/podman/src/lib.rs @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Podman helpers for driver suites. +//! +//! The environment runs a Podman gateway configured with one user-namespace +//! mode and publishes the mode in `OPENSHELL_TEST_PODMAN_USERNS`: +//! +//! | Value | Gateway `[openshell.drivers.podman]` setting | +//! |---|---| +//! | `default` | no `userns` | +//! | `auto` | `userns = "auto"` | +//! | `keep-id` | `userns = "keep-id"` | +//! | `private` | `userns = "private"`, `uidmap` and `gidmap` `["0:0:1", "1:1:65535"]` | +//! +//! Under `openshell-test-runner`, the tmachine +//! `fedora-podman-*-userns-*` environments set the mode and publish it +//! (`tests/tmachine/ansible/playbooks/drivers/podman/userns.yaml`). A +//! missing or unknown value fails the test; it never skips it. +//! +//! [`reference_uid_map`] runs a container with the same mode through Podman +//! directly, with the engine from the `container-engine-support` crate, so tests +//! compare `OpenShell` with Podman's own behavior rather than with a mapping +//! baked into the test. + +use container_engine_support::Engine; +use testkit::{Error, Result, ResultExt as _}; + +pub const USERNS_ENV: &str = "OPENSHELL_TEST_PODMAN_USERNS"; + +/// The gateway's Podman user-namespace mode. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Userns { + /// No `userns` setting: Podman's default. + Default, + Auto, + KeepId, + /// `private` with the explicit maps `0:0:1` and `1:1:65535`. + Private, +} + +impl Userns { + /// Read [`USERNS_ENV`]. + pub fn from_env() -> Result { + let value = std::env::var(USERNS_ENV).unwrap_or_default(); + testkit::ensure!( + !value.is_empty(), + "{USERNS_ENV} is not set; this suite needs the gateway's Podman user-namespace mode (see the podman crate docs)" + ); + Self::parse(&value) + } + + pub fn parse(value: &str) -> Result { + match value { + "default" => Ok(Self::Default), + "auto" => Ok(Self::Auto), + "keep-id" => Ok(Self::KeepId), + "private" => Ok(Self::Private), + other => Err(Error::msg(format!( + "{USERNS_ENV} is {other:?}; expected default, auto, keep-id or private" + ))), + } + } + + /// The `podman run` arguments for the same user namespace as the gateway. + pub fn podman_args(self) -> &'static [&'static str] { + match self { + Self::Default => &[], + Self::Auto => &["--userns", "auto"], + Self::KeepId => &["--userns", "keep-id"], + // Podman infers a private namespace from explicit maps; its CLI + // rejects combining --userns private with --uidmap/--gidmap. + Self::Private => &[ + "--uidmap", + "0:0:1", + "--uidmap", + "1:1:65535", + "--gidmap", + "0:0:1", + "--gidmap", + "1:1:65535", + ], + } + } +} + +/// The UID map, normalized by [`normalize_uid_map`], of a container Podman +/// runs from `image` in `userns`, pulling `image` if the engine's store +/// doesn't have it. The container is removed before this returns. +/// +/// In `auto` mode Podman gives each running container its own range, so call +/// this before creating the sandbox it is compared with. The sandbox then gets +/// the range the removed reference container released. +pub async fn reference_uid_map(engine: &Engine, userns: Userns, image: &str) -> Result { + let mut args = vec!["run", "--rm", "--pull", "missing"]; + args.extend(userns.podman_args()); + args.extend(["--entrypoint", "/bin/cat", image, "/proc/self/uid_map"]); + let output = engine + .output(args) + .await + .context(format!("run a {userns:?} reference container"))?; + normalize_uid_map(&output).ok_or_else(|| { + Error::msg(format!( + "the {userns:?} reference container printed no UID mappings:\n{output}" + )) + }) +} + +/// The mappings in `/proc//uid_map` output, one `inside outside length` +/// per line, with adjacent ranges merged so equivalent maps compare equal. +/// Lines that aren't mappings are skipped. `None` if there are no mappings. +pub fn normalize_uid_map(value: &str) -> Option { + let mut mappings: Vec<(u64, u64, u64)> = Vec::new(); + for line in value.lines() { + let fields = line + .split_whitespace() + .map(str::parse::) + .collect::, _>>(); + let Ok(fields) = fields else { continue }; + let [inside, outside, length] = fields.as_slice() else { + continue; + }; + if let Some(previous) = mappings.last_mut() + && previous.0.checked_add(previous.2) == Some(*inside) + && previous.1.checked_add(previous.2) == Some(*outside) + { + previous.2 += *length; + } else { + mappings.push((*inside, *outside, *length)); + } + } + (!mappings.is_empty()).then(|| { + mappings + .iter() + .map(|(inside, outside, length)| format!("{inside} {outside} {length}")) + .collect::>() + .join("\n") + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn adjacent_uid_ranges_match_a_combined_mapping() { + assert_eq!( + normalize_uid_map("0 0 1\n1 1 65535\n"), + normalize_uid_map("0 0 65536\n") + ); + } + + #[test] + fn non_mapping_lines_are_skipped() { + assert_eq!( + normalize_uid_map("podman-userns-workspace-ok\n0 1000 1\n"), + Some("0 1000 1".to_string()) + ); + assert_eq!(normalize_uid_map("no mappings\n"), None); + } +} diff --git a/tests/testkit/README.md b/tests/testkit/README.md index ae71d0f81b..0bb82022b6 100644 --- a/tests/testkit/README.md +++ b/tests/testkit/README.md @@ -53,6 +53,10 @@ test creates, such as providers, in the test itself. create, so a create that fails after the gateway accepted it is still cleaned up. `cx.adopt(name)` returns a `Sandbox` handle for one the CLI created. - `cx.expect_listed` and `cx.expect_absent` to check a sandbox in list and get. +- `cx.expect_compute_driver(name)` to fail unless the gateway runs only that + compute driver. Driver suites call it first. +- `testkit::base_image()` for the image to create sandboxes from or build on: + `OPENSHELL_TEST_BASE_IMAGE`, or OpenShell's default sandbox image. - `cx.cli([...])` to run the installed CLI against the same gateway, with `success()`, `failure()`, and `json()` on the output. - `cx.client()` for SDK calls testkit does not wrap. diff --git a/tests/testkit/testkit/src/context.rs b/tests/testkit/testkit/src/context.rs index ccbb6edf79..d11f26c897 100644 --- a/tests/testkit/testkit/src/context.rs +++ b/tests/testkit/testkit/src/context.rs @@ -162,6 +162,37 @@ impl Context { Ok(()) } + /// Fail unless the gateway runs exactly one compute driver, `name`, as + /// gateway info reports it. Driver suites check this first, so a suite + /// pointed at another driver's gateway fails instead of testing it. + pub async fn expect_compute_driver(&self, name: &str) -> Result { + let info = self + .grpc + .client() + .await? + .get_gateway_info(proto::GetGatewayInfoRequest {}) + .await + .map_err(|status| Error::from_error(&status)) + .context("get gateway info")? + .into_inner(); + let drivers: Vec<(&str, &str)> = info + .compute_drivers + .iter() + .map(|driver| { + let reported = driver + .capabilities + .as_ref() + .map_or("", |capabilities| capabilities.driver_name.as_str()); + (driver.name.as_str(), reported) + }) + .collect(); + crate::ensure!( + matches!(drivers.as_slice(), [(configured, reported)] if *configured == name && *reported == name), + "the gateway reports compute drivers (name, driver name) {drivers:?}; expected only {name}" + ); + Ok(()) + } + /// Fail unless `list` shows the sandbox in `phase`. pub async fn expect_listed(&self, sandbox: &Sandbox, phase: SandboxPhase) -> Result { let name = sandbox.name(); diff --git a/tests/testkit/testkit/src/image.rs b/tests/testkit/testkit/src/image.rs new file mode 100644 index 0000000000..4e398800b1 --- /dev/null +++ b/tests/testkit/testkit/src/image.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! The base image tests create sandboxes from or build their images on. + +/// Overrides [`DEFAULT_BASE_IMAGE`], e.g. with a mirror. +pub const BASE_IMAGE_ENV: &str = "OPENSHELL_TEST_BASE_IMAGE"; +/// OpenShell's built-in default sandbox image. Pulling it needs access to +/// nvcr.io. +pub const DEFAULT_BASE_IMAGE: &str = "nvcr.io/nvidia/base/ubuntu:24.04"; + +/// The image named by [`BASE_IMAGE_ENV`], or [`DEFAULT_BASE_IMAGE`]. +pub fn base_image() -> String { + std::env::var(BASE_IMAGE_ENV) + .ok() + .filter(|image| !image.trim().is_empty()) + .unwrap_or_else(|| DEFAULT_BASE_IMAGE.to_string()) +} diff --git a/tests/testkit/testkit/src/lib.rs b/tests/testkit/testkit/src/lib.rs index 168fdbe894..429e411252 100644 --- a/tests/testkit/testkit/src/lib.rs +++ b/tests/testkit/testkit/src/lib.rs @@ -26,6 +26,7 @@ mod cli; mod context; mod error; pub mod gateway; +mod image; mod poll; mod run; @@ -36,6 +37,7 @@ pub use context::{ }; pub use error::{Error, Result, ResultExt}; pub use gateway::Gateway; +pub use image::{BASE_IMAGE_ENV, DEFAULT_BASE_IMAGE, base_image}; pub use openshell_sdk as sdk; pub use poll::{Poll, poll_until}; pub use run::{TestInfo, run}; diff --git a/tests/tmachine/ansible/playbooks/docker.yaml b/tests/tmachine/ansible/playbooks/docker.yaml index a91bc391b9..a760334c08 100644 --- a/tests/tmachine/ansible/playbooks/docker.yaml +++ b/tests/tmachine/ansible/playbooks/docker.yaml @@ -17,3 +17,12 @@ roles: - role: geerlingguy.docker become: true + tasks: + # tmachine is in the docker group, so tests reach the gateway's engine + # directly. See tests/support/container-engine. + - name: Publish container engine test variables + ansible.builtin.include_tasks: test-env.yaml + vars: + test_env_name: container-engine + test_env_vars: + OPENSHELL_TEST_CONTAINER_ENGINE: docker diff --git a/tests/tmachine/ansible/playbooks/drivers/podman/default-userns-baseline.yaml b/tests/tmachine/ansible/playbooks/drivers/podman/default-userns-baseline.yaml deleted file mode 100644 index 7356ae3c5e..0000000000 --- a/tests/tmachine/ansible/playbooks/drivers/podman/default-userns-baseline.yaml +++ /dev/null @@ -1,106 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - ---- -- name: Capture the default Podman user-namespace mapping - hosts: all - gather_facts: false - vars: - podman_reference_image: "{{ openshell_podman_reference_image }}" - podman_reference_uid_map: /var/lib/openshell-test-inputs/podman/reference-uid-map - tasks: - - name: Wait for SSH - ansible.builtin.wait_for_connection: - - - name: Detect tmachine container runtime - ansible.builtin.include_role: - name: tmachine_container_runtime - - - name: Require a Podman gateway - ansible.builtin.assert: - that: - - tmachine_container_runtime_name == "podman" - fail_msg: >- - The Podman default-userns suite requires a Podman gateway, not - {{ tmachine_container_runtime_name }} - - # podman_reference_user only selects who runs the direct `podman` - # reference commands below; it must match the daemon's rootful/rootless - # mode. The captured reference file itself stays owned by tmachine - # (the inventory's ansible_user), since the archived test binary that - # later reads it always runs unprivileged as tmachine, in both modes. - - name: Resolve the Podman reference execution context - ansible.builtin.set_fact: - podman_reference_user: "{{ 'tmachine' if tmachine_container_runtime_is_rootless else 'root' }}" - podman_reference_home: "{{ '/home/tmachine' if tmachine_container_runtime_is_rootless else '/root' }}" - podman_reference_uid: "{{ tmachine_container_runtime_tmachine_uid.stdout if tmachine_container_runtime_is_rootless else '0' }}" - - - name: Read OpenShell gateway configuration - become: true - ansible.builtin.slurp: - src: /etc/openshell/gateway.toml - register: openshell_gateway_config - - - name: Require unconfigured Podman user namespaces - ansible.builtin.assert: - that: - - >- - (openshell_gateway_config.content | b64decode) - is not regex('(?m)^\\s*(userns|uidmap|gidmap)\\s*=') - fail_msg: >- - The Podman default-userns suite requires gateway.toml to omit userns, - uidmap, and gidmap. - - - name: Create Podman test-input directory - become: true - ansible.builtin.file: - path: "{{ podman_reference_uid_map | dirname }}" - state: directory - owner: tmachine - group: tmachine - mode: "0700" - - - name: Pull the Podman reference image - become: true - become_user: "{{ podman_reference_user }}" - ansible.builtin.command: - argv: [podman, pull, "{{ podman_reference_image }}"] - environment: - HOME: "{{ podman_reference_home }}" - XDG_RUNTIME_DIR: "/run/user/{{ podman_reference_uid }}" - changed_when: false - - - name: Capture direct Podman default UID mapping - become: true - become_user: "{{ podman_reference_user }}" - ansible.builtin.command: - argv: - - podman - - run - - --rm - - --pull - - never - - --entrypoint - - /bin/cat - - "{{ podman_reference_image }}" - - /proc/self/uid_map - environment: - HOME: "{{ podman_reference_home }}" - XDG_RUNTIME_DIR: "/run/user/{{ podman_reference_uid }}" - changed_when: false - register: podman_default_uid_map - - - name: Require a direct Podman UID mapping - ansible.builtin.assert: - that: - - podman_default_uid_map.stdout | trim | length > 0 - fail_msg: Direct Podman reference container returned no UID mapping - - - name: Store direct Podman default UID mapping - become: true - ansible.builtin.copy: - content: "{{ podman_default_uid_map.stdout | trim }}\n" - dest: "{{ podman_reference_uid_map }}" - owner: tmachine - group: tmachine - mode: "0600" diff --git a/tests/tmachine/ansible/playbooks/drivers/podman/tests.yaml b/tests/tmachine/ansible/playbooks/drivers/podman/tests.yaml deleted file mode 100644 index abe2f6e553..0000000000 --- a/tests/tmachine/ansible/playbooks/drivers/podman/tests.yaml +++ /dev/null @@ -1,165 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - ---- -- name: Run Podman archive tests - hosts: all - gather_facts: false - vars: - podman_test_root: /var/lib/openshell-driver-tests/podman - podman_test_input_dir: /var/lib/openshell-test-inputs/podman - tasks: - - name: Wait for SSH - ansible.builtin.wait_for_connection: - - - name: Detect tmachine container runtime - ansible.builtin.include_role: - name: tmachine_container_runtime - - - name: Resolve the Podman reference execution context - ansible.builtin.set_fact: - podman_reference_user: "{{ 'tmachine' if tmachine_container_runtime_is_rootless else 'root' }}" - podman_reference_home: "{{ '/home/tmachine' if tmachine_container_runtime_is_rootless else '/root' }}" - podman_reference_uid: "{{ tmachine_container_runtime_tmachine_uid.stdout if tmachine_container_runtime_is_rootless else '0' }}" - - - name: Create Podman test directory - become: true - ansible.builtin.file: - path: "{{ podman_test_root }}" - state: directory - owner: tmachine - group: tmachine - mode: "0700" - - - name: Install Podman test bundle - become: true - ansible.builtin.unarchive: - src: "{{ openshell_podman_test_bundle }}" - dest: "{{ podman_test_root }}" - owner: tmachine - group: tmachine - - - name: Remove any previous OpenShell gateway registration - ansible.builtin.command: - argv: [/usr/local/bin/openshell, gateway, remove, tmachine] - changed_when: false - failed_when: false - - - name: Register the configured OpenShell gateway - ansible.builtin.command: - argv: [/usr/local/bin/openshell, gateway, add, http://127.0.0.1:17670, --local, --name, tmachine] - - - name: Run Podman archive tests - ansible.builtin.command: - argv: [cargo-nextest, nextest, run, --archive-file, "{{ podman_test_root }}/tests.tar.zst", --workspace-remap, "{{ podman_test_root }}", --no-capture] - environment: - OPENSHELL_BIN: /usr/local/bin/openshell - OPENSHELL_TEST_INPUT_DIR: "{{ podman_test_input_dir }}" - OPENSHELL_PODMAN_TEST_IMAGE: "{{ openshell_podman_test_image | default('') }}" - register: podman_test_result - changed_when: false - failed_when: false - - - name: Show Podman test diagnostics - ansible.builtin.debug: - var: podman_test_result.stderr_lines - - - name: Show Podman test result - ansible.builtin.debug: - var: podman_test_result.stdout_lines - - - name: Capture OpenShell gateway journal after Podman test failure - become: true - ansible.builtin.command: - argv: - - journalctl - - --unit - - openshell-gateway.service - - --no-pager - - --lines - - "200" - changed_when: false - failed_when: false - when: podman_test_result.rc != 0 - register: podman_gateway_journal - - - name: Show OpenShell gateway journal after Podman test failure - ansible.builtin.debug: - var: podman_gateway_journal.stdout_lines - when: podman_test_result.rc != 0 - - - name: Discover Podman containers after test failure - become: true - become_user: "{{ podman_reference_user }}" - ansible.builtin.command: - argv: [timeout, "15", podman, ps, --all, --quiet, --no-trunc] - environment: - HOME: "{{ podman_reference_home }}" - XDG_RUNTIME_DIR: "/run/user/{{ podman_reference_uid }}" - changed_when: false - failed_when: false - when: podman_test_result.rc != 0 - register: podman_failed_containers - - - name: Capture Podman journal after test failure - become: true - become_user: "{{ podman_reference_user }}" - ansible.builtin.command: - argv: >- - {{ ['journalctl'] + (['--user'] if tmachine_container_runtime_is_rootless else []) - + ['--unit', 'podman.service', '--no-pager', '--lines', '200'] }} - environment: - HOME: "{{ podman_reference_home }}" - XDG_RUNTIME_DIR: "/run/user/{{ podman_reference_uid }}" - changed_when: false - failed_when: false - when: podman_test_result.rc != 0 - register: podman_service_journal - - - name: Show Podman journal after test failure - ansible.builtin.debug: - var: podman_service_journal.stdout_lines - when: podman_test_result.rc != 0 - - - name: Capture Podman container inspection after test failure - become: true - become_user: "{{ podman_reference_user }}" - ansible.builtin.command: - argv: [timeout, "15", podman, inspect, "{{ item }}"] - environment: - HOME: "{{ podman_reference_home }}" - XDG_RUNTIME_DIR: "/run/user/{{ podman_reference_uid }}" - changed_when: false - failed_when: false - when: podman_test_result.rc != 0 - loop: "{{ podman_failed_containers.stdout_lines | default([]) }}" - register: podman_failed_container_inspection - - - name: Show Podman container inspection after test failure - ansible.builtin.debug: - var: podman_failed_container_inspection.results - when: podman_test_result.rc != 0 - - - name: Capture Podman container logs after test failure - become: true - become_user: "{{ podman_reference_user }}" - ansible.builtin.command: - argv: [timeout, "15", podman, logs, "{{ item }}"] - environment: - HOME: "{{ podman_reference_home }}" - XDG_RUNTIME_DIR: "/run/user/{{ podman_reference_uid }}" - changed_when: false - failed_when: false - when: podman_test_result.rc != 0 - loop: "{{ podman_failed_containers.stdout_lines | default([]) }}" - register: podman_failed_container_logs - - - name: Show Podman container logs after test failure - ansible.builtin.debug: - var: podman_failed_container_logs.results - when: podman_test_result.rc != 0 - - - name: Require Podman archive success - ansible.builtin.assert: - that: [podman_test_result.rc == 0] - fail_msg: Podman archive tests failed diff --git a/tests/tmachine/ansible/playbooks/drivers/podman/userns-auto.yaml b/tests/tmachine/ansible/playbooks/drivers/podman/userns-auto.yaml deleted file mode 100644 index 353f902c2c..0000000000 --- a/tests/tmachine/ansible/playbooks/drivers/podman/userns-auto.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - ---- -- import_playbook: userns-profile.yaml - vars: - podman_userns_profile: auto - podman_userns_config: "{{ openshell_podman_userns_auto_config }}" - podman_userns_reference_args: [--userns, auto] diff --git a/tests/tmachine/ansible/playbooks/drivers/podman/userns-keep-id.yaml b/tests/tmachine/ansible/playbooks/drivers/podman/userns-keep-id.yaml deleted file mode 100644 index e7ec08cce4..0000000000 --- a/tests/tmachine/ansible/playbooks/drivers/podman/userns-keep-id.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - ---- -- import_playbook: userns-profile.yaml - vars: - podman_userns_profile: keep-id - podman_userns_config: "{{ openshell_podman_userns_keep_id_config }}" - podman_userns_reference_args: [--userns, keep-id] diff --git a/tests/tmachine/ansible/playbooks/drivers/podman/userns-private.yaml b/tests/tmachine/ansible/playbooks/drivers/podman/userns-private.yaml deleted file mode 100644 index d24c0f38bc..0000000000 --- a/tests/tmachine/ansible/playbooks/drivers/podman/userns-private.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - ---- -- import_playbook: userns-profile.yaml - vars: - podman_userns_profile: private - podman_userns_config: "{{ openshell_podman_userns_private_config }}" - podman_userns_reference_args: - # Podman infers a private namespace from explicit maps; its CLI rejects - # combining --userns private with --uidmap/--gidmap. - - --uidmap - - 0:0:1 - - --uidmap - - 1:1:65535 - - --gidmap - - 0:0:1 - - --gidmap - - 1:1:65535 diff --git a/tests/tmachine/ansible/playbooks/drivers/podman/userns-profile.yaml b/tests/tmachine/ansible/playbooks/drivers/podman/userns-profile.yaml deleted file mode 100644 index a3071c2833..0000000000 --- a/tests/tmachine/ansible/playbooks/drivers/podman/userns-profile.yaml +++ /dev/null @@ -1,92 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - ---- -- name: Configure and capture a Podman user-namespace reference - hosts: all - gather_facts: false - vars: - podman_reference_image: "{{ openshell_podman_reference_image }}" - podman_reference_uid_map: /var/lib/openshell-test-inputs/podman/reference-uid-map - tasks: - - name: Wait for SSH - ansible.builtin.wait_for_connection: - - - name: Detect tmachine container runtime - ansible.builtin.include_role: - name: tmachine_container_runtime - - - name: Require a Podman gateway - ansible.builtin.assert: - that: - - tmachine_container_runtime_name == "podman" - fail_msg: >- - The Podman {{ podman_userns_profile }} suite requires a Podman gateway, not - {{ tmachine_container_runtime_name }} - - # podman_reference_user only selects who runs the direct `podman` - # reference command below; it must match the daemon's rootful/rootless - # mode. The captured reference file itself stays owned by tmachine - # (the inventory's ansible_user), since the archived test binary that - # later reads it always runs unprivileged as tmachine, in both modes. - - name: Resolve the Podman reference execution context - ansible.builtin.set_fact: - podman_reference_user: "{{ 'tmachine' if tmachine_container_runtime_is_rootless else 'root' }}" - podman_reference_home: "{{ '/home/tmachine' if tmachine_container_runtime_is_rootless else '/root' }}" - podman_reference_uid: "{{ tmachine_container_runtime_tmachine_uid.stdout if tmachine_container_runtime_is_rootless else '0' }}" - - - name: Apply the Podman user-namespace fixture - become: true - ansible.builtin.blockinfile: - path: /etc/openshell/gateway.toml - marker: "# {mark} OpenShell Podman userns test fixture" - block: "{{ lookup('ansible.builtin.file', podman_userns_config) | trim }}" - - - name: Restart OpenShell gateway with the Podman user-namespace fixture - become: true - ansible.builtin.systemd_service: - name: openshell-gateway.service - state: restarted - - - name: Wait for the configured OpenShell gateway - ansible.builtin.wait_for: - host: 127.0.0.1 - port: 17670 - timeout: 60 - - - name: Capture direct Podman user-namespace mapping - become: true - become_user: "{{ podman_reference_user }}" - ansible.builtin.command: - argv: "{{ [\"podman\", \"run\", \"--rm\", \"--pull\", \"never\"] + podman_userns_reference_args + [\"--entrypoint\", \"/bin/cat\", podman_reference_image, \"/proc/self/uid_map\"] }}" - environment: - HOME: "{{ podman_reference_home }}" - XDG_RUNTIME_DIR: "/run/user/{{ podman_reference_uid }}" - changed_when: false - register: podman_userns_uid_map - - - name: Require a direct Podman user-namespace mapping - ansible.builtin.assert: - that: - - podman_userns_uid_map.stdout | trim | length > 0 - fail_msg: >- - Direct Podman {{ podman_userns_profile }} reference container - returned no UID mapping - - - name: Create Podman test-input directory - become: true - ansible.builtin.file: - path: "{{ podman_reference_uid_map | dirname }}" - state: directory - owner: tmachine - group: tmachine - mode: "0700" - - - name: Store direct Podman user-namespace mapping - become: true - ansible.builtin.copy: - content: "{{ podman_userns_uid_map.stdout | trim }}\n" - dest: "{{ podman_reference_uid_map }}" - owner: tmachine - group: tmachine - mode: "0600" diff --git a/tests/tmachine/ansible/playbooks/drivers/podman/userns.yaml b/tests/tmachine/ansible/playbooks/drivers/podman/userns.yaml new file mode 100644 index 0000000000..89022bd769 --- /dev/null +++ b/tests/tmachine/ansible/playbooks/drivers/podman/userns.yaml @@ -0,0 +1,75 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Configure the Podman gateway's user-namespace mode and publish it. +# +# `podman_userns_mode` is default, auto, keep-id or private. Every mode except +# default writes its `[openshell.drivers.podman]` settings to +# `podman_userns_driver_config`, which the openshell_gateway role (the +# `binaries` installer) appends to gateway.toml; default removes the file. The +# Podman support crate (tests/support/podman) reads the published mode and +# runs Podman's own reference container in it. +--- +- name: Podman user namespaces for tests + hosts: all + gather_facts: false + vars: + podman_userns_driver_config: /etc/openshell-test/podman-driver.toml + podman_userns_settings: + default: "" + auto: | + userns = "auto" + keep-id: | + userns = "keep-id" + private: | + userns = "private" + uidmap = ["0:0:1", "1:1:65535"] + gidmap = ["0:0:1", "1:1:65535"] + tasks: + - name: Wait for SSH + ansible.builtin.wait_for_connection: + + - name: Require a known user-namespace mode + ansible.builtin.assert: + that: + - podman_userns_mode is defined + - podman_userns_mode in podman_userns_settings + fail_msg: >- + podman_userns_mode must be one of + {{ podman_userns_settings.keys() | join(', ') }}, not + {{ podman_userns_mode | default('unset') }} + + - name: Create the test configuration directory + become: true + ansible.builtin.file: + path: "{{ podman_userns_driver_config | dirname }}" + state: directory + owner: root + group: root + mode: "0755" + + - name: Write the Podman driver user-namespace settings + become: true + ansible.builtin.copy: + dest: "{{ podman_userns_driver_config }}" + content: "{{ podman_userns_settings[podman_userns_mode] }}" + owner: root + group: root + mode: "0644" + when: podman_userns_mode != "default" + + # The fragment is the only source of userns, uidmap and gidmap settings in + # the rendered gateway.toml, so default mode leaves them unset. + - name: Remove Podman driver user-namespace settings for default mode + become: true + ansible.builtin.file: + path: "{{ podman_userns_driver_config }}" + state: absent + when: podman_userns_mode == "default" + + - name: Publish Podman user-namespace test variables + ansible.builtin.include_tasks: ../../test-env.yaml + vars: + test_env_name: podman-userns + test_env_vars: + OPENSHELL_TEST_PODMAN_USERNS: "{{ podman_userns_mode }}" diff --git a/tests/tmachine/ansible/playbooks/features/oci-image.yaml b/tests/tmachine/ansible/playbooks/features/oci-image.yaml deleted file mode 100644 index 6c6361263a..0000000000 --- a/tests/tmachine/ansible/playbooks/features/oci-image.yaml +++ /dev/null @@ -1,125 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - ---- -- name: Run OCI image feature tests - hosts: all - gather_facts: false - vars: - oci_image_test_root: /var/lib/openshell-oci-image/tests - # The tests are skipped when the gateway never connects. - oci_image_failed: "{{ gateway_status is failed or oci_image_result.rc != 0 }}" - tasks: - - name: Wait for SSH - ansible.builtin.wait_for_connection: - - - name: Detect tmachine container runtime - ansible.builtin.include_role: - name: tmachine_container_runtime - - # Tests build images into the store the gateway reads. Rootful Podman's - # store belongs to root, so the unprivileged test user builds through sudo. - - name: Select the gateway's container engine command - ansible.builtin.set_fact: - oci_image_container_engine: >- - {{ 'docker' if tmachine_container_runtime_name == 'docker' - else 'podman' if tmachine_container_runtime_is_rootless - else 'sudo -n podman' }} - - - name: Create OCI image test directory - become: true - ansible.builtin.file: - path: "{{ oci_image_test_root }}" - state: directory - owner: tmachine - group: tmachine - mode: "0700" - - - name: Extract OCI image test bundle - become: true - ansible.builtin.unarchive: - src: "{{ oci_image_test_bundle }}" - dest: "{{ oci_image_test_root }}" - owner: tmachine - group: tmachine - - - name: Check OCI image nextest archive - ansible.builtin.stat: - path: "{{ oci_image_test_root }}/tests.tar.zst" - register: oci_image_archive - - - name: Require OCI image nextest archive - ansible.builtin.assert: - that: - - oci_image_archive.stat.isreg | default(false) - fail_msg: OCI image test bundle did not contain tests.tar.zst - - - name: Resolve installed OpenShell CLI - ansible.builtin.command: - argv: - - /bin/sh - - -c - - command -v openshell - register: openshell_cli - changed_when: false - - # Every test boot restarts the gateway, and some installers take minutes to - # recover. Wait once here; the runner's per-scenario preflight stays short. - - name: Wait for the registered gateway - ansible.builtin.command: - argv: [timeout, 10s, "{{ openshell_cli.stdout }}", status, --output, json] - register: gateway_status - changed_when: false - until: >- - gateway_status.rc == 0 and - (gateway_status.stdout | from_json).status == 'connected' - retries: 60 - delay: 5 - ignore_errors: true - - - name: Run OCI image archive - ansible.builtin.command: - argv: - - cargo-nextest - - nextest - - run - - --archive-file - - "{{ oci_image_test_root }}/tests.tar.zst" - - --workspace-remap - - "{{ oci_image_test_root }}" - - --no-capture - # The guest has 4 GiB; keep sandbox creates from competing for it. - - --test-threads - - "1" - - --no-fail-fast - environment: - HOME: /home/tmachine - OPENSHELL_BIN: "{{ openshell_cli.stdout }}" - OPENSHELL_TEST_CONTAINER_ENGINE: "{{ oci_image_container_engine }}" - XDG_RUNTIME_DIR: "/run/user/{{ tmachine_container_runtime_tmachine_uid.stdout }}" - register: oci_image_result - changed_when: false - failed_when: false - when: gateway_status is succeeded - - - name: Show OCI image diagnostics - ansible.builtin.debug: - var: oci_image_result - when: oci_image_failed - - - name: Collect OpenShell gateway diagnostics - ansible.builtin.include_role: - name: openshell_diagnostics - when: oci_image_failed - - - name: Require a connected gateway - ansible.builtin.assert: - that: - - gateway_status is succeeded - fail_msg: OpenShell gateway did not connect before the OCI image tests; see gateway diagnostics - - - name: Require OCI image success - ansible.builtin.assert: - that: - - oci_image_result.rc == 0 - fail_msg: OCI image feature tests failed diff --git a/tests/tmachine/ansible/playbooks/features/provider-refresh/keycloak.yaml b/tests/tmachine/ansible/playbooks/features/provider-refresh/keycloak.yaml deleted file mode 100644 index 1aca72a488..0000000000 --- a/tests/tmachine/ansible/playbooks/features/provider-refresh/keycloak.yaml +++ /dev/null @@ -1,110 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - ---- -- name: Run Keycloak provider refresh tests - hosts: all - gather_facts: false - roles: - - keycloak - tasks: - - name: Wait for SSH - ansible.builtin.wait_for_connection: - - - name: Create Keycloak provider refresh test directory - become: true - ansible.builtin.file: - path: /var/lib/openshell-provider-refresh/tests - state: directory - owner: tmachine - group: tmachine - mode: "0700" - - - name: Install Keycloak provider refresh test bundle - become: true - ansible.builtin.copy: - src: "{{ provider_refresh_keycloak_test_bundle }}" - dest: /var/lib/openshell-provider-refresh/tests/bundle.tar - owner: tmachine - group: tmachine - mode: "0600" - - - name: Extract Keycloak provider refresh test bundle - become: true - ansible.builtin.unarchive: - src: /var/lib/openshell-provider-refresh/tests/bundle.tar - dest: /var/lib/openshell-provider-refresh/tests - owner: tmachine - group: tmachine - remote_src: true - - - name: Check Keycloak provider refresh test archive - ansible.builtin.stat: - path: /var/lib/openshell-provider-refresh/tests/tests.tar.zst - register: provider_refresh_keycloak_test_archive - - - name: Require Keycloak provider refresh test archive - ansible.builtin.assert: - that: - - provider_refresh_keycloak_test_archive.stat.isreg | default(false) - fail_msg: Keycloak provider refresh test bundle did not contain tests.tar.zst - - - name: Resolve installed OpenShell CLI - ansible.builtin.command: - argv: - - /bin/sh - - -c - - command -v openshell - register: openshell_cli - changed_when: false - - - name: Run Keycloak provider refresh archive - ansible.builtin.command: - argv: - - cargo-nextest - - nextest - - run - - --archive-file - - /var/lib/openshell-provider-refresh/tests/tests.tar.zst - - --workspace-remap - - /var/lib/openshell-provider-refresh/tests - - --no-capture - environment: - OPENSHELL_BIN: "{{ openshell_cli.stdout }}" - OPENSHELL_E2E_OIDC_ISSUER: http://127.0.0.1:8180/realms/openshell - OPENSHELL_E2E_OIDC_USERNAME: admin@test - OPENSHELL_E2E_OIDC_PASSWORD: admin - register: provider_refresh_result - changed_when: false - failed_when: false - - - name: Show Keycloak provider refresh result - ansible.builtin.debug: - var: provider_refresh_result - when: provider_refresh_result.rc != 0 - - - name: Read OpenShell gateway logs - become: true - ansible.builtin.command: - argv: - - journalctl - - --no-pager - - --lines - - "500" - - _SYSTEMD_UNIT=openshell-gateway.service - - "+" - - _SYSTEMD_USER_UNIT=openshell-gateway.service - register: openshell_gateway_logs - changed_when: false - failed_when: false - when: provider_refresh_result.rc != 0 - - - name: Show OpenShell gateway logs - ansible.builtin.debug: - var: openshell_gateway_logs.stdout_lines - when: provider_refresh_result.rc != 0 - - - name: Require Keycloak provider refresh success - ansible.builtin.assert: - that: provider_refresh_result.rc == 0 - fail_msg: Keycloak provider refresh test failed diff --git a/tests/tmachine/ansible/playbooks/podman-rootful.yaml b/tests/tmachine/ansible/playbooks/podman-rootful.yaml index 05eee89581..679b24e2ec 100644 --- a/tests/tmachine/ansible/playbooks/podman-rootful.yaml +++ b/tests/tmachine/ansible/playbooks/podman-rootful.yaml @@ -27,3 +27,12 @@ name: podman.socket enabled: true state: started + + # Tests run as tmachine and reach root's image store through sudo. See + # tests/support/container-engine. + - name: Publish container engine test variables + ansible.builtin.include_tasks: test-env.yaml + vars: + test_env_name: container-engine + test_env_vars: + OPENSHELL_TEST_CONTAINER_ENGINE: sudo -n podman diff --git a/tests/tmachine/ansible/playbooks/podman-rootless.yaml b/tests/tmachine/ansible/playbooks/podman-rootless.yaml index 816def359b..acea07dc99 100644 --- a/tests/tmachine/ansible/playbooks/podman-rootless.yaml +++ b/tests/tmachine/ansible/playbooks/podman-rootless.yaml @@ -25,3 +25,12 @@ HOME: /home/tmachine XDG_RUNTIME_DIR: /run/user/1000 DBUS_SESSION_BUS_ADDRESS: unix:path=/run/user/1000/bus + + # Tests run as tmachine, the user whose image store the gateway uses. See + # tests/support/container-engine. + - name: Publish container engine test variables + ansible.builtin.include_tasks: test-env.yaml + vars: + test_env_name: container-engine + test_env_vars: + OPENSHELL_TEST_CONTAINER_ENGINE: podman diff --git a/tests/tmachine/ansible/roles/keycloak/tasks/main.yaml b/tests/tmachine/ansible/roles/keycloak/tasks/main.yaml index 4ba39533b4..391f5ccabc 100644 --- a/tests/tmachine/ansible/roles/keycloak/tasks/main.yaml +++ b/tests/tmachine/ansible/roles/keycloak/tasks/main.yaml @@ -18,6 +18,10 @@ keycloak_runtime_state_directory: >- {{ '/home/tmachine/.local/share/openshell-keycloak' if tmachine_container_runtime_is_rootless else keycloak_state_directory }} + # Docker restarts the container itself. Podman has no daemon: its + # podman-restart.service starts `--restart always` containers at boot. + keycloak_restart_policy: >- + {{ 'unless-stopped' if tmachine_container_runtime_name == 'docker' else 'always' }} - name: Require Keycloak container runtime become: true @@ -75,7 +79,7 @@ # Environment setup bakes the container into a cached disk; it must # come back when a session boots from that disk. - --restart - - unless-stopped + - "{{ keycloak_restart_policy }}" - --publish - "127.0.0.1:{{ keycloak_port }}:8080" - --env @@ -91,6 +95,32 @@ HOME: "{{ keycloak_runtime_home }}" changed_when: true +- name: Restart Keycloak with rootful Podman at boot + become: true + ansible.builtin.systemd_service: + name: podman-restart.service + enabled: true + when: + - tmachine_container_runtime_name == 'podman' + - not tmachine_container_runtime_is_rootless + +# tmachine lingers (see podman-rootless.yaml), so its user manager, and this +# unit, start at boot. +- name: Restart Keycloak with rootless Podman at boot + become: true + become_user: tmachine + ansible.builtin.systemd_service: + name: podman-restart.service + scope: user + enabled: true + environment: + HOME: /home/tmachine + XDG_RUNTIME_DIR: "/run/user/{{ tmachine_container_runtime_tmachine_uid.stdout }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ tmachine_container_runtime_tmachine_uid.stdout }}/bus" + when: + - tmachine_container_runtime_name == 'podman' + - tmachine_container_runtime_is_rootless + - name: Wait for Keycloak discovery endpoint ansible.builtin.uri: url: "http://127.0.0.1:{{ keycloak_port }}/realms/{{ keycloak_realm }}/.well-known/openid-configuration" diff --git a/tests/tmachine/ansible/roles/openshell_gateway/tasks/podman.yaml b/tests/tmachine/ansible/roles/openshell_gateway/tasks/podman.yaml index 434015052e..784c56f6cb 100644 --- a/tests/tmachine/ansible/roles/openshell_gateway/tasks/podman.yaml +++ b/tests/tmachine/ansible/roles/openshell_gateway/tasks/podman.yaml @@ -31,3 +31,22 @@ environment: HOME: "{{ openshell_gateway_home }}" XDG_RUNTIME_DIR: "/run/user/{{ openshell_gateway_uid }}" + +# An environment can add [openshell.drivers.podman] settings, such as the +# user-namespace mode (playbooks/drivers/podman/userns.yaml). +- name: Check for environment Podman driver settings + ansible.builtin.stat: + path: /etc/openshell-test/podman-driver.toml + register: openshell_gateway_podman_driver_file + +- name: Read environment Podman driver settings + ansible.builtin.slurp: + src: /etc/openshell-test/podman-driver.toml + register: openshell_gateway_podman_driver_slurp + when: openshell_gateway_podman_driver_file.stat.exists + +- name: Select environment Podman driver settings + ansible.builtin.set_fact: + openshell_gateway_podman_driver_settings: >- + {{ openshell_gateway_podman_driver_slurp.content | b64decode | trim + if openshell_gateway_podman_driver_file.stat.exists else '' }} diff --git a/tests/tmachine/ansible/roles/openshell_gateway/templates/gateway-podman.toml.j2 b/tests/tmachine/ansible/roles/openshell_gateway/templates/gateway-podman.toml.j2 index e59314f632..0ee292ea29 100644 --- a/tests/tmachine/ansible/roles/openshell_gateway/templates/gateway-podman.toml.j2 +++ b/tests/tmachine/ansible/roles/openshell_gateway/templates/gateway-podman.toml.j2 @@ -8,4 +8,7 @@ image_pull_policy = "if_not_present" network_name = "tmachine" health_check_interval_secs = 10 stop_timeout_secs = 15 +{% if openshell_gateway_podman_driver_settings %} +{{ openshell_gateway_podman_driver_settings }} +{% endif %} {% endblock %} diff --git a/tests/tmachine/ansible/roles/openshell_packaged_gateway/tasks/main.yaml b/tests/tmachine/ansible/roles/openshell_packaged_gateway/tasks/main.yaml index 4d1277f5f4..155eaa44c8 100644 --- a/tests/tmachine/ansible/roles/openshell_packaged_gateway/tasks/main.yaml +++ b/tests/tmachine/ansible/roles/openshell_packaged_gateway/tasks/main.yaml @@ -21,6 +21,17 @@ name: openshell_gateway tasks_from: "{{ openshell_gateway_driver }}.yaml" +# The packaged gateway's configuration comes from its environment file, so it +# can't apply an environment's Podman driver settings. Fail rather than test a +# gateway without them. +- name: Require no environment Podman driver settings + ansible.builtin.assert: + that: + - not (openshell_gateway_podman_driver_settings | default('')) + fail_msg: >- + This environment configures Podman driver settings, which only the + binaries installer applies. + - name: Create packaged gateway environment directory become: true ansible.builtin.file: diff --git a/tests/tmachine/artifacts.nix b/tests/tmachine/artifacts.nix index 4dc79b4a4e..ef1be7ed98 100644 --- a/tests/tmachine/artifacts.nix +++ b/tests/tmachine/artifacts.nix @@ -82,23 +82,6 @@ let ''; }; - providerRefreshKeycloakArchive = mkTestArchive { - name = "provider-refresh-keycloak"; - workspacePath = "tests/suites/features"; - manifestPath = "tests/suites/features/Cargo.toml"; - package = "openshell-test-feature-provider-refresh-keycloak"; - target = muslToolchain.target; - output = "artifacts/test-archives/${muslToolchain.target}/provider-refresh-keycloak-tests.tar"; - }; - ociImageArchive = mkTestArchive { - name = "oci-image"; - workspacePath = "tests/suites/features"; - manifestPath = "tests/suites/features/Cargo.toml"; - package = "openshell-test-feature-oci-image"; - target = muslToolchain.target; - output = "artifacts/test-archives/${muslToolchain.target}/oci-image-tests.tar"; - }; - # Follow-up: binaries that do not yet run in the rootless tmachine guest. Each # entry says what it needs. Entries marked "wrapper lane" still run in the # wrapper-based Podman lane (`mise run e2e:podman:ci`), so they are covered @@ -174,14 +157,6 @@ let ''; }; - podmanDriverArchive = mkTestArchive { - name = "podman-driver"; - workspacePath = "tests/suites/drivers"; - manifestPath = "tests/suites/drivers/Cargo.toml"; - package = "openshell-test-suite-podman"; - target = muslToolchain.target; - output = "artifacts/test-archives/${muslToolchain.target}/openshell-podman-tests.tar"; - }; podmanE2eArchive = mkTestArchive { name = "podman-e2e"; workspacePath = "e2e/rust"; @@ -195,9 +170,6 @@ let in rec { inherit - providerRefreshKeycloakArchive - ociImageArchive - podmanDriverArchive podmanE2eArchive podmanE2eCiTests ; @@ -307,15 +279,9 @@ rec { testArchives = pkgs.writeShellApplication { name = "build-artifacts-test-archives"; runtimeInputs = [ - providerRefreshKeycloakArchive - ociImageArchive - podmanDriverArchive podmanE2eArchive ]; text = '' - build-provider-refresh-keycloak-test-archive - build-oci-image-test-archive - build-podman-driver-test-archive build-podman-e2e-test-archive ''; }; diff --git a/tests/tmachine/config.nix b/tests/tmachine/config.nix index 8355c8101b..ac1aa28d6d 100644 --- a/tests/tmachine/config.nix +++ b/tests/tmachine/config.nix @@ -31,6 +31,66 @@ let openshell_sandbox_image = "../../artifacts/images/openshell-sandbox-tmachine.tar"; openshell_supervisor_image = "../../artifacts/images/openshell-supervisor-tmachine.tar"; }; + # A fedora-podman- environment ("rootful" or "rootless") with more + # setup playbooks after the base ones. + fedoraPodman = + { + name, + podman, + playbooks, + inputs, + }: + { + inherit name; + machine = "fedora"; + setup = { + use_galaxy = false; + playbooks = [ + "ansible/playbooks/nextest.yaml" + "ansible/playbooks/test-tools.yaml" + "ansible/playbooks/selinux.yaml" + "ansible/playbooks/podman-${podman}.yaml" + ] + ++ playbooks; + inherit inputs; + }; + }; + # fedora-podman-{rootful,rootless}-keycloak run Keycloak for the + # keycloak-provider-refresh suite. + keycloakEnvironments = map ( + podman: + fedoraPodman { + name = "fedora-podman-${podman}-keycloak"; + inherit podman; + playbooks = [ "ansible/playbooks/keycloak.yaml" ]; + inputs.keycloak_realm_file = "../../scripts/keycloak-realm.json"; + } + ) podmanModes; + # fedora-podman-{rootful,rootless}-userns-{default,auto,keep-id,private} + # configure the Podman gateway's user-namespace mode, for the Podman driver + # suite. Only the binaries installer applies the mode. + podmanUsernsEnvironments = pkgs.lib.concatMap ( + podman: + map ( + userns: + fedoraPodman { + name = "fedora-podman-${podman}-userns-${userns}"; + inherit podman; + playbooks = [ "ansible/playbooks/drivers/podman/userns.yaml" ]; + inputs.podman_userns_mode = userns; + } + ) podmanUsernsModes + ) podmanModes; + podmanModes = [ + "rootful" + "rootless" + ]; + podmanUsernsModes = [ + "default" + "auto" + "keep-id" + "private" + ]; config = (pkgs.formats.yaml { }).generate "tmachine-config.yaml" { machines = [ { @@ -109,7 +169,9 @@ let ]; }; } - ]; + ] + ++ keycloakEnvironments + ++ podmanUsernsEnvironments; installers = [ { @@ -178,21 +240,6 @@ let inputs = { }; interactive = true; } - { - name = "provider-refresh"; - playbooks = [ "ansible/playbooks/features/provider-refresh/keycloak.yaml" ]; - inputs = { - keycloak_realm_file = "../../scripts/keycloak-realm.json"; - provider_refresh_keycloak_test_bundle = "../../artifacts/test-archives/${muslTarget}/provider-refresh-keycloak-tests.tar"; - }; - } - { - name = "oci-image"; - playbooks = [ "ansible/playbooks/features/oci-image.yaml" ]; - inputs = { - oci_image_test_bundle = "../../artifacts/test-archives/${muslTarget}/oci-image-tests.tar"; - }; - } { name = "e2e-podman"; playbooks = [ "ansible/playbooks/drivers/podman/e2e.yaml" ]; @@ -201,28 +248,6 @@ let openshell_podman_e2e_workload_image = "../../artifacts/images/openshell-e2e-python-dev.tar"; }; } - { - name = "driver-podman"; - playbooks = [ - "ansible/playbooks/drivers/podman/default-userns-baseline.yaml" - "ansible/playbooks/drivers/podman/tests.yaml" - "ansible/playbooks/drivers/podman/userns-auto.yaml" - "ansible/playbooks/drivers/podman/tests.yaml" - "ansible/playbooks/drivers/podman/userns-keep-id.yaml" - "ansible/playbooks/drivers/podman/tests.yaml" - "ansible/playbooks/drivers/podman/userns-private.yaml" - "ansible/playbooks/drivers/podman/tests.yaml" - ]; - inputs = { - openshell_podman_test_bundle = "../../artifacts/test-archives/${muslTarget}/openshell-podman-tests.tar"; - # Match OpenShell's compiled-in default so direct Podman and - # OpenShell containers resolve the same workload image metadata. - openshell_podman_reference_image = "nvcr.io/nvidia/base/ubuntu:24.04"; - openshell_podman_userns_auto_config = "../suites/drivers/podman/fixtures/userns-auto.toml"; - openshell_podman_userns_keep_id_config = "../suites/drivers/podman/fixtures/userns-keep-id.toml"; - openshell_podman_userns_private_config = "../suites/drivers/podman/fixtures/userns-private.toml"; - }; - } ]; };