fix: make sure demo works on both local & remote target #107
| @ -2,6 +2,8 @@ use log::debug; | |||||||
| use once_cell::sync::Lazy; | use once_cell::sync::Lazy; | ||||||
| use tokio::sync::broadcast; | use tokio::sync::broadcast; | ||||||
| 
 | 
 | ||||||
|  | use crate::modules::application::ApplicationFeatureStatus; | ||||||
|  | 
 | ||||||
| use super::{ | use super::{ | ||||||
|     interpret::{InterpretError, Outcome}, |     interpret::{InterpretError, Outcome}, | ||||||
|     topology::TopologyStatus, |     topology::TopologyStatus, | ||||||
| @ -30,6 +32,12 @@ pub enum HarmonyEvent { | |||||||
|         status: TopologyStatus, |         status: TopologyStatus, | ||||||
|         message: Option<String>, |         message: Option<String>, | ||||||
|     }, |     }, | ||||||
|  |     ApplicationFeatureStateChanged { | ||||||
|  |         topology: String, | ||||||
|  |         application: String, | ||||||
|  |         feature: String, | ||||||
|  |         status: ApplicationFeatureStatus, | ||||||
|  |     }, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static HARMONY_EVENT_BUS: Lazy<broadcast::Sender<HarmonyEvent>> = Lazy::new(|| { | static HARMONY_EVENT_BUS: Lazy<broadcast::Sender<HarmonyEvent>> = Lazy::new(|| { | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| use std::{io::Write, process::Command, sync::Arc}; | use std::{io::Write, process::Command, sync::Arc}; | ||||||
| 
 | 
 | ||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| use log::{debug, error}; | use log::info; | ||||||
| use serde_yaml::Value; | use serde_yaml::Value; | ||||||
| use tempfile::NamedTempFile; | use tempfile::NamedTempFile; | ||||||
| 
 | 
 | ||||||
| @ -56,14 +56,11 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> { | |||||||
|         chart_url: String, |         chart_url: String, | ||||||
|         image_name: String, |         image_name: String, | ||||||
|     ) -> Result<(), String> { |     ) -> Result<(), String> { | ||||||
|         error!( |         // TODO: This works only with local k3d installations, which is fine only for current demo purposes. We assume usage of K8sAnywhereTopology"
 | ||||||
|             "FIXME This works only with local k3d installations, which is fine only for current demo purposes. We assume usage of K8sAnywhereTopology" |         // https://git.nationtech.io/NationTech/harmony/issues/106
 | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         error!("TODO hardcoded k3d bin path is wrong"); |  | ||||||
|         let k3d_bin_path = (*HARMONY_DATA_DIR).join("k3d").join("k3d"); |         let k3d_bin_path = (*HARMONY_DATA_DIR).join("k3d").join("k3d"); | ||||||
|         // --- 1. Import the container image into the k3d cluster ---
 |         // --- 1. Import the container image into the k3d cluster ---
 | ||||||
|         debug!( |         info!( | ||||||
|             "Importing image '{}' into k3d cluster 'harmony'", |             "Importing image '{}' into k3d cluster 'harmony'", | ||||||
|             image_name |             image_name | ||||||
|         ); |         ); | ||||||
| @ -80,7 +77,7 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // --- 2. Get the kubeconfig for the k3d cluster and write it to a temp file ---
 |         // --- 2. Get the kubeconfig for the k3d cluster and write it to a temp file ---
 | ||||||
|         debug!("Retrieving kubeconfig for k3d cluster 'harmony'"); |         info!("Retrieving kubeconfig for k3d cluster 'harmony'"); | ||||||
|         let kubeconfig_output = Command::new(&k3d_bin_path) |         let kubeconfig_output = Command::new(&k3d_bin_path) | ||||||
|             .args(["kubeconfig", "get", "harmony"]) |             .args(["kubeconfig", "get", "harmony"]) | ||||||
|             .output() |             .output() | ||||||
| @ -101,7 +98,7 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> { | |||||||
|         let kubeconfig_path = temp_kubeconfig.path().to_str().unwrap(); |         let kubeconfig_path = temp_kubeconfig.path().to_str().unwrap(); | ||||||
| 
 | 
 | ||||||
|         // --- 3. Install or upgrade the Helm chart in the cluster ---
 |         // --- 3. Install or upgrade the Helm chart in the cluster ---
 | ||||||
|         debug!( |         info!( | ||||||
|             "Deploying Helm chart '{}' to namespace '{}'", |             "Deploying Helm chart '{}' to namespace '{}'", | ||||||
|             chart_url, app_name |             chart_url, app_name | ||||||
|         ); |         ); | ||||||
| @ -131,7 +128,7 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> { | |||||||
|             )); |             )); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         debug!("Successfully deployed '{}' to local k3d cluster.", app_name); |         info!("Successfully deployed '{}' to local k3d cluster.", app_name); | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -151,14 +148,12 @@ impl< | |||||||
|         // Or ask for it when unknown
 |         // Or ask for it when unknown
 | ||||||
| 
 | 
 | ||||||
|         let helm_chart = self.application.build_push_helm_package(&image).await?; |         let helm_chart = self.application.build_push_helm_package(&image).await?; | ||||||
|         debug!("Pushed new helm chart {helm_chart}"); |  | ||||||
| 
 | 
 | ||||||
|         error!("TODO Make building image configurable/skippable if image already exists (prompt)"); |         // TODO: Make building image configurable/skippable if image already exists (prompt)")
 | ||||||
|  |         // https://git.nationtech.io/NationTech/harmony/issues/104
 | ||||||
|         let image = self.application.build_push_oci_image().await?; |         let image = self.application.build_push_oci_image().await?; | ||||||
|         debug!("Pushed new docker image {image}"); |  | ||||||
| 
 | 
 | ||||||
|         debug!("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
 |  | ||||||
|         // by the topology only and we should not have to know how to perform tasks like this for
 |         // by the topology only and we should not have to know how to perform tasks like this for
 | ||||||
|         // which the topology should be responsible.
 |         // which the topology should be responsible.
 | ||||||
|         //
 |         //
 | ||||||
| @ -171,17 +166,20 @@ impl< | |||||||
|         // access it. This forces every Topology to understand the concept of targets though... So
 |         // 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
 |         // instead I'll create a new Capability which is MultiTargetTopology and we'll see how it
 | ||||||
|         // goes. It still does not feel right though.
 |         // goes. It still does not feel right though.
 | ||||||
|  |         //
 | ||||||
|  |         // https://git.nationtech.io/NationTech/harmony/issues/106
 | ||||||
|         match topology.current_target() { |         match topology.current_target() { | ||||||
|             DeploymentTarget::LocalDev => { |             DeploymentTarget::LocalDev => { | ||||||
|  |                 info!("Deploying {} locally...", self.application.name()); | ||||||
|                 self.deploy_to_local_k3d(self.application.name(), helm_chart, image) |                 self.deploy_to_local_k3d(self.application.name(), helm_chart, image) | ||||||
|                     .await?; |                     .await?; | ||||||
|             } |             } | ||||||
|             target => { |             target => { | ||||||
|                 debug!("Deploying to target {target:?}"); |                 info!("Deploying {} to target {target:?}", self.application.name()); | ||||||
|                 let score = ArgoHelmScore { |                 let score = ArgoHelmScore { | ||||||
|                     namespace: "harmonydemo-staging".to_string(), |                     namespace: "harmony-example-rust-webapp".to_string(), | ||||||
|                     openshift: false, |                     openshift: false, | ||||||
|                     domain: "argo.harmonydemo.apps.st.mcd".to_string(), |                     domain: "argo.harmonydemo.apps.ncd0.harmony.mcd".to_string(), | ||||||
|                     argo_apps: vec![ArgoApplication::from(CDApplicationConfig { |                     argo_apps: vec![ArgoApplication::from(CDApplicationConfig { | ||||||
|                         // helm pull oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart --version 0.1.0
 |                         // helm pull oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart --version 0.1.0
 | ||||||
|                         version: Version::from("0.1.0").unwrap(), |                         version: Version::from("0.1.0").unwrap(), | ||||||
| @ -189,7 +187,7 @@ impl< | |||||||
|                         helm_chart_name: "harmony-example-rust-webapp-chart".to_string(), |                         helm_chart_name: "harmony-example-rust-webapp-chart".to_string(), | ||||||
|                         values_overrides: None, |                         values_overrides: None, | ||||||
|                         name: "harmony-demo-rust-webapp".to_string(), |                         name: "harmony-demo-rust-webapp".to_string(), | ||||||
|                         namespace: "harmonydemo-staging".to_string(), |                         namespace: "harmony-example-rust-webapp".to_string(), | ||||||
|                     })], |                     })], | ||||||
|                 }; |                 }; | ||||||
|                 score |                 score | ||||||
|  | |||||||
| @ -1,5 +1,4 @@ | |||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| use log::error; |  | ||||||
| use non_blank_string_rs::NonBlankString; | use non_blank_string_rs::NonBlankString; | ||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| use std::str::FromStr; | use std::str::FromStr; | ||||||
| @ -50,7 +49,6 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for ArgoInterpret { | |||||||
|         inventory: &Inventory, |         inventory: &Inventory, | ||||||
|         topology: &T, |         topology: &T, | ||||||
|     ) -> Result<Outcome, InterpretError> { |     ) -> Result<Outcome, InterpretError> { | ||||||
|         error!("Uncomment below, only disabled for debugging"); |  | ||||||
|         self.score.interpret(inventory, topology).await?; |         self.score.interpret(inventory, topology).await?; | ||||||
| 
 | 
 | ||||||
|         let k8s_client = topology.k8s_client().await?; |         let k8s_client = topology.k8s_client().await?; | ||||||
| @ -58,9 +56,14 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for ArgoInterpret { | |||||||
|             .apply_yaml_many(&self.argo_apps.iter().map(|a| a.to_yaml()).collect(), None) |             .apply_yaml_many(&self.argo_apps.iter().map(|a| a.to_yaml()).collect(), None) | ||||||
|             .await |             .await | ||||||
|             .unwrap(); |             .unwrap(); | ||||||
|  | 
 | ||||||
|         Ok(Outcome::success(format!( |         Ok(Outcome::success(format!( | ||||||
|             "ArgoCD installed with {} applications", |             "ArgoCD installed with {} {}", | ||||||
|             self.argo_apps.len() |             self.argo_apps.len(), | ||||||
|  |             match self.argo_apps.len() { | ||||||
|  |                 1 => "application", | ||||||
|  |                 _ => "applications", | ||||||
|  |             } | ||||||
|         ))) |         ))) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -14,11 +14,19 @@ use serde::Serialize; | |||||||
| 
 | 
 | ||||||
| use crate::{ | use crate::{ | ||||||
|     data::{Id, Version}, |     data::{Id, Version}, | ||||||
|  |     instrumentation::{self, HarmonyEvent}, | ||||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, |     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||||
|     inventory::Inventory, |     inventory::Inventory, | ||||||
|     topology::Topology, |     topology::Topology, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | #[derive(Clone, Debug)] | ||||||
|  | pub enum ApplicationFeatureStatus { | ||||||
|  |     Installing, | ||||||
|  |     Installed, | ||||||
|  |     Failed { details: String }, | ||||||
|  | } | ||||||
|  | 
 | ||||||
| pub trait Application: std::fmt::Debug + Send + Sync { | pub trait Application: std::fmt::Debug + Send + Sync { | ||||||
|     fn name(&self) -> String; |     fn name(&self) -> String; | ||||||
| } | } | ||||||
| @ -47,13 +55,34 @@ impl<A: Application, T: Topology + std::fmt::Debug> Interpret<T> for Application | |||||||
|                 .join(", ") |                 .join(", ") | ||||||
|         ); |         ); | ||||||
|         for feature in self.features.iter() { |         for feature in self.features.iter() { | ||||||
|             debug!( |             instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged { | ||||||
|                 "Installing feature {} for application {app_name}", |                 topology: topology.name().into(), | ||||||
|                 feature.name() |                 application: self.application.name(), | ||||||
|             ); |                 feature: feature.name(), | ||||||
|  |                 status: ApplicationFeatureStatus::Installing, | ||||||
|  |             }) | ||||||
|  |             .unwrap(); | ||||||
|  | 
 | ||||||
|             let _ = match feature.ensure_installed(topology).await { |             let _ = match feature.ensure_installed(topology).await { | ||||||
|                 Ok(()) => (), |                 Ok(()) => { | ||||||
|  |                     instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged { | ||||||
|  |                         topology: topology.name().into(), | ||||||
|  |                         application: self.application.name(), | ||||||
|  |                         feature: feature.name(), | ||||||
|  |                         status: ApplicationFeatureStatus::Installed, | ||||||
|  |                     }) | ||||||
|  |                     .unwrap(); | ||||||
|  |                 } | ||||||
|                 Err(msg) => { |                 Err(msg) => { | ||||||
|  |                     instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged { | ||||||
|  |                         topology: topology.name().into(), | ||||||
|  |                         application: self.application.name(), | ||||||
|  |                         feature: feature.name(), | ||||||
|  |                         status: ApplicationFeatureStatus::Failed { | ||||||
|  |                             details: msg.clone(), | ||||||
|  |                         }, | ||||||
|  |                     }) | ||||||
|  |                     .unwrap(); | ||||||
|                     return Err(InterpretError::new(format!( |                     return Err(InterpretError::new(format!( | ||||||
|                         "Application Interpret failed to install feature : {msg}" |                         "Application Interpret failed to install feature : {msg}" | ||||||
|                     ))); |                     ))); | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ use dockerfile_builder::Dockerfile; | |||||||
| use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR}; | use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR}; | ||||||
| use dockerfile_builder::instruction_builder::CopyBuilder; | use dockerfile_builder::instruction_builder::CopyBuilder; | ||||||
| use futures_util::StreamExt; | use futures_util::StreamExt; | ||||||
| use log::{debug, error, log_enabled}; | use log::{debug, info, log_enabled}; | ||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| use tar::Archive; | use tar::Archive; | ||||||
| 
 | 
 | ||||||
| @ -73,19 +73,19 @@ impl Application for RustWebapp { | |||||||
| #[async_trait] | #[async_trait] | ||||||
| impl HelmPackage for RustWebapp { | impl HelmPackage for RustWebapp { | ||||||
|     async fn build_push_helm_package(&self, image_url: &str) -> Result<String, String> { |     async fn build_push_helm_package(&self, image_url: &str) -> Result<String, String> { | ||||||
|         debug!("Starting Helm chart build and push for '{}'", self.name); |         info!("Starting Helm chart build and push for '{}'", self.name); | ||||||
| 
 | 
 | ||||||
|         // 1. Create the Helm chart files on disk.
 |         // 1. Create the Helm chart files on disk.
 | ||||||
|         let chart_dir = self |         let chart_dir = self | ||||||
|             .create_helm_chart_files(image_url) |             .create_helm_chart_files(image_url) | ||||||
|             .map_err(|e| format!("Failed to create Helm chart files: {}", e))?; |             .map_err(|e| format!("Failed to create Helm chart files: {}", e))?; | ||||||
|         debug!("Successfully created Helm chart files in {:?}", chart_dir); |         info!("Successfully created Helm chart files in {:?}", chart_dir); | ||||||
| 
 | 
 | ||||||
|         // 2. Package the chart into a .tgz archive.
 |         // 2. Package the chart into a .tgz archive.
 | ||||||
|         let packaged_chart_path = self |         let packaged_chart_path = self | ||||||
|             .package_helm_chart(&chart_dir) |             .package_helm_chart(&chart_dir) | ||||||
|             .map_err(|e| format!("Failed to package Helm chart: {}", e))?; |             .map_err(|e| format!("Failed to package Helm chart: {}", e))?; | ||||||
|         debug!( |         info!( | ||||||
|             "Successfully packaged Helm chart: {}", |             "Successfully packaged Helm chart: {}", | ||||||
|             packaged_chart_path.to_string_lossy() |             packaged_chart_path.to_string_lossy() | ||||||
|         ); |         ); | ||||||
| @ -94,7 +94,7 @@ impl HelmPackage for RustWebapp { | |||||||
|         let oci_chart_url = self |         let oci_chart_url = self | ||||||
|             .push_helm_chart(&packaged_chart_path) |             .push_helm_chart(&packaged_chart_path) | ||||||
|             .map_err(|e| format!("Failed to push Helm chart: {}", e))?; |             .map_err(|e| format!("Failed to push Helm chart: {}", e))?; | ||||||
|         debug!("Successfully pushed Helm chart to: {}", oci_chart_url); |         info!("Successfully pushed Helm chart to: {}", oci_chart_url); | ||||||
| 
 | 
 | ||||||
|         Ok(oci_chart_url) |         Ok(oci_chart_url) | ||||||
|     } |     } | ||||||
| @ -107,20 +107,20 @@ impl OCICompliant for RustWebapp { | |||||||
|     async fn build_push_oci_image(&self) -> Result<String, String> { |     async fn build_push_oci_image(&self) -> Result<String, String> { | ||||||
|         // This function orchestrates the build and push process.
 |         // This function orchestrates the build and push process.
 | ||||||
|         // It's async to match the trait definition, though the underlying docker commands are blocking.
 |         // It's async to match the trait definition, though the underlying docker commands are blocking.
 | ||||||
|         debug!("Starting OCI image build and push for '{}'", self.name); |         info!("Starting OCI image build and push for '{}'", self.name); | ||||||
| 
 | 
 | ||||||
|         // 1. Build the image by calling the synchronous helper function.
 |         // 1. Build the image by calling the synchronous helper function.
 | ||||||
|         let image_tag = self.image_name(); |         let image_tag = self.image_name(); | ||||||
|         self.build_docker_image(&image_tag) |         self.build_docker_image(&image_tag) | ||||||
|             .await |             .await | ||||||
|             .map_err(|e| format!("Failed to build Docker image: {}", e))?; |             .map_err(|e| format!("Failed to build Docker image: {}", e))?; | ||||||
|         debug!("Successfully built Docker image: {}", image_tag); |         info!("Successfully built Docker image: {}", image_tag); | ||||||
| 
 | 
 | ||||||
|         // 2. Push the image to the registry.
 |         // 2. Push the image to the registry.
 | ||||||
|         self.push_docker_image(&image_tag) |         self.push_docker_image(&image_tag) | ||||||
|             .await |             .await | ||||||
|             .map_err(|e| format!("Failed to push Docker image: {}", e))?; |             .map_err(|e| format!("Failed to push Docker image: {}", e))?; | ||||||
|         debug!("Successfully pushed Docker image to: {}", image_tag); |         info!("Successfully pushed Docker image to: {}", image_tag); | ||||||
| 
 | 
 | ||||||
|         Ok(image_tag) |         Ok(image_tag) | ||||||
|     } |     } | ||||||
| @ -195,7 +195,7 @@ impl RustWebapp { | |||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         while let Some(msg) = image_build_stream.next().await { |         while let Some(msg) = image_build_stream.next().await { | ||||||
|             println!("Message: {msg:?}"); |             debug!("Message: {msg:?}"); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         Ok(image_name.to_string()) |         Ok(image_name.to_string()) | ||||||
| @ -219,7 +219,7 @@ impl RustWebapp { | |||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         while let Some(msg) = push_image_stream.next().await { |         while let Some(msg) = push_image_stream.next().await { | ||||||
|             println!("Message: {msg:?}"); |             debug!("Message: {msg:?}"); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         Ok(image_tag.to_string()) |         Ok(image_tag.to_string()) | ||||||
| @ -288,9 +288,8 @@ impl RustWebapp { | |||||||
|                         .unwrap(), |                         .unwrap(), | ||||||
|                 ); |                 ); | ||||||
|                 // Copy the compiled binary from the builder stage.
 |                 // Copy the compiled binary from the builder stage.
 | ||||||
|                 error!( |                 // TODO: Should not be using score name here, instead should use name from Cargo.toml
 | ||||||
|                     "FIXME Should not be using score name here, instead should use name from Cargo.toml" |                 // https://git.nationtech.io/NationTech/harmony/issues/105
 | ||||||
|                 ); |  | ||||||
|                 let binary_path_in_builder = format!("/app/target/release/{}", self.name); |                 let binary_path_in_builder = format!("/app/target/release/{}", self.name); | ||||||
|                 let binary_path_in_final = format!("/home/appuser/{}", self.name); |                 let binary_path_in_final = format!("/home/appuser/{}", self.name); | ||||||
|                 dockerfile.push( |                 dockerfile.push( | ||||||
| @ -328,9 +327,8 @@ impl RustWebapp { | |||||||
|                 )); |                 )); | ||||||
| 
 | 
 | ||||||
|                 // Copy only the compiled binary from the builder stage.
 |                 // Copy only the compiled binary from the builder stage.
 | ||||||
|                 error!( |                 // TODO: Should not be using score name here, instead should use name from Cargo.toml
 | ||||||
|                     "FIXME Should not be using score name here, instead should use name from Cargo.toml" |                 // https://git.nationtech.io/NationTech/harmony/issues/105
 | ||||||
|                 ); |  | ||||||
|                 let binary_path_in_builder = format!("/app/target/release/{}", self.name); |                 let binary_path_in_builder = format!("/app/target/release/{}", self.name); | ||||||
|                 let binary_path_in_final = format!("/usr/local/bin/{}", self.name); |                 let binary_path_in_final = format!("/usr/local/bin/{}", self.name); | ||||||
|                 dockerfile.push( |                 dockerfile.push( | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| use harmony::{ | use harmony::{ | ||||||
|     instrumentation::{self, HarmonyEvent}, |     instrumentation::{self, HarmonyEvent}, | ||||||
|  |     modules::application::ApplicationFeatureStatus, | ||||||
|     topology::TopologyStatus, |     topology::TopologyStatus, | ||||||
| }; | }; | ||||||
| use indicatif::MultiProgress; | use indicatif::MultiProgress; | ||||||
| @ -160,6 +161,35 @@ async fn handle_events(base_progress: MultiProgress) { | |||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|  |                     HarmonyEvent::ApplicationFeatureStateChanged { | ||||||
|  |                         topology: _, | ||||||
|  |                         application, | ||||||
|  |                         feature, | ||||||
|  |                         status, | ||||||
|  |                     } => { | ||||||
|  |                         if let Some(score) = &(*current_score) { | ||||||
|  |                             let section_key = score_key(score); | ||||||
|  |                             let task_key = app_feature_key(&application, &feature); | ||||||
|  | 
 | ||||||
|  |                             match status { | ||||||
|  |                                 ApplicationFeatureStatus::Installing => { | ||||||
|  |                                     let message = format!("Feature '{}' installing...", feature); | ||||||
|  |                                     progress_tracker.add_task(§ion_key, &task_key, &message); | ||||||
|  |                                 } | ||||||
|  |                                 ApplicationFeatureStatus::Installed => { | ||||||
|  |                                     let message = format!("Feature '{}' installed", feature); | ||||||
|  |                                     progress_tracker.finish_task(&task_key, &message); | ||||||
|  |                                 } | ||||||
|  |                                 ApplicationFeatureStatus::Failed { details } => { | ||||||
|  |                                     let message = format!( | ||||||
|  |                                         "Feature '{}' installation failed: {}", | ||||||
|  |                                         feature, details | ||||||
|  |                                     ); | ||||||
|  |                                     progress_tracker.fail_task(&task_key, &message); | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|                 true |                 true | ||||||
|             } |             } | ||||||
| @ -175,3 +205,7 @@ fn topology_key(topology: &str) -> String { | |||||||
| fn score_key(score: &str) -> String { | fn score_key(score: &str) -> String { | ||||||
|     format!("score-{score}") |     format!("score-{score}") | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | fn app_feature_key(application: &str, feature: &str) -> String { | ||||||
|  |     format!("app-{application}-{feature}") | ||||||
|  | } | ||||||
|  | |||||||
| @ -33,29 +33,13 @@ pub struct IndicatifProgressTracker { | |||||||
| 
 | 
 | ||||||
| impl IndicatifProgressTracker { | impl IndicatifProgressTracker { | ||||||
|     pub fn new(base: MultiProgress) -> Self { |     pub fn new(base: MultiProgress) -> Self { | ||||||
|         // The indicatif log bridge will insert a progress bar at the top.
 |         let sections = HashMap::new(); | ||||||
|         // To prevent our first section from being erased, we need to create
 |         let tasks = HashMap::new(); | ||||||
|         // a dummy progress bar as our first progress bar.
 |  | ||||||
|         let _ = base.clear(); |  | ||||||
|         let log_pb = base.add(ProgressBar::new(1)); |  | ||||||
| 
 |  | ||||||
|         let mut sections = HashMap::new(); |  | ||||||
|         sections.insert( |  | ||||||
|             "__log__".into(), |  | ||||||
|             Section { |  | ||||||
|                 header_index: 0, |  | ||||||
|                 task_count: 0, |  | ||||||
|                 pb: log_pb.clone(), |  | ||||||
|             }, |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         let mut tasks = HashMap::new(); |  | ||||||
|         tasks.insert("__log__".into(), log_pb); |  | ||||||
| 
 | 
 | ||||||
|         let state = Arc::new(Mutex::new(IndicatifProgressTrackerState { |         let state = Arc::new(Mutex::new(IndicatifProgressTrackerState { | ||||||
|             sections, |             sections, | ||||||
|             tasks, |             tasks, | ||||||
|             pb_count: 1, |             pb_count: 0, | ||||||
|         })); |         })); | ||||||
| 
 | 
 | ||||||
|         Self { mp: base, state } |         Self { mp: base, state } | ||||||
|  | |||||||
| @ -21,10 +21,14 @@ lazy_static! { | |||||||
|     pub static ref SUCCESS_SPINNER_STYLE: ProgressStyle = SPINNER_STYLE |     pub static ref SUCCESS_SPINNER_STYLE: ProgressStyle = SPINNER_STYLE | ||||||
|         .clone() |         .clone() | ||||||
|         .tick_strings(&[format!("{}", EMOJI_SUCCESS).as_str()]); |         .tick_strings(&[format!("{}", EMOJI_SUCCESS).as_str()]); | ||||||
|     pub static ref SKIP_SPINNER_STYLE: ProgressStyle = SPINNER_STYLE |     pub static ref SKIP_SPINNER_STYLE: ProgressStyle = ProgressStyle::default_spinner() | ||||||
|  |         .template("    {spinner:.orange} {wide_msg}") | ||||||
|  |         .unwrap() | ||||||
|         .clone() |         .clone() | ||||||
|         .tick_strings(&[format!("{}", EMOJI_SKIP).as_str()]); |         .tick_strings(&[format!("{}", EMOJI_SKIP).as_str()]); | ||||||
|     pub static ref ERROR_SPINNER_STYLE: ProgressStyle = SPINNER_STYLE |     pub static ref ERROR_SPINNER_STYLE: ProgressStyle = ProgressStyle::default_spinner() | ||||||
|  |         .template("    {spinner:.red} {wide_msg}") | ||||||
|  |         .unwrap() | ||||||
|         .clone() |         .clone() | ||||||
|         .tick_strings(&[format!("{}", EMOJI_ERROR).as_str()]); |         .tick_strings(&[format!("{}", EMOJI_ERROR).as_str()]); | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,6 +1,5 @@ | |||||||
| use harmony_cli::progress::{IndicatifProgressTracker, ProgressTracker}; | use harmony_cli::progress::{IndicatifProgressTracker, ProgressTracker}; | ||||||
| use indicatif::MultiProgress; | use indicatif::MultiProgress; | ||||||
| use log::error; |  | ||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
| 
 | 
 | ||||||
| use crate::instrumentation::{self, HarmonyComposerEvent}; | use crate::instrumentation::{self, HarmonyComposerEvent}; | ||||||
| @ -53,15 +52,13 @@ pub async fn handle_events() { | |||||||
|                         progress_tracker.finish_task(COMPILTATION_TASK, "project compiled"); |                         progress_tracker.finish_task(COMPILTATION_TASK, "project compiled"); | ||||||
|                     } |                     } | ||||||
|                     HarmonyComposerEvent::ProjectCompilationFailed { details } => { |                     HarmonyComposerEvent::ProjectCompilationFailed { details } => { | ||||||
|                         progress_tracker.fail_task(COMPILTATION_TASK, "failed to compile project"); |                         progress_tracker.fail_task(COMPILTATION_TASK, &format!("failed to compile project:\n{details}")); | ||||||
| 
 |  | ||||||
|                         error!("{details}"); |  | ||||||
|                     } |                     } | ||||||
|                     HarmonyComposerEvent::DeploymentStarted { target } => { |                     HarmonyComposerEvent::DeploymentStarted { target, profile } => { | ||||||
|                         progress_tracker.add_section( |                         progress_tracker.add_section( | ||||||
|                             PROGRESS_DEPLOYMENT, |                             PROGRESS_DEPLOYMENT, | ||||||
|                             &format!( |                             &format!( | ||||||
|                                 "\n{} Deploying project to {target}...\n", |                                 "\n{} Deploying project on target '{target}' with profile '{profile}'...\n", | ||||||
|                                 harmony_cli::theme::EMOJI_DEPLOY, |                                 harmony_cli::theme::EMOJI_DEPLOY, | ||||||
|                             ), |                             ), | ||||||
|                         ); |                         ); | ||||||
| @ -69,6 +66,10 @@ pub async fn handle_events() { | |||||||
|                     HarmonyComposerEvent::DeploymentCompleted => { |                     HarmonyComposerEvent::DeploymentCompleted => { | ||||||
|                         progress_tracker.clear(); |                         progress_tracker.clear(); | ||||||
|                     } |                     } | ||||||
|  |                     HarmonyComposerEvent::DeploymentFailed { details } => { | ||||||
|  |                         progress_tracker.add_task(PROGRESS_DEPLOYMENT, "deployment-failed", ""); | ||||||
|  |                         progress_tracker.fail_task("deployment-failed", &details); | ||||||
|  |                     }, | ||||||
|                     HarmonyComposerEvent::Shutdown => { |                     HarmonyComposerEvent::Shutdown => { | ||||||
|                         return false; |                         return false; | ||||||
|                     } |                     } | ||||||
|  | |||||||
| @ -2,16 +2,28 @@ use log::debug; | |||||||
| use once_cell::sync::Lazy; | use once_cell::sync::Lazy; | ||||||
| use tokio::sync::broadcast; | use tokio::sync::broadcast; | ||||||
| 
 | 
 | ||||||
|  | use crate::{HarmonyProfile, HarmonyTarget}; | ||||||
|  | 
 | ||||||
| #[derive(Debug, Clone)] | #[derive(Debug, Clone)] | ||||||
| pub enum HarmonyComposerEvent { | pub enum HarmonyComposerEvent { | ||||||
|     HarmonyComposerStarted, |     HarmonyComposerStarted, | ||||||
|     ProjectInitializationStarted, |     ProjectInitializationStarted, | ||||||
|     ProjectInitialized, |     ProjectInitialized, | ||||||
|     ProjectCompilationStarted { details: String }, |     ProjectCompilationStarted { | ||||||
|  |         details: String, | ||||||
|  |     }, | ||||||
|     ProjectCompiled, |     ProjectCompiled, | ||||||
|     ProjectCompilationFailed { details: String }, |     ProjectCompilationFailed { | ||||||
|     DeploymentStarted { target: String }, |         details: String, | ||||||
|  |     }, | ||||||
|  |     DeploymentStarted { | ||||||
|  |         target: HarmonyTarget, | ||||||
|  |         profile: HarmonyProfile, | ||||||
|  |     }, | ||||||
|     DeploymentCompleted, |     DeploymentCompleted, | ||||||
|  |     DeploymentFailed { | ||||||
|  |         details: String, | ||||||
|  |     }, | ||||||
|     Shutdown, |     Shutdown, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -49,14 +49,11 @@ struct CheckArgs { | |||||||
| 
 | 
 | ||||||
| #[derive(Args, Clone, Debug)] | #[derive(Args, Clone, Debug)] | ||||||
| struct DeployArgs { | struct DeployArgs { | ||||||
|     #[arg(long, default_value_t = false)] |     #[arg(long = "target", short = 't', default_value = "local")] | ||||||
|     staging: bool, |     harmony_target: HarmonyTarget, | ||||||
| 
 | 
 | ||||||
|     #[arg(long, default_value_t = false)] |     #[arg(long = "profile", short = 'p', default_value = "dev")] | ||||||
|     prod: bool, |     harmony_profile: HarmonyProfile, | ||||||
| 
 |  | ||||||
|     #[arg(long, default_value_t = false)] |  | ||||||
|     smoke_test: bool, |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Args, Clone, Debug)] | #[derive(Args, Clone, Debug)] | ||||||
| @ -68,6 +65,38 @@ struct AllArgs { | |||||||
|     deploy: DeployArgs, |     deploy: DeployArgs, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[derive(Clone, Debug, clap::ValueEnum)] | ||||||
|  | enum HarmonyTarget { | ||||||
|  |     Local, | ||||||
|  |     Remote, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl std::fmt::Display for HarmonyTarget { | ||||||
|  |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
|  |         match self { | ||||||
|  |             HarmonyTarget::Local => f.write_str("local"), | ||||||
|  |             HarmonyTarget::Remote => f.write_str("remote"), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone, Debug, clap::ValueEnum)] | ||||||
|  | enum HarmonyProfile { | ||||||
|  |     Dev, | ||||||
|  |     Staging, | ||||||
|  |     Production, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl std::fmt::Display for HarmonyProfile { | ||||||
|  |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
|  |         match self { | ||||||
|  |             HarmonyProfile::Dev => f.write_str("dev"), | ||||||
|  |             HarmonyProfile::Staging => f.write_str("staging"), | ||||||
|  |             HarmonyProfile::Production => f.write_str("production"), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[tokio::main] | #[tokio::main] | ||||||
| async fn main() { | async fn main() { | ||||||
|     let hc_logger_handle = harmony_composer_logger::init(); |     let hc_logger_handle = harmony_composer_logger::init(); | ||||||
| @ -122,26 +151,39 @@ async fn main() { | |||||||
|                 ); |                 ); | ||||||
|             } |             } | ||||||
|             Commands::Deploy(args) => { |             Commands::Deploy(args) => { | ||||||
|                 let deploy = if args.staging { |  | ||||||
|                 instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted { |                 instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted { | ||||||
|                         target: "staging".to_string(), |                     target: args.harmony_target.clone(), | ||||||
|  |                     profile: args.harmony_profile.clone(), | ||||||
|                 }) |                 }) | ||||||
|                 .unwrap(); |                 .unwrap(); | ||||||
|                     todo!("implement staging deployment") | 
 | ||||||
|                 } else if args.prod { |                 if matches!(args.harmony_profile, HarmonyProfile::Dev) | ||||||
|                     instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted { |                     && !matches!(args.harmony_target, HarmonyTarget::Local) | ||||||
|                         target: "prod".to_string(), |                 { | ||||||
|                     }) |                     instrumentation::instrument(HarmonyComposerEvent::DeploymentFailed { | ||||||
|                     .unwrap(); |                         details: format!( | ||||||
|                     todo!("implement prod deployment") |                             "Cannot run profile '{}' on target '{}'. Profile '{}' can run locally only.", | ||||||
|                 } else { |                             args.harmony_profile, args.harmony_target, args.harmony_profile | ||||||
|                     instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted { |                         ), | ||||||
|                         target: "dev".to_string(), |                     }).unwrap(); | ||||||
|                     }) |                     return; | ||||||
|                     .unwrap(); |  | ||||||
|                     Command::new(harmony_bin_path).arg("-y").arg("-a").spawn() |  | ||||||
|                 } |                 } | ||||||
|                 .expect("failed to run harmony deploy"); | 
 | ||||||
|  |                 let use_local_k3d = match args.harmony_target { | ||||||
|  |                     HarmonyTarget::Local => true, | ||||||
|  |                     HarmonyTarget::Remote => false, | ||||||
|  |                 }; | ||||||
|  | 
 | ||||||
|  |                 let mut command = Command::new(harmony_bin_path); | ||||||
|  |                 command | ||||||
|  |                     .env("HARMONY_USE_LOCAL_K3D", format!("{use_local_k3d}")) | ||||||
|  |                     .env("HARMONY_PROFILE", format!("{}", args.harmony_profile)) | ||||||
|  |                     .arg("-y") | ||||||
|  |                     .arg("-a"); | ||||||
|  | 
 | ||||||
|  |                 info!("{:?}", command); | ||||||
|  | 
 | ||||||
|  |                 let deploy = command.spawn().expect("failed to run harmony deploy"); | ||||||
| 
 | 
 | ||||||
|                 let deploy_output = deploy.wait_with_output().unwrap(); |                 let deploy_output = deploy.wait_with_output().unwrap(); | ||||||
|                 debug!("{}", String::from_utf8(deploy_output.stdout).unwrap()); |                 debug!("{}", String::from_utf8(deploy_output.stdout).unwrap()); | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user