From 4443ae7336c9acad865de864c67b04efa1ebca2b Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Thu, 17 Sep 2026 20:46:57 -0700 Subject: [PATCH 1/8] fix(supervisor): allow withheld credentials at startup (#3438) * fix(supervisor): allow withheld credentials at startup Signed-off-by: Drew Newberry * fix(supervisor): accept withheld credentials on reload Signed-off-by: Drew Newberry --------- Signed-off-by: Drew Newberry --- crates/openshell-supervisor/src/lib.rs | 122 ++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 2 deletions(-) diff --git a/crates/openshell-supervisor/src/lib.rs b/crates/openshell-supervisor/src/lib.rs index 0398cb09c..1de1a2544 100644 --- a/crates/openshell-supervisor/src/lib.rs +++ b/crates/openshell-supervisor/src/lib.rs @@ -2670,7 +2670,7 @@ fn prepare_startup_configuration( "Effective configuration admission rejected" )); } - if provider.readiness_reason != ProviderReadinessReason::Unspecified { + if !provider_environment_is_installable(provider.readiness_reason) { return Err(miette::miette!( "Provider credentials are not ready for installation" )); @@ -2686,6 +2686,18 @@ fn prepare_startup_configuration( Ok((engine, process_policy, credentials)) } +/// Whether the provider response contains a complete environment snapshot that +/// can be installed. Withheld and expired credentials are intentionally absent, +/// so those responses remain valid fail-closed snapshots. +fn provider_environment_is_installable(reason: ProviderReadinessReason) -> bool { + matches!( + reason, + ProviderReadinessReason::Unspecified + | ProviderReadinessReason::CredentialsWithheld + | ProviderReadinessReason::CredentialExpired + ) +} + fn prepare_provider_environment( provider: &openshell_core::grpc_client::ProviderEnvironmentResult, ) -> Result { @@ -4296,7 +4308,7 @@ async fn run_policy_poll_loop_with_client( { Ok(provider) if EnvironmentIdentity::from_environment(&provider) == desired_identity - && provider.readiness_reason == ProviderReadinessReason::Unspecified => + && provider_environment_is_installable(provider.readiness_reason) => { provider } @@ -5562,6 +5574,49 @@ network_policies: assert_eq!(credentials.revision(), 10); } + #[test] + fn startup_configuration_accepts_fail_closed_provider_environment() { + let policy = proto_policy_fixture(); + let mut snapshot = settings_poll_result( + Some(policy.clone()), + 1, + openshell_core::proto::PolicySource::Sandbox, + ); + snapshot.provider_env_revision = 10; + + for reason in [ + ProviderReadinessReason::CredentialsWithheld, + ProviderReadinessReason::CredentialExpired, + ] { + let mut provider = startup_provider(10); + provider.readiness_reason = reason; + provider + .environment + .insert("PROJECT_ID".to_string(), "example-project".to_string()); + provider + .non_secret_environment_keys + .push("PROJECT_ID".to_string()); + let (_, _, credentials) = prepare_startup_configuration(&snapshot, &policy, &provider) + .expect("withheld credentials preserve fail-closed startup"); + assert_eq!(credentials.revision(), 10); + assert!(credentials.snapshot().child_env.contains_key("PROJECT_ID")); + } + } + + #[test] + fn startup_configuration_rejects_provider_installation_failure() { + let policy = proto_policy_fixture(); + let snapshot = settings_poll_result( + Some(policy.clone()), + 1, + openshell_core::proto::PolicySource::Sandbox, + ); + let mut provider = startup_provider(0); + provider.readiness_reason = ProviderReadinessReason::CredentialInstallFailed; + + assert!(prepare_startup_configuration(&snapshot, &policy, &provider).is_err()); + } + #[test] fn startup_configuration_revalidates_on_restart_and_accepts_repair() { let policy = proto_policy_fixture(); @@ -6153,6 +6208,69 @@ network_policies: let _ = task.await; } + #[tokio::test] + async fn provider_poll_installs_fail_closed_environment_and_acknowledges_policy() { + let policy = proto_policy_fixture(); + let initial = settings_poll_result( + Some(policy.clone()), + 1, + openshell_core::proto::PolicySource::Sandbox, + ); + let engine = Arc::new(OpaEngine::from_proto(&policy).unwrap()); + let ctx = policy_poll_test_context( + engine, + LoadedPolicyOrigin::Gateway { + revision: Some(LoadedPolicyRevision::from_snapshot(&initial)), + has_last_valid_policy: true, + }, + default_middleware_connector(), + ); + let credentials = ctx.provider_credentials.clone(); + let (policy_gateway, polls, mut reports) = scripted_policy_gateway(); + let (requests, mut received) = tokio::sync::mpsc::unbounded_channel(); + polls.send(initial).unwrap(); + let task = tokio::spawn(run_policy_poll_loop_with_client( + ctx, + ScriptedProviderGateway { + policy: policy_gateway, + requests, + }, + )); + expect_policy_report(&mut reports, 1).await; + + let mut changed = settings_poll_result( + Some(policy), + 2, + openshell_core::proto::PolicySource::Sandbox, + ); + changed.provider_env_revision = 1; + polls.send(changed).unwrap(); + let response = timeout(Duration::from_secs(1), received.recv()) + .await + .expect("provider refresh requested") + .expect("poll loop active"); + let mut withheld = static_provider_environment(1, None); + withheld.policy_hash = "hash-v2".to_string(); + withheld.readiness_reason = ProviderReadinessReason::CredentialsWithheld; + withheld + .environment + .insert("PROJECT_ID".to_string(), "example-project".to_string()); + withheld + .non_secret_environment_keys + .push("PROJECT_ID".to_string()); + assert!(response.send(Ok(withheld)).is_ok()); + + expect_policy_report(&mut reports, 2).await; + assert_eq!(credentials.revision(), 1); + assert!( + credentials.snapshot().child_env.contains_key("PROJECT_ID"), + "the reduced snapshot retains non-secret provider configuration" + ); + + task.abort(); + let _ = task.await; + } + #[tokio::test] async fn provider_poll_preserves_static_references_across_rotation_failure_and_detach() { let engine = Arc::new(OpaEngine::from_proto(&proto_policy_fixture()).unwrap()); From c108c31696bee299a374cda0b2969cf9ffe4e74d Mon Sep 17 00:00:00 2001 From: Piotr Mlocek Date: Fri, 18 Sep 2026 03:43:01 +0000 Subject: [PATCH 2/8] docs(fern): sync announcement configuration (#3436) * docs(fern): sync announcement configuration Signed-off-by: Piotr Mlocek * fix(docs): scope announcements to synced channel Signed-off-by: Piotr Mlocek * docs(fern): use dev as source version Signed-off-by: Piotr Mlocek * docs(fern): use channel-neutral logo link Signed-off-by: Piotr Mlocek --------- Signed-off-by: Piotr Mlocek --- fern/README.md | 4 +- fern/docs.yml | 11 +- tasks/scripts/sync_docs_website.py | 43 ++++- tasks/scripts/sync_docs_website_test.py | 245 +++++++++++++++++++++++- 4 files changed, 292 insertions(+), 11 deletions(-) diff --git a/fern/README.md b/fern/README.md index b8e0ea396..8e86d8304 100644 --- a/fern/README.md +++ b/fern/README.md @@ -13,7 +13,7 @@ OpenShell uses [Fern](https://buildwithfern.com/) to validate, preview, and publ | `fern/assets/` | Logos and other shared assets. | | `fern/main.css` | Site-wide styles. | -In a normal source checkout, `fern/docs.yml` points the `latest` version at `docs/index.yml`. Release automation builds the multi-version configuration on the generated `docs-website` branch. +In a normal source checkout, `fern/docs.yml` points the `dev` version at `docs/index.yml`. Release automation builds the multi-version configuration on the generated `docs-website` branch and maps the source documentation to the channel being published. ## Local development @@ -54,6 +54,8 @@ The sync and publish workflows share the `docs-website` concurrency group. This The `dev` snapshot also owns the shared Fern configuration, components, assets, and CSS on `docs-website`. The `latest` snapshot copies its documentation and navigation but does not replace those shared files. This keeps the site configuration aligned with `main` while preserving the released content. +A `dev` sync copies the top-level `announcement` from the source `fern/docs.yml`. This announcement is the global fallback, and removing it from the source removes it from `docs-website`. Each snapshot sync copies the source version announcement only to the channel being updated. A version announcement overrides the global announcement for that version, so Release Dev cannot change the `latest` announcement and Release Tag cannot change the `dev` announcement. + ## Manual maintenance and publishing Maintainers can run `.github/workflows/sync-docs.yml` manually to add, refresh, or remove a historical version snapshot. The workflow preserves snapshots that were not selected. Production publishing is disabled by default for a manual sync. diff --git a/fern/docs.yml b/fern/docs.yml index 116151202..ad6ea31a0 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # Fern site config — merged with skills/convert-to-fern/assets/theme/nvidia/docs-theme.yml pattern # Production: https://docs.nvidia.com/openshell/ — preview + custom-domain share path prefix `openshell`. -# Internal MDX links use /latest/...; basepath-aware resolves them under /openshell/ on production. +# Source checkouts and pull request previews expose the current documentation at /dev. instances: - url: openshell.docs.buildwithfern.com/openshell custom-domain: docs.nvidia.com/openshell @@ -38,7 +38,7 @@ logo: dark: ./assets/NVIDIA_dark.svg light: ./assets/NVIDIA_light.svg height: 20 - href: /openshell/latest + href: /openshell right-text: OpenShell favicon: ./assets/NVIDIA_symbol.svg @@ -56,11 +56,10 @@ experimental: - ./components versions: - - display-name: Latest + - display-name: Dev path: ../docs/index.yml - slug: latest - announcement: - message: 'OpenShell 0.1.0 is coming soon. Track progress in the 0.1.0 milestone, read the prerelease documentation, or install a prerelease.' + slug: dev + availability: beta redirects: - source: "/openshell/latest/sandboxes/providers-v2" diff --git a/tasks/scripts/sync_docs_website.py b/tasks/scripts/sync_docs_website.py index 62000394d..396dd7126 100644 --- a/tasks/scripts/sync_docs_website.py +++ b/tasks/scripts/sync_docs_website.py @@ -36,6 +36,7 @@ class VersionEntry: display_name: str path: str availability: str | None = None + announcement: YamlMapping | None = None def parse_args() -> argparse.Namespace: @@ -312,6 +313,7 @@ def parse_versions(raw_versions: object) -> list[VersionEntry]: display_name = entry.get("display-name") path = entry.get("path") availability = entry.get("availability") + announcement = entry.get("announcement") if ( isinstance(slug, str) and isinstance(display_name, str) @@ -325,6 +327,9 @@ def parse_versions(raw_versions: object) -> list[VersionEntry]: availability=availability if isinstance(availability, str) else None, + announcement=cast("YamlMapping", announcement) + if isinstance(announcement, dict) + else None, ) ) return entries @@ -349,8 +354,8 @@ def ordered_entries( return [by_slug[slug] for slug in order] -def render_versions(entries: list[VersionEntry]) -> list[dict[str, str]]: - rendered: list[dict[str, str]] = [] +def render_versions(entries: list[VersionEntry]) -> list[YamlMapping]: + rendered: list[YamlMapping] = [] for entry in entries: item = { "display-name": entry.display_name, @@ -359,10 +364,36 @@ def render_versions(entries: list[VersionEntry]) -> list[dict[str, str]]: } if entry.availability is not None: item["availability"] = entry.availability + if entry.announcement is not None: + item["announcement"] = entry.announcement rendered.append(item) return rendered +def sync_global_announcement(source_docs_yml: Path, target_docs_yml: Path) -> None: + source_data = read_yaml(source_docs_yml) + source_announcement = source_data.get("announcement") + if source_announcement is not None and not isinstance(source_announcement, dict): + raise ValueError("docs.yml announcement must be a mapping") + + target_data = read_yaml(target_docs_yml) + if source_announcement is None: + target_data.pop("announcement", None) + else: + target_data["announcement"] = source_announcement + write_yaml(target_docs_yml, target_data) + + +def source_version_announcement(docs_yml: Path, slug: str) -> YamlMapping | None: + entries = parse_versions(read_yaml(docs_yml).get("versions")) + for entry in entries: + if entry.slug == slug: + return entry.announcement + if len(entries) == 1: + return entries[0].announcement + return None + + def component_dirs(fern_dir: Path) -> list[str]: dirs: list[str] = [] preferred = ["pages-latest", "pages-dev"] @@ -408,6 +439,7 @@ def write_snapshot( copy_if_exists( source_fern / "fern.config.json", target_fern / "fern.config.json" ) + sync_global_announcement(source_fern / "docs.yml", target_fern / "docs.yml") versions_dir = target_fern / "versions" versions_dir.mkdir(parents=True, exist_ok=True) @@ -483,6 +515,9 @@ def sync_docs(args: argparse.Namespace) -> None: display_name=slug, path=f"./versions/{slug}.yml", availability=stable_availability, + announcement=source_version_announcement( + source_fern / "docs.yml", slug + ), ), refresh_shared=False, ) @@ -509,6 +544,9 @@ def sync_docs(args: argparse.Namespace) -> None: display_name=display_override or f"Latest ({slug})", path="./versions/latest.yml", availability=stable_availability, + announcement=source_version_announcement( + source_fern / "docs.yml", "latest" + ), ), refresh_shared=False, ) @@ -548,6 +586,7 @@ def sync_docs(args: argparse.Namespace) -> None: display_name=display_name, path=f"./versions/{slug}.yml", availability=availability, + announcement=source_version_announcement(source_fern / "docs.yml", slug), ), refresh_shared=channel == "dev", ) diff --git a/tasks/scripts/sync_docs_website_test.py b/tasks/scripts/sync_docs_website_test.py index 076181ee3..bef5ac995 100644 --- a/tasks/scripts/sync_docs_website_test.py +++ b/tasks/scripts/sync_docs_website_test.py @@ -147,13 +147,14 @@ def test_resolve_availability() -> None: sdw.resolve_availability("dev", "alpha") -def test_parse_and_render_versions_preserves_availability() -> None: +def test_parse_and_render_versions_preserves_version_settings() -> None: raw_versions = [ { "display-name": "v0.0.36", "path": "./versions/v0.0.36.yml", "slug": "v0.0.36", "availability": "deprecated", + "announcement": {"message": "Upgrade to the latest version."}, } ] @@ -165,11 +166,104 @@ def test_parse_and_render_versions_preserves_availability() -> None: "v0.0.36", "./versions/v0.0.36.yml", "deprecated", + {"message": "Upgrade to the latest version."}, ) ] assert sdw.render_versions(entries) == raw_versions +def test_sync_global_announcement_applies_source_config(tmp_path: Path) -> None: + source_docs_yml = tmp_path / "source.yml" + target_docs_yml = tmp_path / "target.yml" + source_docs_yml.write_text( + yaml.safe_dump( + { + "announcement": {"message": "Current global announcement."}, + "versions": [], + } + ), + encoding="utf-8", + ) + target_docs_yml.write_text( + yaml.safe_dump( + { + "announcement": {"message": "Stale global announcement."}, + "versions": [ + { + "display-name": "Latest (v0.0.116)", + "path": "./versions/latest.yml", + "slug": "latest", + }, + { + "display-name": "Dev", + "path": "./versions/dev.yml", + "slug": "dev", + "announcement": {"message": "Development docs."}, + }, + ], + } + ), + encoding="utf-8", + ) + + sdw.sync_global_announcement(source_docs_yml, target_docs_yml) + + target_data = read_yaml(target_docs_yml) + assert target_data["announcement"] == {"message": "Current global announcement."} + assert target_data["versions"] == [ + { + "display-name": "Latest (v0.0.116)", + "path": "./versions/latest.yml", + "slug": "latest", + }, + { + "display-name": "Dev", + "path": "./versions/dev.yml", + "slug": "dev", + "announcement": {"message": "Development docs."}, + }, + ] + + source_docs_yml.write_text( + yaml.safe_dump({"versions": []}), + encoding="utf-8", + ) + + sdw.sync_global_announcement(source_docs_yml, target_docs_yml) + + target_data = read_yaml(target_docs_yml) + assert "announcement" not in target_data + versions = target_data["versions"] + assert "announcement" not in versions[0] + assert versions[1]["announcement"] == {"message": "Development docs."} + + +def test_source_version_announcement_maps_single_source_version_to_channel( + tmp_path: Path, +) -> None: + docs_yml = tmp_path / "docs.yml" + docs_yml.write_text( + yaml.safe_dump( + { + "versions": [ + { + "display-name": "Dev", + "path": "../docs/index.yml", + "slug": "dev", + "announcement": {"message": "Snapshot announcement."}, + } + ] + } + ), + encoding="utf-8", + ) + + expected = {"message": "Snapshot announcement."} + assert sdw.source_version_announcement(docs_yml, "latest") == expected + assert sdw.source_version_announcement(docs_yml, "dev") == expected + assert sdw.source_version_announcement(docs_yml, "v0.0.116") == expected + + def test_ordered_entries_pins_latest_then_dev() -> None: existing = [ sdw.VersionEntry("v0.0.36", "v0.0.36", "./versions/v0.0.36.yml"), @@ -212,6 +306,8 @@ def _make_source_tree(root: Path) -> None: encoding="utf-8", ) fern = root / "fern" + fern.mkdir(parents=True) + (fern / "docs.yml").write_text(yaml.safe_dump({"versions": []}), encoding="utf-8") (fern / "assets").mkdir(parents=True) (fern / "assets" / "logo.svg").write_text("", encoding="utf-8") (fern / "components").mkdir(parents=True) @@ -228,11 +324,39 @@ def _make_docs_website_tree(root: Path) -> None: (fern / "docs.yml").write_text(yaml.safe_dump({"versions": []}), encoding="utf-8") -def test_sync_docs_creates_planned_latest_and_dev_selector(tmp_path: Path) -> None: +def test_sync_docs_scopes_version_announcements_to_updated_channel( + tmp_path: Path, +) -> None: source = tmp_path / "source" website = tmp_path / "docs-website" _make_source_tree(source) _make_docs_website_tree(website) + (source / "fern" / "docs.yml").write_text( + yaml.safe_dump( + { + "versions": [ + { + "display-name": "Latest", + "path": "../docs/index.yml", + "slug": "latest", + "announcement": { + "message": "Version 0.0.116 is the final alpha release." + }, + } + ] + } + ), + encoding="utf-8", + ) + (website / "fern" / "docs.yml").write_text( + yaml.safe_dump( + { + "announcement": {"message": "Stale global announcement."}, + "versions": [], + } + ), + encoding="utf-8", + ) sdw.sync_docs( Namespace( @@ -248,6 +372,21 @@ def test_sync_docs_creates_planned_latest_and_dev_selector(tmp_path: Path) -> No availability="", ) ) + (source / "fern" / "docs.yml").write_text( + yaml.safe_dump( + { + "versions": [ + { + "display-name": "Dev", + "path": "../docs/index.yml", + "slug": "dev", + "announcement": {"message": "OpenShell 0.1.0 is coming soon."}, + } + ] + } + ), + encoding="utf-8", + ) sdw.sync_docs( Namespace( operation="sync", @@ -276,16 +415,53 @@ def test_sync_docs_creates_planned_latest_and_dev_selector(tmp_path: Path) -> No "display-name": "Latest (v0.0.116)", "path": "./versions/latest.yml", "slug": "latest", + "announcement": {"message": "Version 0.0.116 is the final alpha release."}, }, { "display-name": "Dev", "path": "./versions/dev.yml", "slug": "dev", "availability": "beta", + "announcement": {"message": "OpenShell 0.1.0 is coming soon."}, }, ] + assert "announcement" not in docs_yml assert "./components" in docs_yml["experimental"]["mdx-components"] + (source / "fern" / "docs.yml").write_text( + yaml.safe_dump( + { + "versions": [ + { + "display-name": "Dev", + "path": "../docs/index.yml", + "slug": "dev", + "announcement": {"message": "OpenShell 0.1.0 is released."}, + } + ] + } + ), + encoding="utf-8", + ) + sdw.sync_docs( + Namespace( + operation="sync", + source_root=source, + docs_website_root=website, + channel="latest", + source_ref="release-0.1.0-sha", + source_sha="release-0.1.0-sha", + release_version="0.1.0", + version_slug="", + display_name="Latest (v0.1.0)", + availability="", + ) + ) + + versions = read_yaml(fern / "docs.yml")["versions"] + assert versions[0]["announcement"] == {"message": "OpenShell 0.1.0 is released."} + assert versions[1]["announcement"] == {"message": "OpenShell 0.1.0 is coming soon."} + def test_sync_docs_preserves_other_version_availability(tmp_path: Path) -> None: source = tmp_path / "source" @@ -341,6 +517,71 @@ def test_sync_docs_preserves_other_version_availability(tmp_path: Path) -> None: ] +def test_latest_sync_updates_legacy_snapshot_announcement(tmp_path: Path) -> None: + source = tmp_path / "source" + website = tmp_path / "docs-website" + _make_source_tree(source) + _make_docs_website_tree(website) + (source / "fern" / "docs.yml").write_text( + yaml.safe_dump( + { + "versions": [ + { + "display-name": "Latest", + "path": "../docs/index.yml", + "slug": "latest", + "announcement": { + "message": "Version 0.0.116 is the final alpha release." + }, + } + ] + } + ), + encoding="utf-8", + ) + docs_yml_path = website / "fern" / "docs.yml" + docs_yml_path.write_text( + yaml.safe_dump( + { + "versions": [ + { + "display-name": "Latest (v0.0.116)", + "path": "./versions/latest.yml", + "slug": "latest", + } + ] + } + ), + encoding="utf-8", + ) + + sdw.sync_docs( + Namespace( + operation="sync", + source_root=source, + docs_website_root=website, + channel="latest", + source_ref="docs/v0.0.116-announcement", + source_sha="announcement-sha", + release_version="0.0.116", + version_slug="", + display_name="Latest (v0.0.116)", + availability="", + ) + ) + + latest = read_yaml(docs_yml_path)["versions"][0] + assert latest["announcement"] == { + "message": "Version 0.0.116 is the final alpha release." + } + snapshots = read_yaml(website / "fern" / sdw.SNAPSHOT_METADATA_FILE)["snapshots"] + assert snapshots["latest"] == { + "source-ref": "docs/v0.0.116-announcement", + "source-sha": "announcement-sha", + "version": "0.0.116", + } + + def test_stable_sync_creates_immutable_version_and_promotes_latest( tmp_path: Path, ) -> None: From 473d1e997439c3577fabd9014ba8e8959e3c2444 Mon Sep 17 00:00:00 2001 From: Piotr Mlocek Date: Fri, 18 Sep 2026 05:05:14 +0000 Subject: [PATCH 3/8] docs(fern): restore dev announcement (#3441) Signed-off-by: Piotr Mlocek --- fern/docs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fern/docs.yml b/fern/docs.yml index ad6ea31a0..fc280965d 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -60,6 +60,8 @@ versions: path: ../docs/index.yml slug: dev availability: beta + announcement: + message: 'OpenShell 0.1.0 is coming soon. Track progress in the 0.1.0 milestone, read the prerelease documentation, or install a prerelease.' redirects: - source: "/openshell/latest/sandboxes/providers-v2" From 2263685cf39d3aff7c2b6f8f4005f8f23847712d Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Fri, 18 Sep 2026 13:56:07 +0200 Subject: [PATCH 4/8] test(tmachine): migrate Keycloak provider refresh coverage (#3404) * test(tmachine): add Keycloak provider refresh suite Signed-off-by: Evan Lezar * refactor(tmachine): share container runtime detection Signed-off-by: Evan Lezar * ci(tmachine): run feature suites in GitHub Actions Signed-off-by: Evan Lezar * ci(tmachine): run conformance with Podman tests Signed-off-by: Evan Lezar * ci(tmachine): cover provider refresh with Podman Signed-off-by: Evan Lezar * ci(integration): split input preparation from runners Signed-off-by: Evan Lezar --------- Signed-off-by: Evan Lezar --- .github/workflows/branch-e2e.yml | 90 +- .github/workflows/conformance.yml | 137 -- .github/workflows/e2e-podman-test.yml | 4 +- .github/workflows/integration-runner.yml | 85 + .github/workflows/integration-test.yml | 48 + .../workflows/prepare-integration-inputs.yml | 96 + .github/workflows/release-dev.yml | 59 +- .github/workflows/release-tag.yml | 61 +- architecture/build.md | 4 + crates/openshell-conformance/src/executor.rs | 15 +- crates/openshell-conformance/src/lib.rs | 122 +- e2e/rust/Cargo.toml | 6 - flake.nix | 2 +- tasks/test.toml | 6 - .../features/provider-refresh/keycloak.yaml | 100 + .../ansible/roles/keycloak/defaults/main.yaml | 15 + tests/ansible/roles/keycloak/tasks/main.yaml | 126 ++ .../roles/openshell_gateway/tasks/detect.yaml | 61 +- .../tasks/main.yaml | 50 + tests/artifacts.nix | 26 +- tests/config.nix | 8 + tests/suites/features/Cargo.lock | 1868 +++++++++++++++++ tests/suites/features/Cargo.toml | 6 + .../provider-refresh/keycloak/Cargo.toml | 14 + .../keycloak/tests/provider_refresh.rs | 2 +- 25 files changed, 2746 insertions(+), 265 deletions(-) delete mode 100644 .github/workflows/conformance.yml create mode 100644 .github/workflows/integration-runner.yml create mode 100644 .github/workflows/integration-test.yml create mode 100644 .github/workflows/prepare-integration-inputs.yml create mode 100644 tests/ansible/playbooks/features/provider-refresh/keycloak.yaml create mode 100644 tests/ansible/roles/keycloak/defaults/main.yaml create mode 100644 tests/ansible/roles/keycloak/tasks/main.yaml create mode 100644 tests/ansible/roles/tmachine_container_runtime/tasks/main.yaml create mode 100644 tests/suites/features/Cargo.lock create mode 100644 tests/suites/features/Cargo.toml create mode 100644 tests/suites/features/provider-refresh/keycloak/Cargo.toml rename e2e/rust/tests/provider_refresh_keycloak.rs => tests/suites/features/provider-refresh/keycloak/tests/provider_refresh.rs (99%) diff --git a/.github/workflows/branch-e2e.yml b/.github/workflows/branch-e2e.yml index 642af52da..a4fbc9750 100644 --- a/.github/workflows/branch-e2e.yml +++ b/.github/workflows/branch-e2e.yml @@ -23,6 +23,7 @@ jobs: pull-requests: read outputs: should_run: ${{ steps.gate.outputs.should_run }} + run_integration: ${{ steps.labels.outputs.run_core_e2e }} run_core_e2e: ${{ steps.labels.outputs.run_core_e2e }} run_gpu_e2e: ${{ steps.labels.outputs.run_gpu_e2e }} run_kubernetes_ha_e2e: ${{ steps.labels.outputs.run_kubernetes_ha_e2e }} @@ -199,6 +200,69 @@ jobs: packages: write uses: ./.github/workflows/build-images.yml + prepare-integration: + needs: [pr_metadata, build-binaries, build-images] + if: needs.pr_metadata.outputs.run_integration == 'true' + permissions: + actions: read + contents: read + packages: read + uses: ./.github/workflows/prepare-integration-inputs.yml + + # Run driver-independent conformance tests. + conformance-integration: + needs: prepare-integration + permissions: + actions: read + contents: read + packages: read + uses: ./.github/workflows/integration-runner.yml + with: + category: conformance + source-sha: ${{ needs.prepare-integration.outputs.source_sha }} + integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} + test-matrix: >- + [ + {"scenario":"ubuntu-docker-rootful","testsuite":"conformance"}, + {"scenario":"fedora-podman-rootful","testsuite":"conformance"}, + {"scenario":"fedora-podman-rootless","testsuite":"conformance"} + ] + + # Run feature-specific integration tests: + feature-specific-integration: + needs: prepare-integration + permissions: + actions: read + contents: read + packages: read + uses: ./.github/workflows/integration-runner.yml + with: + category: feature-specific + source-sha: ${{ needs.prepare-integration.outputs.source_sha }} + integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} + test-matrix: >- + [ + {"scenario":"fedora-podman-rootful","testsuite":"provider-refresh"}, + {"scenario":"fedora-podman-rootless","testsuite":"provider-refresh"} + ] + + # Run driver-specific integration tests: + # TODO: This should be added as soon as we have driver-specific tests enabled in tmachine. + # driver-specific: + # needs: prepare-integration + # permissions: + # actions: read + # contents: read + # packages: read + # uses: ./.github/workflows/integration-runner.yml + # with: + # category: driver-specific + # source-sha: ${{ needs.prepare-integration.outputs.source_sha }} + # integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} + # test-matrix: >- + # [ + # ] + docker-e2e: needs: [pr_metadata, build-binaries, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' @@ -212,18 +276,6 @@ jobs: runner: linux-arm64-cpu8 conformance-artifact-prefix: openshell-conformance - podman-e2e: - needs: [pr_metadata, build-binaries, build-images] - if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' - permissions: - actions: read - contents: read - packages: read - uses: ./.github/workflows/e2e-podman-test.yml - with: - image-tag: ${{ github.sha }} - conformance-artifact-prefix: openshell-conformance - vm-e2e: needs: [pr_metadata, build-binaries, build-vm-driver] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' @@ -392,7 +444,19 @@ jobs: core-e2e-result: name: Core E2E result - needs: [pr_metadata, docker-e2e, podman-e2e, vm-e2e, docker-external-driver-e2e, podman-external-driver-e2e, vm-external-driver-e2e, kubernetes-e2e, kubernetes-external-driver-e2e, kubernetes-workspace-managed-e2e, kubernetes-workspace-operator-e2e] + needs: + - pr_metadata + - conformance-integration + - feature-specific-integration + - docker-e2e + - vm-e2e + - docker-external-driver-e2e + - podman-external-driver-e2e + - vm-external-driver-e2e + - kubernetes-e2e + - kubernetes-external-driver-e2e + - kubernetes-workspace-managed-e2e + - kubernetes-workspace-operator-e2e if: always() && needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' runs-on: ubuntu-latest steps: diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml deleted file mode 100644 index c3e102cde..000000000 --- a/.github/workflows/conformance.yml +++ /dev/null @@ -1,137 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -name: Conformance - -on: - workflow_call: - inputs: - scenarios: - description: JSON array of tmachine scenarios to run - required: true - type: string - testsuites: - description: JSON array of tmachine test suites to run - required: true - type: string - -permissions: - actions: read - contents: read - -concurrency: - group: ${{ github.workflow }}-conformance-${{ github.ref }} - cancel-in-progress: true - -jobs: - prepare: - name: Prepare conformance inputs - runs-on: ubuntu-24.04 - timeout-minutes: 30 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - ref: ${{ github.sha }} - - - uses: ./.github/actions/setup-nix - - - name: Download CLI artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: openshell-x86_64-unknown-linux-musl - path: artifacts/binaries/x86_64-unknown-linux-musl - - - name: Download gateway artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: openshell-gateway-x86_64-unknown-linux-gnu - path: artifacts/binaries/x86_64-unknown-linux-gnu - - - name: Download sandbox image artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: image-sandbox - path: image-input/sandbox - - - name: Download supervisor image artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: image-supervisor - path: image-input/supervisor - - - name: Convert runtime images - shell: nix develop .#testing -c bash -euo pipefail {0} - run: | - mkdir -p artifacts/images - - skopeo copy \ - --override-os linux \ - --override-arch amd64 \ - oci-archive:image-input/sandbox/images/sandbox.tar \ - docker-archive:artifacts/images/openshell-sandbox-tmachine.tar:openshell/sandbox:tmachine - - skopeo copy \ - --override-os linux \ - --override-arch amd64 \ - oci-archive:image-input/supervisor/images/supervisor.tar \ - docker-archive:artifacts/images/openshell-supervisor-tmachine.tar:openshell/supervisor:tmachine - - - name: Build conformance test archive - run: nix run .#build-artifacts-test-archives - - - name: Upload conformance inputs - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: openshell-conformance-inputs - path: artifacts - compression-level: 0 - retention-days: 5 - if-no-files-found: error - - conformance: - name: Conformance (${{ matrix.scenario }}, ${{ matrix.testsuite }}) - needs: prepare - strategy: - fail-fast: false - matrix: - scenario: ${{ fromJSON(inputs.scenarios) }} - testsuite: ${{ fromJSON(inputs.testsuites) }} - runs-on: ubuntu-24.04 - timeout-minutes: 90 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - ref: ${{ github.sha }} - - - name: Enable KVM access - run: | - set -euo pipefail - if [[ ! -c /dev/kvm ]]; then - echo "::error::The runner did not expose /dev/kvm" - exit 1 - fi - sudo chmod 0666 /dev/kvm - exec 3<>/dev/kvm - exec 3>&- - - - uses: ./.github/actions/setup-nix - - - name: Cache tmachine disks - uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 - with: - path: ~/.cache/tmachine - key: tmachine-${{ runner.os }}-${{ runner.arch }} - - - name: Download conformance inputs - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: openshell-conformance-inputs - path: artifacts - - - name: Run conformance - env: - SCENARIO: ${{ matrix.scenario }} - TESTSUITE: ${{ matrix.testsuite }} - run: nix run .#tmachine -- test "${SCENARIO}" "${TESTSUITE}" diff --git a/.github/workflows/e2e-podman-test.yml b/.github/workflows/e2e-podman-test.yml index c285e63a3..4ca40da39 100644 --- a/.github/workflows/e2e-podman-test.yml +++ b/.github/workflows/e2e-podman-test.yml @@ -26,10 +26,8 @@ on: type: string default: "" suite-matrix: - required: false + required: true type: string - default: >- - [{"suite":"provider-refresh-keycloak","runner":"ubuntu-26.04","podman_major":"5","podman_package_version":"5.7.0+ds2-3build1","conmon_package_version":"2.1.13+ds1-2","cmd":"mise run --no-deps --skip-deps e2e:provider-refresh-keycloak"}] permissions: actions: read diff --git a/.github/workflows/integration-runner.yml b/.github/workflows/integration-runner.yml new file mode 100644 index 000000000..3af804ef2 --- /dev/null +++ b/.github/workflows/integration-runner.yml @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Integration Test Runner + +on: + workflow_call: + inputs: + source-sha: + description: Source revision of the prepared candidate artifacts + required: true + type: string + integration-inputs-artifact-id: + description: Immutable ID of the prepared integration inputs artifact + required: true + type: string + category: + description: Integration-test category, used for the job name and concurrency key + required: false + type: string + test-matrix: + description: JSON array of scenario and testsuite combinations + required: false + type: string + default: >- + [ + {"scenario":"ubuntu-docker-rootful","testsuite":"conformance"}, + {"scenario":"fedora-podman-rootful","testsuite":"conformance"}, + {"scenario":"fedora-podman-rootless","testsuite":"conformance"} + ] + +permissions: + actions: read + contents: read + packages: read + +concurrency: + group: ${{ github.workflow }}-integration-${{ inputs.category }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + integration: + name: Integration test (${{ inputs.category }}, ${{ matrix.scenario }}, ${{ matrix.testsuite }}) + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(inputs['test-matrix']) }} + runs-on: ubuntu-24.04 + timeout-minutes: 90 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs['source-sha'] }} + persist-credentials: false + + - name: Enable KVM access + run: | + set -euo pipefail + if [[ ! -c /dev/kvm ]]; then + echo "::error::The runner did not expose /dev/kvm" + exit 1 + fi + sudo chmod 0666 /dev/kvm + exec 3<>/dev/kvm + exec 3>&- + + - uses: ./.github/actions/setup-nix + + - name: Cache tmachine disks + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + with: + path: ~/.cache/tmachine + key: tmachine-${{ runner.os }}-${{ runner.arch }} + + - name: Download integration inputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + artifact-ids: ${{ inputs['integration-inputs-artifact-id'] }} + path: artifacts + + - name: Run test suite + env: + SCENARIO: ${{ matrix.scenario }} + TESTSUITE: ${{ matrix.testsuite }} + run: nix run .#tmachine -- test "${SCENARIO}" "${TESTSUITE}" diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 000000000..8b4b03d7d --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Integration Tests + +on: + workflow_dispatch: + inputs: + artifact-run-id: + description: Workflow run containing the candidate artifacts + required: false + type: string + default: "" + category: + description: Integration-test category, used for the job name and concurrency key + required: true + type: string + default: conformance + test-matrix: + description: JSON array of scenario and testsuite combinations + required: true + type: string + default: >- + [ + {"scenario":"ubuntu-docker-rootful","testsuite":"conformance"}, + {"scenario":"fedora-podman-rootful","testsuite":"conformance"}, + {"scenario":"fedora-podman-rootless","testsuite":"conformance"} + ] + +permissions: + actions: read + contents: read + packages: read + +jobs: + prepare: + uses: ./.github/workflows/prepare-integration-inputs.yml + with: + artifact-run-id: ${{ inputs['artifact-run-id'] }} + + integration: + needs: prepare + uses: ./.github/workflows/integration-runner.yml + with: + category: ${{ inputs.category }} + source-sha: ${{ needs.prepare.outputs.source_sha }} + integration-inputs-artifact-id: ${{ needs.prepare.outputs.integration_inputs_artifact_id }} + test-matrix: ${{ inputs.test-matrix }} diff --git a/.github/workflows/prepare-integration-inputs.yml b/.github/workflows/prepare-integration-inputs.yml new file mode 100644 index 000000000..af6d61d29 --- /dev/null +++ b/.github/workflows/prepare-integration-inputs.yml @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Prepare Integration Inputs + +on: + workflow_call: + inputs: + artifact-run-id: + description: Workflow run containing the candidate artifacts + required: false + type: string + default: "" + outputs: + source_sha: + description: Source revision of the candidate artifacts + value: ${{ jobs.prepare.outputs.source_sha }} + integration_inputs_artifact_id: + description: Immutable ID of the prepared integration inputs artifact + value: ${{ jobs.prepare.outputs.integration_inputs_artifact_id }} + +permissions: + actions: read + contents: read + packages: read + +jobs: + prepare: + name: Prepare tmachine inputs + runs-on: ubuntu-24.04 + timeout-minutes: 30 + outputs: + source_sha: ${{ steps.artifact-run.outputs.source_sha }} + integration_inputs_artifact_id: ${{ steps.upload-integration-inputs.outputs.artifact-id }} + steps: + - name: Resolve artifact run + id: artifact-run + env: + ARTIFACT_RUN_ID: ${{ inputs['artifact-run-id'] || github.run_id }} + GH_TOKEN: ${{ github.token }} + run: | + head_sha=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${ARTIFACT_RUN_ID}" --jq .head_sha) + echo "source_sha=${head_sha}" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ steps.artifact-run.outputs.source_sha }} + persist-credentials: false + + - uses: ./.github/actions/setup-nix + + - name: Download CLI artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: openshell-x86_64-unknown-linux-musl + path: artifacts/binaries/x86_64-unknown-linux-musl + github-token: ${{ github.token }} + run-id: ${{ inputs['artifact-run-id'] || github.run_id }} + + - name: Download gateway artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: openshell-gateway-x86_64-unknown-linux-gnu + path: artifacts/binaries/x86_64-unknown-linux-gnu + github-token: ${{ github.token }} + run-id: ${{ inputs['artifact-run-id'] || github.run_id }} + + - name: Log in to GHCR + run: echo "${{ github.token }}" | docker login ghcr.io -u "${GITHUB_ACTOR}" --password-stdin + + - name: Export runtime images + env: + IMAGE_TAG: ${{ steps.artifact-run.outputs.source_sha }} + run: | + mkdir -p artifacts/images + + docker pull "ghcr.io/nvidia/openshell/sandbox:${IMAGE_TAG}" + docker tag "ghcr.io/nvidia/openshell/sandbox:${IMAGE_TAG}" openshell/sandbox:tmachine + docker save --output artifacts/images/openshell-sandbox-tmachine.tar openshell/sandbox:tmachine + + docker pull "ghcr.io/nvidia/openshell/supervisor:${IMAGE_TAG}" + docker tag "ghcr.io/nvidia/openshell/supervisor:${IMAGE_TAG}" openshell/supervisor:tmachine + docker save --output artifacts/images/openshell-supervisor-tmachine.tar openshell/supervisor:tmachine + + - name: Build test archives + run: nix run .#build-artifacts-test-archives + + - name: Upload integration inputs + id: upload-integration-inputs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: openshell-tmachine-inputs + path: artifacts + compression-level: 0 + retention-days: 5 + if-no-files-found: error diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 7dd1e98a8..73a39391b 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -101,15 +101,42 @@ jobs: with: checkout-ref: ${{ github.sha }} - conformance: + prepare-integration: needs: [build-binaries, build-images] permissions: actions: read contents: read - uses: ./.github/workflows/conformance.yml + packages: read + uses: ./.github/workflows/prepare-integration-inputs.yml + + conformance-integration: + needs: prepare-integration + permissions: + actions: read + contents: read + packages: read + uses: ./.github/workflows/integration-runner.yml with: - scenarios: '["ubuntu-docker-rootful", "fedora-podman-rootful", "fedora-podman-rootless"]' - testsuites: '["conformance"]' + category: conformance + source-sha: ${{ needs.prepare-integration.outputs.source_sha }} + integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} + + feature-specific-integration: + needs: prepare-integration + permissions: + actions: read + contents: read + packages: read + uses: ./.github/workflows/integration-runner.yml + with: + category: feature-specific + source-sha: ${{ needs.prepare-integration.outputs.source_sha }} + integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} + test-matrix: >- + [ + {"scenario":"fedora-podman-rootful","testsuite":"provider-refresh"}, + {"scenario":"fedora-podman-rootless","testsuite":"provider-refresh"} + ] docker-e2e: needs: [build-binaries, build-images] @@ -124,18 +151,6 @@ jobs: runner: linux-arm64-cpu8 conformance-artifact-prefix: openshell-conformance - podman-e2e: - needs: [build-binaries, build-images] - permissions: - actions: read - contents: read - packages: read - uses: ./.github/workflows/e2e-podman-test.yml - with: - image-tag: ${{ github.sha }} - checkout-ref: ${{ github.sha }} - conformance-artifact-prefix: openshell-conformance - vm-e2e: needs: [build-binaries, build-vm-driver] permissions: @@ -244,7 +259,17 @@ jobs: # --------------------------------------------------------------------------- release-dev: name: Release Dev - needs: [compute-versions, package-binaries, build-python-wheel, conformance, docker-e2e, podman-e2e, vm-e2e, build-deb, build-rpm, build-snap] + needs: + - compute-versions + - package-binaries + - build-python-wheel + - conformance-integration + - feature-specific-integration + - docker-e2e + - vm-e2e + - build-deb + - build-rpm + - build-snap runs-on: linux-amd64-cpu8 timeout-minutes: 10 permissions: diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index bc6291c09..7e6891923 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -129,6 +129,43 @@ jobs: image-tag: ${{ needs.compute-versions.outputs.source_sha }} checkout-ref: ${{ needs.compute-versions.outputs.source_sha }} + prepare-integration: + needs: [compute-versions, build-binaries, build-images] + permissions: + actions: read + contents: read + packages: read + uses: ./.github/workflows/prepare-integration-inputs.yml + + conformance-integration: + needs: prepare-integration + permissions: + actions: read + contents: read + packages: read + uses: ./.github/workflows/integration-runner.yml + with: + category: conformance + source-sha: ${{ needs.prepare-integration.outputs.source_sha }} + integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} + + feature-specific-integration: + needs: prepare-integration + permissions: + actions: read + contents: read + packages: read + uses: ./.github/workflows/integration-runner.yml + with: + category: feature-specific + source-sha: ${{ needs.prepare-integration.outputs.source_sha }} + integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} + test-matrix: >- + [ + {"scenario":"fedora-podman-rootful","testsuite":"provider-refresh"}, + {"scenario":"fedora-podman-rootless","testsuite":"provider-refresh"} + ] + docker-e2e: needs: [compute-versions, build-binaries, build-images] permissions: @@ -142,18 +179,6 @@ jobs: runner: linux-arm64-cpu8 conformance-artifact-prefix: openshell-conformance - podman-e2e: - needs: [compute-versions, build-binaries, build-images] - permissions: - actions: read - contents: read - packages: read - uses: ./.github/workflows/e2e-podman-test.yml - with: - image-tag: ${{ needs.compute-versions.outputs.source_sha }} - checkout-ref: ${{ needs.compute-versions.outputs.source_sha }} - conformance-artifact-prefix: openshell-conformance - vm-e2e: needs: [compute-versions, build-binaries, build-vm-driver] permissions: @@ -273,7 +298,17 @@ jobs: # --------------------------------------------------------------------------- release: name: Release - needs: [compute-versions, package-binaries, build-python-wheel, docker-e2e, podman-e2e, vm-e2e, build-deb, build-rpm, build-snap] + needs: + - compute-versions + - package-binaries + - build-python-wheel + - conformance-integration + - feature-specific-integration + - docker-e2e + - vm-e2e + - build-deb + - build-rpm + - build-snap runs-on: linux-amd64-cpu8 timeout-minutes: 10 permissions: diff --git a/architecture/build.md b/architecture/build.md index e767f36f6..42effcee0 100644 --- a/architecture/build.md +++ b/architecture/build.md @@ -258,6 +258,10 @@ for explicit publication. CLI conformance runs after target provisioning and operates only through the configured OpenShell CLI. The smoke scenario verifies the black-box sandbox lifecycle by creating, inspecting, executing in, and deleting a sandbox. +Feature suites use the same disposable guest but may provision isolated +dependencies after installation. The Keycloak provider-refresh suite starts a +guest-local Keycloak realm and verifies a successful OAuth refresh followed by +revocation and the gateway's reauthorization-required recovery state. The `tests/tmachine` setup and installation caches include a digest of the entire directory containing `ANSIBLE_CONFIG`, including local roles, task diff --git a/crates/openshell-conformance/src/executor.rs b/crates/openshell-conformance/src/executor.rs index b45c102ee..032edf1e9 100644 --- a/crates/openshell-conformance/src/executor.rs +++ b/crates/openshell-conformance/src/executor.rs @@ -15,7 +15,12 @@ pub type CliExecution<'a> = Pin> + Send + 'a>>; pub trait CliExecutor: Send + Sync { - fn execute(&self, args: Vec, command_timeout: Duration) -> CliExecution<'_>; + fn execute( + &self, + args: Vec, + environment: Vec<(String, String)>, + command_timeout: Duration, + ) -> CliExecution<'_>; } pub enum CliExecutionError { @@ -34,11 +39,17 @@ impl ProcessCli { } impl CliExecutor for ProcessCli { - fn execute(&self, args: Vec, command_timeout: Duration) -> CliExecution<'_> { + 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) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .kill_on_drop(true); diff --git a/crates/openshell-conformance/src/lib.rs b/crates/openshell-conformance/src/lib.rs index a9689be11..46f385ef7 100644 --- a/crates/openshell-conformance/src/lib.rs +++ b/crates/openshell-conformance/src/lib.rs @@ -248,6 +248,8 @@ pub struct OpenShellRunner { run_id: String, scenario: String, known_sandboxes: BTreeSet, + known_providers: BTreeSet, + known_provider_profiles: BTreeSet, finished: bool, } @@ -311,6 +313,8 @@ impl OpenShellRunner { run_id: generate_run_id(), scenario: scenario.to_string(), known_sandboxes: BTreeSet::new(), + known_providers: BTreeSet::new(), + known_provider_profiles: BTreeSet::new(), finished: false, } } @@ -393,6 +397,16 @@ impl OpenShellRunner { 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, @@ -440,6 +454,7 @@ impl OpenShellRunner { step: &str, expectation: &str, args: Vec, + environment: Vec<(String, String)>, command_timeout: Duration, ) -> Result { let context = self.context(step); @@ -447,22 +462,22 @@ impl OpenShellRunner { eprintln!("{context} running: {command}"); let started = Instant::now(); - let output = - self.cli - .execute(args, 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 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 {}", @@ -484,7 +499,10 @@ impl OpenShellRunner { } async fn cleanup(&self) -> Result<(), String> { - if self.known_sandboxes.is_empty() { + if self.known_sandboxes.is_empty() + && self.known_providers.is_empty() + && self.known_provider_profiles.is_empty() + { return Ok(()); } @@ -516,6 +534,56 @@ impl OpenShellRunner { } } + 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 { @@ -550,11 +618,24 @@ impl<'a> CommandStep<'a> { 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 @@ -724,7 +805,12 @@ mod tests { } impl CliExecutor for MockCli { - fn execute(&self, args: Vec, _command_timeout: Duration) -> CliExecution<'_> { + 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); diff --git a/e2e/rust/Cargo.toml b/e2e/rust/Cargo.toml index e2ed36391..333acd0fe 100644 --- a/e2e/rust/Cargo.toml +++ b/e2e/rust/Cargo.toml @@ -35,7 +35,6 @@ e2e-kubernetes-workspace-operator = ["e2e-kubernetes"] e2e-podman = ["e2e", "e2e-host-gateway", "e2e-local-container-driver"] e2e-podman-gpu = ["e2e-podman", "e2e-gpu"] e2e-oidc-pkce = [] -e2e-provider-refresh-keycloak = [] e2e-vm = ["e2e", "e2e-host-gateway"] [[test]] @@ -48,11 +47,6 @@ name = "oidc_pkce" path = "tests/oidc_pkce.rs" required-features = ["e2e-oidc-pkce"] -[[test]] -name = "provider_refresh_keycloak" -path = "tests/provider_refresh_keycloak.rs" -required-features = ["e2e-provider-refresh-keycloak"] - [[test]] name = "vm_overlay" path = "tests/vm_overlay.rs" diff --git a/flake.nix b/flake.nix index 77f1d2c3b..0aaf9273c 100644 --- a/flake.nix +++ b/flake.nix @@ -138,7 +138,7 @@ }; build-artifacts-test-archives = { type = "app"; - program = "${artifacts.conformanceCliArchive}/bin/build-openshell-conformance-test-archive"; + program = "${artifacts.testArchives}/bin/build-artifacts-test-archives"; }; build-artifacts-helm = { type = "app"; diff --git a/tasks/test.toml b/tasks/test.toml index 2b0feb2bb..30f348beb 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -146,12 +146,6 @@ run = [ "CONTAINER_RUNTIME=podman e2e/with-keycloak.sh env OPENSHELL_E2E_OIDC_GATEWAY=1 e2e/with-podman-gateway.sh cargo test --manifest-path e2e/rust/Cargo.toml --features e2e-oidc-pkce --test oidc_pkce", ] -["e2e:provider-refresh-keycloak"] -description = "Run provider refresh recovery e2e against Keycloak and a Podman gateway" -run = [ - "CONTAINER_RUNTIME=podman e2e/with-keycloak.sh e2e/with-podman-gateway.sh cargo test --manifest-path e2e/rust/Cargo.toml --features e2e-provider-refresh-keycloak --test provider_refresh_keycloak -- --nocapture", -] - ["e2e:oidc-pkce:docker"] description = "Run Linux browser PKCE and RBAC e2e tests against Keycloak and a Docker gateway" run = [ diff --git a/tests/ansible/playbooks/features/provider-refresh/keycloak.yaml b/tests/ansible/playbooks/features/provider-refresh/keycloak.yaml new file mode 100644 index 000000000..81bcba5ed --- /dev/null +++ b/tests/ansible/playbooks/features/provider-refresh/keycloak.yaml @@ -0,0 +1,100 @@ +# 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: 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: /usr/local/bin/openshell + 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 + - --unit + - openshell-gateway.service + - --no-pager + - --lines + - "500" + 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/ansible/roles/keycloak/defaults/main.yaml b/tests/ansible/roles/keycloak/defaults/main.yaml new file mode 100644 index 000000000..eab02702b --- /dev/null +++ b/tests/ansible/roles/keycloak/defaults/main.yaml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +--- +# Keep these values aligned with scripts/keycloak-dev.sh. The role provisions +# an isolated guest fixture, while the script manages a developer-host fixture. +keycloak_container_name: openshell-keycloak +keycloak_image: quay.io/keycloak/keycloak:24.0 +keycloak_port: 8180 +keycloak_realm: openshell +keycloak_admin_username: admin +keycloak_admin_password: admin +keycloak_state_directory: /var/lib/openshell-keycloak +keycloak_health_retries: 45 +keycloak_health_delay: 2 diff --git a/tests/ansible/roles/keycloak/tasks/main.yaml b/tests/ansible/roles/keycloak/tasks/main.yaml new file mode 100644 index 000000000..ca0743ad2 --- /dev/null +++ b/tests/ansible/roles/keycloak/tasks/main.yaml @@ -0,0 +1,126 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +--- +- name: Wait for SSH + ansible.builtin.wait_for_connection: + +- name: Detect tmachine container runtime + ansible.builtin.include_role: + name: tmachine_container_runtime + +- name: Set Keycloak runtime values + ansible.builtin.set_fact: + keycloak_runtime_user: >- + {{ 'tmachine' if tmachine_container_runtime_is_rootless else 'root' }} + keycloak_runtime_home: >- + {{ '/home/tmachine' if tmachine_container_runtime_is_rootless else '/root' }} + keycloak_runtime_state_directory: >- + {{ '/home/tmachine/.local/share/openshell-keycloak' + if tmachine_container_runtime_is_rootless else keycloak_state_directory }} + +- name: Require Keycloak container runtime + become: true + become_user: "{{ keycloak_runtime_user }}" + ansible.builtin.command: + argv: + - "{{ tmachine_container_runtime_name }}" + - --version + environment: + HOME: "{{ keycloak_runtime_home }}" + changed_when: false + +- name: Create Keycloak state directory + become: true + ansible.builtin.file: + path: "{{ keycloak_runtime_state_directory }}" + state: directory + owner: "{{ keycloak_runtime_user }}" + group: "{{ keycloak_runtime_user }}" + mode: "0755" + +- name: Install Keycloak realm fixture + become: true + ansible.builtin.copy: + src: "{{ keycloak_realm_file }}" + dest: "{{ keycloak_runtime_state_directory }}/realm.json" + owner: "{{ keycloak_runtime_user }}" + group: "{{ keycloak_runtime_user }}" + mode: "0644" + +- name: Remove an existing Keycloak fixture + become: true + become_user: "{{ keycloak_runtime_user }}" + ansible.builtin.command: + argv: + - "{{ tmachine_container_runtime_name }}" + - rm + - --force + - "{{ keycloak_container_name }}" + environment: + HOME: "{{ keycloak_runtime_home }}" + changed_when: false + failed_when: false + +- name: Start Keycloak fixture + become: true + become_user: "{{ keycloak_runtime_user }}" + ansible.builtin.command: + argv: + - "{{ tmachine_container_runtime_name }}" + - run + - --detach + - --name + - "{{ keycloak_container_name }}" + - --publish + - "127.0.0.1:{{ keycloak_port }}:8080" + - --env + - "KEYCLOAK_ADMIN={{ keycloak_admin_username }}" + - --env + - "KEYCLOAK_ADMIN_PASSWORD={{ keycloak_admin_password }}" + - --volume + - "{{ keycloak_runtime_state_directory }}/realm.json:/opt/keycloak/data/import/realm.json:ro,z" + - "{{ keycloak_image }}" + - start-dev + - --import-realm + environment: + HOME: "{{ keycloak_runtime_home }}" + changed_when: true + +- name: Wait for Keycloak discovery endpoint + ansible.builtin.uri: + url: "http://127.0.0.1:{{ keycloak_port }}/realms/{{ keycloak_realm }}/.well-known/openid-configuration" + status_code: 200 + return_content: false + register: keycloak_discovery + retries: "{{ keycloak_health_retries }}" + delay: "{{ keycloak_health_delay }}" + until: keycloak_discovery.status | default(0) == 200 + failed_when: false + +- name: Read Keycloak logs after failed health check + become: true + become_user: "{{ keycloak_runtime_user }}" + ansible.builtin.command: + argv: + - "{{ tmachine_container_runtime_name }}" + - logs + - --tail + - "30" + - "{{ keycloak_container_name }}" + environment: + HOME: "{{ keycloak_runtime_home }}" + register: keycloak_logs + changed_when: false + failed_when: false + when: keycloak_discovery.status | default(0) != 200 + +- name: Show Keycloak logs after failed health check + ansible.builtin.debug: + var: keycloak_logs.stdout_lines + when: keycloak_discovery.status | default(0) != 200 + +- name: Require Keycloak discovery endpoint + ansible.builtin.assert: + that: keycloak_discovery.status | default(0) == 200 + fail_msg: Keycloak did not become healthy within the configured timeout diff --git a/tests/ansible/roles/openshell_gateway/tasks/detect.yaml b/tests/ansible/roles/openshell_gateway/tasks/detect.yaml index 9490420bf..a25cf804a 100644 --- a/tests/ansible/roles/openshell_gateway/tasks/detect.yaml +++ b/tests/ansible/roles/openshell_gateway/tasks/detect.yaml @@ -2,50 +2,21 @@ # SPDX-License-Identifier: Apache-2.0 --- -- name: Resolve tmachine UID - ansible.builtin.command: - argv: - - id - - -u - - tmachine - changed_when: false - register: openshell_tmachine_uid +- name: Detect tmachine container runtime + ansible.builtin.include_role: + name: tmachine_container_runtime -- name: Check Docker socket - become: true - ansible.builtin.stat: - path: /var/run/docker.sock - register: openshell_docker_socket - -- name: Check rootful Podman socket - become: true - ansible.builtin.stat: - path: /run/podman/podman.sock - register: openshell_rootful_podman_socket - -- name: Check rootless Podman socket - become: true - ansible.builtin.stat: - path: "/run/user/{{ openshell_tmachine_uid.stdout }}/podman/podman.sock" - register: openshell_rootless_podman_socket - -- name: Require exactly one container runtime socket - ansible.builtin.assert: - that: - - >- - (openshell_docker_socket.stat.exists | int) - + (openshell_rootful_podman_socket.stat.exists | int) - + (openshell_rootless_podman_socket.stat.exists | int) == 1 - fail_msg: Expected exactly one Docker or Podman socket - -- name: Select container runtime +- name: Set OpenShell gateway runtime values ansible.builtin.set_fact: - openshell_gateway_driver: "{{ 'docker' if openshell_docker_socket.stat.exists else 'podman' }}" - openshell_gateway_user: "{{ 'root' if openshell_rootful_podman_socket.stat.exists else 'tmachine' }}" - openshell_gateway_home: "{{ '/root' if openshell_rootful_podman_socket.stat.exists else '/home/tmachine' }}" - openshell_gateway_uid: "{{ '0' if openshell_rootful_podman_socket.stat.exists else openshell_tmachine_uid.stdout }}" - openshell_gateway_bind_address: "{{ '127.0.0.1:17670' if openshell_docker_socket.stat.exists else '0.0.0.0:17670' }}" - openshell_runtime_socket: >- - {{ '/var/run/docker.sock' if openshell_docker_socket.stat.exists - else '/run/podman/podman.sock' if openshell_rootful_podman_socket.stat.exists - else '/run/user/' ~ openshell_tmachine_uid.stdout ~ '/podman/podman.sock' }} + openshell_gateway_driver: "{{ tmachine_container_runtime_name }}" + openshell_gateway_user: >- + {{ 'root' if tmachine_container_runtime_name == 'podman' + and not tmachine_container_runtime_is_rootless else 'tmachine' }} + openshell_gateway_home: >- + {{ '/root' if tmachine_container_runtime_name == 'podman' + and not tmachine_container_runtime_is_rootless else '/home/tmachine' }} + openshell_gateway_uid: >- + {{ '0' if tmachine_container_runtime_name == 'podman' + and not tmachine_container_runtime_is_rootless else tmachine_container_runtime_tmachine_uid.stdout }} + openshell_gateway_bind_address: "{{ '127.0.0.1:17670' if tmachine_container_runtime_name == 'docker' else '0.0.0.0:17670' }}" + openshell_runtime_socket: "{{ tmachine_container_runtime_socket }}" diff --git a/tests/ansible/roles/tmachine_container_runtime/tasks/main.yaml b/tests/ansible/roles/tmachine_container_runtime/tasks/main.yaml new file mode 100644 index 000000000..5846706c9 --- /dev/null +++ b/tests/ansible/roles/tmachine_container_runtime/tasks/main.yaml @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +--- +- name: Resolve tmachine UID + ansible.builtin.command: + argv: + - id + - -u + - tmachine + changed_when: false + register: tmachine_container_runtime_tmachine_uid + +- name: Check Docker socket + become: true + ansible.builtin.stat: + path: /var/run/docker.sock + register: tmachine_container_runtime_docker_socket + +- name: Check rootful Podman socket + become: true + ansible.builtin.stat: + path: /run/podman/podman.sock + register: tmachine_container_runtime_rootful_podman_socket + +- name: Check rootless Podman socket + become: true + ansible.builtin.stat: + path: "/run/user/{{ tmachine_container_runtime_tmachine_uid.stdout }}/podman/podman.sock" + register: tmachine_container_runtime_rootless_podman_socket + +- name: Require exactly one container runtime socket + ansible.builtin.assert: + that: + - >- + (tmachine_container_runtime_docker_socket.stat.exists | int) + + (tmachine_container_runtime_rootful_podman_socket.stat.exists | int) + + (tmachine_container_runtime_rootless_podman_socket.stat.exists | int) == 1 + fail_msg: Expected exactly one Docker or Podman socket + +- name: Select tmachine container runtime + ansible.builtin.set_fact: + tmachine_container_runtime_name: >- + {{ 'docker' if tmachine_container_runtime_docker_socket.stat.exists else 'podman' }} + tmachine_container_runtime_is_rootless: >- + {{ tmachine_container_runtime_rootless_podman_socket.stat.exists }} + tmachine_container_runtime_socket: >- + {{ '/var/run/docker.sock' if tmachine_container_runtime_docker_socket.stat.exists + else '/run/podman/podman.sock' if tmachine_container_runtime_rootful_podman_socket.stat.exists + else '/run/user/' ~ tmachine_container_runtime_tmachine_uid.stdout ~ '/podman/podman.sock' }} diff --git a/tests/artifacts.nix b/tests/artifacts.nix index fe1d165c4..e44d1751e 100644 --- a/tests/artifacts.nix +++ b/tests/artifacts.nix @@ -84,9 +84,17 @@ let target = muslToolchain.target; output = "artifacts/test-archives/${muslToolchain.target}/openshell-conformance-tests.tar"; }; + 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"; + }; in rec { - inherit conformanceCliArchive; + inherit conformanceCliArchive providerRefreshKeycloakArchive; binaries = pkgs.writeShellApplication { name = "build-artifacts-binaries"; @@ -126,6 +134,18 @@ rec { ''; }; + testArchives = pkgs.writeShellApplication { + name = "build-artifacts-test-archives"; + runtimeInputs = [ + conformanceCliArchive + providerRefreshKeycloakArchive + ]; + text = '' + build-openshell-conformance-test-archive + build-provider-refresh-keycloak-test-archive + ''; + }; + images = pkgs.writeShellApplication { name = "build-artifacts-images"; runtimeInputs = [ @@ -204,13 +224,13 @@ rec { name = "build-artifacts"; runtimeInputs = [ binaries - conformanceCliArchive + testArchives images helm ]; text = '' build-artifacts-binaries - build-openshell-conformance-test-archive + build-artifacts-test-archives build-artifacts-images build-artifacts-helm ''; diff --git a/tests/config.nix b/tests/config.nix index b22c79f6c..1426abe46 100644 --- a/tests/config.nix +++ b/tests/config.nix @@ -114,6 +114,14 @@ let openshell_conformance_test_bundle = "../artifacts/test-archives/${muslTarget}/openshell-conformance-tests.tar"; }; } + { + 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"; + }; + } ]; }; diff --git a/tests/suites/features/Cargo.lock b/tests/suites/features/Cargo.lock new file mode 100644 index 000000000..20ebe5801 --- /dev/null +++ b/tests/suites/features/Cargo.lock @@ -0,0 +1,1868 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +dependencies = [ + "aws-lc-sys", + "untrusted", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +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 = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[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 = "bollard" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" +dependencies = [ + "base64", + "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", + "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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" +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" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[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 = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[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", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[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-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "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", + "libc", + "wasi", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[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.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +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 = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-named-pipe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fab3637d6b04a8037af8a266fdf6cf92ea957e8c53981a2bf6136572531025bf" +dependencies = [ + "hex", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "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 = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "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 = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[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.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "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 = "jsonwebtoken" +version = "10.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" +dependencies = [ + "aws-lc-rs", + "base64", + "getrandom 0.2.17", + "js-sys", + "pem", + "serde", + "serde_json", + "signature", + "simple_asn1", + "zeroize", +] + +[[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 = "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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[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 = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[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 = "noyalib" +version = "0.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f075ef19fa3bcf8697c0ef96c37d5c435d339a40ab8081cae3aac3a4e7fee9a" +dependencies = [ + "hashbrown", + "indexmap", + "libm", + "memchr", + "rustc-hash", + "serde", + "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +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 = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openshell-conformance" +version = "0.0.0" +dependencies = [ + "rand", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "openshell-e2e" +version = "0.1.0" +dependencies = [ + "base64", + "bollard", + "bytes", + "futures-util", + "hex", + "http-body-util", + "hyper", + "hyper-util", + "jsonwebtoken", + "nix", + "noyalib", + "prost", + "rand", + "serde", + "serde_json", + "sha1", + "sha2", + "tempfile", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tower", + "url", +] + +[[package]] +name = "openshell-test-feature-provider-refresh-keycloak" +version = "0.0.0" +dependencies = [ + "openshell-conformance", + "openshell-e2e", + "serde_json", + "tempfile", + "tokio", +] + +[[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", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +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 = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +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.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[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 = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[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", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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_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_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[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 = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[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", + "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.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +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" + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[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.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[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", + "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", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[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 = "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-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 = [ + "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.119", +] + +[[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 = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[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", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[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-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", + "synstructure", +] + +[[package]] +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.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tests/suites/features/Cargo.toml b/tests/suites/features/Cargo.toml new file mode 100644 index 000000000..7acd72b5d --- /dev/null +++ b/tests/suites/features/Cargo.toml @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[workspace] +resolver = "2" +members = ["provider-refresh/keycloak"] diff --git a/tests/suites/features/provider-refresh/keycloak/Cargo.toml b/tests/suites/features/provider-refresh/keycloak/Cargo.toml new file mode 100644 index 000000000..f42a1c8bd --- /dev/null +++ b/tests/suites/features/provider-refresh/keycloak/Cargo.toml @@ -0,0 +1,14 @@ +# 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/e2e/rust/tests/provider_refresh_keycloak.rs b/tests/suites/features/provider-refresh/keycloak/tests/provider_refresh.rs similarity index 99% rename from e2e/rust/tests/provider_refresh_keycloak.rs rename to tests/suites/features/provider-refresh/keycloak/tests/provider_refresh.rs index b0ba3236a..cddc5b7bb 100644 --- a/e2e/rust/tests/provider_refresh_keycloak.rs +++ b/tests/suites/features/provider-refresh/keycloak/tests/provider_refresh.rs @@ -3,7 +3,7 @@ #![cfg(target_os = "linux")] -//! End-to-end coverage for provider OAuth refresh recovery against Keycloak. +//! 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 From 4b2cb7f007da2f701422daf394643b0fd9d4daef Mon Sep 17 00:00:00 2001 From: Simon Scatton <44714756+SDAChess@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:57:13 +0200 Subject: [PATCH 5/8] test(tmachine): verify SELinux in Fedora scenarios (#3457) Closes #2973 Closes #2976 Signed-off-by: Simon Scatton --- tests/ansible/playbooks/selinux.yaml | 20 ++++++++++++++++++++ tests/config.nix | 2 ++ 2 files changed, 22 insertions(+) create mode 100644 tests/ansible/playbooks/selinux.yaml diff --git a/tests/ansible/playbooks/selinux.yaml b/tests/ansible/playbooks/selinux.yaml new file mode 100644 index 000000000..996ccba2e --- /dev/null +++ b/tests/ansible/playbooks/selinux.yaml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +--- +- name: Verify SELinux + hosts: all + gather_facts: false + + tasks: + - name: Read the current SELinux mode + ansible.builtin.command: + cmd: getenforce + register: selinux_mode + changed_when: false + + - name: Verify enforcing mode + ansible.builtin.assert: + that: + - selinux_mode.stdout == "Enforcing" + fail_msg: SELinux is not enforcing. diff --git a/tests/config.nix b/tests/config.nix index 1426abe46..759e3c9ed 100644 --- a/tests/config.nix +++ b/tests/config.nix @@ -63,6 +63,7 @@ let use_galaxy = false; playbooks = [ "ansible/playbooks/nextest.yaml" + "ansible/playbooks/selinux.yaml" "ansible/playbooks/podman-rootful.yaml" ]; }; @@ -87,6 +88,7 @@ let use_galaxy = false; playbooks = [ "ansible/playbooks/nextest.yaml" + "ansible/playbooks/selinux.yaml" "ansible/playbooks/podman-rootless.yaml" ]; }; From 8bd3dcc565f380ddfc464fc41b1faa936d72af3e Mon Sep 17 00:00:00 2001 From: Simon Scatton <44714756+SDAChess@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:59:20 +0000 Subject: [PATCH 6/8] refactor(tmachine): separate installers from environments (#3419) Signed-off-by: Simon Scatton --- .github/workflows/branch-e2e.yml | 10 ++-- .github/workflows/integration-runner.yml | 15 +++--- .github/workflows/integration-test.yml | 8 +-- .github/workflows/release-dev.yml | 4 +- .github/workflows/release-tag.yml | 4 +- architecture/build.md | 12 +++-- tests/config.nix | 56 ++++++------------- tests/tmachine/src/config.rs | 9 ++-- tests/tmachine/src/main.rs | 68 ++++++++++++++++-------- tests/tmachine/src/qemu/install.rs | 29 +++++----- tests/tmachine/src/qemu/setup.rs | 19 +++---- tests/tmachine/src/qemu/test.rs | 11 ++-- 12 files changed, 132 insertions(+), 113 deletions(-) diff --git a/.github/workflows/branch-e2e.yml b/.github/workflows/branch-e2e.yml index a4fbc9750..827823df4 100644 --- a/.github/workflows/branch-e2e.yml +++ b/.github/workflows/branch-e2e.yml @@ -223,9 +223,9 @@ jobs: integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} test-matrix: >- [ - {"scenario":"ubuntu-docker-rootful","testsuite":"conformance"}, - {"scenario":"fedora-podman-rootful","testsuite":"conformance"}, - {"scenario":"fedora-podman-rootless","testsuite":"conformance"} + {"environment":"ubuntu-docker-rootful","installer":"binaries","testsuite":"conformance"}, + {"environment":"fedora-podman-rootful","installer":"binaries","testsuite":"conformance"}, + {"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"conformance"} ] # Run feature-specific integration tests: @@ -242,8 +242,8 @@ jobs: integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} test-matrix: >- [ - {"scenario":"fedora-podman-rootful","testsuite":"provider-refresh"}, - {"scenario":"fedora-podman-rootless","testsuite":"provider-refresh"} + {"environment":"fedora-podman-rootful","installer":"binaries","testsuite":"provider-refresh"}, + {"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"provider-refresh"} ] # Run driver-specific integration tests: diff --git a/.github/workflows/integration-runner.yml b/.github/workflows/integration-runner.yml index 3af804ef2..69e366dc5 100644 --- a/.github/workflows/integration-runner.yml +++ b/.github/workflows/integration-runner.yml @@ -19,14 +19,14 @@ on: required: false type: string test-matrix: - description: JSON array of scenario and testsuite combinations + description: JSON array of environment, installer, and testsuite combinations required: false type: string default: >- [ - {"scenario":"ubuntu-docker-rootful","testsuite":"conformance"}, - {"scenario":"fedora-podman-rootful","testsuite":"conformance"}, - {"scenario":"fedora-podman-rootless","testsuite":"conformance"} + {"environment":"ubuntu-docker-rootful","installer":"binaries","testsuite":"conformance"}, + {"environment":"fedora-podman-rootful","installer":"binaries","testsuite":"conformance"}, + {"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"conformance"} ] permissions: @@ -40,7 +40,7 @@ concurrency: jobs: integration: - name: Integration test (${{ inputs.category }}, ${{ matrix.scenario }}, ${{ matrix.testsuite }}) + name: Integration test (${{ inputs.category }}, ${{ matrix.environment }}, ${{ matrix.installer }}, ${{ matrix.testsuite }}) strategy: fail-fast: false matrix: @@ -80,6 +80,7 @@ jobs: - name: Run test suite env: - SCENARIO: ${{ matrix.scenario }} + ENVIRONMENT: ${{ matrix.environment }} + INSTALLER: ${{ matrix.installer }} TESTSUITE: ${{ matrix.testsuite }} - run: nix run .#tmachine -- test "${SCENARIO}" "${TESTSUITE}" + run: nix run .#tmachine -- test "${ENVIRONMENT}" "${INSTALLER}" "${TESTSUITE}" diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 8b4b03d7d..bf23de2ae 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -17,14 +17,14 @@ on: type: string default: conformance test-matrix: - description: JSON array of scenario and testsuite combinations + description: JSON array of environment, installer, and testsuite combinations required: true type: string default: >- [ - {"scenario":"ubuntu-docker-rootful","testsuite":"conformance"}, - {"scenario":"fedora-podman-rootful","testsuite":"conformance"}, - {"scenario":"fedora-podman-rootless","testsuite":"conformance"} + {"environment":"ubuntu-docker-rootful","installer":"binaries","testsuite":"conformance"}, + {"environment":"fedora-podman-rootful","installer":"binaries","testsuite":"conformance"}, + {"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"conformance"} ] permissions: diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 73a39391b..23dd415d2 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -134,8 +134,8 @@ jobs: integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} test-matrix: >- [ - {"scenario":"fedora-podman-rootful","testsuite":"provider-refresh"}, - {"scenario":"fedora-podman-rootless","testsuite":"provider-refresh"} + {"environment":"fedora-podman-rootful","installer":"binaries","testsuite":"provider-refresh"}, + {"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"provider-refresh"} ] docker-e2e: diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 7e6891923..e7e479543 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -162,8 +162,8 @@ jobs: integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }} test-matrix: >- [ - {"scenario":"fedora-podman-rootful","testsuite":"provider-refresh"}, - {"scenario":"fedora-podman-rootless","testsuite":"provider-refresh"} + {"environment":"fedora-podman-rootful","installer":"binaries","testsuite":"provider-refresh"}, + {"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"provider-refresh"} ] docker-e2e: diff --git a/architecture/build.md b/architecture/build.md index 42effcee0..5ac5ff5f1 100644 --- a/architecture/build.md +++ b/architecture/build.md @@ -263,12 +263,18 @@ dependencies after installation. The Keycloak provider-refresh suite starts a guest-local Keycloak realm and verifies a successful OAuth refresh followed by revocation and the gateway's reauthorization-required recovery state. -The `tests/tmachine` setup and installation caches include a digest of the +Tmachine environments define the guest machine and runtime setup, while named +installers define how OpenShell is installed. This keeps the runtime mode +independent from binary or package installation and lets multiple installers +reuse the same prepared setup disk. The test command is +`tmachine test `. + +The `tests/tmachine` setup and install caches include a digest of the entire directory containing `ANSIBLE_CONFIG`, including local roles, task includes, templates, inventory, and requirements. The digest uses sorted relative paths, file contents, and executable permissions; source symlinks are unsupported. Both keys also retain the ordered playbook paths and contents, -their base disk contents, and whether Galaxy is enabled; installation keys +their base disk contents, and whether Galaxy is enabled; install keys include named binary inputs. The top-level `.roles` directory is excluded: Galaxy release pins in `requirements.yaml` are treated as immutable, including any transitive dependency pins. Cache misses with Galaxy enabled reinstall @@ -279,7 +285,7 @@ with musl, and the gateway and supervisor with GNU. Image assembly stages the gateway, sandbox, and supervisor as separate binaries for their respective Dockerfiles. The helpers stage binaries under `artifacts/binaries` so local and CI builds expose the same inputs to tmachine and image assembly. The Ubuntu -Docker and Fedora Podman scenarios import both local runtime images and +Docker and Fedora Podman environments import both local runtime images and configure the gateway to use them. ## Python Wheel Packaging diff --git a/tests/config.nix b/tests/config.nix index 759e3c9ed..461d3ddfb 100644 --- a/tests/config.nix +++ b/tests/config.nix @@ -31,7 +31,7 @@ let } ]; - scenarios = [ + environments = [ { name = "ubuntu-docker-rootful"; machine = "ubuntu"; @@ -42,19 +42,6 @@ let "ansible/playbooks/docker.yaml" ]; }; - install = { - use_galaxy = false; - playbooks = [ - "ansible/playbooks/openshell.yaml" - "ansible/playbooks/gateway.yaml" - ]; - inputs = { - openshell_cli_binary = "../artifacts/binaries/${muslTarget}/openshell"; - openshell_gateway_binary = "../artifacts/binaries/${gnuTarget}/openshell-gateway"; - openshell_supervisor_image = "../artifacts/images/openshell-supervisor-tmachine.tar"; - openshell_sandbox_image = "../artifacts/images/openshell-sandbox-tmachine.tar"; - }; - }; } { name = "fedora-podman-rootful"; @@ -67,19 +54,6 @@ let "ansible/playbooks/podman-rootful.yaml" ]; }; - install = { - use_galaxy = false; - playbooks = [ - "ansible/playbooks/openshell.yaml" - "ansible/playbooks/gateway.yaml" - ]; - inputs = { - openshell_cli_binary = "../artifacts/binaries/${muslTarget}/openshell"; - openshell_gateway_binary = "../artifacts/binaries/${gnuTarget}/openshell-gateway"; - openshell_supervisor_image = "../artifacts/images/openshell-supervisor-tmachine.tar"; - openshell_sandbox_image = "../artifacts/images/openshell-sandbox-tmachine.tar"; - }; - }; } { name = "fedora-podman-rootless"; @@ -92,18 +66,22 @@ let "ansible/playbooks/podman-rootless.yaml" ]; }; - install = { - use_galaxy = false; - playbooks = [ - "ansible/playbooks/openshell.yaml" - "ansible/playbooks/gateway.yaml" - ]; - inputs = { - openshell_cli_binary = "../artifacts/binaries/${muslTarget}/openshell"; - openshell_gateway_binary = "../artifacts/binaries/${gnuTarget}/openshell-gateway"; - openshell_supervisor_image = "../artifacts/images/openshell-supervisor-tmachine.tar"; - openshell_sandbox_image = "../artifacts/images/openshell-sandbox-tmachine.tar"; - }; + } + ]; + + installers = [ + { + name = "binaries"; + use_galaxy = false; + playbooks = [ + "ansible/playbooks/openshell.yaml" + "ansible/playbooks/gateway.yaml" + ]; + inputs = { + openshell_cli_binary = "../artifacts/binaries/${muslTarget}/openshell"; + openshell_gateway_binary = "../artifacts/binaries/${gnuTarget}/openshell-gateway"; + openshell_supervisor_image = "../artifacts/images/openshell-supervisor-tmachine.tar"; + openshell_sandbox_image = "../artifacts/images/openshell-sandbox-tmachine.tar"; }; } ]; diff --git a/tests/tmachine/src/config.rs b/tests/tmachine/src/config.rs index 7f8dab92b..f9c1657b2 100644 --- a/tests/tmachine/src/config.rs +++ b/tests/tmachine/src/config.rs @@ -10,7 +10,8 @@ use serde::Deserialize; #[derive(Clone, Deserialize)] pub struct Config { pub machines: Vec, - pub scenarios: Vec, + pub environments: Vec, + pub installers: Vec, pub testsuites: Vec, } @@ -21,11 +22,10 @@ pub struct Machine { } #[derive(Clone, Deserialize)] -pub struct Scenario { +pub struct Environment { pub name: String, pub machine: String, pub setup: Setup, - pub install: Install, } #[derive(Clone, Deserialize)] @@ -35,7 +35,8 @@ pub struct Setup { } #[derive(Clone, Deserialize)] -pub struct Install { +pub struct Installer { + pub name: String, pub use_galaxy: bool, pub playbooks: Vec, pub inputs: BTreeMap, diff --git a/tests/tmachine/src/main.rs b/tests/tmachine/src/main.rs index e2dadbb18..3c0d1a4ee 100644 --- a/tests/tmachine/src/main.rs +++ b/tests/tmachine/src/main.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; -use config::{Config, Machine, Scenario}; +use config::{Config, Environment, Installer, Machine}; mod ansible; mod config; @@ -23,9 +23,18 @@ struct Cli { #[derive(Subcommand)] enum Command { - Setup { scenario: String }, - Install { scenario: String }, - Test { scenario: String, testsuite: String }, + Setup { + environment: String, + }, + Install { + environment: String, + installer: String, + }, + Test { + environment: String, + installer: String, + testsuite: String, + }, } #[tokio::main(flavor = "current_thread")] @@ -34,49 +43,64 @@ async fn main() -> Result<()> { let config = Config::load(&cli.config)?; match cli.command { - Command::Setup { scenario } => { - let (machine, scenario) = find_scenario(&config, &scenario)?; - qemu::setup(&machine, &scenario).await?; + Command::Setup { environment } => { + let (machine, environment) = find_environment(&config, &environment)?; + qemu::setup(&machine, &environment).await?; } - Command::Install { scenario } => { - let (machine, scenario) = find_scenario(&config, &scenario)?; - qemu::install(&machine, &scenario).await?; + Command::Install { + environment, + installer, + } => { + let (machine, environment) = find_environment(&config, &environment)?; + let installer = find_installer(&config, &installer)?; + qemu::install(&machine, &environment, &installer).await?; } Command::Test { - scenario, + environment, + installer, testsuite, } => { - let (machine, scenario) = find_scenario(&config, &scenario)?; + let (machine, environment) = find_environment(&config, &environment)?; + let installer = find_installer(&config, &installer)?; let testsuite = config .testsuites .iter() .find(|candidate| candidate.name == testsuite) .with_context(|| format!("testsuite {testsuite:?} is not defined"))?; - qemu::test(&machine, &scenario, testsuite).await?; + qemu::test(&machine, &environment, &installer, testsuite).await?; } } Ok(()) } -fn find_scenario(config: &Config, name: &str) -> Result<(Machine, Scenario)> { - let scenario = config - .scenarios +fn find_installer(config: &Config, name: &str) -> Result { + config + .installers .iter() - .find(|scenario| scenario.name == name) - .with_context(|| format!("scenario {name:?} is not defined"))? + .find(|installer| installer.name == name) + .with_context(|| format!("installer {name:?} is not defined")) + .cloned() +} + +fn find_environment(config: &Config, name: &str) -> Result<(Machine, Environment)> { + let environment = config + .environments + .iter() + .find(|environment| environment.name == name) + .with_context(|| format!("environment {name:?} is not defined"))? .clone(); let machine = config .machines .iter() - .find(|machine| machine.name == scenario.machine) + .find(|machine| machine.name == environment.machine) .with_context(|| { format!( - "machine {:?} referenced by scenario {:?} is not defined", - scenario.machine, scenario.name + "machine {:?} referenced by environment {:?} is not defined", + environment.machine, environment.name ) })? .clone(); - Ok((machine, scenario)) + Ok((machine, environment)) } diff --git a/tests/tmachine/src/qemu/install.rs b/tests/tmachine/src/qemu/install.rs index 194f69c95..e1e591052 100644 --- a/tests/tmachine/src/qemu/install.rs +++ b/tests/tmachine/src/qemu/install.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use blake3::Hasher; -use crate::config::{Machine, Scenario}; +use crate::config::{Environment, Installer, Machine}; use super::ansible_hash::hash_sources; use super::layer::{cached_layer, hash_file, hash_files, hash_inputs}; @@ -14,31 +14,34 @@ use super::setup::setup; const INSTALL_CACHE_VERSION: &[u8] = b"tmachine-install-blake3-v1"; -pub async fn install(machine: &Machine, scenario: &Scenario) -> Result { - let setup_disk = setup(machine, scenario).await?; - if scenario.install.playbooks.is_empty() && scenario.install.inputs.is_empty() { +pub async fn install( + machine: &Machine, + environment: &Environment, + installer: &Installer, +) -> Result { + let setup_disk = setup(machine, environment).await?; + if installer.playbooks.is_empty() && installer.inputs.is_empty() { return Ok(setup_disk); } - let hash = install_hash(&setup_disk, scenario)?; + let hash = install_hash(&setup_disk, installer)?; cached_layer( &setup_disk, &hash, - scenario.install.use_galaxy, - &scenario.install.playbooks, - &scenario.install.inputs, + installer.use_galaxy, + &installer.playbooks, + &installer.inputs, ) .await } -fn install_hash(setup_disk: &Path, scenario: &Scenario) -> Result { +fn install_hash(setup_disk: &Path, installer: &Installer) -> Result { let mut hasher = Hasher::new(); hasher.update(INSTALL_CACHE_VERSION); hash_file(&mut hasher, setup_disk).context("failed to hash setup disk")?; - hasher.update(&[u8::from(scenario.install.use_galaxy)]); + hasher.update(&[u8::from(installer.use_galaxy)]); hash_sources(&mut hasher).context("failed to hash Ansible sources")?; - hash_files(&mut hasher, &scenario.install.playbooks) - .context("failed to hash install playbooks")?; - hash_inputs(&mut hasher, &scenario.install.inputs)?; + hash_files(&mut hasher, &installer.playbooks).context("failed to hash install playbooks")?; + hash_inputs(&mut hasher, &installer.inputs)?; Ok(hasher.finalize().to_hex().to_string()) } diff --git a/tests/tmachine/src/qemu/setup.rs b/tests/tmachine/src/qemu/setup.rs index 41625a70a..993e7932b 100644 --- a/tests/tmachine/src/qemu/setup.rs +++ b/tests/tmachine/src/qemu/setup.rs @@ -7,36 +7,37 @@ use std::path::PathBuf; use anyhow::{Context, Result}; use blake3::Hasher; -use crate::config::{Machine, Scenario}; +use crate::config::{Environment, Machine}; use super::ansible_hash::hash_sources; use super::layer::{cached_layer, hash_file, hash_files}; const SETUP_CACHE_VERSION: &[u8] = b"tmachine-disk-blake3-v1"; -pub async fn setup(machine: &Machine, scenario: &Scenario) -> Result { - if scenario.setup.playbooks.is_empty() { +pub async fn setup(machine: &Machine, environment: &Environment) -> Result { + if environment.setup.playbooks.is_empty() { return Ok(machine.base_image.clone()); } - let hash = setup_hash(machine, scenario)?; + let hash = setup_hash(machine, environment)?; cached_layer( &machine.base_image, &hash, - scenario.setup.use_galaxy, - &scenario.setup.playbooks, + environment.setup.use_galaxy, + &environment.setup.playbooks, &BTreeMap::new(), ) .await } -fn setup_hash(machine: &Machine, scenario: &Scenario) -> Result { +fn setup_hash(machine: &Machine, environment: &Environment) -> Result { let mut hasher = Hasher::new(); hasher.update(SETUP_CACHE_VERSION); hash_file(&mut hasher, &machine.base_image) .with_context(|| format!("failed to hash base image for machine {:?}", machine.name))?; - hasher.update(&[u8::from(scenario.setup.use_galaxy)]); + hasher.update(&[u8::from(environment.setup.use_galaxy)]); hash_sources(&mut hasher).context("failed to hash Ansible sources")?; - hash_files(&mut hasher, &scenario.setup.playbooks).context("failed to hash setup playbooks")?; + hash_files(&mut hasher, &environment.setup.playbooks) + .context("failed to hash setup playbooks")?; Ok(hasher.finalize().to_hex().to_string()) } diff --git a/tests/tmachine/src/qemu/test.rs b/tests/tmachine/src/qemu/test.rs index 6606f4865..c103477ee 100644 --- a/tests/tmachine/src/qemu/test.rs +++ b/tests/tmachine/src/qemu/test.rs @@ -3,7 +3,7 @@ use tempfile::tempdir; -use crate::config::{Machine, Scenario, Testsuite}; +use crate::config::{Environment, Installer, Machine, Testsuite}; use anyhow::Result; use super::img::QemuImage; @@ -11,8 +11,13 @@ use super::install::install; use super::layer::run_playbooks; use super::vm::QemuVm; -pub async fn test(machine: &Machine, scenario: &Scenario, testsuite: &Testsuite) -> Result<()> { - let install_disk = install(machine, scenario).await?; +pub async fn test( + machine: &Machine, + environment: &Environment, + installer: &Installer, + testsuite: &Testsuite, +) -> Result<()> { + let install_disk = install(machine, environment, installer).await?; let test_dir = tempdir().unwrap(); let test_disk = test_dir.path().join("test.qcow2"); let image = QemuImage::create(&install_disk, test_disk).await; From c5a8c4d220c79185ed27df4494157ac5ca887837 Mon Sep 17 00:00:00 2001 From: Florent BENOIT Date: Fri, 18 Sep 2026 13:32:04 +0000 Subject: [PATCH 7/8] chore(vm): bump libkrun to v1.19.4 and libkrunfw to v5.6.1 (#3451) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The libkrun and libkrunfw projects moved from the containers/ GitHub org to libkrun/. Update all clone URLs and version pins accordingly. libkrun v1.19.4 introduces krun_add_virtiofs4 with a permissions semantics parameter, enabling correct file ownership mapping on host-to-guest bind mounts — required for issue #2585. - libkrun: v1.17.4 → v1.19.4 (libkrun/libkrun) - libkrunfw: 463f717b → v5.6.1 (libkrun/libkrunfw) - Centralise LIBKRUN_REF in pins.env instead of hardcoding in each build script - Source pins.env in the macOS build script for consistency with the Linux build script - Remove the init/init make target step — since libkrun/libkrun@05c4eb7 the init blob moved into the init_blob crate and is built by cargo Ref: https://github.com/NVIDIA/OpenShell/issues/2585 Signed-off-by: Florent Benoit --- crates/openshell-driver-vm/runtime/pins.env | 11 ++++++++--- tasks/scripts/vm/build-libkrun-macos.sh | 9 +++++++-- tasks/scripts/vm/build-libkrun.sh | 12 ++++-------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/crates/openshell-driver-vm/runtime/pins.env b/crates/openshell-driver-vm/runtime/pins.env index 9977eddcc..5595394b5 100644 --- a/crates/openshell-driver-vm/runtime/pins.env +++ b/crates/openshell-driver-vm/runtime/pins.env @@ -34,6 +34,11 @@ COMMUNITY_SANDBOX_IMAGE="${COMMUNITY_SANDBOX_IMAGE:-ghcr.io/nvidia/openshell-com UMOCI_VERSION="${UMOCI_VERSION:-v0.6.0}" # ── libkrunfw upstream (commit-pinned) ───────────────────────────────── -# Repo: https://github.com/containers/libkrunfw -# Pinned: 2026-03-27 (main branch HEAD at time of pinning) -LIBKRUNFW_REF="${LIBKRUNFW_REF:-463f717bbdd916e1352a025b6fb2456e882b0b39}" +# Repo: https://github.com/libkrun/libkrunfw +# Pinned: v5.6.1 (2026-09-16) +LIBKRUNFW_REF="${LIBKRUNFW_REF:-617938cf2ae9a3a0e5696d23a5d6252cd7a1ef4f}" + +# ── libkrun upstream (commit-pinned) ────────────────────────────────── +# Repo: https://github.com/libkrun/libkrun +# Pinned: v1.19.4 (2026-09-16) +LIBKRUN_REF="${LIBKRUN_REF:-728df8125077d0db44265f6e997c72b81b65c015}" diff --git a/tasks/scripts/vm/build-libkrun-macos.sh b/tasks/scripts/vm/build-libkrun-macos.sh index 339a73666..f560a19ba 100755 --- a/tasks/scripts/vm/build-libkrun-macos.sh +++ b/tasks/scripts/vm/build-libkrun-macos.sh @@ -31,6 +31,10 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" + +# Source pinned dependency versions. +source "${ROOT}/crates/openshell-driver-vm/runtime/pins.env" 2>/dev/null || true + BUILD_DIR="${ROOT}/target/libkrun-build" OUTPUT_DIR="${BUILD_DIR}" BREW_PREFIX="$(brew --prefix 2>/dev/null || echo /opt/homebrew)" @@ -149,11 +153,12 @@ echo " Built: libkrunfw.${ABI_VERSION}.dylib ($(du -sh "${OUTPUT_DIR}/libkrun # ── Clone libkrun ─────────────────────────────────────────────────────── -LIBKRUN_REF="${LIBKRUN_REF:-e5922f6}" +# LIBKRUN_REF is sourced from pins.env; env-var override still works. +LIBKRUN_REF="${LIBKRUN_REF:-728df8125077d0db44265f6e997c72b81b65c015}" if [ ! -d libkrun ]; then echo "==> Cloning libkrun..." - git clone https://github.com/containers/libkrun.git + git clone https://github.com/libkrun/libkrun.git fi echo "==> Checking out ${LIBKRUN_REF}..." diff --git a/tasks/scripts/vm/build-libkrun.sh b/tasks/scripts/vm/build-libkrun.sh index e204dc082..bd40f2f1d 100755 --- a/tasks/scripts/vm/build-libkrun.sh +++ b/tasks/scripts/vm/build-libkrun.sh @@ -134,7 +134,7 @@ ensure_python3_with_pyelftools_for_libkrunfw if [ ! -d libkrunfw ]; then echo " Cloning libkrunfw (pinned: ${LIBKRUNFW_REF:-HEAD})..." - git clone https://github.com/containers/libkrunfw.git + git clone https://github.com/libkrun/libkrunfw.git fi cd libkrunfw @@ -405,11 +405,12 @@ echo "==> Building libkrun..." ensure_cargo_for_libkrun ensure_libclang_for_libkrun -LIBKRUN_REF="${LIBKRUN_REF:-v1.17.4}" +# LIBKRUN_REF is sourced from pins.env (line 32); env-var override still works. +LIBKRUN_REF="${LIBKRUN_REF:-728df8125077d0db44265f6e997c72b81b65c015}" if [ ! -d libkrun ]; then echo " Cloning libkrun..." - git clone https://github.com/containers/libkrun.git + git clone https://github.com/libkrun/libkrun.git fi cd libkrun @@ -420,11 +421,6 @@ if [ -n "${LIBKRUN_REF:-}" ]; then git checkout "${LIBKRUN_REF}" 2>/dev/null || git checkout "origin/${LIBKRUN_REF}" 2>/dev/null || true fi -if [ -f init/Makefile ] || grep -q 'init/init' Makefile 2>/dev/null; then - echo " Building init/init binary..." - make init/init -fi - echo " Building libkrun with NET=1 BLK=1..." cargo build --release --features blk --features net --target-dir="$(pwd)/target" From e2939b919decda8605a1e4037b2cae9d935a870d Mon Sep 17 00:00:00 2001 From: Jorge Date: Fri, 18 Sep 2026 15:50:03 +0000 Subject: [PATCH 8/8] feat(e2e): run the Kubernetes e2e suite on cargo-nextest with machine- and human-readable reports (#3344) * feat(e2e): run kubernetes suite on cargo-nextest with JUnit/HTML reports Switch e2e:kubernetes (and all its variants) from `cargo test` to `cargo nextest run` for per-test process isolation and output consistent with the other nextest-based CI runs. - Add a dedicated `e2e-kubernetes` nextest profile with a JUnit report and a generous slow-timeout (60s flag, 5-min terminate) suited to live-cluster tests; kept separate from `ci` so its JUnit path and timeouts don't affect the workspace run. - Pin `--target-dir` for the run so the profile's relative JUnit path resolves to the repo-root results/ regardless of any inherited CARGO_TARGET_DIR (nextest ignores absolute JUnit paths). - Render the JUnit XML to a standalone HTML report via xsltproc and a committed XSLT stylesheet (best-effort; never masks the test exit code). - Name each report via `OPENSHELL_E2E_REPORT_NAME` (default `e2e-kubernetes`), used verbatim for both the `results/.{xml,html}` filenames and the HTML heading. Tasks that invoke the script multiple times in one run set a distinct name per invocation so the reports no longer clobber the single fixed path: the credential-driver runs write results/e2e-kubernetes-secrets.xml and -vault.xml, and e2e:kubernetes:agent-sandbox-versions writes results/e2e-kubernetes-agent-sandbox-v1beta1.xml and -v1alpha1.xml. - Declare cargo-nextest in mise [tools] so the task runs without the Nix shell. - Ignore the results/ output directory. The results/ reports do not leak information. They are gitignored and no workflow uploads them as artifacts, so they stay on the ephemeral CI runner and are discarded when it is torn down. The HTML template renders only test names, status, timings, and failure messages (no captured stdout/stderr). Moving from `cargo test -- --nocapture` to nextest's captured, failure-only output also reduces what lands in the retained, viewable console logs. Signed-off-by: Jorge Garcia Oncins * chore(e2e): revert per-lane report names for agent-sandbox-versions The agent-sandbox-versions task runs two lanes sequentially against the same cluster: v0.5.0 (v1beta1 storage version) then v0.4.6 (v1alpha1). On a reused cluster the second lane fails when kubectl applies the older CRD, because Kubernetes refuses to drop v1beta1 from spec.versions while it remains in status.storedVersions (the storage-version downgrade guardrail). This is a pre-existing issue with the v0.4.6 lane, unrelated to the nextest reporting work. The per-lane OPENSHELL_E2E_REPORT_NAME additions do not address that downgrade failure, so revert them to keep this PR scoped to the nextest change. Agent Sandbox 0.4.x is also superseded (1.0.0 is published); dropping or bumping the v1alpha1 lane is left as a follow-up. Signed-off-by: Jorge Garcia Oncins --------- Signed-off-by: Jorge Garcia Oncins --- .config/nextest.toml | 12 +++ .gitignore | 3 + e2e/rust/e2e-kubernetes.sh | 57 +++++++++--- mise.lock | 11 ++- mise.toml | 2 +- scripts/junit-to-html.xsl | 172 +++++++++++++++++++++++++++++++++++++ 6 files changed, 239 insertions(+), 18 deletions(-) create mode 100644 scripts/junit-to-html.xsl diff --git a/.config/nextest.toml b/.config/nextest.toml index 1c1235c6e..b8267fb5f 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -8,3 +8,15 @@ final-status-level = "fail" failure-output = "immediate-final" # Print slow test names after 30 seconds and terminate them after two minutes. slow-timeout = { period = "30s", terminate-after = 4 } + +[profile.e2e-kubernetes] +fail-fast = false +status-level = "slow" +final-status-level = "fail" +failure-output = "immediate-final" +# Flag slow tests after 60s and terminate them after five minutes. +slow-timeout = { period = "60s", terminate-after = 5 } + +# Relative to the profile store dir (`e2e/rust/target/nextest/e2e-kubernetes/`). +[profile.e2e-kubernetes.junit] +path = "../../../../../results/e2e-kubernetes.xml" diff --git a/.gitignore b/.gitignore index 9689a7417..b4e8cd163 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ /target/ e2e/rust/target/ target/ + +# Test result artifacts (JUnit XML, etc.) +/results/ debug/ release/ diff --git a/e2e/rust/e2e-kubernetes.sh b/e2e/rust/e2e-kubernetes.sh index c54057bb9..e49c7a3b7 100755 --- a/e2e/rust/e2e-kubernetes.sh +++ b/e2e/rust/e2e-kubernetes.sh @@ -14,6 +14,9 @@ # via `server.hostGatewayIP`. Targeting a cluster where the test host is # unreachable from pods? Set OPENSHELL_E2E_KUBERNETES_FEATURES=e2e to drop the # alias-dependent tests entirely. +# +# Results: `run_suite` writes a JUnit + HTML report under `results/`. Set +# `OPENSHELL_E2E_REPORT_NAME` to name it per run when invoking this script repeatedly. set -euo pipefail @@ -24,6 +27,25 @@ source "${ROOT}/e2e/support/conformance.sh" E2E_FEATURES="${OPENSHELL_E2E_KUBERNETES_FEATURES-e2e,e2e-host-gateway,e2e-kubernetes}" +# Fixed output path of the `e2e-kubernetes` nextest profile (`.config/nextest.toml`). +JUNIT_XML="${ROOT}/results/e2e-kubernetes.xml" + +# Render a sibling HTML report from a JUnit XML. Best-effort: failures only warn. +render_html() { + local xml="${1:-${JUNIT_XML}}" + local title="${2:-e2e-kubernetes}" + [ -f "${xml}" ] || return 0 + local html="${xml%.xml}.html" + if command -v xsltproc >/dev/null 2>&1; then + xsltproc --stringparam title "${title}" \ + "${ROOT}/scripts/junit-to-html.xsl" "${xml}" >"${html}" \ + && echo "HTML report: ${html}" \ + || echo "WARNING: failed to render HTML report from ${xml}" >&2 + else + echo "WARNING: xsltproc not found; skipping HTML report (${xml} still written)" >&2 + fi +} + # Docker and Podman build their local gateway and CLI together in the shared # gateway wrapper. Kubernetes consumes published gateway images, so only its # local CLI needs to be built when CI has not supplied a prebuilt one. @@ -50,9 +72,19 @@ run_conformance() { e2e_run_openshell_conformance "Kubernetes" } +# `OPENSHELL_E2E_REPORT_NAME` (default `e2e-kubernetes`) names the report +# `results/.{xml,html}` and its heading, so repeated runs do not clobber. run_suite() { + local name="${OPENSHELL_E2E_REPORT_NAME:-e2e-kubernetes}" + local report="${ROOT}/results/${name}.xml" + local status=0 "${ROOT}/e2e/with-kube-gateway.sh" \ - bash "${BASH_SOURCE[0]}" "${RUN_WITH_GATEWAY_COMMAND}" + bash "${BASH_SOURCE[0]}" "${RUN_WITH_GATEWAY_COMMAND}" || status=$? + if [ "${report}" != "${JUNIT_XML}" ]; then + mv -f "${JUNIT_XML}" "${report}" 2>/dev/null || true + fi + render_html "${report}" "${name}" + return "${status}" } run_e2e() { @@ -61,11 +93,13 @@ run_e2e() { return 0 fi - cargo test --manifest-path "${ROOT}/e2e/rust/Cargo.toml" \ + # Pin `--target-dir` so the profile `junit.path` resolves regardless of `CARGO_TARGET_DIR`. + cargo nextest run --profile e2e-kubernetes \ + --config-file "${ROOT}/.config/nextest.toml" \ + --target-dir "${ROOT}/e2e/rust/target" \ + --manifest-path "${ROOT}/e2e/rust/Cargo.toml" \ --features "${E2E_FEATURES}" \ - --no-fail-fast \ - ${test_filter[@]+"${test_filter[@]}"} \ - -- --nocapture + ${test_filter[@]+"${test_filter[@]}"} } if [ "${1:-}" = "${RUN_WITH_GATEWAY_COMMAND}" ]; then @@ -73,12 +107,13 @@ if [ "${1:-}" = "${RUN_WITH_GATEWAY_COMMAND}" ]; then exit 0 fi +# Credential-driver mode: run once per storage backend, each with its own report. if [ "${OPENSHELL_E2E_CREDENTIAL_DRIVERS:-0}" = "1" ] \ && [ -z "${OPENSHELL_E2E_CREDENTIAL_DRIVER:-}" ]; then - OPENSHELL_E2E_CREDENTIAL_DRIVER=kubernetes-secrets run_suite - OPENSHELL_E2E_CREDENTIAL_DRIVER=vault run_suite - exit 0 + OPENSHELL_E2E_CREDENTIAL_DRIVER=kubernetes-secrets \ + OPENSHELL_E2E_REPORT_NAME=e2e-kubernetes-secrets run_suite + OPENSHELL_E2E_CREDENTIAL_DRIVER=vault \ + OPENSHELL_E2E_REPORT_NAME=e2e-kubernetes-vault run_suite +else + run_suite fi - -exec "${ROOT}/e2e/with-kube-gateway.sh" \ - bash "${BASH_SOURCE[0]}" "${RUN_WITH_GATEWAY_COMMAND}" diff --git a/mise.lock b/mise.lock index 42a635f96..ec8cccf08 100644 --- a/mise.lock +++ b/mise.lock @@ -136,11 +136,6 @@ checksum = "sha256:f73a5c39f96bb6ebb89cc7915cf182260d4cbf30765322c5e793d0fe8bd80 url = "https://github.com/mozilla/sccache/releases/download/v0.16.0/sccache-v0.16.0-aarch64-unknown-linux-musl.tar.gz" url_api = "https://api.github.com/repos/mozilla/sccache/releases/assets/452060468" -[tools."github:mozilla/sccache"."platforms.linux-x64"] -checksum = "sha256:aec995a83ad3dff3d14b6314e08858b7b73d35ca85a5bcf3d3a9ec07dee35588" -url = "https://github.com/mozilla/sccache/releases/download/v0.16.0/sccache-v0.16.0-x86_64-unknown-linux-musl.tar.gz" -url_api = "https://api.github.com/repos/mozilla/sccache/releases/assets/452060682" - [tools."github:mozilla/sccache"."platforms.macos-arm64"] checksum = "sha256:ded590cae2c72042c61178632906bef62d635fa20d45f8b22110a2241f430960" url = "https://github.com/mozilla/sccache/releases/download/v0.16.0/sccache-v0.16.0-aarch64-apple-darwin.tar.gz" @@ -169,9 +164,12 @@ url = "https://github.com/mozilla/sccache/releases/download/v0.16.0/sccache-v0.1 url_api = "https://api.github.com/repos/mozilla/sccache/releases/assets/452060682" [[tools."github:nextest-rs/nextest"]] -version = "cargo-nextest-0.9.143" +version = "0.9.143" backend = "github:nextest-rs/nextest" +[tools."github:nextest-rs/nextest".options] +version_prefix = "cargo-nextest-" + [tools."github:nextest-rs/nextest"."platforms.linux-arm64"] checksum = "sha256:2a64b3566a92508550a7ab29c3e8db25472ca37730ecb4d22100b6aa440c2a68" url = "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-0.9.143/cargo-nextest-0.9.143-aarch64-unknown-linux-gnu.tar.gz" @@ -183,6 +181,7 @@ checksum = "sha256:66786b9abe23920d022a182d1416b1bbc8130dd4872a9553d76985a1708dc url = "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-0.9.143/cargo-nextest-0.9.143-x86_64-unknown-linux-gnu.tar.gz" url_api = "https://api.github.com/repos/nextest-rs/nextest/releases/assets/501882467" provenance = "github-attestations" +provenance_verified = true [tools."github:nextest-rs/nextest"."platforms.macos-arm64"] checksum = "sha256:4830d430411148d17602a75cc880bfb4dc8dac153dea59a48a2ef4cc93577f07" diff --git a/mise.toml b/mise.toml index de414e590..9504f8278 100644 --- a/mise.toml +++ b/mise.toml @@ -44,8 +44,8 @@ k3d = { version = "5.8.3", os = ["macos"] } "github:EmbarkStudios/cargo-deny" = { version = "0.20.2", version_prefix = "" } zig = "0.14.1" "github:rust-secure-code/cargo-auditable" = "0.7.5" +"github:nextest-rs/nextest" = { version = "0.9.143", version_prefix = "cargo-nextest-" } "github:rust-cross/cargo-zigbuild" = "0.22.3" -"github:nextest-rs/nextest" = { version = "cargo-nextest-0.9.143", os = ["windows"] } "npm:markdownlint-cli2" = "0.22.0" [tools."github:mozilla/sccache"] diff --git a/scripts/junit-to-html.xsl b/scripts/junit-to-html.xsl new file mode 100644 index 000000000..02b2960f7 --- /dev/null +++ b/scripts/junit-to-html.xsl @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + <xsl:value-of select="$title"/> + + + +

+
+
+ Total + +
+
+ card c-fail zero + Failures + +
+
+ card c-fail zero + Errors + +
+
+ card c-skip zero + Skipped + +
+
+ Time + s +
+
+ +

+ + ( tests, s) +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TestStatusTime
+
+
FAILs
SKIPs
PASSs
+
+ + +
+