From 6149249a6ca5cdc66304f9fe0fa9652cb8da18d9 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Fri, 4 Jul 2025 09:48:17 -0400 Subject: [PATCH] feat: create Argo interpret and kube client apply_yaml to install Argo Applications. Very messy implementation though, must be refactored/improved --- harmony/src/domain/interpret/mod.rs | 2 + harmony/src/domain/score_with_dep.rs | 59 +++++++++++++ harmony/src/domain/topology/k8s.rs | 55 +++++++++++- harmony/src/domain/topology/k8s_anywhere.rs | 2 +- .../application/features/argo_types.rs | 17 ++-- .../features/continuous_delivery.rs | 50 ++++++----- .../application/features/helm_argocd_score.rs | 84 ++++++++++++++++++- 7 files changed, 235 insertions(+), 34 deletions(-) create mode 100644 harmony/src/domain/score_with_dep.rs diff --git a/harmony/src/domain/interpret/mod.rs b/harmony/src/domain/interpret/mod.rs index c89b163..add7b70 100644 --- a/harmony/src/domain/interpret/mod.rs +++ b/harmony/src/domain/interpret/mod.rs @@ -22,6 +22,7 @@ pub enum InterpretName { K3dInstallation, TenantInterpret, Application, + ArgoCD, } impl std::fmt::Display for InterpretName { @@ -39,6 +40,7 @@ impl std::fmt::Display for InterpretName { InterpretName::K3dInstallation => f.write_str("K3dInstallation"), InterpretName::TenantInterpret => f.write_str("Tenant"), InterpretName::Application => f.write_str("Application"), + InterpretName::ArgoCD => f.write_str("ArgoCD"), } } } diff --git a/harmony/src/domain/score_with_dep.rs b/harmony/src/domain/score_with_dep.rs new file mode 100644 index 0000000..327679b --- /dev/null +++ b/harmony/src/domain/score_with_dep.rs @@ -0,0 +1,59 @@ +//////////////////// +/// Working idea +/// +/// +trait ScoreWithDep { + fn create_interpret(&self) -> Box>; + fn name(&self) -> String; + fn get_dependencies(&self) -> Vec; // Force T to impl Installer or something + // like that +} + +struct PrometheusAlertScore; + +impl ScoreWithDep for PrometheusAlertScore { + fn create_interpret(&self) -> Box> { + todo!() + } + + fn name(&self) -> String { + todo!() + } + + fn get_dependencies(&self) -> Vec { + // We have to find a way to constrait here so at compile time we are only allowed to return + // TypeId for types which can be installed by T + // + // This means, for example that T must implement HelmCommand if the impl Installable for + // KubePrometheus calls for HelmCommand. + vec![TypeId::of::()] + } +} + +trait Installable{} + +struct KubePrometheus; + +impl Installable for KubePrometheus; + + +struct Maestro { + topology: T +} + +impl Maestro { + fn execute_store(&self, score: ScoreWithDep) { + score.get_dependencies().iter().for_each(|dep| { + self.topology.ensure_dependency_ready(dep); + }); + } +} + +struct TopologyWithDep { +} + +impl TopologyWithDep { + fn ensure_dependency_ready(&self, type_id: TypeId) -> Result<(), String> { + self.installer + } +} diff --git a/harmony/src/domain/topology/k8s.rs b/harmony/src/domain/topology/k8s.rs index d84a136..d31e5bc 100644 --- a/harmony/src/domain/topology/k8s.rs +++ b/harmony/src/domain/topology/k8s.rs @@ -4,8 +4,6 @@ use k8s_openapi::{ ClusterResourceScope, NamespaceResourceScope, api::{apps::v1::Deployment, core::v1::Pod}, }; -use kube::runtime::conditions; -use kube::runtime::wait::await_condition; use kube::{ Client, Config, Error, Resource, api::{Api, AttachParams, ListParams, Patch, PatchParams, ResourceExt}, @@ -13,6 +11,11 @@ use kube::{ core::ErrorResponse, runtime::reflector::Lookup, }; +use kube::{api::DynamicObject, runtime::conditions}; +use kube::{ + api::{ApiResource, GroupVersionKind}, + runtime::wait::await_condition, +}; use log::{debug, error, trace}; use serde::de::DeserializeOwned; use similar::{DiffableStr, TextDiff}; @@ -239,6 +242,54 @@ impl K8sClient { Ok(result) } + pub async fn apply_yaml_many( + &self, + yaml: &Vec, + ns: Option<&str>, + ) -> Result<(), Error> { + for y in yaml.iter() { + self.apply_yaml(y, ns).await?; + } + Ok(()) + } + + pub async fn apply_yaml( + &self, + yaml: &serde_yaml::Value, + ns: Option<&str>, + ) -> Result<(), Error> { + let obj: DynamicObject = serde_yaml::from_value(yaml.clone()).expect("TODO do not unwrap"); + let name = obj.metadata.name.as_ref().expect("YAML must have a name"); + let namespace = obj + .metadata + .namespace + .as_ref() + .expect("YAML must have a namespace"); + + // 4. Define the API resource type using the GVK from the object. + // The plural name 'applications' is taken from your CRD definition. + error!("This only supports argocd application harcoded, very rrrong"); + let gvk = GroupVersionKind::gvk("argoproj.io", "v1alpha1", "Application"); + let api_resource = ApiResource::from_gvk_with_plural(&gvk, "applications"); + + // 5. Create a dynamic API client for this resource type. + let api: Api = + Api::namespaced_with(self.client.clone(), namespace, &api_resource); + + // 6. Apply the object to the cluster using Server-Side Apply. + // This will create the resource if it doesn't exist, or update it if it does. + println!( + "Applying Argo Application '{}' in namespace '{}'...", + name, namespace + ); + let patch_params = PatchParams::apply("harmony"); // Use a unique field manager name + let result = api.patch(name, &patch_params, &Patch::Apply(&obj)).await?; + + println!("Successfully applied '{}'.", result.name_any()); + + Ok(()) + } + pub(crate) async fn from_kubeconfig(path: &str) -> Option { let k = match Kubeconfig::read_from(path) { Ok(k) => k, diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 17cadd1..81e4546 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -246,7 +246,7 @@ pub struct K8sAnywhereConfig { /// /// default: true pub use_local_k3d: bool, - harmony_profile: String, + pub harmony_profile: String, } impl K8sAnywhereConfig { diff --git a/harmony/src/modules/application/features/argo_types.rs b/harmony/src/modules/application/features/argo_types.rs index bb69ac7..2c7a077 100644 --- a/harmony/src/modules/application/features/argo_types.rs +++ b/harmony/src/modules/application/features/argo_types.rs @@ -1,5 +1,7 @@ use std::{backtrace, collections::HashMap}; +use k8s_openapi::{Metadata, NamespaceResourceScope, Resource}; +use log::debug; use serde::Serialize; use serde_yaml::{Mapping, Value}; use url::Url; @@ -174,15 +176,15 @@ impl From for ArgoApplication { } impl ArgoApplication { - fn to_yaml(self) -> serde_yaml::Value { - let name = self.name; - let namespace = if let Some(ns) = self.namespace { - ns + pub fn to_yaml(&self) -> serde_yaml::Value { + let name = &self.name; + let namespace = if let Some(ns) = self.namespace.as_ref() { + &ns } else { - "argocd".to_string() + "argocd" }; - let project = self.project; - let source = self.source; + let project = &self.project; + let source = &self.source; let mut yaml_str = format!( r#" @@ -221,6 +223,7 @@ spec: .expect("couldn't serialize revision history to yaml string"), ); + debug!("yaml serialize of :\n{yaml_str}"); serde_yaml::from_str(&yaml_str).expect("Couldn't parse YAML") } } diff --git a/harmony/src/modules/application/features/continuous_delivery.rs b/harmony/src/modules/application/features/continuous_delivery.rs index 1dc0f69..2dade67 100644 --- a/harmony/src/modules/application/features/continuous_delivery.rs +++ b/harmony/src/modules/application/features/continuous_delivery.rs @@ -10,11 +10,14 @@ use crate::{ data::Version, inventory::Inventory, modules::{ - application::{Application, ApplicationFeature, HelmPackage, OCICompliant}, + application::{ + Application, ApplicationFeature, HelmPackage, OCICompliant, + features::{ArgoApplication, ArgoHelmScore}, + }, helm::chart::HelmChartScore, }, score::Score, - topology::{DeploymentTarget, HelmCommand, MultiTargetTopology, Topology, Url}, + topology::{DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology, Url}, }; /// ContinuousDelivery in Harmony provides this functionality : @@ -139,7 +142,7 @@ impl ContinuousDelivery { #[async_trait] impl< A: OCICompliant + HelmPackage + Clone + 'static, - T: Topology + HelmCommand + MultiTargetTopology + 'static, + T: Topology + HelmCommand + MultiTargetTopology + K8sclient + 'static, > ApplicationFeature for ContinuousDelivery { async fn ensure_installed(&self, topology: &T) -> Result<(), String> { @@ -153,9 +156,8 @@ impl< let helm_chart = self.application.build_push_helm_package(&image).await?; info!("Pushed new helm chart {helm_chart}"); - // let image = self.application.build_push_oci_image().await?; - // info!("Pushed new docker image {image}"); - error!("uncomment above"); + let image = self.application.build_push_oci_image().await?; + info!("Pushed new docker image {image}"); info!("Installing ContinuousDelivery feature"); // TODO this is a temporary hack for demo purposes, the deployment target should be driven @@ -178,29 +180,33 @@ impl< } target => { info!("Deploying to target {target:?}"); - let cd_server = HelmChartScore { - namespace: todo!( - "ArgoCD Helm chart with proper understanding of Tenant, see how Will did it for Monitoring for now" - ), - release_name: todo!("argocd helm chart whatever"), - chart_name: todo!(), - chart_version: todo!(), - values_overrides: todo!(), - values_yaml: todo!(), - create_namespace: todo!(), - install_only: todo!(), - repository: todo!(), + let score = ArgoHelmScore { + namespace: "harmonydemo-staging".to_string(), + openshift: true, + domain: "argo.harmonydemo.apps.st.mcd".to_string(), + argo_apps: vec![ArgoApplication::from(CDApplicationConfig { + // helm pull oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart/harmony-example-rust-webapp-chart --version 0.1.0 + version: Version::from("0.1.0").unwrap(), + helm_chart_repo_url: Url::Url(url::Url::parse("oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart/harmony-example-rust-webapp-chart").unwrap()), + helm_chart_name: "harmony-example-rust-webapp-chart".to_string(), + values_overrides: Value::Null, + name: "harmony-demo-rust-webapp".to_string(), + namespace: "harmonydemo-staging".to_string(), + })], }; - let interpret = cd_server.create_interpret(); - interpret.execute(&Inventory::empty(), topology); + score + .create_interpret() + .execute(&Inventory::empty(), topology) + .await + .unwrap(); } }; todo!("1. Create ArgoCD score that installs argo using helm chart, see if Taha's already done it - [X] Package app (docker image, helm chart) - [X] Push to registry - - [ ] Push only if staging or prod - - [ ] Deploy to local k3d when target is local + - [X] Push only if staging or prod + - [X] Deploy to local k3d when target is local - [ ] Poke Argo - [ ] Ensure app is up") } diff --git a/harmony/src/modules/application/features/helm_argocd_score.rs b/harmony/src/modules/application/features/helm_argocd_score.rs index ab9cd1e..92a468b 100644 --- a/harmony/src/modules/application/features/helm_argocd_score.rs +++ b/harmony/src/modules/application/features/helm_argocd_score.rs @@ -1,9 +1,89 @@ +use async_trait::async_trait; +use k8s_openapi::Resource; use non_blank_string_rs::NonBlankString; +use serde::Serialize; use std::str::FromStr; -use crate::modules::helm::chart::{HelmChartScore, HelmRepository}; +use crate::{ + data::{Id, Version}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + modules::helm::chart::{HelmChartScore, HelmRepository}, + score::Score, + topology::{HelmCommand, K8sclient, Topology}, +}; -pub fn argo_helm_chart_score(namespace: String, openshift: bool, domain: String) -> HelmChartScore { +use super::ArgoApplication; + +#[derive(Debug, Serialize, Clone)] +pub struct ArgoHelmScore { + pub namespace: String, + pub openshift: bool, + pub domain: String, + pub argo_apps: Vec, +} + +impl Score for ArgoHelmScore { + fn create_interpret(&self) -> Box> { + let helm_score = argo_helm_chart_score(&self.namespace, self.openshift, &self.domain); + Box::new(ArgoInterpret { + score: helm_score, + argo_apps: self.argo_apps.clone(), + }) + } + + fn name(&self) -> String { + "ArgoHelmScore".to_string() + } +} + +#[derive(Debug)] +pub struct ArgoInterpret { + score: HelmChartScore, + argo_apps: Vec, +} + +#[async_trait] +impl Interpret for ArgoInterpret { + async fn execute( + &self, + inventory: &Inventory, + topology: &T, + ) -> Result { + self.score + .create_interpret() + .execute(inventory, topology) + .await?; + + let k8s_client = topology.k8s_client().await?; + k8s_client + .apply_yaml_many(&self.argo_apps.iter().map(|a| a.to_yaml()).collect(), None) + .await + .unwrap(); + Ok(Outcome::success(format!( + "Successfully installed ArgoCD and {} Applications", + self.argo_apps.len() + ))) + } + + fn get_name(&self) -> InterpretName { + InterpretName::ArgoCD + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + todo!() + } +} + +pub fn argo_helm_chart_score(namespace: &str, openshift: bool, domain: &str) -> HelmChartScore { let values = format!( r#" # -- Create aggregated roles that extend existing cluster roles to interact with argo-cd resources