From 41f1bca69c88d28c260228b6b200eb1ef66e1fe0 Mon Sep 17 00:00:00 2001 From: Willem Date: Fri, 12 Sep 2025 16:03:08 -0400 Subject: [PATCH] wip(update-ingress): built basic structure for the okd commands to update the default ingress to allow dynamically provisioning tls certificates for ingresses added quality of life function to check if a deployement exists using the k8sclient --- harmony/src/domain/topology/k8s.rs | 54 ++++++++ .../cert_manager/generate_cert_score.rs | 124 ++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 harmony/src/modules/cert_manager/generate_cert_score.rs diff --git a/harmony/src/domain/topology/k8s.rs b/harmony/src/domain/topology/k8s.rs index 88bd2e8..d4b1f1a 100644 --- a/harmony/src/domain/topology/k8s.rs +++ b/harmony/src/domain/topology/k8s.rs @@ -8,6 +8,7 @@ use kube::{ api::{Api, AttachParams, DeleteParams, ListParams, Patch, PatchParams, ResourceExt}, config::{KubeConfigOptions, Kubeconfig}, core::ErrorResponse, + error::DiscoveryError, runtime::reflector::Lookup, }; use kube::{api::DynamicObject, runtime::conditions}; @@ -21,6 +22,8 @@ use serde_json::{Value, json}; use similar::TextDiff; use tokio::io::AsyncReadExt; +use crate::interpret::Outcome; + #[derive(new, Clone)] pub struct K8sClient { client: Client, @@ -53,6 +56,57 @@ impl K8sClient { }) } + pub async fn ensure_deployment( + &self, + resource_name: &str, + resource_namespace: &str, + ) -> Result { + match self + .get_deployment(resource_name, Some(&resource_namespace)) + .await + { + Ok(Some(deployment)) => { + if let Some(status) = deployment.status { + let ready_count = status.ready_replicas.unwrap_or(0); + if ready_count >= 1 { + Ok(Outcome::success(format!( + "'{}' is ready with {} replica(s).", + resource_name, ready_count + ))) + } else { + Err(Error::Discovery(DiscoveryError::MissingResource(format!( + "Deployment '{}' in namespace '{}' has 0 ready replicas", + resource_name, resource_namespace + )))) + } + } else { + Err(Error::Api(ErrorResponse { + status: "Failure".to_string(), + message: format!( + "No status found for deployment '{}' in namespace '{}'", + resource_name, resource_namespace + ), + reason: "MissingStatus".to_string(), + code: 404, + })) + } + } + Ok(None) => Err(Error::Discovery(DiscoveryError::MissingResource(format!( + "Deployment '{}' not found in namespace '{}'", + resource_name, resource_namespace + )))), + Err(e) => Err(Error::Api(ErrorResponse { + status: "Failure".to_string(), + message: format!( + "Failed to fetch deployment '{}' in namespace '{}': {}", + resource_name, resource_namespace, e + ), + reason: "ApiError".to_string(), + code: 500, + })), + } + } + pub async fn get_resource_json_value( &self, name: &str, diff --git a/harmony/src/modules/cert_manager/generate_cert_score.rs b/harmony/src/modules/cert_manager/generate_cert_score.rs new file mode 100644 index 0000000..5c67bda --- /dev/null +++ b/harmony/src/modules/cert_manager/generate_cert_score.rs @@ -0,0 +1,124 @@ +use std::{path::PathBuf, sync::Arc}; + +use fqdn::FQDN; +use harmony_types::id::Id; + +use crate::{ + data::Version, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + score::Score, + topology::{k8s::K8sClient, K8sclient, Topology}, +}; + +pub struct UpdateDefaultOkdIngressScore { + ca_name: String, + domain: FQDN, +} + +impl Score for UpdateDefaultOkdIngressScore { + fn name(&self) -> String { + "UpdateDefaultOkdIngressScore".to_string() + } + + #[doc(hidden)] + fn create_interpret(&self) -> Box> { + Box::new(UpdateDefaultOkdIngressInterpret { + score: self.clone(), + }) + } +} + +pub struct UpdateDefaultOkdIngressInterpret { + score: UpdateDefaultOkdIngressScore, +} + +#[async_trait] +impl Interpret for UpdateDefaultOkdIngressInterpret { + async fn execute(&self, inventory: &Inventory, topology: &T) -> Result { + let client = topology.k8s_client().await?; + let ca_name = self.score.ca_name.clone(); + let domain = self.score.domain.clone(); + let secret_name = "ingress_ca_secret"; + self.ensure_ingress_operator(&client).await?; + self.create_ca_cm(&client, &domain, &ca_name).await?; + self.patch_proxy(&client, &ca_name).await?; + self.create_tls_secret(&client, &secret_name).await?; + self.patch_ingress(&client, &secret_name).await?; + + } + + fn get_name(&self) -> InterpretName { + InterpretName::Custom("UpdateDefaultOkd") + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + todo!() + } +} + +impl UpdateDefaultOkdIngressInterpret { + async fn ensure_ingress_operator( + &self, + client: &Arc, + ) -> Result { + let operator_name = "ingress-operator"; + let operator_namespace = "openshift-ingress-operator"; + client + .ensure_deployment(operator_name, Some(operator_namespace)) + .await? + } + + async fn create_ca_cm( + &self, + client: &Arc, + fqdn: &FQDN, + ca_name: &str, + ) -> Result { + let cm = format!( + r"# +apiVersion: v1 +kind: ConfigMap +metadata: + name: custom-ca + namespace: openshift-config +data: + ca-bundle.crt: {} + #", fqdn + ); + client.apply_yaml(serde_yaml::to_value(&cm), None).await?; + Ok(Outcome::success(format!( + "successfully created cm : {} in default namespace", + ca_name + ))) + } + + async fn patch_proxy( + &self, + client: &Arc, + ca_name: &str, + ) -> Result { + } + + async fn create_tls_secret( + &self, + client: &Arc, + secret_name: &str, + ) -> Result { + } + + async fn patch_ingress( + &self, + client: &Arc, + secret_name: &str, + ) -> Result { + } +}