feat(cert-manager): add cluster issuer to okd cluster score #157

Merged
wjro merged 4 commits from feat/okd_set_ingress_certs into master 2025-10-21 15:55:56 +00:00
2 changed files with 210 additions and 0 deletions

View File

@ -0,0 +1,209 @@
use std::sync::Arc;
use async_trait::async_trait;
use harmony_types::id::Id;
use kube::{CustomResource, api::ObjectMeta};
use schemars::JsonSchema;
use serde::{Deserialize, 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 ClusterIssuerScore {
email: String,
server: String,
issuer_name: String,
namespace: String,
}
impl<T: Topology + K8sclient> Score<T> for ClusterIssuerScore {
fn name(&self) -> String {
"ClusterIssuerScore".to_string()
}
#[doc(hidden)]
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(ClusterIssuerInterpret {
score: self.clone(),
})
}
}
#[derive(Debug, Clone)]
pub struct ClusterIssuerInterpret {
score: ClusterIssuerScore,
}
#[async_trait]
impl<T: Topology + K8sclient> Interpret<T> for ClusterIssuerInterpret {
async fn execute(
&self,
_inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
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<Id> {
todo!()
}
}
impl ClusterIssuerInterpret {
async fn validate_cert_manager(
&self,
client: &Arc<K8sClient>,
wjro marked this conversation as resolved
Review

is it a typo? cet-manager instead of cert-manager?

is it a typo? `cet-manager` instead of `cert-manager`?
) -> Result<Outcome, InterpretError> {
let cert_manager = "cert-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<ClusterIssuer, InterpretError> {
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 = 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(),
},
wjro marked this conversation as resolved Outdated

Considering we try to do as little yaml as possible, maybe it would be better to introduce a Resource for this?

Something like:

#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)]
#[kube(
    group = "cert-manager.io",
    version = "v1",
    kind = "ClusterIssuer",
    namespaced = false // ClusterIssuer is a cluster-scoped resource
)]
#[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<SolverSpec>,
}

#[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<Http01Solver>,
    // 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,
}

And it would be used like:

    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(),
                            },
                        }),
                    }
                ],
            },
        },
    }
Considering we try to do as little yaml as possible, maybe it would be better to introduce a Resource for this? Something like: ```rs #[derive(CustomResource, Deserialize, Serialize, Clone, Debug, JsonSchema)] #[kube( group = "cert-manager.io", version = "v1", kind = "ClusterIssuer", namespaced = false // ClusterIssuer is a cluster-scoped resource )] #[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<SolverSpec>, } #[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<Http01Solver>, // 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, } ``` And it would be used like: ```rs 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)
}
pub async fn apply_cluster_issuer(
&self,
client: Arc<K8sClient>,
) -> Result<Outcome, InterpretError> {
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
)))
}
}
#[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<SolverSpec>,
}
#[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<Http01Solver>,
// 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,
}

View File

@ -1,2 +1,3 @@
pub mod cluster_issuer;
mod helm;
pub use helm::*;