feat/cd/localdeploymentdemo #79
							
								
								
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -1765,6 +1765,7 @@ dependencies = [ | |||||||
|  "strum 0.27.1", |  "strum 0.27.1", | ||||||
|  "temp-dir", |  "temp-dir", | ||||||
|  "temp-file", |  "temp-file", | ||||||
|  |  "tempfile", | ||||||
|  "tokio", |  "tokio", | ||||||
|  "tokio-util", |  "tokio-util", | ||||||
|  "url", |  "url", | ||||||
|  | |||||||
| @ -57,3 +57,4 @@ similar.workspace = true | |||||||
| futures-util = "0.3.31" | futures-util = "0.3.31" | ||||||
| tokio-util = "0.7.15" | tokio-util = "0.7.15" | ||||||
| strum = { version = "0.27.1", features = ["derive"] } | strum = { version = "0.27.1", features = ["derive"] } | ||||||
|  | tempfile = "3.20.0" | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ use crate::{ | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| use super::{ | use super::{ | ||||||
|     HelmCommand, K8sclient, Topology, |     DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology, | ||||||
|     k8s::K8sClient, |     k8s::K8sClient, | ||||||
|     tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager}, |     tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager}, | ||||||
| }; | }; | ||||||
| @ -246,6 +246,7 @@ pub struct K8sAnywhereConfig { | |||||||
|     ///
 |     ///
 | ||||||
|     /// default: true
 |     /// default: true
 | ||||||
|     pub use_local_k3d: bool, |     pub use_local_k3d: bool, | ||||||
|  |     harmony_profile: String, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl K8sAnywhereConfig { | impl K8sAnywhereConfig { | ||||||
| @ -256,6 +257,11 @@ impl K8sAnywhereConfig { | |||||||
|                 .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)), |                 .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)), | ||||||
|             autoinstall: std::env::var("HARMONY_AUTOINSTALL") |             autoinstall: std::env::var("HARMONY_AUTOINSTALL") | ||||||
|                 .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)), |                 .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") |             use_local_k3d: std::env::var("HARMONY_USE_LOCAL_K3D") | ||||||
|                 .map_or_else(|_| true, |v| v.parse().ok().unwrap_or(true)), |                 .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 {} | impl HelmCommand for K8sAnywhereTopology {} | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
|  | |||||||
| @ -62,6 +62,17 @@ pub trait Topology: Send + Sync { | |||||||
|     async fn ensure_ready(&self) -> Result<Outcome, InterpretError>; |     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; | pub type IpAddress = IpAddr; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone)] | #[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 async_trait::async_trait; | ||||||
| use log::{error, info}; | use log::{error, info}; | ||||||
| use serde_json::Value; | use serde_json::Value; | ||||||
|  | use tempfile::NamedTempFile; | ||||||
| 
 | 
 | ||||||
| use crate::{ | use crate::{ | ||||||
|     data::Version, |     data::Version, | ||||||
| @ -12,7 +13,7 @@ use crate::{ | |||||||
|         helm::chart::HelmChartScore, |         helm::chart::HelmChartScore, | ||||||
|     }, |     }, | ||||||
|     score::Score, |     score::Score, | ||||||
|     topology::{HelmCommand, Topology, Url}, |     topology::{DeploymentTarget, HelmCommand, MultiTargetTopology, Topology, Url}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /// ContinuousDelivery in Harmony provides this functionality :
 | /// ContinuousDelivery in Harmony provides this functionality :
 | ||||||
| @ -47,9 +48,95 @@ pub struct ContinuousDelivery<A: OCICompliant + HelmPackage> { | |||||||
|     pub application: Arc<A>, |     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] | #[async_trait] | ||||||
| impl<A: OCICompliant + HelmPackage + Clone + 'static, T: Topology + HelmCommand + 'static> | impl< | ||||||
|     ApplicationFeature<T> for ContinuousDelivery<A> |     A: OCICompliant + HelmPackage + Clone + 'static, | ||||||
|  |     T: Topology + HelmCommand + MultiTargetTopology + 'static, | ||||||
|  | > ApplicationFeature<T> for ContinuousDelivery<A> | ||||||
| { | { | ||||||
|     async fn ensure_installed(&self, topology: &T) -> Result<(), String> { |     async fn ensure_installed(&self, topology: &T) -> Result<(), String> { | ||||||
|         let image = self.application.image_name(); |         let image = self.application.image_name(); | ||||||
| @ -66,27 +153,51 @@ impl<A: OCICompliant + HelmPackage + Clone + 'static, T: Topology + HelmCommand | |||||||
|         info!("Pushed new docker image {image}"); |         info!("Pushed new docker image {image}"); | ||||||
| 
 | 
 | ||||||
|         info!("Installing ContinuousDelivery feature"); |         info!("Installing ContinuousDelivery feature"); | ||||||
|         let cd_server = HelmChartScore { |         // TODO this is a temporary hack for demo purposes, the deployment target should be driven
 | ||||||
|             namespace: todo!( |         // by the topology only and we should not have to know how to perform tasks like this for
 | ||||||
|                 "ArgoCD Helm chart with proper understanding of Tenant, see how Will did it for Monitoring for now" |         // which the topology should be responsible.
 | ||||||
|             ), |         //
 | ||||||
|             release_name: todo!("argocd helm chart whatever"), |         // That said, this will require some careful architectural decisions, since the concept of
 | ||||||
|             chart_name: todo!(), |         // deployment targets / profiles is probably a layer of complexity that we won't be
 | ||||||
|             chart_version: todo!(), |         // completely able to avoid
 | ||||||
|             values_overrides: todo!(), |         //
 | ||||||
|             values_yaml: todo!(), |         // I'll try something for now that must be thought through after : att a deployment_profile
 | ||||||
|             create_namespace: todo!(), |         // function to the topology trait that returns a profile, then anybody who needs it can
 | ||||||
|             install_only: todo!(), |         // access it. This forces every Topology to understand the concept of targets though... So
 | ||||||
|             repository: todo!(), |         // 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" | ||||||
|  |                     ), | ||||||
|  |                     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
 |         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) |             - [X] Package app (docker image, helm chart) | ||||||
|             3. Push to registry if staging or prod |             - [X] Push to registry | ||||||
|             4. Poke Argo |             - [ ] Push only if staging or prod | ||||||
|             5. Ensure app is up")
 |             - [ ] Deploy to local k3d when target is local | ||||||
|  |             - [ ] Poke Argo | ||||||
|  |             - [ ] Ensure app is up")
 | ||||||
|     } |     } | ||||||
|     fn name(&self) -> String { |     fn name(&self) -> String { | ||||||
|         "ContinuousDelivery".to_string() |         "ContinuousDelivery".to_string() | ||||||
|  | |||||||
| @ -381,7 +381,7 @@ service: | |||||||
|   port: 80 |   port: 80 | ||||||
| 
 | 
 | ||||||
| ingress: | ingress: | ||||||
|   enabled: false |   enabled: true | ||||||
|   # Annotations for cert-manager to handle SSL. |   # Annotations for cert-manager to handle SSL. | ||||||
|   annotations: |   annotations: | ||||||
|     cert-manager.io/cluster-issuer: "letsencrypt-prod" |     cert-manager.io/cluster-issuer: "letsencrypt-prod" | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user