From 841a66f9c842f5bb3e0b250481db98be949643d1 Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Thu, 17 Sep 2026 17:58:46 +0200 Subject: [PATCH] refactor(tmachine): separate installers from environments 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 a4fbc97506..827823df42 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 3af804ef27..69e366dc55 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 8b4b03d7d7..bf23de2ae5 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 73a39391b0..23dd415d2f 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 7e6891923d..e7e4795437 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 42effcee06..5ac5ff5f18 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 759e3c9ed8..461d3ddfbf 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 7f8dab92bb..f9c1657b20 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 e2dadbb189..3c0d1a4eee 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(|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(|scenario| scenario.name == name) - .with_context(|| format!("scenario {name:?} is not defined"))? + .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 194f69c95f..e1e5910520 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 41625a70a7..993e7932b3 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 6606f4865b..c103477ee0 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;