diff --git a/Makefile b/Makefile index d621e624..d7cda745 100644 --- a/Makefile +++ b/Makefile @@ -7,13 +7,18 @@ SHELL := /bin/bash +# Define directory of this Makefile so it can be `include`d from +# elsewhere without variables breaking, e.g. for use of controller-gen +# & kopium from this directory's LOCALBIN. +MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) + NAMESPACE ?= trusted-execution-clusters PLATFORM ?= kind KUBECTL=kubectl INTEGRATION_TEST_THREADS ?= 1 -LOCALBIN ?= $(shell pwd)/bin +LOCALBIN ?= $(MAKEFILE_DIR)/bin # either linux or darwin OS ?= $(shell uname -s | tr '[:upper:]' '[:lower:]') # either x86_64/amd64 or aarch64/arm64 @@ -28,11 +33,11 @@ else ifeq ($(OS),darwin) KOPIUM_TARGET := $(KOPIUM_RUST_ARCH)-apple-darwin endif -CONTROLLER_TOOLS_VERSION ?= $(shell go list -m -f '{{.Version}}' sigs.k8s.io/controller-tools) +CONTROLLER_TOOLS_VERSION ?= $(shell cd $(MAKEFILE_DIR) && go list -m -f '{{.Version}}' sigs.k8s.io/controller-tools) CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen-$(CONTROLLER_TOOLS_VERSION) -YQ_VERSION ?= $(shell go list -m -f '{{.Version}}' github.com/mikefarah/yq/v4) +YQ_VERSION ?= $(shell cd $(MAKEFILE_DIR) && go list -m -f '{{.Version}}' github.com/mikefarah/yq/v4) YQ ?= $(LOCALBIN)/yq-$(YQ_VERSION) -KOPIUM_VERSION ?= $(shell cargo metadata --format-version 1 | jq -r '.resolve.nodes[] | select(.deps[]?.name == "kopium") | .deps[] | select(.name == "kopium") | .pkg | split("@")[1]') +KOPIUM_VERSION ?= $(shell cd $(MAKEFILE_DIR) && cargo metadata --format-version 1 | jq -r '.resolve.nodes[] | select(.deps[]?.name == "kopium") | .deps[] | select(.name == "kopium") | .pkg | split("@")[1]') KOPIUM ?= $(LOCALBIN)/kopium-$(KOPIUM_VERSION) REGISTRY ?= quay.io/trusted-execution-clusters diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index 9dc75538..89c24d75 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs @@ -4,6 +4,7 @@ // SPDX-License-Identifier: MIT use anyhow::{Context, Result, anyhow}; +use constants::APPROVED_IMAGE_NAME; use fs_extra::dir; use glob::glob; use k8s_openapi::api::apps::v1::{Deployment, DeploymentCondition, DeploymentStatus}; @@ -15,6 +16,7 @@ use k8s_openapi::apimachinery::pkg::apis::meta::v1::Condition; use kube::api::{DeleteParams, ObjectMeta, Patch}; use kube::runtime::wait::await_condition; use kube::{Api, Client}; +use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode}; use serde_json::json; use std::path::{Path, PathBuf}; use std::{collections::BTreeMap, env, sync::Once, time::Duration}; @@ -43,6 +45,7 @@ use compute_pcrs_lib::Pcr; const TEST_TIMEOUT_MULTIPLIER_ENV: &str = "TEST_TIMEOUT_MULTIPLIER"; const EXPOSE_MAX_ATTEMPTS: u32 = 3; +const UPSTREAM_DIR_ENV: &str = "UPSTREAM_DIR"; const PLATFORM_ENV: &str = "PLATFORM"; const CLUSTER_URL_ENV: &str = "CLUSTER_URL"; const SET_CLUSTER_ERR: &str = "Set $CLUSTER_URL when $PLATFORM is none of: kind, openshift"; @@ -159,7 +162,7 @@ fn get_virt_provider() -> Result { } } -fn get_env(name: &str) -> Result { +pub fn get_env(name: &str) -> Result { env::var(name).map_err(|e| anyhow!("Environment variable {name} is required: {e}")) } @@ -452,8 +455,20 @@ pub async fn get_cluster_url( .await } +pub async fn get_encoded_root_pem(client: Client, namespace: &str) -> Result { + let secrets: Api = Api::namespaced(client.clone(), namespace); + let root_secret = secrets.get(ROOT_SECRET).await?; + let ctx = format!("Root secret {ROOT_SECRET} had no ca.crt"); + let root_secret_data = root_secret.data.context(ctx.clone())?; + let ca_pem_bytes = root_secret_data.get("ca.crt").context(ctx)?; + let root_pem = String::from_utf8(ca_pem_bytes.0.clone())?; + let encoded = utf8_percent_encode(&root_pem, NON_ALPHANUMERIC); + Ok(format!("data:,{encoded}")) +} + static INIT: Once = Once::new(); +#[derive(Clone)] pub struct TestContext { client: Client, test_namespace: String, @@ -477,7 +492,7 @@ impl TestContext { let ctx = Self { client, - test_namespace: namespace, + test_namespace: namespace.clone(), manifests_dir: String::new(), test_name: test_name.to_string(), delayed_approved_image, @@ -490,11 +505,7 @@ impl TestContext { ctx.create_namespace().await?; ctx.apply_operator_manifests(approved_images).await?; - test_info!( - &ctx.test_name, - "Execute test in the namespace {}", - ctx.test_namespace - ); + test_info!(&ctx.test_name, "Execute test in the namespace {namespace}"); Ok(ctx) } @@ -533,11 +544,7 @@ impl TestContext { } async fn create_namespace(&self) -> Result<()> { - test_info!( - &self.test_name, - "Creating test namespace: {}", - self.test_namespace - ); + self.info(format!("Creating test namespace: {}", self.test_namespace)); let namespace_api: Api = Api::all(self.client.clone()); let namespace = Namespace { metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { @@ -576,20 +583,12 @@ impl TestContext { for tec in &tec_list.items { if let Some(name) = &tec.metadata.name { - test_info!( - &self.test_name, - "Deleting TrustedExecutionCluster: {}", - name - ); + self.info(format!("Deleting TrustedExecutionCluster: {name}")); tec_api.delete(name, &dp).await?; // Wait for the resource to be deleted wait_for_resource_deleted(&tec_api, name, scaled_timeout(120)).await?; - test_info!( - &self.test_name, - "TrustedExecutionCluster {} has been deleted", - name - ); + self.info(format!("TrustedExecutionCluster {name} has been deleted")); } } @@ -605,10 +604,10 @@ impl TestContext { namespace_api.delete(&self.test_namespace, &dp).await?; let timeout = scaled_timeout(300); wait_for_resource_deleted(&namespace_api, &self.test_namespace, timeout).await?; - test_info!(&self.test_name, "Deleted namespace {}", self.test_namespace); + self.info(format!("Deleted namespace {}", self.test_namespace)); } Err(kube::Error::Api(ae)) if ae.code == 404 => { - test_info!(&self.test_name, "Namespace already deleted"); + self.info("Namespace already deleted"); } Err(e) => return Err(e.into()), } @@ -620,21 +619,15 @@ impl TestContext { let manifests_dir = temp_dir.join(format!("manifests-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&manifests_dir)?; let dir_str = manifests_dir.to_str().unwrap(); - test_info!( - &self.test_name, - "Created temp manifests directory: {dir_str}", - ); + self.info(format!("Created temp manifests directory: {dir_str}")); Ok(dir_str.to_string()) } fn cleanup_manifests_dir(&self) -> Result<()> { if Path::new(&self.manifests_dir).exists() { std::fs::remove_dir_all(&self.manifests_dir)?; - test_info!( - &self.test_name, - "Removed manifests directory: {}", - self.manifests_dir - ); + let manifests_dir = &self.manifests_dir; + self.info(format!("Removed manifests directory: {manifests_dir}",)); } Ok(()) } @@ -748,11 +741,10 @@ impl TestContext { let err = anyhow!("No controller-gen found in bin/, run `make build-tools` first"); let controller_gen_path = glob::glob(pattern)?.next().ok_or(err)??; - test_info!( - &self.test_name, + self.info(format!( "Generating CRDs and RBAC with controller-gen at: {}", - controller_gen_path.display() - ); + controller_gen_path.display(), + )); let crd_temp_dir = Path::new(&self.manifests_dir).join("crd"); let rbac_dir = workspace_root.join("config/rbac/"); @@ -777,8 +769,7 @@ impl TestContext { let stderr = String::from_utf8_lossy(&crd_gen_output.stderr); return Err(anyhow!("Failed to generate CRDs and RBAC: {stderr}")); } - - test_info!(&self.test_name, "CRDs and RBAC generated successfully"); + self.info("CRDs and RBAC generated successfully"); let trusted_cluster_gen_path = workspace_root.join("trusted-cluster-gen"); if !trusted_cluster_gen_path.exists() { @@ -826,12 +817,15 @@ impl TestContext { async fn apply_operator_manifests(&self, approved_images: &[(&str, &str)]) -> Result<()> { let manifests_dir = &self.manifests_dir; - test_info!(&self.test_name, "Generating manifests in {manifests_dir}"); - let workspace_root = env::current_dir()?.join(".."); + self.info(format!("Generating manifests in {manifests_dir}")); + let workspace_root = env::var(UPSTREAM_DIR_ENV) + .ok() + .map(PathBuf::from) + .unwrap_or(env::current_dir()?.join("..")); let (crd_temp_dir, rbac_temp_dir) = self .generate_manifests(&workspace_root, approved_images) .await?; - test_info!(&self.test_name, "Manifests generated successfully"); + self.info("Manifests generated successfully"); self.set_certificates().await?; let tec = "trustedexecutionclusters.trusted-execution-clusters.io"; @@ -839,10 +833,7 @@ impl TestContext { let crd_check_output = kubectl().args(args).output().await?; if crd_check_output.status.success() { - test_info!( - &self.test_name, - "TrustedExecutionCluster CRD already exists, skipping CRD creation" - ); + self.info("TrustedExecutionCluster CRD already exists, skipping CRD creation"); } else { kube_apply!( crd_temp_dir.to_str().unwrap(), @@ -852,8 +843,7 @@ impl TestContext { ); } - test_info!(&self.test_name, "Preparing RBAC manifests"); - + self.info("Preparing RBAC manifests"); let ns = self.test_namespace.clone(); let sa_src = workspace_root.join("config/rbac/service_account.yaml"); let sa_content = std::fs::read_to_string(&sa_src)? @@ -890,7 +880,7 @@ impl TestContext { let le_rb_dst = rbac_temp_dir.join("leader_election_role_binding.yaml"); std::fs::write(&le_rb_dst, le_rb_content)?; - test_info!(&self.test_name, "Preparing RBAC kustomization"); + self.info("Preparing RBAC kustomization"); let platform = get_k8s_platform(&self.client, &self.test_namespace); let kustomization_src = workspace_root.join("config/rbac/kustomization.yaml.in"); let kustomization_content = std::fs::read_to_string(&kustomization_src)?; @@ -927,10 +917,7 @@ impl TestContext { "Applying operator manifest" ); - test_info!( - &self.test_name, - "Updating CR manifest with publicTrusteeAddr" - ); + self.info("Updating CR manifest with publicTrusteeAddr"); self.apply_cr_manifests(manifests_path).await } @@ -1005,8 +992,7 @@ impl TestContext { TRUSTEE_DEPLOYMENT, ATTESTATION_KEY_REGISTER_DEPLOYMENT, ] { - let info = format!("Waiting for deployment {depl} to be ready"); - test_info!(&self.test_name, "{info}"); + self.info(format!("Waiting for deployment {depl} to be ready")); let done = await_condition(depls.clone(), depl, depl_ready); let ctx = format!("waiting for deployment {depl} to be ready"); timeout(scaled_duration(300), done).await.context(ctx)??; @@ -1044,20 +1030,14 @@ impl TestContext { tecs.patch("trusted-execution-cluster", &Default::default(), &patch) .await?; let info = format!("Updated TEC resource with publicTrusteeAddr: {trustee_addr}"); - test_info!(&self.test_name, "{info}"); + self.info(info); - test_info!( - &self.test_name, - "Waiting for image-pcrs ConfigMap to be created" - ); + self.info("Waiting for image-pcrs ConfigMap to be created"); let configmap_api: Api = Api::namespaced(self.client.clone(), ns); wait_for_resource_created(&configmap_api, "image-pcrs", scaled_timeout(60)).await?; - let info = format!( - "Waiting for ApprovedImage {} to be Committed", - constants::APPROVED_IMAGE_NAME - ); - test_info!(&self.test_name, "{info}"); + let info = format!("Waiting for ApprovedImage {APPROVED_IMAGE_NAME} to be Committed"); + self.info(info); let images: Api = Api::namespaced(self.client.clone(), ns); let image_ready = |img: Option<&ApprovedImage>| { let chk_cond = |c: &Condition| c.type_ == COMMITTED_CONDITION && c.status == "True"; diff --git a/test_utils/src/virt/azure.rs b/test_utils/src/virt/azure.rs index f2b09e8a..65c62619 100644 --- a/test_utils/src/virt/azure.rs +++ b/test_utils/src/virt/azure.rs @@ -3,11 +3,12 @@ // SPDX-License-Identifier: MIT use anyhow::{Context, Result, anyhow}; +use kube::Client; use serde_json::Value; use std::{env, time}; use tokio::process::Command; -use super::{VmBackend, VmConfig, generate_ignition, ssh_exec}; +use super::{NodeBackend, VmBackend, VmConfig, generate_ignition, sh_exec}; use crate::{Poller, ensure_command, warn_frame}; const KEEP_ALIVE_MINUTES: i64 = 60; @@ -56,6 +57,26 @@ impl AzureBackend { } } +#[async_trait::async_trait] +impl NodeBackend for AzureBackend { + async fn ssh_exec(&self, command: &str) -> Result { + let (rg, ip_name) = (&self.resource_group, format!("{}-ip", self.config.vm_name)); + let mut args = vec!["network", "public-ip", "show", "--resource-group", rg]; + args.extend(["--name", &ip_name]); + let result = self.az(&args).await?; + + let public_ip = result["ipAddress"].as_str().unwrap(); + sh_exec(&format!( + "ssh -i {} -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null core@{public_ip} '{command}'", + self.config.ssh_private_key.display() + )).await + } + + async fn get_root_key(&self, _: Client, _: &str) -> Result>> { + Ok(None) + } +} + #[async_trait::async_trait] impl VmBackend for AzureBackend { async fn create_vm(&self) -> Result<()> { @@ -162,23 +183,6 @@ impl VmBackend for AzureBackend { poller.poll_async(check_fn).await } - async fn ssh_exec(&self, command: &str) -> Result { - let (rg, ip_name) = (&self.resource_group, format!("{}-ip", self.config.vm_name)); - let mut args = vec!["network", "public-ip", "show", "--resource-group", rg]; - args.extend(["--name", &ip_name]); - let result = self.az(&args).await?; - - let public_ip = result["ipAddress"].as_str().unwrap(); - ssh_exec(&format!( - "ssh -i {} -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null core@{public_ip} '{command}'", - self.config.ssh_private_key.display() - )).await - } - - async fn get_root_key(&self) -> Result>> { - Ok(None) - } - async fn cleanup(&self) -> Result<()> { self.config.cleanup(); let rg = &self.resource_group; diff --git a/test_utils/src/virt/kubevirt.rs b/test_utils/src/virt/kubevirt.rs index 25c2d84a..2a1cd93a 100644 --- a/test_utils/src/virt/kubevirt.rs +++ b/test_utils/src/virt/kubevirt.rs @@ -3,18 +3,32 @@ // // SPDX-License-Identifier: MIT -use anyhow::{Context, Result, anyhow}; +use anyhow::{Context, Result}; use k8s_openapi::{api::core::v1::Secret, apimachinery::pkg::util::intstr::IntOrString}; use kube::{Api, api::ObjectMeta, runtime::wait::await_condition}; use std::{collections::BTreeMap, time::Duration}; use tokio::time::timeout; use trusted_cluster_operator_lib::virtualmachines::*; -use super::{VmBackend, VmConfig, generate_ignition, ssh_exec}; +use super::{NodeBackend, VmBackend, VmConfig, generate_ignition, sh_exec}; use crate::ensure_command; pub struct KubevirtBackend(pub VmConfig); +#[async_trait::async_trait] +impl NodeBackend for KubevirtBackend { + async fn ssh_exec(&self, command: &str) -> Result { + let full_cmd = format!( + "virtctl ssh -i {} core@vmi/{}/{} -t '-o IdentitiesOnly=yes' -t '-o StrictHostKeyChecking=no' --known-hosts /dev/null -c '{command}'", + self.0.ssh_private_key.display(), + self.0.vm_name, + self.0.namespace, + ); + + sh_exec(&full_cmd).await + } +} + #[async_trait::async_trait] impl VmBackend for KubevirtBackend { async fn create_vm(&self) -> Result<()> { @@ -155,39 +169,6 @@ impl VmBackend for KubevirtBackend { Ok(()) } - async fn ssh_exec(&self, command: &str) -> Result { - let full_cmd = format!( - "virtctl ssh -i {} core@vmi/{}/{} -t '-o IdentitiesOnly=yes' -t '-o StrictHostKeyChecking=no' --known-hosts /dev/null -c '{command}'", - self.0.ssh_private_key.display(), - self.0.vm_name, - self.0.namespace, - ); - - ssh_exec(&full_cmd).await - } - - async fn get_root_key(&self) -> Result>> { - // Extract the UUID from the Clevis token in the LUKS header - let uuid_cmd = "sudo cryptsetup token export --token-id 0 /dev/vda4 | jq -r \".jwe.protected\" | base64 -d | jq -r \".clevis.path\" | cut -d/ -f2"; - let uuid_output = self - .ssh_exec(uuid_cmd) - .await - .context("Failed to extract UUID from VM")?; - let uuid = uuid_output.trim(); - - if uuid.is_empty() { - return Err(anyhow!("Retrieved empty UUID from VM")); - } - - // Use the UUID to get the secret (secrets are named with just the UUID) - let secrets: Api = Api::namespaced(self.0.client.clone(), &self.0.namespace); - let secret = secrets - .get(uuid) - .await - .context(format!("Failed to get secret for UUID {uuid}"))?; - Ok(Some(secret.data.unwrap().get("root").unwrap().0.clone())) - } - async fn cleanup(&self) -> Result<()> { self.0.cleanup(); Ok(()) diff --git a/test_utils/src/virt/mod.rs b/test_utils/src/virt/mod.rs index 77f849f6..151e6e10 100644 --- a/test_utils/src/virt/mod.rs +++ b/test_utils/src/virt/mod.rs @@ -6,11 +6,10 @@ pub mod azure; pub mod kubevirt; -use anyhow::{Result, anyhow}; +use anyhow::{Context, Result, anyhow}; use clevis_pin_trustee_lib::Key as ClevisKey; use k8s_openapi::api::core::v1::Secret; use kube::{Api, Client}; -use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode}; use std::{env, path::PathBuf, time::Duration}; use tokio::process::Command; @@ -18,7 +17,7 @@ use endpoints::*; use trusted_cluster_operator_lib::*; use super::Poller; -use crate::{ROOT_SECRET, VirtProvider, get_cluster_url, get_env, get_virt_provider}; +use crate::{VirtProvider, get_cluster_url, get_encoded_root_pem, get_env, get_virt_provider}; #[derive(Clone)] pub struct VmConfig { @@ -28,7 +27,7 @@ pub struct VmConfig { pub ssh_public_key: String, pub ssh_private_key: PathBuf, pub image: String, - pub ca_pem: String, + pub encoded_ca_pem: String, } impl VmConfig { @@ -80,7 +79,6 @@ pub async fn generate_ignition(config: &VmConfig) -> Result { let ns = &config.namespace; let port = Some(REGISTER_SERVER_PORT); let register_server_url = get_cluster_url(&client, ns, REGISTER_SERVER_SERVICE, port).await?; - let root_pem_encoded = utf8_percent_encode(&config.ca_pem, NON_ALPHANUMERIC); let ignition = Ignition { version: "3.6.0".to_string(), config: Some(IgnitionConfig { @@ -95,7 +93,7 @@ pub async fn generate_ignition(config: &VmConfig) -> Result { security: Some(Security { tls: Some(SecurityTls { certificate_authorities: Some(vec![Resource { - source: Some(format!("data:,{root_pem_encoded}")), + source: Some(config.encoded_ca_pem.clone()), ..Default::default() }]), }), @@ -137,7 +135,7 @@ pub async fn generate_ignition(config: &VmConfig) -> Result { Ok(ignition_json) } -pub async fn ssh_exec(command: &str) -> Result { +pub async fn sh_exec(command: &str) -> Result { let output = Command::new("sh").arg("-c").arg(command).output().await?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); @@ -152,12 +150,7 @@ pub async fn create_backend( namespace: &str, vm_name: &str, ) -> Result> { - let secrets: Api = Api::namespaced(client.clone(), namespace); - let root_secret = secrets.get(ROOT_SECRET).await?; - let root_secret_data = root_secret.data.unwrap(); - let ca_pem_bytes = root_secret_data.get("ca.crt").unwrap(); - let ca_pem = String::from_utf8(ca_pem_bytes.0.clone())?; - + let encoded_ca_pem = get_encoded_root_pem(client.clone(), namespace).await?; let provider = get_virt_provider()?; let (public_key, key_path) = generate_ssh_key_pair()?; let image = get_env("TEST_IMAGE")?; @@ -168,7 +161,7 @@ pub async fn create_backend( ssh_public_key: public_key, ssh_private_key: key_path, image, - ca_pem, + encoded_ca_pem, }; match provider { VirtProvider::Kubevirt => Ok(Box::new(kubevirt::KubevirtBackend(config))), @@ -178,40 +171,25 @@ pub async fn create_backend( #[async_trait::async_trait] #[auto_impl::auto_impl(Box)] -pub trait VmBackend: Send + Sync { - async fn create_vm(&self) -> Result<()>; - async fn wait_for_running(&self, timeout_secs: u64) -> Result<()>; +pub trait NodeBackend: Send + Sync { async fn ssh_exec(&self, command: &str) -> Result; - async fn get_root_key(&self) -> Result>>; - async fn cleanup(&self) -> Result<()>; - async fn get_boot_id(&self) -> Result { - let id = self.ssh_exec("cat /proc/sys/kernel/random/boot_id").await?; - Ok(id.trim().to_string()) - } + async fn get_root_key(&self, client: Client, namespace: &str) -> Result>> { + // Extract the UUID from the Clevis token in the LUKS header + let uuid_cmd = "sudo cryptsetup token export --token-id 0 /dev/vda4 | jq -r \".jwe.protected\" | base64 -d | jq -r \".clevis.path\" | cut -d/ -f2"; + let ctx = "Failed to extract UUID from VM"; + let uuid_output = self.ssh_exec(uuid_cmd).await.context(ctx)?; + let uuid = uuid_output.trim(); - async fn wait_for_vm_ssh_ready( - &self, - timeout_secs: u64, - prev_boot_id: Option<&str>, - ) -> Result<()> { - let err = format!("SSH access to VM did not become available after {timeout_secs} seconds"); - let poller = Poller::new() - .with_timeout(Duration::from_secs(timeout_secs)) - .with_interval(Duration::from_secs(10)) - .with_error_message(err); + if uuid.is_empty() { + return Err(anyhow!("Retrieved empty UUID from VM")); + } - let check_fn = || async move { - let cat = self.ssh_exec("cat /proc/sys/kernel/random/boot_id").await; - let boot_id = cat.map_err(|e| anyhow!("SSH not available yet: {e}"))?; - if let Some(prev) = prev_boot_id - && boot_id.trim() == prev - { - return Err(anyhow!("VM has not rebooted yet (boot ID unchanged)")); - } - Ok(()) - }; - poller.poll_async(check_fn).await + // Use the UUID to get the secret (secrets are named with just the UUID) + let secrets: Api = Api::namespaced(client, namespace); + let ctx = format!("Failed to get secret for UUID {uuid}"); + let secret = secrets.get(uuid).await.context(ctx)?; + Ok(Some(secret.data.unwrap().get("root").unwrap().0.clone())) } async fn verify_encrypted_root(&self, encryption_key: Option<&[u8]>) -> Result { @@ -248,3 +226,40 @@ pub trait VmBackend: Send + Sync { Ok(false) } } + +#[async_trait::async_trait] +#[auto_impl::auto_impl(Box)] +pub trait VmBackend: Send + Sync + NodeBackend { + async fn create_vm(&self) -> Result<()>; + async fn wait_for_running(&self, timeout_secs: u64) -> Result<()>; + async fn cleanup(&self) -> Result<()>; + + async fn get_boot_id(&self) -> Result { + let id = self.ssh_exec("cat /proc/sys/kernel/random/boot_id").await?; + Ok(id.trim().to_string()) + } + + async fn wait_for_vm_ssh_ready( + &self, + timeout_secs: u64, + prev_boot_id: Option<&str>, + ) -> Result<()> { + let err = format!("SSH access to VM did not become available after {timeout_secs} seconds"); + let poller = Poller::new() + .with_timeout(Duration::from_secs(timeout_secs)) + .with_interval(Duration::from_secs(10)) + .with_error_message(err); + + let check_fn = || async move { + let cat = self.ssh_exec("cat /proc/sys/kernel/random/boot_id").await; + let boot_id = cat.map_err(|e| anyhow!("SSH not available yet: {e}"))?; + if let Some(prev) = prev_boot_id + && boot_id.trim() == prev + { + return Err(anyhow!("VM has not rebooted yet (boot ID unchanged)")); + } + Ok(()) + }; + poller.poll_async(check_fn).await + } +} diff --git a/tests/attestation.rs b/tests/attestation.rs index a1e1ef29..6b04b6c1 100644 --- a/tests/attestation.rs +++ b/tests/attestation.rs @@ -52,7 +52,7 @@ impl SingleAttestationContext { backend.wait_for_vm_ssh_ready(scaled_timeout(600), None).await?; test_ctx.info("SSH access is ready"); - let root_key = backend.get_root_key().await?; + let root_key = backend.get_root_key(client.clone(), namespace).await?; if root_key.is_none() { test_ctx.warn(ENCRYPTED_ROOT_WARN); } @@ -121,8 +121,8 @@ async fn test_parallel_vm_attestation() -> anyhow::Result<()> { test_ctx.info("SSH access ready on both VMs"); // Verify attestation on both VMs in parallel - let root_key1 = backend1.get_root_key().await?; - let root_key2 = backend2.get_root_key().await?; + let root_key1 = backend1.get_root_key(client.clone(), namespace).await?; + let root_key2 = backend2.get_root_key(client.clone(), namespace).await?; if root_key1.is_none() || root_key2.is_none() { test_ctx.warn(ENCRYPTED_ROOT_WARN); }