diff --git a/Cargo.lock b/Cargo.lock index 4f0e1d3..4449379 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1588,6 +1588,7 @@ dependencies = [ "serde-value", "serde_json", "serde_yaml", + "similar", "temp-dir", "temp-file", "tokio", @@ -4102,6 +4103,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "simple_asn1" version = "0.6.3" diff --git a/Cargo.toml b/Cargo.toml index 970300d..c081c86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,34 +20,23 @@ readme = "README.md" license = "GNU AGPL v3" [workspace.dependencies] -log = "0.4.22" -env_logger = "0.11.5" -derive-new = "0.7.0" -async-trait = "0.1.82" -tokio = { version = "1.40.0", features = [ - "io-std", - "fs", - "macros", - "rt-multi-thread", -] } +log = "0.4" +env_logger = "0.11" +derive-new = "0.7" +async-trait = "0.1" +tokio = { version = "1.40", features = ["io-std", "fs", "macros", "rt-multi-thread"] } cidr = { features = ["serde"], version = "0.2" } -russh = "0.45.0" -russh-keys = "0.45.0" -rand = "0.8.5" -url = "2.5.4" -kube = "0.98.0" -k8s-openapi = { version = "0.24.0", features = ["v1_30"] } -serde_yaml = "0.9.34" -serde-value = "0.7.0" -http = "1.2.0" -inquire = "0.7.5" -convert_case = "0.8.0" +russh = "0.45" +russh-keys = "0.45" +rand = "0.8" +url = "2.5" +kube = "0.98" +k8s-openapi = { version = "0.24", features = ["v1_30"] } +serde_yaml = "0.9" +serde-value = "0.7" +http = "1.2" +inquire = "0.7" +convert_case = "0.8" chrono = "0.4" - -[workspace.dependencies.uuid] -version = "1.11.0" -features = [ - "v4", # Lets you generate random UUIDs - "fast-rng", # Use a faster (but still sufficiently random) RNG - "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs -] +similar = "2" +uuid = { version = "1.11", features = [ "v4", "fast-rng", "macro-diagnostics" ] } diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index 54cf36d..f84bd63 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -53,3 +53,4 @@ fqdn = { version = "0.4.6", features = [ ] } temp-dir = "0.1.14" dyn-clone = "1.0.19" +similar.workspace = true diff --git a/harmony/src/domain/config.rs b/harmony/src/domain/config.rs index 53d7446..7812616 100644 --- a/harmony/src/domain/config.rs +++ b/harmony/src/domain/config.rs @@ -10,4 +10,6 @@ lazy_static! { std::env::var("HARMONY_REGISTRY_URL").unwrap_or_else(|_| "hub.nationtech.io".to_string()); pub static ref REGISTRY_PROJECT: String = std::env::var("HARMONY_REGISTRY_PROJECT").unwrap_or_else(|_| "harmony".to_string()); + pub static ref DRY_RUN: bool = + std::env::var("HARMONY_DRY_RUN").map_or(true, |value| value.parse().unwrap_or(true)); } diff --git a/harmony/src/domain/topology/k8s.rs b/harmony/src/domain/topology/k8s.rs index 9e0a6db..fc91b0c 100644 --- a/harmony/src/domain/topology/k8s.rs +++ b/harmony/src/domain/topology/k8s.rs @@ -4,15 +4,28 @@ use kube::{ Api, Client, Config, Error, Resource, api::{Patch, PatchParams}, config::{KubeConfigOptions, Kubeconfig}, + core::ErrorResponse, }; use log::{debug, error, trace}; use serde::de::DeserializeOwned; +use similar::TextDiff; -#[derive(new)] +#[derive(new, Clone)] pub struct K8sClient { client: Client, } +impl std::fmt::Debug for K8sClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // This is a poor man's debug implementation for now as kube::Client does not provide much + // useful information + f.write_fmt(format_args!( + "K8sClient {{ kube client using default namespace {} }}", + self.client.default_namespace() + )) + } +} + impl K8sClient { pub async fn try_default() -> Result { Ok(Self { @@ -48,8 +61,79 @@ impl K8sClient { .name .as_ref() .expect("K8s Resource should have a name"); - api.patch(name, &patch_params, &Patch::Apply(resource)) - .await + + if *crate::config::DRY_RUN { + match api.get(name).await { + Ok(current) => { + trace!("Received current value {current:#?}"); + // The resource exists, so we calculate and display a diff. + println!("\nPerforming dry-run for resource: '{}'", name); + let mut current_yaml = serde_yaml::to_value(¤t) + .expect(&format!("Could not serialize current value : {current:#?}")); + if current_yaml.is_mapping() && current_yaml.get("status").is_some() { + let map = current_yaml.as_mapping_mut().unwrap(); + let removed = map.remove_entry("status"); + trace!("Removed status {:?}", removed); + } else { + trace!( + "Did not find status entry for current object {}/{}", + current.meta().namespace.as_ref().unwrap_or(&"".to_string()), + current.meta().name.as_ref().unwrap_or(&"".to_string()) + ); + } + let current_yaml = serde_yaml::to_string(¤t_yaml) + .unwrap_or_else(|_| "Failed to serialize current resource".to_string()); + let new_yaml = serde_yaml::to_string(resource) + .unwrap_or_else(|_| "Failed to serialize new resource".to_string()); + + if current_yaml == new_yaml { + println!("No changes detected."); + // Return the current resource state as there are no changes. + return Ok(current); + } + + println!("Changes detected:"); + let diff = TextDiff::from_lines(¤t_yaml, &new_yaml); + + // Iterate over the changes and print them in a git-like diff format. + for change in diff.iter_all_changes() { + let sign = match change.tag() { + similar::ChangeTag::Delete => "-", + similar::ChangeTag::Insert => "+", + similar::ChangeTag::Equal => " ", + }; + print!("{}{}", sign, change); + } + // In a dry run, we return the new resource state that would have been applied. + Ok(resource.clone()) + } + Err(Error::Api(ErrorResponse { code: 404, .. })) => { + // The resource does not exist, so the "diff" is the entire new resource. + println!("\nPerforming dry-run for new resource: '{}'", name); + println!( + "Resource does not exist. It would be created with the following content:" + ); + let new_yaml = serde_yaml::to_string(resource) + .unwrap_or_else(|_| "Failed to serialize new resource".to_string()); + + // Print each line of the new resource with a '+' prefix. + for line in new_yaml.lines() { + println!("+{}", line); + } + // In a dry run, we return the new resource state that would have been created. + Ok(resource.clone()) + } + Err(e) => { + // Another API error occurred. + error!("Failed to get resource '{}': {}", name, e); + Err(e) + } + } + } else { + return api + .patch(name, &patch_params, &Patch::Apply(resource)) + .await; + } } pub async fn apply_many(&self, resource: &Vec, ns: Option<&str>) -> Result, Error> diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index ea8b033..cb5cd76 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -3,6 +3,7 @@ use std::{process::Command, sync::Arc}; use async_trait::async_trait; use inquire::Confirm; use log::{debug, info, warn}; +use serde::Serialize; use tokio::sync::OnceCell; use crate::{ @@ -20,23 +21,25 @@ use super::{ tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager}, }; +#[derive(Clone, Debug)] struct K8sState { client: Arc, _source: K8sSource, message: String, } -#[derive(Debug)] +#[derive(Debug, Clone)] enum K8sSource { LocalK3d, Kubeconfig, } +#[derive(Clone, Debug)] pub struct K8sAnywhereTopology { - k8s_state: OnceCell>, - tenant_manager: OnceCell, + k8s_state: Arc>>, + tenant_manager: Arc>, + config: Arc, tenant_manager_config: OnceCell, - config: K8sAnywhereConfig, } #[async_trait] @@ -56,22 +59,31 @@ impl K8sclient for K8sAnywhereTopology { } } +impl Serialize for K8sAnywhereTopology { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + todo!() + } +} + impl K8sAnywhereTopology { pub fn from_env() -> Self { Self { - k8s_state: OnceCell::new(), - tenant_manager: OnceCell::new(), + k8s_state: Arc::new(OnceCell::new()), + tenant_manager: Arc::new(OnceCell::new()), + config: Arc::new(K8sAnywhereConfig::from_env()), tenant_manager_config: OnceCell::new(), - config: K8sAnywhereConfig::from_env(), } } pub fn with_config(config: K8sAnywhereConfig) -> Self { Self { - k8s_state: OnceCell::new(), - tenant_manager: OnceCell::new(), + k8s_state: Arc::new(OnceCell::new()), + tenant_manager: Arc::new(OnceCell::new()), + config: Arc::new(config), tenant_manager_config: OnceCell::new(), - config, } } @@ -208,6 +220,7 @@ impl K8sAnywhereTopology { } } +#[derive(Clone, Debug)] pub struct K8sAnywhereConfig { /// The path of the KUBECONFIG file that Harmony should use to interact with the Kubernetes /// cluster diff --git a/harmony/src/domain/topology/tenant/k8s.rs b/harmony/src/domain/topology/tenant/k8s.rs index d0b3345..f3a7cb2 100644 --- a/harmony/src/domain/topology/tenant/k8s.rs +++ b/harmony/src/domain/topology/tenant/k8s.rs @@ -22,7 +22,7 @@ use serde_json::json; use super::{TenantConfig, TenantManager}; -#[derive(new)] +#[derive(new, Clone, Debug)] pub struct K8sTenantManager { k8s_client: Arc, } @@ -137,7 +137,8 @@ impl K8sTenantManager { "apiVersion": "networking.k8s.io/v1", "kind": "NetworkPolicy", "metadata": { - "name": format!("{}-network-policy", config.name) + "name": format!("{}-network-policy", config.name), + "namespace": self.get_namespace_name(config), }, "spec": { "podSelector": {}, @@ -253,8 +254,29 @@ impl K8sTenantManager { }) }) .collect(); + let ports: Option> = + c.1.as_ref().map(|spec| match &spec.data { + super::PortSpecData::SinglePort(port) => vec![NetworkPolicyPort { + port: Some(IntOrString::Int(port.clone().into())), + ..Default::default() + }], + super::PortSpecData::PortRange(start, end) => vec![NetworkPolicyPort { + port: Some(IntOrString::Int(start.clone().into())), + end_port: Some(end.clone().into()), + protocol: None, // Not currently supported by Harmony + }], + + super::PortSpecData::ListOfPorts(items) => items + .iter() + .map(|i| NetworkPolicyPort { + port: Some(IntOrString::Int(i.clone().into())), + ..Default::default() + }) + .collect(), + }); let rule = serde_json::from_value::(json!({ - "from": cidr_list + "from": cidr_list, + "ports": ports, })) .map_err(|e| { ExecutorError::ConfigurationError(format!( diff --git a/harmony/src/modules/application/features/continuous_delivery.rs b/harmony/src/modules/application/features/continuous_delivery.rs new file mode 100644 index 0000000..3530142 --- /dev/null +++ b/harmony/src/modules/application/features/continuous_delivery.rs @@ -0,0 +1,42 @@ +use async_trait::async_trait; +use log::info; + +use crate::{modules::application::ApplicationFeature, topology::Topology}; + +/// ContinuousDelivery in Harmony provides this functionality : +/// +/// - **Package** the application +/// - **Push** to an artifact registry +/// - **Deploy** to a testing environment +/// - **Deploy** to a production environment +/// +/// It is intended to be used as an application feature passed down to an ApplicationInterpret. For +/// example : +/// +/// ```rust,ignore +/// let app = RustApplicationScore { +/// name: "My Rust App".to_string(), +/// features: vec![ContinuousDelivery::default()], +/// }; +/// ``` +/// +/// *Note :* +/// +/// By default, the Harmony Opinionated Pipeline is built using these technologies : +/// +/// - Gitea Action (executes pipeline steps) +/// - Docker to build an OCI container image +/// - Helm chart to package Kubernetes resources +/// - Harbor as artifact registru +/// - ArgoCD to install/upgrade/rollback/inspect k8s resources +/// - Kubernetes for runtime orchestration +#[derive(Debug, Default)] +pub struct ContinuousDelivery {} + +#[async_trait] +impl ApplicationFeature for ContinuousDelivery { + async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { + info!("Installing ContinuousDelivery feature"); + todo!() + } +} diff --git a/harmony/src/modules/application/features/endpoint.rs b/harmony/src/modules/application/features/endpoint.rs new file mode 100644 index 0000000..f4940ed --- /dev/null +++ b/harmony/src/modules/application/features/endpoint.rs @@ -0,0 +1,39 @@ +use async_trait::async_trait; +use log::info; + +use crate::{ + modules::application::ApplicationFeature, + topology::{K8sclient, Topology}, +}; + +#[derive(Debug)] +pub struct PublicEndpoint { + application_port: u16, +} + +/// Use port 3000 as default port. Harmony wants to provide "sane defaults" in general, and in this +/// particular context, using port 80 goes against our philosophy to provide production grade +/// defaults out of the box. Using an unprivileged port is a good security practice and will allow +/// for unprivileged containers to work with this out of the box. +/// +/// Now, why 3000 specifically? Many popular web/network frameworks use it by default, there is no +/// perfect answer for this but many Rust and Python libraries tend to use 3000. +impl Default for PublicEndpoint { + fn default() -> Self { + Self { + application_port: 3000, + } + } +} + +/// For now we only suport K8s ingress, but we will support more stuff at some point +#[async_trait] +impl ApplicationFeature for PublicEndpoint { + async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { + info!( + "Making sure public endpoint is installed for port {}", + self.application_port + ); + todo!() + } +} diff --git a/harmony/src/modules/application/features/mod.rs b/harmony/src/modules/application/features/mod.rs new file mode 100644 index 0000000..0e034fc --- /dev/null +++ b/harmony/src/modules/application/features/mod.rs @@ -0,0 +1,8 @@ +mod endpoint; +pub use endpoint::*; + +mod monitoring; +pub use monitoring::*; + +mod continuous_delivery; +pub use continuous_delivery::*; diff --git a/harmony/src/modules/application/features/monitoring.rs b/harmony/src/modules/application/features/monitoring.rs new file mode 100644 index 0000000..91ad72d --- /dev/null +++ b/harmony/src/modules/application/features/monitoring.rs @@ -0,0 +1,18 @@ +use async_trait::async_trait; +use log::info; + +use crate::{ + modules::application::ApplicationFeature, + topology::{HelmCommand, Topology}, +}; + +#[derive(Debug, Default)] +pub struct Monitoring {} + +#[async_trait] +impl ApplicationFeature for Monitoring { + async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { + info!("Ensuring monitoring is available for application"); + todo!("create and execute k8s prometheus score, depends on Will's work") + } +} diff --git a/harmony/src/modules/application/mod.rs b/harmony/src/modules/application/mod.rs new file mode 100644 index 0000000..2a3ee24 --- /dev/null +++ b/harmony/src/modules/application/mod.rs @@ -0,0 +1,67 @@ +pub mod features; +mod rust; +pub use rust::*; + +use async_trait::async_trait; +use serde::Serialize; + +use crate::{ + data::{Id, Version}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + topology::Topology, +}; + +#[derive(Debug)] +pub struct ApplicationInterpret { + features: Vec>>, +} + +#[async_trait] +impl Interpret for ApplicationInterpret { + async fn execute( + &self, + _inventory: &Inventory, + _topology: &T, + ) -> Result { + todo!() + } + + fn get_name(&self) -> InterpretName { + todo!() + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + todo!() + } +} + +/// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability, +/// ContinuousIntegration, ContinuousDelivery +#[async_trait] +pub trait ApplicationFeature: std::fmt::Debug + Send + Sync { + async fn ensure_installed(&self, topology: &T) -> Result<(), String>; +} + +impl Serialize for Box> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + todo!() + } +} + +impl Clone for Box> { + fn clone(&self) -> Self { + todo!() + } +} diff --git a/harmony/src/modules/application/rust.rs b/harmony/src/modules/application/rust.rs new file mode 100644 index 0000000..0ef41b3 --- /dev/null +++ b/harmony/src/modules/application/rust.rs @@ -0,0 +1,25 @@ +use serde::Serialize; + +use crate::{ + score::Score, + topology::{Topology, Url}, +}; + +use super::{ApplicationFeature, ApplicationInterpret}; + +#[derive(Debug, Serialize, Clone)] +pub struct RustWebappScore { + pub name: String, + pub domain: Url, + pub features: Vec>>, +} + +impl Score for RustWebappScore { + fn create_interpret(&self) -> Box> { + Box::new(ApplicationInterpret { features: todo!() }) + } + + fn name(&self) -> String { + format!("{}-RustWebapp", self.name) + } +} diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index ec5f050..e9b6c52 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -1,3 +1,4 @@ +pub mod application; pub mod cert_manager; pub mod dhcp; pub mod dns; diff --git a/harmony/src/modules/monitoring/alert_channel/mod.rs b/harmony/src/modules/monitoring/alert_channel/mod.rs index fabc6dd..1bca2bc 100644 --- a/harmony/src/modules/monitoring/alert_channel/mod.rs +++ b/harmony/src/modules/monitoring/alert_channel/mod.rs @@ -1 +1,2 @@ pub mod discord_alert_channel; +pub mod webhook_receiver; diff --git a/harmony/src/modules/monitoring/alert_channel/webhook_receiver.rs b/harmony/src/modules/monitoring/alert_channel/webhook_receiver.rs new file mode 100644 index 0000000..8f608d0 --- /dev/null +++ b/harmony/src/modules/monitoring/alert_channel/webhook_receiver.rs @@ -0,0 +1,124 @@ +use async_trait::async_trait; +use serde::Serialize; +use serde_yaml::{Mapping, Value}; + +use crate::{ + interpret::{InterpretError, Outcome}, + modules::monitoring::kube_prometheus::{ + prometheus::{Prometheus, PrometheusReceiver}, + types::{AlertChannelConfig, AlertManagerChannelConfig}, + }, + topology::{Url, oberservability::monitoring::AlertReceiver}, +}; + +#[derive(Debug, Clone, Serialize)] +pub struct WebhookReceiver { + pub name: String, + pub url: Url, +} + +#[async_trait] +impl AlertReceiver for WebhookReceiver { + async fn install(&self, sender: &Prometheus) -> Result { + sender.install_receiver(self).await + } + fn clone_box(&self) -> Box> { + Box::new(self.clone()) + } +} + +#[async_trait] +impl PrometheusReceiver for WebhookReceiver { + fn name(&self) -> String { + self.name.clone() + } + async fn configure_receiver(&self) -> AlertManagerChannelConfig { + self.get_config().await + } +} + +#[async_trait] +impl AlertChannelConfig for WebhookReceiver { + async fn get_config(&self) -> AlertManagerChannelConfig { + let channel_global_config = None; + let channel_receiver = self.alert_channel_receiver().await; + let channel_route = self.alert_channel_route().await; + + AlertManagerChannelConfig { + channel_global_config, + channel_receiver, + channel_route, + } + } +} + +impl WebhookReceiver { + async fn alert_channel_route(&self) -> serde_yaml::Value { + let mut route = Mapping::new(); + route.insert( + Value::String("receiver".to_string()), + Value::String(self.name.clone()), + ); + route.insert( + Value::String("matchers".to_string()), + Value::Sequence(vec![Value::String("alertname!=Watchdog".to_string())]), + ); + route.insert(Value::String("continue".to_string()), Value::Bool(true)); + Value::Mapping(route) + } + + async fn alert_channel_receiver(&self) -> serde_yaml::Value { + let mut receiver = Mapping::new(); + receiver.insert( + Value::String("name".to_string()), + Value::String(self.name.clone()), + ); + + let mut webhook_config = Mapping::new(); + webhook_config.insert( + Value::String("url".to_string()), + Value::String(self.url.to_string()), + ); + + receiver.insert( + Value::String("webhook_configs".to_string()), + Value::Sequence(vec![Value::Mapping(webhook_config)]), + ); + + Value::Mapping(receiver) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[tokio::test] + async fn webhook_serialize_should_match() { + let webhook_receiver = WebhookReceiver { + name: "test-webhook".to_string(), + url: Url::Url(url::Url::parse("https://webhook.i.dont.exist.com").unwrap()), + }; + + let webhook_receiver_receiver = + serde_yaml::to_string(&webhook_receiver.alert_channel_receiver().await).unwrap(); + println!("receiver \n{:#}", webhook_receiver_receiver); + let webhook_receiver_receiver_yaml = r#"name: test-webhook +webhook_configs: +- url: https://webhook.i.dont.exist.com/ +"# + .to_string(); + + let webhook_receiver_route = + serde_yaml::to_string(&webhook_receiver.alert_channel_route().await).unwrap(); + println!("route \n{:#}", webhook_receiver_route); + let webhook_receiver_route_yaml = r#"receiver: test-webhook +matchers: +- alertname!=Watchdog +continue: true +"# + .to_string(); + + assert_eq!(webhook_receiver_receiver, webhook_receiver_receiver_yaml); + assert_eq!(webhook_receiver_route, webhook_receiver_route_yaml); + } +} diff --git a/harmony_cli/src/lib.rs b/harmony_cli/src/lib.rs index 33759fa..50beb6e 100644 --- a/harmony_cli/src/lib.rs +++ b/harmony_cli/src/lib.rs @@ -12,15 +12,15 @@ use harmony_tui; pub struct Args { /// Run score(s) without prompt #[arg(short, long, default_value_t = false, conflicts_with = "interactive")] - yes: bool, + pub yes: bool, /// Filter query #[arg(short, long, conflicts_with = "interactive")] - filter: Option, + pub filter: Option, /// Run interactive TUI or not #[arg(short, long, default_value_t = false)] - interactive: bool, + pub interactive: bool, /// Run all or nth, defaults to all #[arg( @@ -31,15 +31,15 @@ pub struct Args { conflicts_with = "number", conflicts_with = "interactive" )] - all: bool, + pub all: bool, /// Run nth matching, zero indexed #[arg(short, long, default_value_t = 0, conflicts_with = "interactive")] - number: usize, + pub number: usize, /// list scores, will also be affected by run filter #[arg(short, long, default_value_t = false, conflicts_with = "interactive")] - list: bool, + pub list: bool, } fn maestro_scores_filter( diff --git a/harmony_composer/README.md b/harmony_composer/README.md index 279ad8a..45ac542 100644 --- a/harmony_composer/README.md +++ b/harmony_composer/README.md @@ -9,12 +9,20 @@ It's designed to simplify the build process by either compiling a Harmony projec You can download and run the latest snapshot build with a single command. This will place the binary in ~/.local/bin, which should be in your PATH on most modern Linux distributions. ```bash - -curl -Ls https://git.nationtech.io/NationTech/harmony/releases/download/snapshot-latest/harmony_composer \ +mkdir -p ~/.local/bin && \ +curl -L https://git.nationtech.io/NationTech/harmony/releases/download/snapshot-latest/harmony_composer \ -o ~/.local/bin/harmony_composer && \ -chmod +x ~/.local/bin/harmony_composer +chmod +x ~/.local/bin/harmony_composer && \ +alias hc=~/.local/bin/harmony_composer && \ +echo "\n\nharmony_composer installed successfully\!\n\nUse \`hc\` to run it.\n\nNote : this hc alias only works for the current shell session. Add 'alias hc=~/.local/bin/harmony_composer' to your '~/.bashrc' or '~/.zshrc' file to make it permanently available to your user." ``` +Then you can start using it with either : + +- `harmony_composer` if `~/.local/bin` is in you `$PATH` +- `hc` alias set up in your current shell session. + - If you want to make the `hc` command always available, add `alias hc=~/.local/bin/harmony_composer` to your shell profile. Usually `~/.bashrc` for bash, `~/.zshrc` for zsh. + > ⚠️ Warning: Unstable Builds > The snapshot-latest tag points to the latest build from the master branch. It is unstable, unsupported, and intended only for early testing of new features. Please do not use it in production environments. diff --git a/harmony_composer/src/main.rs b/harmony_composer/src/main.rs index 0bdb7be..648977e 100644 --- a/harmony_composer/src/main.rs +++ b/harmony_composer/src/main.rs @@ -73,10 +73,9 @@ async fn main() { .try_exists() .expect("couldn't check if path exists"); - let harmony_bin_path: PathBuf; - match harmony_path { + let harmony_bin_path: PathBuf = match harmony_path { true => { - harmony_bin_path = compile_harmony( + compile_harmony( cli_args.compile_method, cli_args.compile_platform, cli_args.harmony_path.clone(), @@ -84,7 +83,7 @@ async fn main() { .await } false => todo!("implement autodetect code"), - } + }; match cli_args.command { Some(command) => match command { @@ -103,8 +102,10 @@ async fn main() { }; let check_output = Command::new(check_script) - .output() - .expect("failed to run check script"); + .spawn() + .expect("failed to run check script") + .wait_with_output() + .unwrap(); info!( "check stdout: {}, check stderr: {}", String::from_utf8(check_output.stdout).expect("couldn't parse from utf8"), @@ -112,18 +113,16 @@ async fn main() { ); } Commands::Deploy(args) => { - if args.staging { - todo!("implement staging deployment"); + let deploy = if args.staging { + todo!("implement staging deployment") + } else if args.prod { + todo!("implement prod deployment") + } else { + Command::new(harmony_bin_path).arg("-y").arg("-a").spawn() } + .expect("failed to run harmony deploy"); - if args.prod { - todo!("implement prod deployment"); - } - let deploy_output = Command::new(harmony_bin_path) - .arg("-y") - .arg("-a") - .output() - .expect("failed to run harmony deploy"); + let deploy_output = deploy.wait_with_output().unwrap(); println!( "deploy output: {}", String::from_utf8(deploy_output.stdout).expect("couldn't parse from utf8")