From 99ca65bf183b162566040f0888fcba845f0acc71 Mon Sep 17 00:00:00 2001 From: Jakob Naucke Date: Tue, 7 Oct 2025 15:58:46 +0200 Subject: [PATCH 01/12] update_reference_values: open API only once Signed-off-by: Jakob Naucke --- operator/src/trustee.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/operator/src/trustee.rs b/operator/src/trustee.rs index f582b24f..36064d32 100644 --- a/operator/src/trustee.rs +++ b/operator/src/trustee.rs @@ -87,11 +87,10 @@ fn recompute_reference_values(image_pcrs: ImagePcrs) -> Vec { } pub async fn update_reference_values(ctx: RvContextData) -> Result<()> { - let operator_config_maps: Api = Api::default_namespaced(ctx.client.clone()); - let image_pcrs_map = operator_config_maps.get(PCR_CONFIG_MAP).await?; + let config_maps: Api = Api::default_namespaced(ctx.client); + let image_pcrs_map = config_maps.get(PCR_CONFIG_MAP).await?; let reference_values = recompute_reference_values(get_image_pcrs(image_pcrs_map)?); - let config_maps: Api = Api::default_namespaced(ctx.client); let existing_data = config_maps.get(TRUSTEE_DATA_MAP).await?; let err = format!("ConfigMap {TRUSTEE_DATA_MAP} existed, but had no data"); let existing_data_map = existing_data.data.context(err)?; From 0b7ecd91a751aa166f22de0c08cc3fbd462b6824 Mon Sep 17 00:00:00 2001 From: Jakob Naucke Date: Thu, 9 Oct 2025 14:24:34 +0200 Subject: [PATCH 02/12] Run mount_secret from reconciler instead as subroutine for better testability Signed-off-by: Jakob Naucke --- operator/src/register_server.rs | 4 +++- operator/src/trustee.rs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/operator/src/register_server.rs b/operator/src/register_server.rs index d52671f4..581ebc4b 100644 --- a/operator/src/register_server.rs +++ b/operator/src/register_server.rs @@ -257,8 +257,10 @@ async fn keygen_reconcile( machine: Arc, client: Arc, ) -> Result { + let client = Arc::unwrap_or_clone(client); let id = &machine.spec.id; - trustee::generate_secret(Arc::unwrap_or_clone(client), id).await?; + trustee::generate_secret(client.clone(), id).await?; + trustee::mount_secret(client.clone(), id).await?; Ok(Action::await_change()) } diff --git a/operator/src/trustee.rs b/operator/src/trustee.rs index 36064d32..433b6925 100644 --- a/operator/src/trustee.rs +++ b/operator/src/trustee.rs @@ -143,7 +143,7 @@ fn generate_secret_volume(id: &str) -> (Volume, VolumeMount) { ) } -async fn mount_secret(client: Client, id: &str) -> Result<()> { +pub async fn mount_secret(client: Client, id: &str) -> Result<()> { let deployments: Api = Api::default_namespaced(client); let mut deployment = deployments.get(DEPLOYMENT_NAME).await?; let err = format!("Deployment {DEPLOYMENT_NAME} existed, but had no spec"); @@ -181,7 +181,7 @@ pub async fn generate_secret(client: Client, id: &str) -> Result<()> { let secrets: Api = Api::default_namespaced(client.clone()); let create = secrets.create(&Default::default(), &secret).await; info_if_exists!(create, "Secret", id); - mount_secret(client, id).await + Ok(()) } pub async fn generate_attestation_policy( From 0e8b7f110870e3ac3877ed3356e40d0f5773c4d7 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Tue, 23 Sep 2025 15:34:04 +0200 Subject: [PATCH 03/12] Create MockClient for testing Signed-off-by: Alice Frosi Co-authored-by: Jakob Naucke --- Cargo.lock | 39 +++++++++++- operator/Cargo.toml | 4 ++ operator/src/main.rs | 2 + operator/src/mock_client.rs | 123 ++++++++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 operator/src/mock_client.rs diff --git a/Cargo.lock b/Cargo.lock index f0a3a871..308d45b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1067,6 +1067,16 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "byteorder", + "num-traits", +] + [[package]] name = "headers" version = "0.3.9" @@ -1735,7 +1745,7 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tokio-util", - "tower", + "tower 0.5.2", "tower-http", "tracing", ] @@ -2198,6 +2208,7 @@ dependencies = [ "env_logger", "futures-util", "hex", + "http 1.3.1", "json-patch", "jsonptr", "k8s-openapi", @@ -2211,6 +2222,7 @@ dependencies = [ "serde_json", "thiserror 2.0.17", "tokio", + "tower 0.4.13", ] [[package]] @@ -2642,7 +2654,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-util", - "tower", + "tower 0.5.2", "tower-http", "tower-service", "url", @@ -3407,6 +3419,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "hdrhistogram", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.2" @@ -3439,7 +3472,7 @@ dependencies = [ "iri-string", "mime", "pin-project-lite", - "tower", + "tower 0.5.2", "tower-layer", "tower-service", "tracing", diff --git a/operator/Cargo.toml b/operator/Cargo.toml index 42147b4e..c195c9e1 100644 --- a/operator/Cargo.toml +++ b/operator/Cargo.toml @@ -31,3 +31,7 @@ serde.workspace = true serde_json.workspace = true thiserror = "2.0.17" tokio.workspace = true + +[dev-dependencies] +http = "1.1.0" +tower = { version = "0.4.13", features = ["full"] } diff --git a/operator/src/main.rs b/operator/src/main.rs index 025ffdcd..bdf9874a 100644 --- a/operator/src/main.rs +++ b/operator/src/main.rs @@ -21,6 +21,8 @@ use kube::{ use log::{error, info, warn}; use crds::ConfidentialCluster; +#[cfg(test)] +mod mock_client; mod reference_values; mod register_server; mod trustee; diff --git a/operator/src/mock_client.rs b/operator/src/mock_client.rs new file mode 100644 index 00000000..8adb00e9 --- /dev/null +++ b/operator/src/mock_client.rs @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: Alice Frosi +// SPDX-FileCopyrightText: Jakob Naucke +// +// SPDX-License-Identifier: MIT + +use http::{Request, Response, StatusCode}; +use kube::{Client, client::Body, error::ErrorResponse}; +use serde::{Deserialize, Serialize}; +use std::convert::Infallible; +use tower::service_fn; + +macro_rules! assert_kube_api_error { + ($err:expr, $code:expr, $reason:expr, $message:expr, $status:expr) => {{ + let kube_error = $err + .downcast_ref::() + .expect(&format!("Expected kube::Error, got: {:?}", $err)); + + if let kube::Error::Api(error_response) = kube_error { + assert_eq!(error_response.code, $code); + assert_eq!(error_response.reason, $reason); + assert_eq!(error_response.message, $message); + assert_eq!(error_response.status, $status); + } else { + assert!(false, "Expected kube::Error::Api, got: {:?}", kube_error); + } + }}; +} + +pub(crate) use assert_kube_api_error; + +pub struct MockClient +where + T: Serialize + for<'de> Deserialize<'de>, +{ + response_data: T, + status_code: StatusCode, + namespace: String, +} + +impl MockClient +where + T: Serialize + for<'de> Deserialize<'de> + Clone + Send + 'static, +{ + pub fn new(status_code: StatusCode, response_data: T, namespace: String) -> Self { + Self { + response_data, + status_code, + namespace, + } + } + + pub fn into_client(self) -> Client { + let response_json = serde_json::to_string(&self.response_data).unwrap(); + let (kind, name) = serde_json::from_str::(&response_json) + .map(|json_value| { + let kind = json_value + .get("kind") + .and_then(|v| v.as_str()) + .unwrap_or("Unknown") + .to_string(); + let name = json_value + .get("metadata") + .and_then(|m| m.get("name")) + .and_then(|n| n.as_str()) + .unwrap_or("Unknown") + .to_string(); + (kind, name) + }) + .unwrap_or(("Unknown".to_string(), "Unknown".to_string())); + let plural = kind.to_lowercase() + "s"; + let namespace = self.namespace.clone(); + + let mock_svc = service_fn(move |_req: Request| { + let body = if self.status_code == StatusCode::OK { + let response_json = serde_json::to_string(&self.response_data).unwrap(); + Body::from(response_json.into_bytes()) + } else { + let code = self.status_code.as_u16(); + let error_response = match self.status_code { + StatusCode::CONFLICT => ErrorResponse { + status: "Failure".to_string(), + message: format!("{plural} \"{name}\" already exists"), + reason: "AlreadyExists".to_string(), + code, + }, + StatusCode::INTERNAL_SERVER_ERROR => ErrorResponse { + status: "Failure".to_string(), + message: "internal server error".to_string(), + reason: "ServerTimeout".to_string(), + code, + }, + StatusCode::NOT_FOUND => ErrorResponse { + status: "Failure".to_string(), + message: "resource not found".to_string(), + reason: "NotFound".to_string(), + code, + }, + StatusCode::BAD_REQUEST => ErrorResponse { + status: "Failure".to_string(), + message: "bad request".to_string(), + reason: "BadRequest".to_string(), + code, + }, + _ => ErrorResponse { + status: "Failure".to_string(), + message: format!("error with status code {}", self.status_code), + reason: "Unknown".to_string(), + code, + }, + }; + let error_json = serde_json::to_string(&error_response).unwrap(); + Body::from(error_json.into_bytes()) + }; + + let response = Response::builder() + .status(self.status_code) + .body(body) + .unwrap(); + async move { Ok::<_, Infallible>(response) } + }); + Client::new(mock_svc, namespace) + } +} From 67dbcab884d3c83cdad4a4213f739448eda9846f Mon Sep 17 00:00:00 2001 From: Dehan Meng Date: Thu, 18 Sep 2025 10:46:16 +0800 Subject: [PATCH 04/12] test: Add for trustee::generate_attestation_policy - `test_generate_att_policy_success`: Verifies the function returns `Ok(())` on a successful API response (200 OK). - `test_generate_att_policy_already_exists`: Verifies the function correctly handles a 409 Conflict and returns `Ok(())`, confirming idempotency. - `test_generate_att_policy_error`: Verifies server errors are returned correctly. Signed-off-by: Dehan Meng Co-authored-by: Alice Frosi Co-authored-by: Jakob Naucke --- operator/src/trustee.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/operator/src/trustee.rs b/operator/src/trustee.rs index 433b6925..67d26133 100644 --- a/operator/src/trustee.rs +++ b/operator/src/trustee.rs @@ -1,5 +1,6 @@ // SPDX-FileCopyrightText: Alice Frosi // SPDX-FileCopyrightText: Jakob Naucke +// SPDX-FileCopyrightText: Dehan Meng // // SPDX-License-Identifier: MIT @@ -390,3 +391,42 @@ pub async fn generate_kbs_deployment( Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::mock_client::*; + use http::StatusCode; + + #[tokio::test] + async fn test_generate_att_policy_success() { + let ns = "test".to_string(); + let client = MockClient::new(StatusCode::OK, ConfigMap::default(), ns).into_client(); + let result = generate_attestation_policy(client, Default::default()).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_generate_att_policy_already_exists() { + let ns = "test".to_string(); + let client = MockClient::new(StatusCode::CONFLICT, ConfigMap::default(), ns).into_client(); + let result = generate_attestation_policy(client, Default::default()).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_generate_att_policy_error() { + let ns = "test".to_string(); + let config = ConfigMap::default(); + let client = MockClient::new(StatusCode::INTERNAL_SERVER_ERROR, config, ns).into_client(); + let result = generate_attestation_policy(client, Default::default()).await; + let err = result.unwrap_err(); + assert_kube_api_error!( + err, + 500, + "ServerTimeout", + "internal server error", + "Failure" + ); + } +} From 66dd3a83dac47fce9ef802623cb87c29bef27cfc Mon Sep 17 00:00:00 2001 From: Dehan Meng Date: Thu, 18 Sep 2025 10:46:16 +0800 Subject: [PATCH 05/12] test: Add unit tests for trustee::get_image_pcrs - `test_get_image_pcrs_success`: Verifies that a valid JSON string in a ConfigMap is correctly deserialized into the `ImagePcrs` struct. - `test_get_image_pcrs_no_data`: Ensures an error is returned when the ConfigMap's `data` field is missing. - `test_get_image_pcrs_no_file`: Ensures an error is returned when the ConfigMap's `data` field is empty. - `test_get_image_pcrs_invalid_json`: Confirms that an error is propagated when the data contains an invalid JSON string. Signed-off-by: Dehan Meng Co-authored-by: Jakob Naucke --- operator/src/trustee.rs | 61 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/operator/src/trustee.rs b/operator/src/trustee.rs index 67d26133..9d3da269 100644 --- a/operator/src/trustee.rs +++ b/operator/src/trustee.rs @@ -396,8 +396,69 @@ pub async fn generate_kbs_deployment( mod tests { use super::*; use crate::mock_client::*; + use compute_pcrs_lib::Pcr; use http::StatusCode; + #[test] + fn test_get_image_pcrs_success() { + let pcrs = BTreeMap::from([( + "cos".to_string(), + ImagePcr { + first_seen: Utc::now(), + pcrs: vec![ + Pcr { + id: 0, + value: "pcr0_val".to_string(), + parts: vec![], + }, + Pcr { + id: 1, + value: "pcr1_val".to_string(), + parts: vec![], + }, + ], + }, + )]); + let data = BTreeMap::from([( + PCR_CONFIG_FILE.to_string(), + serde_json::to_string(&pcrs).unwrap(), + )]); + let config_map = ConfigMap { + data: Some(data), + ..Default::default() + }; + let image_pcrs = get_image_pcrs(config_map).unwrap(); + assert_eq!(image_pcrs.0["cos"].pcrs.len(), 2); + assert_eq!(image_pcrs.0["cos"].pcrs[0].value, "pcr0_val"); + } + + #[test] + fn test_get_image_pcrs_no_data() { + let config_map = ConfigMap::default(); + let err = get_image_pcrs(config_map).err().unwrap(); + assert!(err.to_string().contains("but had no data")); + } + + #[test] + fn test_get_image_pcrs_no_file() { + let config_map = ConfigMap { + data: Some(BTreeMap::new()), + ..Default::default() + }; + let err = get_image_pcrs(config_map).err().unwrap(); + assert!(err.to_string().contains("but had no file")); + } + + #[test] + fn test_get_image_pcrs_invalid_json() { + let data = BTreeMap::from([(PCR_CONFIG_FILE.to_string(), "not json".to_string())]); + let config_map = ConfigMap { + data: Some(data), + ..Default::default() + }; + assert!(get_image_pcrs(config_map).is_err()); + } + #[tokio::test] async fn test_generate_att_policy_success() { let ns = "test".to_string(); From e00bcc567a3f764b1b7a1e85fdeb83d34018be99 Mon Sep 17 00:00:00 2001 From: Dehan Meng Date: Thu, 18 Sep 2025 10:46:16 +0800 Subject: [PATCH 06/12] test: test_generate_luks_key_returns_correct_size A sanity check to validate that `generate_luks_key` runs without errors and returns a key of the expected 32-byte length. Signed-off-by: Dehan Meng Co-authored-by: Jakob Naucke --- operator/src/trustee.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/operator/src/trustee.rs b/operator/src/trustee.rs index 9d3da269..bce453d4 100644 --- a/operator/src/trustee.rs +++ b/operator/src/trustee.rs @@ -459,6 +459,12 @@ mod tests { assert!(get_image_pcrs(config_map).is_err()); } + #[test] + fn test_generate_luks_key_returns_correct_size() { + let jwk: ClevisKey = serde_json::from_slice(&generate_luks_key().unwrap()).unwrap(); + assert_eq!(jwk.key.len(), 32); + } + #[tokio::test] async fn test_generate_att_policy_success() { let ns = "test".to_string(); From 90916e19727fce34292ef0f3dda0c644085015b8 Mon Sep 17 00:00:00 2001 From: Jakob Naucke Date: Tue, 7 Oct 2025 16:01:44 +0200 Subject: [PATCH 07/12] test: Response closure in MockClient Instrument MockClient to respond with a closure on a result of request body and status code. Signed-off-by: Jakob Naucke --- operator/src/mock_client.rs | 41 +++++++++++++++++++------------------ operator/src/trustee.rs | 34 ++++++++++++++++-------------- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/operator/src/mock_client.rs b/operator/src/mock_client.rs index 8adb00e9..f1bab2de 100644 --- a/operator/src/mock_client.rs +++ b/operator/src/mock_client.rs @@ -28,29 +28,30 @@ macro_rules! assert_kube_api_error { pub(crate) use assert_kube_api_error; -pub struct MockClient +pub struct MockClient where - T: Serialize + for<'de> Deserialize<'de>, + F: Fn(&Option>) -> Result + Send + 'static, + T: Default + Serialize + for<'de> Deserialize<'de>, { - response_data: T, - status_code: StatusCode, + response_closure: F, namespace: String, } -impl MockClient +impl MockClient where - T: Serialize + for<'de> Deserialize<'de> + Clone + Send + 'static, + F: Fn(&Option>) -> Result + Send + 'static, + T: Clone + Default + Send + Serialize + for<'de> Deserialize<'de> + 'static, { - pub fn new(status_code: StatusCode, response_data: T, namespace: String) -> Self { + pub fn new(response_closure: F, namespace: String) -> Self { Self { - response_data, - status_code, + response_closure, namespace, } } pub fn into_client(self) -> Client { - let response_json = serde_json::to_string(&self.response_data).unwrap(); + let response_data = (self.response_closure)(&None).unwrap_or_default(); + let response_json = serde_json::to_string(&response_data).unwrap(); let (kind, name) = serde_json::from_str::(&response_json) .map(|json_value| { let kind = json_value @@ -70,13 +71,16 @@ where let plural = kind.to_lowercase() + "s"; let namespace = self.namespace.clone(); - let mock_svc = service_fn(move |_req: Request| { - let body = if self.status_code == StatusCode::OK { - let response_json = serde_json::to_string(&self.response_data).unwrap(); + let mock_svc = service_fn(move |req: Request| { + let mut status_code = StatusCode::OK; + let response = (self.response_closure)(&Some(req)); + let body = if let Ok(response_data) = response { + let response_json = serde_json::to_string(&response_data).unwrap(); Body::from(response_json.into_bytes()) } else { - let code = self.status_code.as_u16(); - let error_response = match self.status_code { + status_code = response.err().unwrap(); + let code = status_code.as_u16(); + let error_response = match status_code { StatusCode::CONFLICT => ErrorResponse { status: "Failure".to_string(), message: format!("{plural} \"{name}\" already exists"), @@ -103,7 +107,7 @@ where }, _ => ErrorResponse { status: "Failure".to_string(), - message: format!("error with status code {}", self.status_code), + message: format!("error with status code {status_code}"), reason: "Unknown".to_string(), code, }, @@ -112,10 +116,7 @@ where Body::from(error_json.into_bytes()) }; - let response = Response::builder() - .status(self.status_code) - .body(body) - .unwrap(); + let response = Response::builder().status(status_code).body(body).unwrap(); async move { Ok::<_, Infallible>(response) } }); Client::new(mock_svc, namespace) diff --git a/operator/src/trustee.rs b/operator/src/trustee.rs index bce453d4..adabe826 100644 --- a/operator/src/trustee.rs +++ b/operator/src/trustee.rs @@ -397,7 +397,7 @@ mod tests { use super::*; use crate::mock_client::*; use compute_pcrs_lib::Pcr; - use http::StatusCode; + use http::{Method, Request, StatusCode}; #[test] fn test_get_image_pcrs_success() { @@ -467,33 +467,37 @@ mod tests { #[tokio::test] async fn test_generate_att_policy_success() { - let ns = "test".to_string(); - let client = MockClient::new(StatusCode::OK, ConfigMap::default(), ns).into_client(); + let clos = |_: &_| Ok(ConfigMap::default()); + let client = MockClient::new(clos, "test".to_string()).into_client(); let result = generate_attestation_policy(client, Default::default()).await; assert!(result.is_ok()); } #[tokio::test] async fn test_generate_att_policy_already_exists() { - let ns = "test".to_string(); - let client = MockClient::new(StatusCode::CONFLICT, ConfigMap::default(), ns).into_client(); + let clos = |req: &Option>| match req { + Some(r) if r.method() == Method::POST => Err::(StatusCode::CONFLICT), + None => Ok(ConfigMap::default()), + _ => panic!("unexpected API interaction: {req:?}"), + }; + let client = MockClient::new(clos, "test".to_string()).into_client(); let result = generate_attestation_policy(client, Default::default()).await; assert!(result.is_ok()); } #[tokio::test] async fn test_generate_att_policy_error() { - let ns = "test".to_string(); - let config = ConfigMap::default(); - let client = MockClient::new(StatusCode::INTERNAL_SERVER_ERROR, config, ns).into_client(); + let clos = |req: &Option>| match req { + Some(r) if r.method() == Method::POST => { + Err::(StatusCode::INTERNAL_SERVER_ERROR) + } + None => Ok(ConfigMap::default()), + _ => panic!("unexpected API interaction: {req:?}"), + }; + let client = MockClient::new(clos, "test".to_string()).into_client(); let result = generate_attestation_policy(client, Default::default()).await; let err = result.unwrap_err(); - assert_kube_api_error!( - err, - 500, - "ServerTimeout", - "internal server error", - "Failure" - ); + let msg = "internal server error"; + assert_kube_api_error!(err, 500, "ServerTimeout", msg, "Failure"); } } From 219fbfd6377a5b7a6fbd4581e658190852def9df Mon Sep 17 00:00:00 2001 From: Jakob Naucke Date: Thu, 9 Oct 2025 11:41:24 +0200 Subject: [PATCH 08/12] test: Add unit tests for update_reference_values Success, missing PCR ConfigMap, missing Trustee ConfigMap, missing Trustee data, missing RV file Signed-off-by: Jakob Naucke --- operator/src/trustee.rs | 97 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 7 deletions(-) diff --git a/operator/src/trustee.rs b/operator/src/trustee.rs index adabe826..766ccf9a 100644 --- a/operator/src/trustee.rs +++ b/operator/src/trustee.rs @@ -399,9 +399,8 @@ mod tests { use compute_pcrs_lib::Pcr; use http::{Method, Request, StatusCode}; - #[test] - fn test_get_image_pcrs_success() { - let pcrs = BTreeMap::from([( + fn dummy_pcrs() -> ImagePcrs { + ImagePcrs(BTreeMap::from([( "cos".to_string(), ImagePcr { first_seen: Utc::now(), @@ -418,15 +417,23 @@ mod tests { }, ], }, - )]); + )])) + } + + fn dummy_pcrs_map() -> ConfigMap { let data = BTreeMap::from([( PCR_CONFIG_FILE.to_string(), - serde_json::to_string(&pcrs).unwrap(), + serde_json::to_string(&dummy_pcrs()).unwrap(), )]); - let config_map = ConfigMap { + ConfigMap { data: Some(data), ..Default::default() - }; + } + } + + #[test] + fn test_get_image_pcrs_success() { + let config_map = dummy_pcrs_map(); let image_pcrs = get_image_pcrs(config_map).unwrap(); assert_eq!(image_pcrs.0["cos"].pcrs.len(), 2); assert_eq!(image_pcrs.0["cos"].pcrs[0].value, "pcr0_val"); @@ -459,6 +466,82 @@ mod tests { assert!(get_image_pcrs(config_map).is_err()); } + fn generate_rv_ctx(client: Client) -> RvContextData { + RvContextData { + client, + owner_reference: Default::default(), + pcrs_compute_image: String::new(), + } + } + + #[tokio::test] + async fn test_update_rvs_success() { + let clos = |req: &Option>| match req { + Some(r) if r.uri().path().contains(PCR_CONFIG_MAP) => Ok(dummy_pcrs_map()), + _ => Ok(ConfigMap { + data: Some(BTreeMap::from([( + REFERENCE_VALUES_FILE.to_string(), + "[]".to_string(), + )])), + ..Default::default() + }), + }; + let ctx = generate_rv_ctx(MockClient::new(clos, "test".to_string()).into_client()); + assert!(update_reference_values(ctx).await.is_ok()); + } + + #[tokio::test] + async fn test_update_rvs_no_pcr_map() { + let clos = |req: &Option>| match req { + Some(r) if r.uri().path().contains(PCR_CONFIG_MAP) && r.method() == Method::GET => { + Err::(StatusCode::NOT_FOUND) + } + None => Ok(ConfigMap::default()), + _ => panic!("unexpected API interaction: {req:?}"), + }; + let ctx = generate_rv_ctx(MockClient::new(clos, "test".to_string()).into_client()); + assert!(update_reference_values(ctx).await.is_err()) + } + + #[tokio::test] + async fn test_update_rvs_no_trustee_map() { + let clos = |req: &Option>| match req { + Some(r) if r.uri().path().contains(PCR_CONFIG_MAP) => Ok(dummy_pcrs_map()), + Some(r) if r.uri().path().contains(TRUSTEE_DATA_MAP) && r.method() == Method::GET => { + Err::(StatusCode::NOT_FOUND) + } + None => Ok(ConfigMap::default()), + _ => panic!("unexpected API interaction: {req:?}"), + }; + let ctx = generate_rv_ctx(MockClient::new(clos, "test".to_string()).into_client()); + assert!(update_reference_values(ctx).await.is_err()) + } + + #[tokio::test] + async fn test_update_rvs_no_trustee_data() { + let clos = |req: &Option>| match req { + Some(r) if r.uri().path().contains(PCR_CONFIG_MAP) => Ok(dummy_pcrs_map()), + _ => Ok(ConfigMap::default()), + }; + let ctx = generate_rv_ctx(MockClient::new(clos, "test".to_string()).into_client()); + let err = update_reference_values(ctx).await.err().unwrap(); + assert!(err.to_string().contains("but had no data")); + } + + #[tokio::test] + async fn test_update_rvs_no_file() { + let clos = |req: &Option>| match req { + Some(r) if r.uri().path().contains(PCR_CONFIG_MAP) => Ok(dummy_pcrs_map()), + _ => Ok(ConfigMap { + data: Some(BTreeMap::new()), + ..Default::default() + }), + }; + let ctx = generate_rv_ctx(MockClient::new(clos, "test".to_string()).into_client()); + let err = update_reference_values(ctx).await.err().unwrap(); + assert!(err.to_string().contains("but had no reference values")); + } + #[test] fn test_generate_luks_key_returns_correct_size() { let jwk: ClevisKey = serde_json::from_slice(&generate_luks_key().unwrap()).unwrap(); From 891ff764f2096cc60bd674e2c155deff6b4c16a4 Mon Sep 17 00:00:00 2001 From: Jakob Naucke Date: Thu, 9 Oct 2025 13:26:09 +0200 Subject: [PATCH 09/12] test: Genericize tests on creation functions Signed-off-by: Jakob Naucke --- operator/src/trustee.rs | 67 +++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/operator/src/trustee.rs b/operator/src/trustee.rs index 766ccf9a..fe79f40d 100644 --- a/operator/src/trustee.rs +++ b/operator/src/trustee.rs @@ -398,6 +398,7 @@ mod tests { use crate::mock_client::*; use compute_pcrs_lib::Pcr; use http::{Method, Request, StatusCode}; + use serde::Deserialize; fn dummy_pcrs() -> ImagePcrs { ImagePcrs(BTreeMap::from([( @@ -548,39 +549,67 @@ mod tests { assert_eq!(jwk.key.len(), 32); } - #[tokio::test] - async fn test_generate_att_policy_success() { - let clos = |_: &_| Ok(ConfigMap::default()); + async fn test_create_success< + F: Fn(Client) -> S, + S: Future>, + T: Clone + Default + Send + Serialize + for<'de> Deserialize<'de> + 'static, + >( + create: F, + ) { + let clos = |_: &_| Ok(T::default()); let client = MockClient::new(clos, "test".to_string()).into_client(); - let result = generate_attestation_policy(client, Default::default()).await; - assert!(result.is_ok()); + assert!(create(client).await.is_ok()); } - #[tokio::test] - async fn test_generate_att_policy_already_exists() { + async fn test_create_already_exists< + F: Fn(Client) -> S, + S: Future>, + T: Clone + Default + Send + Serialize + for<'de> Deserialize<'de> + 'static, + >( + create: F, + ) { let clos = |req: &Option>| match req { - Some(r) if r.method() == Method::POST => Err::(StatusCode::CONFLICT), - None => Ok(ConfigMap::default()), + Some(r) if r.method() == Method::POST => Err::(StatusCode::CONFLICT), + None => Ok(T::default()), _ => panic!("unexpected API interaction: {req:?}"), }; let client = MockClient::new(clos, "test".to_string()).into_client(); - let result = generate_attestation_policy(client, Default::default()).await; - assert!(result.is_ok()); + assert!(create(client).await.is_ok()); } - #[tokio::test] - async fn test_generate_att_policy_error() { + async fn test_create_error< + F: Fn(Client) -> S, + S: Future>, + T: Clone + Default + Send + Serialize + for<'de> Deserialize<'de> + 'static, + >( + create: F, + ) { let clos = |req: &Option>| match req { - Some(r) if r.method() == Method::POST => { - Err::(StatusCode::INTERNAL_SERVER_ERROR) - } - None => Ok(ConfigMap::default()), + Some(r) if r.method() == Method::POST => Err::(StatusCode::INTERNAL_SERVER_ERROR), + None => Ok(T::default()), _ => panic!("unexpected API interaction: {req:?}"), }; let client = MockClient::new(clos, "test".to_string()).into_client(); - let result = generate_attestation_policy(client, Default::default()).await; - let err = result.unwrap_err(); + let err = create(client).await.unwrap_err(); let msg = "internal server error"; assert_kube_api_error!(err, 500, "ServerTimeout", msg, "Failure"); } + + #[tokio::test] + async fn test_generate_att_policy_success() { + let clos = |client| generate_attestation_policy(client, Default::default()); + test_create_success::<_, _, ConfigMap>(clos).await; + } + + #[tokio::test] + async fn test_generate_att_policy_already_exists() { + let clos = |client| generate_attestation_policy(client, Default::default()); + test_create_already_exists::<_, _, ConfigMap>(clos).await; + } + + #[tokio::test] + async fn test_generate_att_policy_error() { + let clos = |client| generate_attestation_policy(client, Default::default()); + test_create_error::<_, _, ConfigMap>(clos).await; + } } From 1343b0bf9d76adfff8911687bc62ab358926b4b9 Mon Sep 17 00:00:00 2001 From: Jakob Naucke Date: Thu, 9 Oct 2025 14:27:12 +0200 Subject: [PATCH 10/12] test: Create success/existing/error tests for generate_{secret,trustee_data,kbs_{service,deployment}} Signed-off-by: Jakob Naucke --- operator/src/trustee.rs | 72 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/operator/src/trustee.rs b/operator/src/trustee.rs index fe79f40d..87bc2280 100644 --- a/operator/src/trustee.rs +++ b/operator/src/trustee.rs @@ -612,4 +612,76 @@ mod tests { let clos = |client| generate_attestation_policy(client, Default::default()); test_create_error::<_, _, ConfigMap>(clos).await; } + + #[tokio::test] + async fn test_generate_secret_success() { + let clos = |client| generate_secret(client, "id"); + test_create_success::<_, _, Secret>(clos).await; + } + + #[tokio::test] + async fn test_generate_secret_already_exists() { + let clos = |client| generate_secret(client, "id"); + test_create_already_exists::<_, _, Secret>(clos).await; + } + + #[tokio::test] + async fn test_generate_secret_error() { + let clos = |client| generate_secret(client, "id"); + test_create_error::<_, _, Secret>(clos).await; + } + + #[tokio::test] + async fn test_generate_trustee_data_success() { + let clos = |client| generate_trustee_data(client, Default::default()); + test_create_success::<_, _, ConfigMap>(clos).await; + } + + #[tokio::test] + async fn test_generate_trustee_data_already_exists() { + let clos = |client| generate_trustee_data(client, Default::default()); + test_create_already_exists::<_, _, ConfigMap>(clos).await; + } + + #[tokio::test] + async fn test_generate_trustee_data_error() { + let clos = |client| generate_trustee_data(client, Default::default()); + test_create_error::<_, _, ConfigMap>(clos).await; + } + + #[tokio::test] + async fn test_generate_kbs_service_success() { + let clos = |client| generate_kbs_service(client, Default::default(), 80); + test_create_success::<_, _, Service>(clos).await; + } + + #[tokio::test] + async fn test_generate_kbs_service_already_exists() { + let clos = |client| generate_kbs_service(client, Default::default(), 80); + test_create_already_exists::<_, _, Service>(clos).await; + } + + #[tokio::test] + async fn test_generate_kbs_service_error() { + let clos = |client| generate_kbs_service(client, Default::default(), 80); + test_create_error::<_, _, Service>(clos).await; + } + + #[tokio::test] + async fn test_generate_kbs_depl_success() { + let clos = |client| generate_kbs_deployment(client, Default::default(), "image"); + test_create_success::<_, _, Deployment>(clos).await; + } + + #[tokio::test] + async fn test_generate_kbs_depl_already_exists() { + let clos = |client| generate_kbs_deployment(client, Default::default(), "image"); + test_create_already_exists::<_, _, Deployment>(clos).await; + } + + #[tokio::test] + async fn test_generate_kbs_depl_error() { + let clos = |client| generate_kbs_deployment(client, Default::default(), "image"); + test_create_error::<_, _, Deployment>(clos).await; + } } From 29e26b6920aacce33326d4b74bbe71c09df5aa89 Mon Sep 17 00:00:00 2001 From: Jakob Naucke Date: Thu, 9 Oct 2025 15:44:15 +0200 Subject: [PATCH 11/12] test: Add unit test for recompute_reference_values Signed-off-by: Jakob Naucke --- operator/src/trustee.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/operator/src/trustee.rs b/operator/src/trustee.rs index 87bc2280..426cc5a1 100644 --- a/operator/src/trustee.rs +++ b/operator/src/trustee.rs @@ -467,6 +467,16 @@ mod tests { assert!(get_image_pcrs(config_map).is_err()); } + #[test] + fn test_recompute_reference_values() { + let result = recompute_reference_values(dummy_pcrs()); + assert_eq!(result.len(), 3); + let rv = result.iter().find(|rv| rv.name == "tpm_pcr0").unwrap(); + let val_arr = rv.value.as_array().unwrap(); + let vals: Vec<_> = val_arr.iter().map(|v| v.as_str().unwrap()).collect(); + assert_eq!(vals, vec!["pcr0_val".to_string()]); + } + fn generate_rv_ctx(client: Client) -> RvContextData { RvContextData { client, From 293c36829c90def2d0228b45900cee9db5f5ac3e Mon Sep 17 00:00:00 2001 From: Jakob Naucke Date: Thu, 9 Oct 2025 16:19:05 +0200 Subject: [PATCH 12/12] test: Add unit tests for mount_secret Signed-off-by: Jakob Naucke --- operator/src/trustee.rs | 66 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/operator/src/trustee.rs b/operator/src/trustee.rs index 426cc5a1..2b32d115 100644 --- a/operator/src/trustee.rs +++ b/operator/src/trustee.rs @@ -559,6 +559,72 @@ mod tests { assert_eq!(jwk.key.len(), 32); } + fn dummy_deployment() -> Deployment { + Deployment { + spec: Some(DeploymentSpec { + template: PodTemplateSpec { + spec: Some(PodSpec { + containers: vec![Container::default()], + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }), + ..Default::default() + } + } + + #[tokio::test] + async fn test_mount_secret_success() { + let clos = |_: &_| Ok(dummy_deployment()); + let client = MockClient::new(clos, "test".to_string()).into_client(); + assert!(mount_secret(client, "id").await.is_ok()); + } + + #[tokio::test] + async fn test_mount_secret_no_depl() { + let clos = |req: &Option>| match req { + Some(r) if r.uri().path().contains(DEPLOYMENT_NAME) && r.method() == Method::GET => { + Err::(StatusCode::NOT_FOUND) + } + None => Ok(Deployment::default()), + _ => panic!("unexpected API interaction: {req:?}"), + }; + let client = MockClient::new(clos, "test".to_string()).into_client(); + assert!(mount_secret(client, "id").await.is_err()); + } + + #[tokio::test] + async fn test_mount_secret_no_spec() { + let mut depl = dummy_deployment(); + depl.spec = None; + let client = MockClient::new(move |_| Ok(depl.clone()), "test".to_string()).into_client(); + let err = mount_secret(client, "id").await.err().unwrap(); + assert!(err.to_string().contains("but had no spec")); + } + + #[tokio::test] + async fn test_mount_secret_no_pod_spec() { + let mut depl = dummy_deployment(); + let spec = depl.spec.as_mut().unwrap(); + spec.template.spec = None; + let client = MockClient::new(move |_| Ok(depl.clone()), "test".to_string()).into_client(); + let err = mount_secret(client, "id").await.err().unwrap(); + assert!(err.to_string().contains("but had no pod spec")); + } + + #[tokio::test] + async fn test_mount_secret_no_containers() { + let mut depl = dummy_deployment(); + let spec = depl.spec.as_mut().unwrap(); + let pod_spec = spec.template.spec.as_mut().unwrap(); + pod_spec.containers = vec![]; + let client = MockClient::new(move |_| Ok(depl.clone()), "test".to_string()).into_client(); + let err = mount_secret(client, "id").await.err().unwrap(); + assert!(err.to_string().contains("but had no containers")); + } + async fn test_create_success< F: Fn(Client) -> S, S: Future>,