From 539b8299aee160872c2f5fb2db12516159b68c36 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 3 Jul 2025 11:55:10 -0400 Subject: [PATCH 1/2] feat(continuousdelivery): Local deployment implementation for demo purposes. Needs a lot of refactoring but it works (or almost works) --- Cargo.lock | 1 + harmony/Cargo.toml | 1 + harmony/src/domain/topology/k8s_anywhere.rs | 22 ++- harmony/src/domain/topology/mod.rs | 11 ++ .../features/continuous_delivery.rs | 155 +++++++++++++++--- harmony/src/modules/application/rust.rs | 2 +- 6 files changed, 168 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c29465e..94f83a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1765,6 +1765,7 @@ dependencies = [ "strum 0.27.1", "temp-dir", "temp-file", + "tempfile", "tokio", "tokio-util", "url", diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index 97ed693..729fe7e 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -57,3 +57,4 @@ similar.workspace = true futures-util = "0.3.31" tokio-util = "0.7.15" strum = { version = "0.27.1", features = ["derive"] } +tempfile = "3.20.0" diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 1265de6..17cadd1 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -16,7 +16,7 @@ use crate::{ }; use super::{ - HelmCommand, K8sclient, Topology, + DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology, k8s::K8sClient, tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager}, }; @@ -246,6 +246,7 @@ pub struct K8sAnywhereConfig { /// /// default: true pub use_local_k3d: bool, + harmony_profile: String, } impl K8sAnywhereConfig { @@ -256,6 +257,11 @@ impl K8sAnywhereConfig { .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)), autoinstall: std::env::var("HARMONY_AUTOINSTALL") .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)), + // TODO harmony_profile should be managed at a more core level than this + harmony_profile: std::env::var("HARMONY_PROFILE").map_or_else( + |_| "dev".to_string(), + |v| v.parse().ok().unwrap_or("dev".to_string()), + ), use_local_k3d: std::env::var("HARMONY_USE_LOCAL_K3D") .map_or_else(|_| true, |v| v.parse().ok().unwrap_or(true)), } @@ -292,6 +298,20 @@ impl Topology for K8sAnywhereTopology { } } +impl MultiTargetTopology for K8sAnywhereTopology { + fn current_target(&self) -> DeploymentTarget { + if self.config.use_local_k3d { + return DeploymentTarget::LocalDev; + } + + match self.config.harmony_profile.to_lowercase().as_str() { + "staging" => DeploymentTarget::Staging, + "production" => DeploymentTarget::Production, + _ => todo!("HARMONY_PROFILE must be set when use_local_k3d is not set"), + } + } +} + impl HelmCommand for K8sAnywhereTopology {} #[async_trait] diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index 7d3830d..c72a898 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -62,6 +62,17 @@ pub trait Topology: Send + Sync { async fn ensure_ready(&self) -> Result; } +#[derive(Debug)] +pub enum DeploymentTarget { + LocalDev, + Staging, + Production, +} + +pub trait MultiTargetTopology: Topology { + fn current_target(&self) -> DeploymentTarget; +} + pub type IpAddress = IpAddr; #[derive(Debug, Clone)] diff --git a/harmony/src/modules/application/features/continuous_delivery.rs b/harmony/src/modules/application/features/continuous_delivery.rs index a779498..2765c0d 100644 --- a/harmony/src/modules/application/features/continuous_delivery.rs +++ b/harmony/src/modules/application/features/continuous_delivery.rs @@ -1,8 +1,9 @@ -use std::sync::Arc; +use std::{io::Write, process::Command, sync::Arc}; use async_trait::async_trait; use log::{error, info}; use serde_json::Value; +use tempfile::NamedTempFile; use crate::{ data::Version, @@ -12,7 +13,7 @@ use crate::{ helm::chart::HelmChartScore, }, score::Score, - topology::{HelmCommand, Topology, Url}, + topology::{DeploymentTarget, HelmCommand, MultiTargetTopology, Topology, Url}, }; /// ContinuousDelivery in Harmony provides this functionality : @@ -47,9 +48,95 @@ pub struct ContinuousDelivery { pub application: Arc, } +impl ContinuousDelivery { + async fn deploy_to_local_k3d( + &self, + app_name: String, + chart_url: String, + image_name: String, + ) -> Result<(), String> { + error!( + "FIXME This works only with local k3d installations, which is fine only for current demo purposes. We assume usage of K8sAnywhereTopology" + ); + // --- 1. Import the container image into the k3d cluster --- + info!( + "Importing image '{}' into k3d cluster 'harmony'", + image_name + ); + let import_output = Command::new("k3d") + .args(["image", "import", &image_name, "--cluster", "harmony"]) + .output() + .map_err(|e| format!("Failed to execute k3d image import: {}", e))?; + + if !import_output.status.success() { + return Err(format!( + "Failed to import image to k3d: {}", + String::from_utf8_lossy(&import_output.stderr) + )); + } + + // --- 2. Get the kubeconfig for the k3d cluster and write it to a temp file --- + info!("Retrieving kubeconfig for k3d cluster 'harmony'"); + let kubeconfig_output = Command::new("k3d") + .args(["kubeconfig", "get", "harmony"]) + .output() + .map_err(|e| format!("Failed to execute k3d kubeconfig get: {}", e))?; + + if !kubeconfig_output.status.success() { + return Err(format!( + "Failed to get kubeconfig from k3d: {}", + String::from_utf8_lossy(&kubeconfig_output.stderr) + )); + } + + let mut temp_kubeconfig = NamedTempFile::new() + .map_err(|e| format!("Failed to create temp file for kubeconfig: {}", e))?; + temp_kubeconfig + .write_all(&kubeconfig_output.stdout) + .map_err(|e| format!("Failed to write to temp kubeconfig file: {}", e))?; + let kubeconfig_path = temp_kubeconfig.path().to_str().unwrap(); + + // --- 3. Install or upgrade the Helm chart in the cluster --- + info!( + "Deploying Helm chart '{}' to namespace '{}'", + chart_url, app_name + ); + let release_name = app_name.to_lowercase(); // Helm release names are often lowercase + let helm_output = Command::new("helm") + .args([ + "upgrade", + "--install", + &release_name, + &chart_url, + "--namespace", + &app_name, + "--create-namespace", + "--wait", // Wait for the deployment to be ready + "--kubeconfig", + kubeconfig_path, + ]) + .spawn() + .map_err(|e| format!("Failed to execute helm upgrade: {}", e))? + .wait_with_output() + .map_err(|e| format!("Failed to execute helm upgrade: {}", e))?; + + if !helm_output.status.success() { + return Err(format!( + "Failed to deploy Helm chart: {}", + String::from_utf8_lossy(&helm_output.stderr) + )); + } + + info!("Successfully deployed '{}' to local k3d cluster.", app_name); + Ok(()) + } +} + #[async_trait] -impl - ApplicationFeature for ContinuousDelivery +impl< + A: OCICompliant + HelmPackage + Clone + 'static, + T: Topology + HelmCommand + MultiTargetTopology + 'static, +> ApplicationFeature for ContinuousDelivery { async fn ensure_installed(&self, topology: &T) -> Result<(), String> { let image = self.application.image_name(); @@ -66,27 +153,51 @@ impl { + self.deploy_to_local_k3d(self.application.name(), helm_chart, image) + .await?; + } + 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 interpret = cd_server.create_interpret(); + interpret.execute(&Inventory::empty(), topology); + } }; - let interpret = cd_server.create_interpret(); - interpret.execute(&Inventory::empty(), topology); todo!("1. Create ArgoCD score that installs argo using helm chart, see if Taha's already done it - 2. Package app (docker image, helm chart) - 3. Push to registry if staging or prod - 4. Poke Argo - 5. Ensure app is up") + - [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 + - [ ] Poke Argo + - [ ] Ensure app is up") } fn name(&self) -> String { "ContinuousDelivery".to_string() diff --git a/harmony/src/modules/application/rust.rs b/harmony/src/modules/application/rust.rs index 39c48c5..045d7d4 100644 --- a/harmony/src/modules/application/rust.rs +++ b/harmony/src/modules/application/rust.rs @@ -381,7 +381,7 @@ service: port: 80 ingress: - enabled: false + enabled: true # Annotations for cert-manager to handle SSL. annotations: cert-manager.io/cluster-issuer: "letsencrypt-prod" -- 2.39.5 From d317c0ba765a6dcea7edb497ab48144696871f6c Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 3 Jul 2025 15:25:43 -0400 Subject: [PATCH 2/2] fix: Continuous delivery now works with rust example to deploy on local k3d, ingress and everything --- examples/rust/src/main.rs | 1 + harmony/src/domain/config.rs | 2 +- .../features/continuous_delivery.rs | 13 +++++++---- harmony/src/modules/application/rust.rs | 22 +++++++++---------- harmony/src/modules/k3d/install.rs | 4 ++-- 5 files changed, 24 insertions(+), 18 deletions(-) diff --git a/examples/rust/src/main.rs b/examples/rust/src/main.rs index e56a30f..235e30c 100644 --- a/examples/rust/src/main.rs +++ b/examples/rust/src/main.rs @@ -17,6 +17,7 @@ async fn main() { project_root: PathBuf::from("./examples/rust/webapp"), framework: Some(RustWebFramework::Leptos), }; + // TODO RustWebappScore should simply take a RustWebApp as config let app = RustWebappScore { name: "Example Rust Webapp".to_string(), domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()), diff --git a/harmony/src/domain/config.rs b/harmony/src/domain/config.rs index 7812616..20f08a2 100644 --- a/harmony/src/domain/config.rs +++ b/harmony/src/domain/config.rs @@ -2,7 +2,7 @@ use lazy_static::lazy_static; use std::path::PathBuf; lazy_static! { - pub static ref HARMONY_CONFIG_DIR: PathBuf = directories::BaseDirs::new() + pub static ref HARMONY_DATA_DIR: PathBuf = directories::BaseDirs::new() .unwrap() .data_dir() .join("harmony"); diff --git a/harmony/src/modules/application/features/continuous_delivery.rs b/harmony/src/modules/application/features/continuous_delivery.rs index 2765c0d..d9f5830 100644 --- a/harmony/src/modules/application/features/continuous_delivery.rs +++ b/harmony/src/modules/application/features/continuous_delivery.rs @@ -6,6 +6,7 @@ use serde_json::Value; use tempfile::NamedTempFile; use crate::{ + config::HARMONY_DATA_DIR, data::Version, inventory::Inventory, modules::{ @@ -58,12 +59,15 @@ impl ContinuousDelivery { error!( "FIXME This works only with local k3d installations, which is fine only for current demo purposes. We assume usage of K8sAnywhereTopology" ); + + error!("TODO hardcoded k3d bin path is wrong"); + let k3d_bin_path = (*HARMONY_DATA_DIR).join("k3d").join("k3d"); // --- 1. Import the container image into the k3d cluster --- info!( "Importing image '{}' into k3d cluster 'harmony'", image_name ); - let import_output = Command::new("k3d") + let import_output = Command::new(&k3d_bin_path) .args(["image", "import", &image_name, "--cluster", "harmony"]) .output() .map_err(|e| format!("Failed to execute k3d image import: {}", e))?; @@ -77,7 +81,7 @@ impl ContinuousDelivery { // --- 2. Get the kubeconfig for the k3d cluster and write it to a temp file --- info!("Retrieving kubeconfig for k3d cluster 'harmony'"); - let kubeconfig_output = Command::new("k3d") + let kubeconfig_output = Command::new(&k3d_bin_path) .args(["kubeconfig", "get", "harmony"]) .output() .map_err(|e| format!("Failed to execute k3d kubeconfig get: {}", e))?; @@ -149,8 +153,9 @@ 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}"); + // let image = self.application.build_push_oci_image().await?; + // info!("Pushed new docker image {image}"); + error!("uncomment above"); info!("Installing ContinuousDelivery feature"); // TODO this is a temporary hack for demo purposes, the deployment target should be driven diff --git a/harmony/src/modules/application/rust.rs b/harmony/src/modules/application/rust.rs index 045d7d4..4a60ac6 100644 --- a/harmony/src/modules/application/rust.rs +++ b/harmony/src/modules/application/rust.rs @@ -378,7 +378,7 @@ image: service: type: ClusterIP - port: 80 + port: 3000 ingress: enabled: true @@ -432,7 +432,7 @@ spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} - targetPort: http + targetPort: 3000 protocol: TCP name: http selector: @@ -462,7 +462,7 @@ spec: imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http - containerPort: 8080 # Assuming the rust app listens on 8080 + containerPort: 3000 protocol: TCP "#; fs::write(templates_dir.join("deployment.yaml"), deployment_yaml)?; @@ -499,7 +499,7 @@ spec: service: name: {{ include "chart.fullname" $ }} port: - name: http + number: 3000 {{- end }} {{- end }} {{- end }} @@ -549,25 +549,25 @@ spec: ) -> Result> { // The chart name is the file stem of the .tgz file let chart_file_name = packaged_chart_path.file_stem().unwrap().to_str().unwrap(); - let oci_url = format!( - "oci://{}/{}/{}-chart", - *REGISTRY_URL, *REGISTRY_PROJECT, self.name - ); + let oci_push_url = format!("oci://{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT); + let oci_pull_url = format!("{oci_push_url}/{}-chart", self.name); info!( "Pushing Helm chart {} to {}", packaged_chart_path.to_string_lossy(), - oci_url + oci_push_url ); let output = process::Command::new("helm") - .args(["push", packaged_chart_path.to_str().unwrap(), &oci_url]) + .args(["push", packaged_chart_path.to_str().unwrap(), &oci_push_url]) .output()?; self.check_output(&output, "Pushing Helm chart failed")?; // The final URL includes the version tag, which is part of the file name let version = chart_file_name.rsplit_once('-').unwrap().1; - Ok(format!("{}:{}", oci_url, version)) + debug!("pull url {oci_pull_url}"); + debug!("push url {oci_push_url}"); + Ok(format!("{}:{}", oci_pull_url, version)) } } diff --git a/harmony/src/modules/k3d/install.rs b/harmony/src/modules/k3d/install.rs index f825f2e..18b91a0 100644 --- a/harmony/src/modules/k3d/install.rs +++ b/harmony/src/modules/k3d/install.rs @@ -5,7 +5,7 @@ use log::info; use serde::Serialize; use crate::{ - config::HARMONY_CONFIG_DIR, + config::HARMONY_DATA_DIR, data::{Id, Version}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, @@ -22,7 +22,7 @@ pub struct K3DInstallationScore { impl Default for K3DInstallationScore { fn default() -> Self { Self { - installation_path: HARMONY_CONFIG_DIR.join("k3d"), + installation_path: HARMONY_DATA_DIR.join("k3d"), cluster_name: "harmony".to_string(), } } -- 2.39.5