From 84ae60fbbfa6838a48008c62ae3d0d4ecef8ed3c Mon Sep 17 00:00:00 2001 From: Willem Date: Tue, 16 Sep 2025 09:02:33 -0400 Subject: [PATCH] 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")) + } +}