From 41f1bca69c88d28c260228b6b200eb1ef66e1fe0 Mon Sep 17 00:00:00 2001 From: Willem Date: Fri, 12 Sep 2025 16:03:08 -0400 Subject: [PATCH 1/4] 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 { + } +} -- 2.39.5 From 84ae60fbbfa6838a48008c62ae3d0d4ecef8ed3c Mon Sep 17 00:00:00 2001 From: Willem Date: Tue, 16 Sep 2025 09:02:33 -0400 Subject: [PATCH 2/4] feat(k8sanywhere):added functions to ensure deployment has ready replicas, as well as a function to patch resource by merge" feat(certificate): completed functions to update the default okd ingress class to use a specfied ca-cert, tls.key, and tls.crt --- harmony/src/domain/topology/k8s.rs | 19 ++ .../cert_manager/generate_cert_score.rs | 124 ---------- .../update_default_okd_ingress_score.rs | 222 ++++++++++++++++++ 3 files changed, 241 insertions(+), 124 deletions(-) delete mode 100644 harmony/src/modules/cert_manager/generate_cert_score.rs create mode 100644 harmony/src/modules/cert_manager/update_default_okd_ingress_score.rs diff --git a/harmony/src/domain/topology/k8s.rs b/harmony/src/domain/topology/k8s.rs index d4b1f1a..942382a 100644 --- a/harmony/src/domain/topology/k8s.rs +++ b/harmony/src/domain/topology/k8s.rs @@ -144,6 +144,25 @@ impl K8sClient { Ok(pods.get_opt(name).await?) } + pub async fn patch_resource_by_merge( + &self, + name: &str, + namespace: Option<&str>, + gvk: &GroupVersionKind, + patch: Value, + ) -> Result<(), Error> { + let gvk = ApiResource::from_gvk(gvk); + let resource: Api = if let Some(ns) = namespace { + Api::namespaced_with(self.client.clone(), ns, &gvk) + } else { + Api::default_namespaced_with(self.client.clone(), &gvk) + }; + let pp = PatchParams::default(); + let merge = Patch::Merge(&patch); + resource.patch(name, &pp, &merge).await?; + Ok(()) + } + pub async fn scale_deployment( &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 deleted file mode 100644 index 5c67bda..0000000 --- a/harmony/src/modules/cert_manager/generate_cert_score.rs +++ /dev/null @@ -1,124 +0,0 @@ -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 { - } -} diff --git a/harmony/src/modules/cert_manager/update_default_okd_ingress_score.rs b/harmony/src/modules/cert_manager/update_default_okd_ingress_score.rs new file mode 100644 index 0000000..079904e --- /dev/null +++ b/harmony/src/modules/cert_manager/update_default_okd_ingress_score.rs @@ -0,0 +1,222 @@ +use std::{ + fs::File, + io::Read, + path::{Path, PathBuf}, + sync::Arc, +}; + +use base64::{Engine, prelude::BASE64_STANDARD}; +use fqdn::Path; +use harmony_types::id::Id; +use kube::api::GroupVersionKind; +use serde_json::json; + +use crate::{ + data::Version, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + score::Score, + topology::{K8sclient, Topology, k8s::K8sClient}, +}; + +pub struct UpdateDefaultOkdIngressScore { + operator_name: String, + operator_namespace: String, + ca_name: String, + path_to_tls_crt: Path, + path_to_tls_key: Path, + path_to_ca_cert: Path, +} + +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 secret_name = "ingress_ca_secret"; + self.ensure_ingress_operator( + &client, + &self.score.operator_name, + &self.score.operator_namespace, + ) + .await?; + self.create_ca_cm(&client, self.score.path_to_ca_cert, &self.score.ca_name) + .await?; + self.patch_proxy(&client, &self.score.ca_name).await?; + self.create_tls_secret( + &client, + self.score.path_to_tls_crt, + self.score.path_to_tls_key, + &self.score.operator_namespace, + &secret_name, + ) + .await?; + self.patch_ingress(&client, &self.score.operator_namespace, &secret_name) + .await?; + } + + fn get_name(&self) -> InterpretName { + InterpretName::Custom("UpdateDefaultOkdIngress") + } + + 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, + operator_name: &str, + operator_namespace: &str, + ) -> Result { + client + .ensure_deployment(operator_name, Some(operator_namespace)) + .await? + } + + fn open_path(&self, path: Path) -> Result { + let mut file = match File::open(&path) { + Ok(file) => file, + Err(e) => InterpretError::new(format!("Could not open file {}", e)), + }; + let s = String::new(); + match file.read_to_string(&mut s) { + Ok(s) => Ok(s), + Err(e) => InterpretError::new(format!("Could not read file {}", e)), + } + } + + async fn create_ca_cm( + &self, + client: &Arc, + path_to_ca_cert: Path, + ca_name: &str, + ) -> Result { + let ca_bundle = BASE64_STANDARD.encode(self.open_path(path_to_ca_cert).unwrap().as_bytes()); + + let cm = format!( + r"# +apiVersion: v1 +kind: ConfigMap +metadata: + name: custom-ca +data: + ca-bundle.crt: {ca_bundle} + #" + ); + 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 { + let gvk = GroupVersionKind { + group: "config.openshift.io".to_string(), + version: "v1".to_string(), + kind: "Proxy".to_string(), + }; + let patch = json!({ + "spec": { + "trustedCA": { + "name": ca_name + } + } + }); + client + .patch_resource_by_merge("cluster", None, &gvk, patch) + .await?; + Ok(Outcome::success(format!( + "successfully merged trusted ca to cluster proxy" + ))) + } + + async fn create_tls_secret( + &self, + client: &Arc, + tls_crt: Path, + tls_key: Path, + operator_namespace: &str, + secret_name: &str, + ) -> Result { + let base64_tls_crt = BASE64_STANDARD.encode(self.open_path(tls_crt).unwrap().as_bytes()); + let base64_tls_key = BASE64_STANDARD.encode(self.open_path(tls_key).unwrap().as_bytes()); + let secret = format!( + r#" +apiVersion: v1 +kind: Secret +metadata: + name: secret-tls + namespace: {operator_namespace} +type: kubernetes.io/tls +data: + # values are base64 encoded, which obscures them but does NOT provide + # any useful level of confidentiality + # Replace the following values with your own base64-encoded certificate and key. + tls.crt: "{base64_tls_crt}" + tls.key: "{base64_tls_key}" + "# + ); + client + .apply_yaml(serde_yaml::to_value(secret), Some(operator_namespace)) + .await?; + Ok(Outcome::success(format!( + "successfully created tls secret trusted ca to cluster proxy" + ))) + } + + async fn patch_ingress( + &self, + client: &Arc, + operator_namespace: &str, + secret_name: &str, + ) -> Result { + let gvk = GroupVersionKind { + group: "operator.openshift.io".to_string(), + version: "v1".to_string(), + kind: "IngressController".to_string(), + }; + let patch = json!( + {"spec":{"defaultCertificate": {"name": secret_name}}}); + client + .patch_resource_by_merge("default", Some(operator_namespace), &gvk, patch) + .await?; + + Ok(Outcome::success("successfully pathed ingress operator to")) + } +} -- 2.39.5 From 5e4ceed91bff16f6c835caa5cc63c21dc4cdfd3b Mon Sep 17 00:00:00 2001 From: Willem Date: Tue, 16 Sep 2025 09:09:13 -0400 Subject: [PATCH 3/4] fix(cert): namespace of cm for okd ingress should be openshift-config not default --- .../cert_manager/update_default_okd_ingress_score.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/harmony/src/modules/cert_manager/update_default_okd_ingress_score.rs b/harmony/src/modules/cert_manager/update_default_okd_ingress_score.rs index 079904e..6a92117 100644 --- a/harmony/src/modules/cert_manager/update_default_okd_ingress_score.rs +++ b/harmony/src/modules/cert_manager/update_default_okd_ingress_score.rs @@ -130,13 +130,14 @@ apiVersion: v1 kind: ConfigMap metadata: name: custom-ca + namespace: openshift-config data: ca-bundle.crt: {ca_bundle} #" ); - client.apply_yaml(serde_yaml::to_value(&cm), None).await?; + client.apply_yaml(serde_yaml::to_value(&cm), Some("openshift-config")).await?; Ok(Outcome::success(format!( - "successfully created cm : {} in default namespace", + "successfully created cm : {} in openshift-config namespace", ca_name ))) } @@ -217,6 +218,6 @@ data: .patch_resource_by_merge("default", Some(operator_namespace), &gvk, patch) .await?; - Ok(Outcome::success("successfully pathed ingress operator to")) + Ok(Outcome::success(format!("successfully pathed ingress operator to use secret {}", secret_name))) } } -- 2.39.5 From e04934fc22de989c8ee8cdc77458e35cd7a0d8a8 Mon Sep 17 00:00:00 2001 From: Willem Date: Tue, 16 Sep 2025 09:31:00 -0400 Subject: [PATCH 4/4] fix(cert): mod file --- harmony/src/modules/cert_manager/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/harmony/src/modules/cert_manager/mod.rs b/harmony/src/modules/cert_manager/mod.rs index 8fd309a..daf8a6b 100644 --- a/harmony/src/modules/cert_manager/mod.rs +++ b/harmony/src/modules/cert_manager/mod.rs @@ -1,2 +1,3 @@ mod helm; +pub mod update_default_okd_ingress_score; pub use helm::*; -- 2.39.5