Compare commits

...

2 Commits

Author SHA1 Message Date
dc421fa099 wip: added scores and basic implentation to create certs and issuers
Some checks failed
Run Check Script / check (pull_request) Failing after 50s
2026-01-13 15:43:58 -05:00
2153edc68c feat(cert-manager): added crds for cert-manager 2026-01-13 14:05:10 -05:00
11 changed files with 514 additions and 7 deletions

View File

@@ -12,7 +12,13 @@ use harmony::{
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let cert_manager = CertificateManagementScore { let cert_manager = CertificateManagementScore {
config: CertificateManagementConfig {}, config: CertificateManagementConfig {
name: todo!(),
namespace: todo!(),
acme_issuer: todo!(),
ca_issuer: todo!(),
self_signed: todo!(),
},
}; };
harmony_cli::run( harmony_cli::run(

View File

@@ -18,7 +18,8 @@ use crate::{
inventory::Inventory, inventory::Inventory,
modules::{ modules::{
cert_manager::{ cert_manager::{
capability::{CertificateManagement, CertificateManagementConfig}, capability::{CertificateManagement, CertificateManagementConfig, Issuer},
crd::{score_certificate::CertificateScore, score_issuer::IssuerScore},
operator::CertManagerOperatorScore, operator::CertManagerOperatorScore,
}, },
k3d::K3DInstallationScore, k3d::K3DInstallationScore,
@@ -382,6 +383,38 @@ impl CertificateManagement for K8sAnywhereTopology {
), ),
}) })
} }
async fn ensure_ready(
&self,
config: &CertificateManagementConfig,
) -> Result<PreparationOutcome, PreparationError> {
self.certificate_issuer_ready(Issuer::Issuer, config)
.await?;
Ok(PreparationOutcome::Success {
details: "issuer ready".to_string(),
})
}
async fn create_certificate(
&self,
cert_name: String,
config: &CertificateManagementConfig,
) -> Result<PreparationOutcome, PreparationError> {
let cert = CertificateScore {
name: cert_name,
config: config.clone(),
};
cert.interpret(&Inventory::empty(), self)
.await
.map_err(|e| PreparationError { msg: e.to_string() })?;
Ok(PreparationOutcome::Success {
details: format!(
"Created cert into ns: {:#?}",
config.namespace.clone()
),
})
}
} }
impl K8sAnywhereTopology { impl K8sAnywhereTopology {
@@ -935,6 +968,29 @@ impl K8sAnywhereTopology {
), ),
}) })
} }
async fn certificate_issuer_ready(
&self,
issuer: Issuer,
config: &CertificateManagementConfig,
) -> Result<PreparationOutcome, PreparationError> {
match issuer {
Issuer::ClusterIssuer => todo!(),
Issuer::Issuer => {
let issuer_score = IssuerScore {
config: config.clone(),
};
issuer_score
.interpret(&Inventory::empty(), self)
.await
.map_err(|e| PreparationError { msg: e.to_string() })?;
Ok(PreparationOutcome::Success {
details: format!("issuer of kind {} is ready", issuer.to_string()),
})
}
}
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]

View File

@@ -1,10 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{modules::cert_manager::crd::{AcmeIssuer, CaIssuer}, topology::{PreparationError, PreparationOutcome}};
interpret::Outcome,
topology::{PreparationError, PreparationOutcome},
};
#[async_trait] #[async_trait]
pub trait CertificateManagement: Send + Sync { pub trait CertificateManagement: Send + Sync {
@@ -12,7 +9,40 @@ pub trait CertificateManagement: Send + Sync {
&self, &self,
config: &CertificateManagementConfig, config: &CertificateManagementConfig,
) -> Result<PreparationOutcome, PreparationError>; ) -> Result<PreparationOutcome, PreparationError>;
async fn ensure_ready(
&self,
config: &CertificateManagementConfig,
) -> Result<PreparationOutcome, PreparationError>;
async fn create_certificate(
&self,
cert_name: String,
config: &CertificateManagementConfig,
) -> Result<PreparationOutcome, PreparationError>;
} }
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct CertificateManagementConfig {} pub struct CertificateManagementConfig {
pub name: String,
pub namespace: Option<String>,
pub acme_issuer: Option<AcmeIssuer>,
pub ca_issuer: Option<CaIssuer>,
pub self_signed: bool,
}
#[derive(Serialize)]
pub enum Issuer {
ClusterIssuer,
Issuer,
}
impl std::fmt::Display for Issuer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Issuer::Issuer => f.write_str("Issuer"),
Issuer::ClusterIssuer => f.write_str("ClusterIssuer"),
}
}
}

View File

@@ -0,0 +1,113 @@
use kube::{CustomResource, api::ObjectMeta};
use serde::{Deserialize, Serialize};
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug)]
#[kube(
group = "cert-manager.io",
version = "v1",
kind = "Certificate",
plural = "certificates",
namespaced = true,
schema = "disabled"
)]
#[serde(rename_all = "camelCase")]
pub struct CertificateSpec {
/// Name of the Secret where the certificate will be stored
pub secret_name: String,
/// Common Name (optional but often discouraged in favor of SANs)
#[serde(skip_serializing_if = "Option::is_none")]
pub common_name: Option<String>,
/// DNS Subject Alternative Names
#[serde(skip_serializing_if = "Option::is_none")]
pub dns_names: Option<Vec<String>>,
/// IP Subject Alternative Names
#[serde(skip_serializing_if = "Option::is_none")]
pub ip_addresses: Option<Vec<String>>,
/// Certificate duration (e.g. "2160h")
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<String>,
/// How long before expiry cert-manager should renew
#[serde(skip_serializing_if = "Option::is_none")]
pub renew_before: Option<String>,
/// Reference to the Issuer or ClusterIssuer
pub issuer_ref: IssuerRef,
/// Is this a CA certificate
#[serde(skip_serializing_if = "Option::is_none")]
pub is_ca: Option<bool>,
/// Private key configuration
#[serde(skip_serializing_if = "Option::is_none")]
pub private_key: Option<PrivateKey>,
}
impl Default for Certificate {
fn default() -> Self {
Certificate {
metadata: ObjectMeta::default(),
spec: CertificateSpec::default(),
}
}
}
impl Default for CertificateSpec {
fn default() -> Self {
Self {
secret_name: String::new(),
common_name: None,
dns_names: None,
ip_addresses: None,
duration: None,
renew_before: None,
issuer_ref: IssuerRef::default(),
is_ca: None,
private_key: None,
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct IssuerRef {
pub name: String,
/// Either "Issuer" or "ClusterIssuer"
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
}
impl Default for IssuerRef {
fn default() -> Self {
Self {
name: String::new(),
kind: None,
group: None,
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PrivateKey {
/// RSA or ECDSA
#[serde(skip_serializing_if = "Option::is_none")]
pub algorithm: Option<String>,
/// Key size (e.g. 2048, 4096)
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<u32>,
/// Rotation policy: "Never" or "Always"
#[serde(skip_serializing_if = "Option::is_none")]
pub rotation_policy: Option<String>,
}

View File

@@ -0,0 +1,45 @@
use kube::{CustomResource, api::ObjectMeta};
use serde::{Deserialize, Serialize};
use crate::modules::cert_manager::crd::{AcmeIssuer, CaIssuer, SelfSignedIssuer};
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug)]
#[kube(
group = "cert-manager.io",
version = "v1",
kind = "ClusterIssuer",
plural = "clusterissuers",
namespaced = false,
schema = "disabled"
)]
#[serde(rename_all = "camelCase")]
pub struct ClusterIssuerSpec {
#[serde(skip_serializing_if = "Option::is_none")]
pub self_signed: Option<SelfSignedIssuer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ca: Option<CaIssuer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub acme: Option<AcmeIssuer>,
}
impl Default for ClusterIssuer {
fn default() -> Self {
ClusterIssuer {
metadata: ObjectMeta::default(),
spec: ClusterIssuerSpec::default(),
}
}
}
impl Default for ClusterIssuerSpec {
fn default() -> Self {
Self {
self_signed: None,
ca: None,
acme: None,
}
}
}

View File

@@ -0,0 +1,44 @@
use kube::{CustomResource, api::ObjectMeta};
use serde::{Deserialize, Serialize};
use crate::modules::cert_manager::crd::{AcmeIssuer, CaIssuer, SelfSignedIssuer};
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug)]
#[kube(
group = "cert-manager.io",
version = "v1",
kind = "Issuer",
plural = "issuers",
namespaced = true,
schema = "disabled"
)]
#[serde(rename_all = "camelCase")]
pub struct IssuerSpec {
#[serde(skip_serializing_if = "Option::is_none")]
pub self_signed: Option<SelfSignedIssuer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ca: Option<CaIssuer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub acme: Option<AcmeIssuer>,
}
impl Default for Issuer {
fn default() -> Self {
Issuer {
metadata: ObjectMeta::default(),
spec: IssuerSpec::default(),
}
}
}
impl Default for IssuerSpec {
fn default() -> Self {
Self {
self_signed: None,
ca: None,
acme: None,
}
}
}

View File

@@ -0,0 +1,66 @@
use serde::{Deserialize, Serialize};
pub mod certificate;
pub mod issuer;
pub mod cluster_issuer;
//pub mod score_cluster_issuer;
pub mod score_issuer;
pub mod score_certificate;
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CaIssuer {
/// Secret containing `tls.crt` and `tls.key`
pub secret_name: String,
}
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct SelfSignedIssuer {}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AcmeIssuer {
pub server: String,
pub email: String,
/// Secret used to store the ACME account private key
pub private_key_secret_ref: SecretKeySelector,
pub solvers: Vec<AcmeSolver>,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SecretKeySelector {
pub name: String,
pub key: String,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AcmeSolver {
#[serde(skip_serializing_if = "Option::is_none")]
pub http01: Option<Http01Solver>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dns01: Option<Dns01Solver>,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Dns01Solver {}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Http01Solver {
pub ingress: IngressSolver,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct IngressSolver {
#[serde(skip_serializing_if = "Option::is_none")]
pub class: Option<String>,
}

View File

@@ -0,0 +1,47 @@
use kube::api::ObjectMeta;
use serde::Serialize;
use crate::{
interpret::Interpret,
modules::{
cert_manager::{
capability::CertificateManagementConfig,
crd::certificate::{Certificate, CertificateSpec, IssuerRef},
},
k8s::resource::K8sResourceScore,
},
score::Score,
topology::{K8sclient, Topology},
};
#[derive(Debug, Clone, Serialize)]
pub struct CertificateScore {
pub name: String,
pub config: CertificateManagementConfig,
}
impl<T: Topology + K8sclient> Score<T> for CertificateScore {
fn name(&self) -> String {
"CertificateScore".to_string()
}
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
let cert = Certificate {
metadata: ObjectMeta {
name: Some(self.name.clone()),
namespace: self.config.namespace.clone(),
..Default::default()
},
spec: CertificateSpec {
secret_name: format!("{}-tls", self.name.clone()),
issuer_ref: IssuerRef {
name: self.config.name.clone(),
kind: Some("Issuer".into()),
group: Some("cert-manager.io".into()),
},
..Default::default()
},
};
K8sResourceScore::single(cert, self.config.namespace.clone()).create_interpret()
}
}

View File

@@ -0,0 +1,51 @@
use kube::api::ObjectMeta;
use serde::Serialize;
use crate::{
interpret::Interpret,
modules::{
cert_manager::crd::{
AcmeIssuer, CaIssuer, SelfSignedIssuer,
cluster_issuer::{ClusterIssuer, ClusterIssuerSpec},
},
k8s::resource::K8sResourceScore,
},
score::Score,
topology::{K8sclient, Topology},
};
#[derive(Debug, Clone, Serialize)]
pub struct ClusterIssuerScore {
name: String,
acme_issuer: Option<AcmeIssuer>,
ca_issuer: Option<CaIssuer>,
self_signed: bool,
}
impl<T: Topology + K8sclient> Score<T> for ClusterIssuerScore {
fn name(&self) -> String {
"ClusterIssuerScore".to_string()
}
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
let metadata = ObjectMeta {
name: Some(self.name.clone()),
namespace: None,
..ObjectMeta::default()
};
let spec = ClusterIssuerSpec {
acme: self.acme_issuer.clone(),
ca: self.ca_issuer.clone(),
self_signed: if self.self_signed {
Some(SelfSignedIssuer::default())
} else {
None
},
};
let cluster_issuer = ClusterIssuer { metadata, spec };
K8sResourceScore::single(cluster_issuer, None).create_interpret()
}
}

View File

@@ -0,0 +1,48 @@
use kube::api::ObjectMeta;
use serde::Serialize;
use crate::{
interpret::Interpret,
modules::{
cert_manager::{capability::CertificateManagementConfig, crd::{
AcmeIssuer, CaIssuer, SelfSignedIssuer,
issuer::{Issuer, IssuerSpec},
}},
k8s::resource::K8sResourceScore,
},
score::Score,
topology::{K8sclient, Topology},
};
#[derive(Debug, Clone, Serialize)]
pub struct IssuerScore {
pub config: CertificateManagementConfig,
}
impl<T: Topology + K8sclient> Score<T> for IssuerScore {
fn name(&self) -> String {
"IssuerScore".to_string()
}
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
let metadata = ObjectMeta {
name: Some(self.config.name.clone()),
namespace: self.config.namespace.clone(),
..ObjectMeta::default()
};
let spec = IssuerSpec {
acme: self.config.acme_issuer.clone(),
ca: self.config.ca_issuer.clone(),
self_signed: if self.config.self_signed {
Some(SelfSignedIssuer::default())
} else {
None
},
};
let issuer = Issuer { metadata, spec };
K8sResourceScore::single(issuer, self.config.namespace.clone()).create_interpret()
}
}

View File

@@ -3,4 +3,5 @@ pub mod cluster_issuer;
mod helm; mod helm;
pub mod operator; pub mod operator;
pub mod score_k8s; pub mod score_k8s;
pub mod crd;
pub use helm::*; pub use helm::*;