Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ jobs:
github.event.pull_request.author_association == 'COLLABORATOR' ||
contains(github.event.pull_request.labels.*.name, 'ok-to-test')
runs-on: "ubuntu-24.04"
timeout-minutes: 120
steps:
- name: "Check out repository"
uses: actions/checkout@v7
Expand Down Expand Up @@ -77,7 +76,8 @@ jobs:
- name: "Run integration tests"
run: |
eval $(ssh-agent -s)
make integration-tests
while make integration-tests; do :; done
exit 1
- name: "Gather must-gather"
if: always()
run: must-gather/gather
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ COMPUTE_PCRS_IMAGE=$(REGISTRY)/compute-pcrs:$(TAG)
REG_SERVER_IMAGE=$(REGISTRY)/registration-server:$(TAG)
ATTESTATION_KEY_REGISTER_IMAGE=$(REGISTRY)/attestation-key-register:$(TAG)
TRUSTEE_IMAGE ?= quay.io/trusted-execution-clusters/key-broker-service:v0.17.0
TEST_IMAGE ?= quay.io/trusted-execution-clusters/fedora-coreos-kubevirt:42.20260622
TEST_IMAGE ?= quay.io/trusted-execution-clusters/fedora-coreos-kubevirt:42.20251012.2.0-console-fwd

# tagged as 42.20251012.2.0
APPROVED_IMAGE ?= quay.io/trusted-execution-clusters/fedora-coreos@sha256:6997f51fd27d1be1b5fc2e6cc3ebf16c17eb94d819b5d44ea8d6cf5f826ee773

Expand Down
7 changes: 6 additions & 1 deletion compute-pcrs/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,12 @@ async fn main() -> Result<()> {
pcrs,
};
image_pcrs.0.insert(args.resource_name.clone(), image_pcr);
update_image_pcrs!(config_maps, image_pcrs_map, image_pcrs);
let image_pcrs_json = serde_json::to_string(&image_pcrs)?;
let data = std::collections::BTreeMap::from([(PCR_CONFIG_FILE.to_string(), image_pcrs_json)]);
image_pcrs_map.data = Some(data);
config_maps
.replace(PCR_CONFIG_MAP, &Default::default(), &image_pcrs_map)
.await?;

let approved_images: Api<ApprovedImage> = Api::default_namespaced(client);
let image = approved_images.get(&args.resource_name).await?;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ pub async fn get_opt_trusted_execution_cluster(
Ok(list.items.into_iter().next())
}

/// Get the single TrustedExecutionCluster in the namespace
/// Get the single TrustedExecutionCluster in the namespace (uncached)
pub async fn get_trusted_execution_cluster(client: Client) -> Result<TrustedExecutionCluster> {
let namespace = client.default_namespace().to_string();
let cluster = get_opt_trusted_execution_cluster(client).await;
Expand Down
12 changes: 0 additions & 12 deletions lib/src/reference_values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,3 @@ pub struct ImagePcr {

#[derive(Default, Deserialize, Serialize)]
pub struct ImagePcrs(pub BTreeMap<String, ImagePcr>);

#[macro_export]
macro_rules! update_image_pcrs {
($api:ident, $map:ident, $pcrs:ident) => {
let image_pcrs_json = serde_json::to_string(&$pcrs)?;
let map = (PCR_CONFIG_FILE.to_string(), image_pcrs_json.to_string());
let data = std::collections::BTreeMap::from([map]);
$map.data = Some(data);
$api.replace(PCR_CONFIG_MAP, &Default::default(), &$map)
.await?
};
}
80 changes: 15 additions & 65 deletions operator/src/attestation_key_register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,71 +13,23 @@ use k8s_openapi::apimachinery::pkg::{
apis::meta::v1::{LabelSelector, ObjectMeta, OwnerReference},
util::intstr::IntOrString,
};
use kube::{
Api, Client, Resource,
api::{Patch, PatchParams},
runtime::{
Controller,
controller::Action,
finalizer,
finalizer::Event,
reflector::{self, ObjectRef, Store},
watcher,
},
};
use kube::api::{Patch, PatchParams};
use kube::runtime::{Controller, controller::Action, reflector::ObjectRef, watcher};
use kube::runtime::{finalizer, finalizer::Event};
use kube::{Api, Client, Resource};
use log::{info, warn};
use serde_json::json;
use std::{collections::BTreeMap, sync::Arc, time::Duration};
use std::{collections::BTreeMap, sync::Arc};

use trusted_cluster_operator_lib::conditions::ATTESTATION_KEY_MACHINE_APPROVE;
use trusted_cluster_operator_lib::endpoints::*;
use trusted_cluster_operator_lib::{AttestationKey, AttestationKeyStatus, Machine, update_status};

use crate::conditions::attestation_key_approved_condition;
use crate::trustee;
use operator::{ControllerError, LONG_REQUEUE, TLS_DIR, controller_error_policy};
use operator::{ControllerError, LONG_REQUEUE, OperatorContext, TLS_DIR, controller_error_policy};
use operator::{create_or_info_if_exists, read_certificate, upsert_condition};

/// Shared context for the three attestation-key controllers.
/// Stores give local cache access to avoid repeated API-server reads.
pub struct AkContextData {
pub client: Client,
pub machine_store: Store<Machine>,
pub ak_store: Store<AttestationKey>,
pub secret_store: Store<Secret>,
pub deployment_store: Store<Deployment>,
}

impl AkContextData {
pub fn new(client: Client) -> Self {
let (machine_store, machine_writer) = reflector::store::<Machine>();
let (ak_store, ak_writer) = reflector::store::<AttestationKey>();
let (secret_store, secret_writer) = reflector::store::<Secret>();
let (deployment_store, deployment_writer) = reflector::store::<Deployment>();

crate::spawn_reflector::<Machine>(machine_writer, client.clone(), "Machine");
crate::spawn_reflector::<AttestationKey>(ak_writer, client.clone(), "AttestationKey");
crate::spawn_reflector::<Secret>(secret_writer, client.clone(), "Secret");
crate::spawn_reflector::<Deployment>(deployment_writer, client.clone(), "Deployment");

Self {
client,
machine_store,
ak_store,
secret_store,
deployment_store,
}
}

pub async fn sync_caches(&self, timeout: Duration) -> Result<()> {
crate::sync_cache(&self.machine_store, "Machine", timeout).await?;
crate::sync_cache(&self.ak_store, "AttestationKey", timeout).await?;
crate::sync_cache(&self.secret_store, "Secret", timeout).await?;
crate::sync_cache(&self.deployment_store, "Deployment", timeout).await?;
Ok(())
}
}

const INTERNAL_ATTESTATION_KEY_REGISTER_PORT: i32 = 8001;
const ATTESTATION_KEY_SECRET_FINALIZER: &str =
"trusted-execution-clusters.io/attestationkey-secret-finalizer";
Expand Down Expand Up @@ -185,7 +137,7 @@ pub async fn create_attestation_key_register_service(

async fn ak_reconcile(
ak: Arc<AttestationKey>,
ctx: Arc<AkContextData>,
ctx: Arc<OperatorContext>,
) -> Result<Action, ControllerError> {
let ak_name = ak.metadata.name.clone().unwrap_or_default();
info!("Attestation Key reconciliation for: {ak_name}");
Expand All @@ -201,7 +153,7 @@ async fn ak_reconcile(

async fn machine_reconcile(
machine: Arc<Machine>,
ctx: Arc<AkContextData>,
ctx: Arc<OperatorContext>,
) -> Result<Action, ControllerError> {
info!(
"Machine reconciliation for: {}",
Expand All @@ -228,7 +180,7 @@ async fn machine_reconcile(
Ok(LONG_REQUEUE)
}

async fn approve_ak(ak: &AttestationKey, machine: &Machine, ctx: &AkContextData) -> Result<()> {
async fn approve_ak(ak: &AttestationKey, machine: &Machine, ctx: &OperatorContext) -> Result<()> {
let name = ak.metadata.name.clone().unwrap_or_default();
let client = &ctx.client;
let aks: Api<AttestationKey> = Api::default_namespaced(client.clone());
Expand Down Expand Up @@ -274,10 +226,8 @@ async fn approve_ak(ak: &AttestationKey, machine: &Machine, ctx: &AkContextData)

let secret_name = name.clone();
let ns = client.default_namespace().to_string();
let secret_exists = ctx
.secret_store
.get(&ObjectRef::new(&secret_name).within(&ns))
.is_some();
let obj_ref = ObjectRef::new(&secret_name).within(&ns);
let secret_exists = ctx.secret_store.get(&obj_ref).is_some();

if !secret_exists {
let public_key_data = ByteString(ak.spec.public_key.as_bytes().to_vec());
Expand Down Expand Up @@ -305,7 +255,7 @@ async fn approve_ak(ak: &AttestationKey, machine: &Machine, ctx: &AkContextData)

async fn secret_reconcile(
secret: Arc<Secret>,
ctx: Arc<AkContextData>,
ctx: Arc<OperatorContext>,
) -> Result<Action, ControllerError> {
let secret_name = secret.metadata.name.clone().unwrap_or_default();

Expand Down Expand Up @@ -357,7 +307,7 @@ async fn secret_reconcile(
.map_err(|e| anyhow!("failed to reconcile attestation key secret: {e}").into())
}

pub async fn launch_ak_controller(ctx: Arc<AkContextData>) {
pub async fn launch_ak_controller(ctx: Arc<OperatorContext>) {
let aks: Api<AttestationKey> = Api::default_namespaced(ctx.client.clone());
tokio::spawn(
Controller::new(aks, watcher::Config::default())
Expand All @@ -371,7 +321,7 @@ pub async fn launch_ak_controller(ctx: Arc<AkContextData>) {
);
}

pub async fn launch_machine_ak_controller(ctx: Arc<AkContextData>) {
pub async fn launch_machine_ak_controller(ctx: Arc<OperatorContext>) {
let machines: Api<Machine> = Api::default_namespaced(ctx.client.clone());
tokio::spawn(
Controller::new(machines, watcher::Config::default())
Expand All @@ -385,7 +335,7 @@ pub async fn launch_machine_ak_controller(ctx: Arc<AkContextData>) {
);
}

pub async fn launch_secret_ak_controller(ctx: Arc<AkContextData>) {
pub async fn launch_secret_ak_controller(ctx: Arc<OperatorContext>) {
let secrets: Api<Secret> = Api::default_namespaced(ctx.client.clone());
tokio::spawn(
Controller::new(secrets, watcher::Config::default())
Expand Down
49 changes: 47 additions & 2 deletions operator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
//
// Use in other crates is not an intended purpose.

use anyhow::Result;
use anyhow::{Result, anyhow};
use futures_util::StreamExt;
use k8s_openapi::api::core::v1::{Secret, SecretVolumeSource, Volume, VolumeMount};
use k8s_openapi::api::apps::v1::Deployment;
use k8s_openapi::api::core::v1::{ConfigMap, Secret, SecretVolumeSource, Volume, VolumeMount};
use k8s_openapi::apimachinery::pkg::apis::meta::v1::{Condition, Time};
use k8s_openapi::jiff::Timestamp;
use kube::Resource;
Expand All @@ -24,6 +25,50 @@ use tokio::time::timeout;

// Re-export common functions from the lib
pub use trusted_cluster_operator_lib::generate_owner_reference;
use trusted_cluster_operator_lib::{
ApprovedImage, AttestationKey, Machine, TrustedExecutionCluster,
};

/// Unified context shared across all controllers.
/// Stores give local cache access to avoid repeated API-server reads.
pub struct OperatorContext {
pub client: Client,
pub tec_store: Store<TrustedExecutionCluster>,
pub cm_store: Store<ConfigMap>,
pub deployment_store: Store<Deployment>,
pub machine_store: Store<Machine>,
pub ak_store: Store<AttestationKey>,
pub secret_store: Store<Secret>,
pub image_store: Store<ApprovedImage>,
}

impl OperatorContext {
pub fn new(client: Client) -> Self {
Self {
client,
tec_store: reflector::store().0,
cm_store: reflector::store().0,
deployment_store: reflector::store().0,
machine_store: reflector::store().0,
ak_store: reflector::store().0,
secret_store: reflector::store().0,
image_store: reflector::store().0,
}
}

/// Return the single TrustedExecutionCluster from the cache, or an error if more than one exists.
pub fn get_opt_tec(&self) -> Result<Option<TrustedExecutionCluster>> {
let state = self.tec_store.state();
if state.len() > 1 {
let ns = self.client.default_namespace();
return Err(anyhow!(
"More than one TrustedExecutionCluster found in namespace {ns}. \
trusted-cluster-operator does not support more than one TrustedExecutionCluster."
));
}
Ok(state.into_iter().next().map(Arc::unwrap_or_clone))
}
}

#[derive(Debug, thiserror::Error)]
pub enum ControllerError {
Expand Down
Loading
Loading