feat/cert_manager_crds #211

Merged
wjro merged 13 commits from feat/cert_manager_crds into master 2026-01-20 18:46:21 +00:00
16 changed files with 983 additions and 2 deletions

View File

@@ -0,0 +1,19 @@
[package]
name = "cert_manager"
edition = "2024"
version.workspace = true
readme.workspace = true
license.workspace = true
publish = false
[dependencies]
harmony = { path = "../../harmony" }
harmony_cli = { path = "../../harmony_cli" }
harmony_types = { path = "../../harmony_types" }
cidr = { workspace = true }
tokio = { workspace = true }
harmony_macros = { path = "../../harmony_macros" }
log = { workspace = true }
env_logger = { workspace = true }
url = { workspace = true }
assert_cmd = "2.0.16"

View File

@@ -0,0 +1,38 @@
use harmony::{
inventory::Inventory,
modules::cert_manager::{
capability::CertificateManagementConfig, score_cert_management::CertificateManagementScore,
score_certificate::CertificateScore, score_issuer::CertificateIssuerScore,
},
topology::K8sAnywhereTopology,
};
#[tokio::main]
async fn main() {
let config = CertificateManagementConfig {
namespace: Some("test".to_string()),
acme_issuer: None,
ca_issuer: None,
self_signed: true,
};
let issuer = CertificateIssuerScore {

Not the place

Not the place
config: config.clone(),
issuer_name: "test-self-signed-issuer".to_string(),
};
let cert = CertificateScore {
config: config.clone(),
cert_name: "test-self-signed-cert".to_string(),
issuer_name: "test-self-signed-issuer".to_string(),
};
harmony_cli::run(
Inventory::autoload(),
K8sAnywhereTopology::from_env(),
vec![Box::new(issuer), Box::new(cert)],
None,
)
.await
.unwrap();
}

View File

@@ -14,11 +14,24 @@ use tokio::sync::OnceCell;
use crate::{ use crate::{
executors::ExecutorError, executors::ExecutorError,
interpret::InterpretStatus, interpret::{InterpretStatus, Outcome},
inventory::Inventory, inventory::Inventory,
modules::{ modules::{
cert_manager::{
capability::{CertificateManagement, CertificateManagementConfig},
crd::{
certificate::Certificate, issuer::Issuer,
score_k8s_certificate::K8sCertManagerCertificateScore,
score_k8s_issuer::K8sCertManagerIssuerScore,
},
operator::CertManagerOperatorScore,
score_cert_management::CertificateManagementScore,
},
k3d::K3DInstallationScore, k3d::K3DInstallationScore,
k8s::ingress::{K8sIngressScore, PathType}, k8s::{
apps::crd::Subscription,
ingress::{K8sIngressScore, PathType},
},
monitoring::{ monitoring::{
grafana::{grafana::Grafana, helm::helm_grafana::grafana_helm_chart_score}, grafana::{grafana::Grafana, helm::helm_grafana::grafana_helm_chart_score},
kube_prometheus::crd::{ kube_prometheus::crd::{
@@ -359,6 +372,142 @@ impl Serialize for K8sAnywhereTopology {
} }
} }
#[async_trait]
impl CertificateManagement for K8sAnywhereTopology {
async fn install(&self) -> Result<Outcome, ExecutorError> {
let cert_management_operator = CertManagerOperatorScore::default();
cert_management_operator
.interpret(&Inventory::empty(), self)
.await
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?;
Ok(Outcome::success(format!(
"Installed cert-manager into ns: {}",
cert_management_operator.namespace
)))
}
async fn ensure_ready(&self) -> Result<Outcome, ExecutorError> {
let k8s_client = self.k8s_client().await.unwrap();
match k8s_client
.get_resource::<Subscription>("cert-manager", Some("openshift-operators"))
.await
.map_err(|e| ExecutorError::UnexpectedError(format!("{}", e)))?
{
Some(subscription) => {
trace!("subscription {:#?}", subscription,);
Ok(Outcome::success(format!("Certificate Management Ready",)))
}
None => self.install().await,
}
}
async fn create_issuer(
&self,
issuer_name: String,
config: &CertificateManagementConfig,
) -> Result<Outcome, ExecutorError> {
let issuer_score = K8sCertManagerIssuerScore {
issuer_name: issuer_name.clone(),
config: config.clone(),
};
issuer_score
.interpret(&Inventory::empty(), self)
.await
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?;
Ok(Outcome::success(format!(
"issuer of kind {} is ready",
issuer_name
)))
}
async fn create_certificate(
&self,
cert_name: String,
issuer_name: String,
config: &CertificateManagementConfig,
) -> Result<Outcome, ExecutorError> {
self.certificate_issuer_ready(
issuer_name.clone(),

This should not return PreparationError and PreparationOutcome.

Most of the time it won't be used as part of a preparation phase.

This should not return PreparationError and PreparationOutcome. Most of the time it won't be used as part of a preparation phase.
self.k8s_client().await.unwrap(),
config,
)
.await?;
let cert = K8sCertManagerCertificateScore {
cert_name: cert_name,
config: config.clone(),
johnride marked this conversation as resolved Outdated

I would have thought that CertificateScore depends on CertificateManagement capability, not the other way around.

The goal is to allow users to easily create certifiactes by calling CertificateScore on any Topology that implements CertificateManagement.

Or am I missing something?

I would have thought that CertificateScore depends on CertificateManagement capability, not the other way around. The goal is to allow users to easily create certifiactes by calling CertificateScore on any Topology that implements CertificateManagement. Or am I missing something?
Outdated
Review

I think this is a naming problem which makes the intent unclear.

I have CertificateCreationScore, CertificateIssuerScore, and CertificateManagementScore which depend on CertificateManagement capability, that way we can create certificates, issuers or ensure that CertificateManagement is ready on the Topology that implements the CertificateManagement trait.

The CertificateScore and IssuerScore are specific implentations for K8s that uses kubernetes CRDs which are applied as K8s resources, this is why further down the CertificateScore requires impl of K8sclient.

I modeled this off of postgresql which has a generic score.rs which calls topology.deploy() -> impl PostgreSQL for K8sAnywhereTopology -> K8sPostgreSQLScore.interpret()

I think this is a naming problem which makes the intent unclear. I have CertificateCreationScore, CertificateIssuerScore, and CertificateManagementScore which depend on CertificateManagement capability, that way we can create certificates, issuers or ensure that CertificateManagement is ready on the Topology that implements the CertificateManagement trait. The CertificateScore and IssuerScore are specific implentations for K8s that uses kubernetes CRDs which are applied as K8s resources, this is why further down the CertificateScore requires impl of K8sclient. I modeled this off of postgresql which has a generic score.rs which calls topology.deploy() -> impl PostgreSQL for K8sAnywhereTopology -> K8sPostgreSQLScore.interpret()
issuer_name,
};
cert.interpret(&Inventory::empty(), self)
.await
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?;
Ok(Outcome::success(format!(
"Created cert into ns: {:#?}",
config.namespace.clone()
)))
}
async fn get_ca_certificate(
&self,
cert_name: String,
config: &CertificateManagementConfig,
) -> Result<String, ExecutorError> {
let namespace = config.namespace.clone().unwrap();
let client = self.k8s_client().await.unwrap();
johnride marked this conversation as resolved Outdated

You should be using the CRD here, not crafting a GVK. See how it's done with NMstate and other CRDs.

You should be using the CRD here, not crafting a GVK. See how it's done with NMstate and other CRDs.
if let Some(certificate) = client
.get_resource::<Certificate>(&cert_name, Some(&namespace))
.await
.map_err(|e| ExecutorError::UnexpectedError(format!("{}", e)))?
{
let secret_name = certificate.spec.secret_name.clone();
trace!("Secret Name {:#?}", secret_name);
if let Some(secret) = client
.get_resource::<Secret>(&secret_name, Some(&namespace))
.await
.map_err(|e| {
ExecutorError::UnexpectedError(format!(
"secret {} not found in namespace {}: {}",
secret_name, namespace, e
))
})?
{
let ca_cert = secret
.data
.as_ref()
.and_then(|d| d.get("ca.crt"))
.ok_or_else(|| {
ExecutorError::UnexpectedError("Secret missing key 'ca.crt'".into())
johnride marked this conversation as resolved Outdated

If you were using the CRD properly here you would not have to mess with JSON, you would simply be walking the Rust struct directly.

If you were using the CRD properly here you would not have to mess with JSON, you would simply be walking the Rust struct directly.
})?;
let ca_cert = String::from_utf8(ca_cert.0.clone()).map_err(|_| {
ExecutorError::UnexpectedError("ca.crt is not valid UTF-8".into())
})?;
return Ok(ca_cert);
} else {
Err(ExecutorError::UnexpectedError(format!(
"Error getting secret associated with cert_name: {}",
cert_name
)))
}
} else {
return Err(ExecutorError::UnexpectedError(format!(
"Certificate {} not found in namespace {}",
cert_name, namespace
)));
}
}
}
impl K8sAnywhereTopology { impl K8sAnywhereTopology {
pub fn from_env() -> Self { pub fn from_env() -> Self {
Self { Self {
@@ -378,6 +527,38 @@ impl K8sAnywhereTopology {
} }
} }
pub async fn certificate_issuer_ready(
&self,
issuer_name: String,
k8s_client: Arc<K8sClient>,
config: &CertificateManagementConfig,
) -> Result<Outcome, ExecutorError> {
let ns = config
.namespace
.clone()
.ok_or_else(|| ExecutorError::UnexpectedError("namespace is required".to_string()))?;
match k8s_client
.get_resource::<Issuer>(&issuer_name, Some(&ns))
.await
{
Ok(Some(_cert_issuer)) => Ok(Outcome::success(format!(

Same, use the CRD properly

Same, use the CRD properly
"issuer of kind {} is ready",
issuer_name
))),
Ok(None) => Err(ExecutorError::UnexpectedError(format!(
"Issuer {} not present in namespace {}",
issuer_name, ns
))),
Err(e) => Err(ExecutorError::UnexpectedError(format!(
"Failed to fetch Issuer {}: {}",
issuer_name, e
))),
}
}
pub async fn get_k8s_distribution(&self) -> Result<&KubernetesDistribution, PreparationError> { pub async fn get_k8s_distribution(&self) -> Result<&KubernetesDistribution, PreparationError> {
self.k8s_distribution self.k8s_distribution
.get_or_try_init(async || { .get_or_try_init(async || {
@@ -1064,6 +1245,12 @@ impl Topology for K8sAnywhereTopology {
.await .await
.map_err(PreparationError::new)?; .map_err(PreparationError::new)?;
let cert_mgmt = CertificateManagementScore {};
cert_mgmt
.interpret(&Inventory::empty(), self)
.await
.map_err(|e| PreparationError::new(format!("{}", e)))?;
match self.is_helm_available() { match self.is_helm_available() {
Ok(()) => Ok(PreparationOutcome::Success { Ok(()) => Ok(PreparationOutcome::Success {
details: format!("{} + helm available", k8s_state.message.clone()), details: format!("{} + helm available", k8s_state.message.clone()),

View File

@@ -0,0 +1,43 @@
use async_trait::async_trait;
use serde::Serialize;
use crate::{
executors::ExecutorError,
interpret::Outcome,
modules::cert_manager::crd::{AcmeIssuer, CaIssuer},
};
///TODO rust doc explaining issuer, certificate etc
#[async_trait]
pub trait CertificateManagement: Send + Sync {
async fn install(&self) -> Result<Outcome, ExecutorError>;
async fn ensure_ready(&self) -> Result<Outcome, ExecutorError>;
async fn create_issuer(
johnride marked this conversation as resolved Outdated

It should be ensure_ready, no need for the long function name. Let's keep is standard-ish across Harmony. Eventually we will very likely extract the readiness state management in a separate trait.

It should be ensure_ready, no need for the long function name. Let's keep is standard-ish across Harmony. Eventually we will very likely extract the readiness state management in a separate trait.
&self,
issuer_name: String,
johnride marked this conversation as resolved Outdated

Change the return types. This is something that will require cleanup across harmony eventually. But the semantic is incorrect here. This trait is not scoped in preparation.

Change the return types. This is something that will require cleanup across harmony eventually. But the semantic is incorrect here. This trait is not scoped in preparation.
config: &CertificateManagementConfig,
) -> Result<Outcome, ExecutorError>;
async fn create_certificate(
&self,
cert_name: String,
issuer_name: String,
config: &CertificateManagementConfig,
) -> Result<Outcome, ExecutorError>;
async fn get_ca_certificate(
&self,
cert_name: String,
config: &CertificateManagementConfig,
) -> Result<String, ExecutorError>;
}
#[derive(Debug, Clone, Serialize)]
pub struct CertificateManagementConfig {
pub namespace: Option<String>,
pub acme_issuer: Option<AcmeIssuer>,
pub ca_issuer: Option<CaIssuer>,
pub self_signed: bool,
}

View File

@@ -0,0 +1,112 @@
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,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 = "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,65 @@
use serde::{Deserialize, Serialize};
pub mod certificate;
pub mod cluster_issuer;
pub mod issuer;
//pub mod score_cluster_issuer;
pub mod score_k8s_certificate;
pub mod score_k8s_issuer;
#[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,49 @@
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 K8sCertManagerCertificateScore {
pub cert_name: String,
pub issuer_name: String,
pub config: CertificateManagementConfig,
}
impl<T: Topology + K8sclient> Score<T> for K8sCertManagerCertificateScore {
fn name(&self) -> String {
"CertificateScore".to_string()
}
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
let cert = Certificate {
metadata: ObjectMeta {
name: Some(self.cert_name.clone()),
namespace: self.config.namespace.clone(),
..Default::default()
},
spec: CertificateSpec {
secret_name: format!("{}-tls", self.cert_name.clone()),
issuer_ref: IssuerRef {
name: self.issuer_name.clone(),
kind: Some("Issuer".into()),
group: Some("cert-manager.io".into()),
},
dns_names: Some(vec!["test.example.local".to_string()]),
..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,52 @@
use kube::api::ObjectMeta;
use serde::Serialize;
use crate::{
interpret::Interpret,
modules::{
cert_manager::{
capability::CertificateManagementConfig,
crd::{
SelfSignedIssuer,
issuer::{Issuer, IssuerSpec},
},
},
k8s::resource::K8sResourceScore,
},
score::Score,
topology::{K8sclient, Topology},
};
#[derive(Debug, Clone, Serialize)]
pub struct K8sCertManagerIssuerScore {
pub issuer_name: String,
pub config: CertificateManagementConfig,
}
impl<T: Topology + K8sclient> Score<T> for K8sCertManagerIssuerScore {
fn name(&self) -> String {
"IssuerScore".to_string()
}
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
let metadata = ObjectMeta {
name: Some(self.issuer_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

@@ -1,3 +1,9 @@
pub mod capability;
pub mod cluster_issuer; pub mod cluster_issuer;
pub mod crd;
mod helm; mod helm;
pub mod operator;
pub mod score_cert_management;
pub mod score_certificate;
pub mod score_issuer;
pub use helm::*; pub use helm::*;

View File

@@ -0,0 +1,64 @@
use kube::api::ObjectMeta;
use serde::Serialize;
use crate::{
interpret::Interpret,
modules::k8s::{
apps::crd::{Subscription, SubscriptionSpec},
resource::K8sResourceScore,
},
score::Score,
topology::{K8sclient, Topology, k8s::K8sClient},
};
/// Install the Cert-Manager Operator via RedHat Community Operators registry.redhat.io/redhat/community-operator-index:v4.19
/// This Score creates a Subscription CR in the specified namespace
#[derive(Debug, Clone, Serialize)]
pub struct CertManagerOperatorScore {
pub namespace: String,
pub channel: String,
pub install_plan_approval: String,
pub source: String,
pub source_namespace: String,
}
impl Default for CertManagerOperatorScore {
fn default() -> Self {
Self {
namespace: "openshift-operators".to_string(),
channel: "stable".to_string(),
install_plan_approval: "Automatic".to_string(),
source: "community-operators".to_string(),
source_namespace: "openshift-marketplace".to_string(),
}
}
}
impl<T: Topology + K8sclient> Score<T> for CertManagerOperatorScore {
fn name(&self) -> String {
"CertManagerOperatorScore".to_string()
}
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
let metadata = ObjectMeta {
name: Some("cert-manager".to_string()),
namespace: Some(self.namespace.clone()),
..ObjectMeta::default()
};
let spec = SubscriptionSpec {
channel: Some(self.channel.clone()),
config: None,
install_plan_approval: Some(self.install_plan_approval.clone()),
name: "cert-manager".to_string(),
source: self.source.clone(),
source_namespace: self.source_namespace.clone(),
starting_csv: None,
};
let subscription = Subscription { metadata, spec };
K8sResourceScore::single(subscription, Some(self.namespace.clone())).create_interpret()
}
}

View File

@@ -0,0 +1,59 @@
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,
modules::cert_manager::capability::{CertificateManagement, CertificateManagementConfig},
score::Score,
topology::Topology,
};
#[derive(Debug, Clone, Serialize)]
pub struct CertificateManagementScore {}
impl<T: Topology + CertificateManagement> Score<T> for CertificateManagementScore {
fn name(&self) -> String {
"CertificateManagementScore".to_string()
}
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(CertificateManagementInterpret {})
}
}
#[derive(Debug)]
struct CertificateManagementInterpret {}
#[async_trait]
impl<T: Topology + CertificateManagement> Interpret<T> for CertificateManagementInterpret {
async fn execute(
&self,
_inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
CertificateManagement::ensure_ready(topology)
.await
.map_err(|e| InterpretError::new(e.to_string()))?;
Ok(Outcome::success(format!("CertificateManagement is ready")))
}
fn get_name(&self) -> InterpretName {
InterpretName::Custom("CertificateManagementInterpret")
}
fn get_version(&self) -> Version {
todo!()
}
fn get_status(&self) -> InterpretStatus {
todo!()
}
fn get_children(&self) -> Vec<Id> {
todo!()
}
}

View File

@@ -0,0 +1,77 @@
use async_trait::async_trait;
use harmony_types::id::Id;
use log::trace;
use serde::Serialize;
use crate::{
data::Version,
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory,
modules::cert_manager::capability::{CertificateManagement, CertificateManagementConfig},
score::Score,
topology::Topology,
};
#[derive(Debug, Clone, Serialize)]
pub struct CertificateScore {
pub cert_name: String,
pub issuer_name: String,
pub config: CertificateManagementConfig,
}
impl<T: Topology + CertificateManagement> Score<T> for CertificateScore {
fn name(&self) -> String {
"CertificateCreationScore".to_string()
}
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(CertificateInterpret {
cert_name: self.cert_name.clone(),
issuer_name: self.issuer_name.clone(),
config: self.config.clone(),
})
}
}
#[derive(Debug)]
struct CertificateInterpret {
cert_name: String,
issuer_name: String,
config: CertificateManagementConfig,
}
#[async_trait]
impl<T: Topology + CertificateManagement> Interpret<T> for CertificateInterpret {
async fn execute(
&self,
inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
let _certificate = topology
.create_certificate(
self.cert_name.clone(),
self.issuer_name.clone(),
&self.config,
)
.await
.map_err(|e| InterpretError::new(e.to_string()))?;
Ok(Outcome::success(format!("Installed CertificateManagement")))
}
fn get_name(&self) -> InterpretName {
InterpretName::Custom("CertificateManagementInterpret")
}
fn get_version(&self) -> Version {
todo!()
}
fn get_status(&self) -> InterpretStatus {
todo!()
}
fn get_children(&self) -> Vec<Id> {
todo!()
}
}

View File

@@ -0,0 +1,71 @@
use async_trait::async_trait;
use harmony_types::id::Id;
use log::debug;
use serde::Serialize;
use crate::{
data::Version,
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory,
modules::cert_manager::capability::{CertificateManagement, CertificateManagementConfig},
score::Score,
topology::Topology,
};
#[derive(Debug, Clone, Serialize)]
pub struct CertificateIssuerScore {
pub issuer_name: String,
pub config: CertificateManagementConfig,
}
impl<T: Topology + CertificateManagement> Score<T> for CertificateIssuerScore {
fn name(&self) -> String {
"CertificateIssuerScore".to_string()
}
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(CertificateIssuerInterpret {
config: self.config.clone(),
issuer_name: self.issuer_name.clone(),
})
}
}
#[derive(Debug)]
struct CertificateIssuerInterpret {
config: CertificateManagementConfig,
issuer_name: String,
}
#[async_trait]
impl<T: Topology + CertificateManagement> Interpret<T> for CertificateIssuerInterpret {
async fn execute(
&self,
inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
debug!("issuer name: {}", self.issuer_name.clone());
let _cert_issuer = topology
.create_issuer(self.issuer_name.clone(), &self.config)
.await
.map_err(|e| InterpretError::new(e.to_string()))?;
Ok(Outcome::success(format!("Installed CertificateManagement")))
}
fn get_name(&self) -> InterpretName {
InterpretName::Custom("CertificateManagementInterpret")
}
fn get_version(&self) -> Version {
todo!()
}
fn get_status(&self) -> InterpretStatus {
todo!()
}
fn get_children(&self) -> Vec<Id> {
todo!()
}
}