feat/cd/localdeploymentdemo #79
							
								
								
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -1765,6 +1765,7 @@ dependencies = [ | ||||
|  "strum 0.27.1", | ||||
|  "temp-dir", | ||||
|  "temp-file", | ||||
|  "tempfile", | ||||
|  "tokio", | ||||
|  "tokio-util", | ||||
|  "url", | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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] | ||||
|  | ||||
| @ -62,6 +62,17 @@ pub trait Topology: Send + Sync { | ||||
|     async fn ensure_ready(&self) -> Result<Outcome, InterpretError>; | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub enum DeploymentTarget { | ||||
|     LocalDev, | ||||
|     Staging, | ||||
|     Production, | ||||
| } | ||||
| 
 | ||||
| pub trait MultiTargetTopology: Topology { | ||||
|     fn current_target(&self) -> DeploymentTarget; | ||||
| } | ||||
| 
 | ||||
| pub type IpAddress = IpAddr; | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
|  | ||||
| @ -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<A: OCICompliant + HelmPackage> { | ||||
|     pub application: Arc<A>, | ||||
| } | ||||
| 
 | ||||
| impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> { | ||||
|     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<A: OCICompliant + HelmPackage + Clone + 'static, T: Topology + HelmCommand + 'static> | ||||
|     ApplicationFeature<T> for ContinuousDelivery<A> | ||||
| impl< | ||||
|     A: OCICompliant + HelmPackage + Clone + 'static, | ||||
|     T: Topology + HelmCommand + MultiTargetTopology + 'static, | ||||
| > ApplicationFeature<T> for ContinuousDelivery<A> | ||||
| { | ||||
|     async fn ensure_installed(&self, topology: &T) -> Result<(), String> { | ||||
|         let image = self.application.image_name(); | ||||
| @ -66,6 +153,26 @@ impl<A: OCICompliant + HelmPackage + Clone + 'static, T: Topology + HelmCommand | ||||
|         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
 | ||||
|         // by the topology only and we should not have to know how to perform tasks like this for
 | ||||
|         // which the topology should be responsible.
 | ||||
|         //
 | ||||
|         // That said, this will require some careful architectural decisions, since the concept of
 | ||||
|         // deployment targets / profiles is probably a layer of complexity that we won't be
 | ||||
|         // completely able to avoid
 | ||||
|         //
 | ||||
|         // I'll try something for now that must be thought through after : att a deployment_profile
 | ||||
|         // function to the topology trait that returns a profile, then anybody who needs it can
 | ||||
|         // access it. This forces every Topology to understand the concept of targets though... So
 | ||||
|         // instead I'll create a new Capability which is MultiTargetTopology and we'll see how it
 | ||||
|         // goes. It still does not feel right though.
 | ||||
|         match topology.current_target() { | ||||
|             DeploymentTarget::LocalDev => { | ||||
|                 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" | ||||
| @ -81,12 +188,16 @@ impl<A: OCICompliant + HelmPackage + Clone + 'static, T: Topology + HelmCommand | ||||
|                 }; | ||||
|                 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() | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user