From 4e63fe4ff280f511e72ac4735bb7a396130c2d9f Mon Sep 17 00:00:00 2001 From: Willem Date: Fri, 12 Sep 2025 14:20:09 -0400 Subject: [PATCH 1/2] feat(cert-manager): add cluster issuer to okd cluster score --- .../modules/cert_manager/cluster_issuer.rs | 157 ++++++++++++++++++ harmony/src/modules/cert_manager/mod.rs | 1 + 2 files changed, 158 insertions(+) create mode 100644 harmony/src/modules/cert_manager/cluster_issuer.rs diff --git a/harmony/src/modules/cert_manager/cluster_issuer.rs b/harmony/src/modules/cert_manager/cluster_issuer.rs new file mode 100644 index 0000000..2c4a199 --- /dev/null +++ b/harmony/src/modules/cert_manager/cluster_issuer.rs @@ -0,0 +1,157 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use harmony_types::id::Id; +use serde::Serialize; + +use crate::{ + data::Version, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + score::Score, + topology::{K8sclient, Topology, k8s::K8sClient}, +}; + +#[derive(Clone, Debug, Serialize)] +pub struct ClusterIssuer { + email: String, + server: String, + issuer_name: String, + namespace: String, +} + +impl Score for ClusterIssuer { + fn name(&self) -> String { + "ClusterIssuerScore".to_string() + } + + #[doc(hidden)] + fn create_interpret(&self) -> Box> { + Box::new(ClusterIssuerInterpret { + score: self.clone(), + }) + } +} + +#[derive(Debug, Clone)] +pub struct ClusterIssuerInterpret { + score: ClusterIssuer, +} + +#[async_trait] +impl Interpret for ClusterIssuerInterpret { + async fn execute( + &self, + _inventory: &Inventory, + topology: &T, + ) -> Result { + self.apply_cluster_issuer(topology.k8s_client().await.unwrap()) + .await + } + + fn get_name(&self) -> InterpretName { + InterpretName::Custom("ClusterIssuer") + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + todo!() + } +} + +impl ClusterIssuerInterpret { + async fn validate_cert_manager( + &self, + client: &Arc, + ) -> Result { + let cert_manager = "cet-manager".to_string(); + let operator_namespace = "openshift-operators".to_string(); + match client + .get_deployment(&cert_manager, Some(&operator_namespace)) + .await + { + Ok(Some(deployment)) => { + if let Some(status) = deployment.status { + let ready_count = status.ready_replicas.unwrap_or(0); + if ready_count >= 1 { + return Ok(Outcome::success(format!( + "'{}' is ready with {} replica(s).", + &cert_manager, ready_count + ))); + } else { + return Err(InterpretError::new( + "cert-manager operator not ready in cluster".to_string(), + )); + } + } else { + Err(InterpretError::new(format!( + "failed to get deployment status {} in ns {}", + &cert_manager, &operator_namespace + ))) + } + } + Ok(None) => Err(InterpretError::new(format!( + "Deployment '{}' not found in namespace '{}'.", + &cert_manager, &operator_namespace + ))), + Err(e) => Err(InterpretError::new(format!( + "Failed to query for deployment '{}': {}", + &cert_manager, e + ))), + } + } + + fn build_cluster_issuer(&self) -> Result { + let issuer_name = &self.score.issuer_name; + let email = &self.score.email; + let server = &self.score.server; + let namespace = &self.score.namespace; + let cluster_issuer = format!( + r#" +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + - apiVersion: cert-manager.io/v1 + manager: cert-manager-clusterissuers + name: {issuer_name} + namespace: {namespace} +spec: + acme: + email: {email} + privateKeySecretRef: + name: {issuer_name} + server: {server} + solvers: + - http01: + ingress: + class: nginx"#, + ); + Ok(cluster_issuer) + } + + pub async fn apply_cluster_issuer( + &self, + client: Arc, + ) -> Result { + let namespace = self.score.namespace.clone(); + self.validate_cert_manager(&client).await?; + let cluster_issuer = self.build_cluster_issuer().unwrap(); + client + .apply_yaml( + &serde_yaml::to_value(cluster_issuer).unwrap(), + Some(&namespace), + ) + .await?; + Ok(Outcome::success(format!( + "successfully deployed cluster operator: {} in namespace: {}", + self.score.issuer_name, self.score.namespace + ))) + } +} diff --git a/harmony/src/modules/cert_manager/mod.rs b/harmony/src/modules/cert_manager/mod.rs index 8fd309a..032439e 100644 --- a/harmony/src/modules/cert_manager/mod.rs +++ b/harmony/src/modules/cert_manager/mod.rs @@ -1,2 +1,3 @@ +pub mod cluster_issuer; mod helm; pub use helm::*; -- 2.39.5 From 8932bf3cf718c33d0dffb735bad8f6fe44a578dd Mon Sep 17 00:00:00 2001 From: Willem Date: Tue, 21 Oct 2025 11:44:01 -0400 Subject: [PATCH 2/2] fix: use derive custom resource for kube-rs rather than a yaml string --- .../modules/cert_manager/cluster_issuer.rs | 104 +++++++++++++----- 1 file changed, 78 insertions(+), 26 deletions(-) diff --git a/harmony/src/modules/cert_manager/cluster_issuer.rs b/harmony/src/modules/cert_manager/cluster_issuer.rs index 2c4a199..70294fe 100644 --- a/harmony/src/modules/cert_manager/cluster_issuer.rs +++ b/harmony/src/modules/cert_manager/cluster_issuer.rs @@ -2,7 +2,9 @@ use std::sync::Arc; use async_trait::async_trait; use harmony_types::id::Id; -use serde::Serialize; +use kube::{CustomResource, api::ObjectMeta}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; use crate::{ data::Version, @@ -13,14 +15,14 @@ use crate::{ }; #[derive(Clone, Debug, Serialize)] -pub struct ClusterIssuer { +pub struct ClusterIssuerScore { email: String, server: String, issuer_name: String, namespace: String, } -impl Score for ClusterIssuer { +impl Score for ClusterIssuerScore { fn name(&self) -> String { "ClusterIssuerScore".to_string() } @@ -35,7 +37,7 @@ impl Score for ClusterIssuer { #[derive(Debug, Clone)] pub struct ClusterIssuerInterpret { - score: ClusterIssuer, + score: ClusterIssuerScore, } #[async_trait] @@ -71,7 +73,7 @@ impl ClusterIssuerInterpret { &self, client: &Arc, ) -> Result { - let cert_manager = "cet-manager".to_string(); + let cert_manager = "cert-manager".to_string(); let operator_namespace = "openshift-operators".to_string(); match client .get_deployment(&cert_manager, Some(&operator_namespace)) @@ -108,31 +110,35 @@ impl ClusterIssuerInterpret { } } - fn build_cluster_issuer(&self) -> Result { + fn build_cluster_issuer(&self) -> Result { let issuer_name = &self.score.issuer_name; let email = &self.score.email; let server = &self.score.server; let namespace = &self.score.namespace; - let cluster_issuer = format!( - r#" -apiVersion: cert-manager.io/v1 -kind: ClusterIssuer -metadata: - - apiVersion: cert-manager.io/v1 - manager: cert-manager-clusterissuers - name: {issuer_name} - namespace: {namespace} -spec: - acme: - email: {email} - privateKeySecretRef: - name: {issuer_name} - server: {server} - solvers: - - http01: - ingress: - class: nginx"#, - ); + let cluster_issuer = ClusterIssuer { + metadata: ObjectMeta { + name: Some(issuer_name.to_string()), + namespace: Some(namespace.to_string()), + ..Default::default() + }, + spec: ClusterIssuerSpec { + acme: AcmeSpec { + email: email.to_string(), + private_key_secret_ref: PrivateKeySecretRef { + name: issuer_name.to_string(), + }, + server: server.to_string(), + solvers: vec![SolverSpec { + http01: Some(Http01Solver { + ingress: Http01Ingress { + class: "nginx".to_string(), + }, + }), + }], + }, + }, + }; + Ok(cluster_issuer) } @@ -155,3 +161,49 @@ spec: ))) } } + +#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[kube( + group = "cert-manager.io", + version = "v1", + kind = "ClusterIssuer", + plural = "clusterissuers" +)] +#[serde(rename_all = "camelCase")] +pub struct ClusterIssuerSpec { + pub acme: AcmeSpec, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct AcmeSpec { + pub email: String, + pub private_key_secret_ref: PrivateKeySecretRef, + pub server: String, + pub solvers: Vec, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct PrivateKeySecretRef { + pub name: String, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct SolverSpec { + pub http01: Option, + // Other solver types (e.g., dns01) would go here as Options +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct Http01Solver { + pub ingress: Http01Ingress, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct Http01Ingress { + pub class: String, +} -- 2.39.5