feat: create Argo interpret and kube client apply_yaml to install Argo Applications. Very messy implementation though, must be refactored/improved

This commit is contained in:
Jean-Gabriel Gill-Couture 2025-07-04 09:48:17 -04:00
parent d9935e20cb
commit 6149249a6c
7 changed files with 235 additions and 34 deletions

View File

@ -22,6 +22,7 @@ pub enum InterpretName {
K3dInstallation, K3dInstallation,
TenantInterpret, TenantInterpret,
Application, Application,
ArgoCD,
} }
impl std::fmt::Display for InterpretName { impl std::fmt::Display for InterpretName {
@ -39,6 +40,7 @@ impl std::fmt::Display for InterpretName {
InterpretName::K3dInstallation => f.write_str("K3dInstallation"), InterpretName::K3dInstallation => f.write_str("K3dInstallation"),
InterpretName::TenantInterpret => f.write_str("Tenant"), InterpretName::TenantInterpret => f.write_str("Tenant"),
InterpretName::Application => f.write_str("Application"), InterpretName::Application => f.write_str("Application"),
InterpretName::ArgoCD => f.write_str("ArgoCD"),
} }
} }
} }

View File

@ -0,0 +1,59 @@
////////////////////
/// Working idea
///
///
trait ScoreWithDep<T> {
fn create_interpret(&self) -> Box<dyn Interpret<T>>;
fn name(&self) -> String;
fn get_dependencies(&self) -> Vec<TypeId>; // Force T to impl Installer<TypeId> or something
// like that
}
struct PrometheusAlertScore;
impl <T> ScoreWithDep<T> for PrometheusAlertScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
todo!()
}
fn name(&self) -> String {
todo!()
}
fn get_dependencies(&self) -> Vec<TypeId> {
// 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 <T: HelmCommand> Installable<T> for
// KubePrometheus calls for HelmCommand.
vec![TypeId::of::<KubePrometheus>()]
}
}
trait Installable{}
struct KubePrometheus;
impl Installable for KubePrometheus;
struct Maestro<T> {
topology: T
}
impl <T>Maestro<T> {
fn execute_store(&self, score: ScoreWithDep<T>) {
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
}
}

View File

@ -4,8 +4,6 @@ use k8s_openapi::{
ClusterResourceScope, NamespaceResourceScope, ClusterResourceScope, NamespaceResourceScope,
api::{apps::v1::Deployment, core::v1::Pod}, api::{apps::v1::Deployment, core::v1::Pod},
}; };
use kube::runtime::conditions;
use kube::runtime::wait::await_condition;
use kube::{ use kube::{
Client, Config, Error, Resource, Client, Config, Error, Resource,
api::{Api, AttachParams, ListParams, Patch, PatchParams, ResourceExt}, api::{Api, AttachParams, ListParams, Patch, PatchParams, ResourceExt},
@ -13,6 +11,11 @@ use kube::{
core::ErrorResponse, core::ErrorResponse,
runtime::reflector::Lookup, runtime::reflector::Lookup,
}; };
use kube::{api::DynamicObject, runtime::conditions};
use kube::{
api::{ApiResource, GroupVersionKind},
runtime::wait::await_condition,
};
use log::{debug, error, trace}; use log::{debug, error, trace};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use similar::{DiffableStr, TextDiff}; use similar::{DiffableStr, TextDiff};
@ -239,6 +242,54 @@ impl K8sClient {
Ok(result) Ok(result)
} }
pub async fn apply_yaml_many(
&self,
yaml: &Vec<serde_yaml::Value>,
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<DynamicObject> =
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<K8sClient> { pub(crate) async fn from_kubeconfig(path: &str) -> Option<K8sClient> {
let k = match Kubeconfig::read_from(path) { let k = match Kubeconfig::read_from(path) {
Ok(k) => k, Ok(k) => k,

View File

@ -246,7 +246,7 @@ pub struct K8sAnywhereConfig {
/// ///
/// default: true /// default: true
pub use_local_k3d: bool, pub use_local_k3d: bool,
harmony_profile: String, pub harmony_profile: String,
} }
impl K8sAnywhereConfig { impl K8sAnywhereConfig {

View File

@ -1,5 +1,7 @@
use std::{backtrace, collections::HashMap}; use std::{backtrace, collections::HashMap};
use k8s_openapi::{Metadata, NamespaceResourceScope, Resource};
use log::debug;
use serde::Serialize; use serde::Serialize;
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
use url::Url; use url::Url;
@ -174,15 +176,15 @@ impl From<CDApplicationConfig> for ArgoApplication {
} }
impl ArgoApplication { impl ArgoApplication {
fn to_yaml(self) -> serde_yaml::Value { pub fn to_yaml(&self) -> serde_yaml::Value {
let name = self.name; let name = &self.name;
let namespace = if let Some(ns) = self.namespace { let namespace = if let Some(ns) = self.namespace.as_ref() {
ns &ns
} else { } else {
"argocd".to_string() "argocd"
}; };
let project = self.project; let project = &self.project;
let source = self.source; let source = &self.source;
let mut yaml_str = format!( let mut yaml_str = format!(
r#" r#"
@ -221,6 +223,7 @@ spec:
.expect("couldn't serialize revision history to yaml string"), .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") serde_yaml::from_str(&yaml_str).expect("Couldn't parse YAML")
} }
} }

View File

@ -10,11 +10,14 @@ use crate::{
data::Version, data::Version,
inventory::Inventory, inventory::Inventory,
modules::{ modules::{
application::{Application, ApplicationFeature, HelmPackage, OCICompliant}, application::{
Application, ApplicationFeature, HelmPackage, OCICompliant,
features::{ArgoApplication, ArgoHelmScore},
},
helm::chart::HelmChartScore, helm::chart::HelmChartScore,
}, },
score::Score, score::Score,
topology::{DeploymentTarget, HelmCommand, MultiTargetTopology, Topology, Url}, topology::{DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology, Url},
}; };
/// ContinuousDelivery in Harmony provides this functionality : /// ContinuousDelivery in Harmony provides this functionality :
@ -139,7 +142,7 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
#[async_trait] #[async_trait]
impl< impl<
A: OCICompliant + HelmPackage + Clone + 'static, A: OCICompliant + HelmPackage + Clone + 'static,
T: Topology + HelmCommand + MultiTargetTopology + 'static, T: Topology + HelmCommand + MultiTargetTopology + K8sclient + 'static,
> ApplicationFeature<T> for ContinuousDelivery<A> > ApplicationFeature<T> for ContinuousDelivery<A>
{ {
async fn ensure_installed(&self, topology: &T) -> Result<(), String> { 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?; let helm_chart = self.application.build_push_helm_package(&image).await?;
info!("Pushed new helm chart {helm_chart}"); info!("Pushed new helm chart {helm_chart}");
// let image = self.application.build_push_oci_image().await?; let image = self.application.build_push_oci_image().await?;
// info!("Pushed new docker image {image}"); info!("Pushed new docker image {image}");
error!("uncomment above");
info!("Installing ContinuousDelivery feature"); info!("Installing ContinuousDelivery feature");
// TODO this is a temporary hack for demo purposes, the deployment target should be driven // TODO this is a temporary hack for demo purposes, the deployment target should be driven
@ -178,29 +180,33 @@ impl<
} }
target => { target => {
info!("Deploying to target {target:?}"); info!("Deploying to target {target:?}");
let cd_server = HelmChartScore { let score = ArgoHelmScore {
namespace: todo!( namespace: "harmonydemo-staging".to_string(),
"ArgoCD Helm chart with proper understanding of Tenant, see how Will did it for Monitoring for now" openshift: true,
), domain: "argo.harmonydemo.apps.st.mcd".to_string(),
release_name: todo!("argocd helm chart whatever"), argo_apps: vec![ArgoApplication::from(CDApplicationConfig {
chart_name: todo!(), // helm pull oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart/harmony-example-rust-webapp-chart --version 0.1.0
chart_version: todo!(), version: Version::from("0.1.0").unwrap(),
values_overrides: todo!(), 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()),
values_yaml: todo!(), helm_chart_name: "harmony-example-rust-webapp-chart".to_string(),
create_namespace: todo!(), values_overrides: Value::Null,
install_only: todo!(), name: "harmony-demo-rust-webapp".to_string(),
repository: todo!(), namespace: "harmonydemo-staging".to_string(),
})],
}; };
let interpret = cd_server.create_interpret(); score
interpret.execute(&Inventory::empty(), topology); .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 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] Package app (docker image, helm chart)
- [X] Push to registry - [X] Push to registry
- [ ] Push only if staging or prod - [X] Push only if staging or prod
- [ ] Deploy to local k3d when target is local - [X] Deploy to local k3d when target is local
- [ ] Poke Argo - [ ] Poke Argo
- [ ] Ensure app is up") - [ ] Ensure app is up")
} }

View File

@ -1,9 +1,89 @@
use async_trait::async_trait;
use k8s_openapi::Resource;
use non_blank_string_rs::NonBlankString; use non_blank_string_rs::NonBlankString;
use serde::Serialize;
use std::str::FromStr; 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<ArgoApplication>,
}
impl<T: Topology + HelmCommand + K8sclient> Score<T> for ArgoHelmScore {
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
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<ArgoApplication>,
}
#[async_trait]
impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for ArgoInterpret {
async fn execute(
&self,
inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
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<Id> {
todo!()
}
}
pub fn argo_helm_chart_score(namespace: &str, openshift: bool, domain: &str) -> HelmChartScore {
let values = format!( let values = format!(
r#" r#"
# -- Create aggregated roles that extend existing cluster roles to interact with argo-cd resources # -- Create aggregated roles that extend existing cluster roles to interact with argo-cd resources