From 027114c48ce91e1f6f5e5d6038c297b7bbe57827 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Fri, 11 Apr 2025 16:20:59 -0400 Subject: [PATCH 01/12] feat: introduce topology readiness and initialization Adds a `ensure_ready` method to the `Topology` trait to ensure the infrastructure is prepared before score execution. - Introduces a new `Outcome` status to indicate the result of the readiness check. - Implements a `topology_preparation_result` field in `Maestro` to track initialization status. - Adds a check in `interpret` to warn if the topology isn't fully initialized. - Provides detailed documentation for the `Topology` trait and `ensure_ready` method, including recommended patterns for complex setups. - Adds `async_trait` dependency. --- harmony/src/domain/maestro/mod.rs | 49 +++++++++++++++++++++++++----- harmony/src/domain/topology/mod.rs | 33 +++++++++++++++++++- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index 256c759..2bea72d 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -1,9 +1,9 @@ -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, Mutex, RwLock}; -use log::info; +use log::{info, warn}; use super::{ - interpret::{InterpretError, Outcome}, + interpret::{InterpretError, InterpretStatus, Outcome}, inventory::Inventory, score::Score, topology::Topology, @@ -15,6 +15,7 @@ pub struct Maestro { inventory: Inventory, topology: T, scores: Arc>>, + topology_preparation_result: Mutex>, } impl Maestro { @@ -23,9 +24,28 @@ impl Maestro { inventory, topology, scores: Arc::new(RwLock::new(Vec::new())), + topology_preparation_result: None.into(), } } + /// Ensures the associated Topology is ready for operations. + /// Delegates the readiness check and potential setup actions to the Topology. + pub async fn prepare_topology(&self) -> Result { + info!("Ensuring topology '{}' is ready...", self.topology.name()); + let outcome = self.topology.ensure_ready().await?; + info!( + "Topology '{}' readiness check complete: {}", + self.topology.name(), + outcome.status + ); + + self.topology_preparation_result + .lock() + .unwrap() + .replace(outcome.clone()); + Ok(outcome) + } + // Load the inventory and inventory from environment. // This function is able to discover the context that it is running in, such as k8s clusters, aws cloud, linux host, etc. // When the HARMONY_TOPOLOGY environment variable is not set, it will default to install k3s @@ -47,16 +67,31 @@ impl Maestro { } } - pub fn start(&mut self) { - info!("Starting Maestro"); - } - pub fn register_all(&mut self, mut scores: ScoreVec) { let mut score_mut = self.scores.write().expect("Should acquire lock"); score_mut.append(&mut scores); } + fn is_topology_initialized(&self) -> bool { + let result = self.topology_preparation_result.lock().unwrap(); + if let Some(outcome) = result.as_ref() { + match outcome.status { + InterpretStatus::SUCCESS => return true, + _ => return false, + } + } else { + false + } + } + pub async fn interpret(&self, score: Box>) -> Result { + if !self.is_topology_initialized() { + warn!( + "Launching interpret for score {} but Topology {} is not fully initialized!", + score.name(), + self.topology.name(), + ); + } info!("Running score {score:?}"); let interpret = score.create_interpret(); info!("Launching interpret {interpret:?}"); diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index fa0b7fe..16525f5 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -5,6 +5,7 @@ mod load_balancer; pub mod openshift; mod router; mod tftp; +use async_trait::async_trait; pub use ha_cluster::*; pub use load_balancer::*; pub use router::*; @@ -17,8 +18,38 @@ pub use tftp::*; use std::net::IpAddr; -pub trait Topology { +use super::interpret::{InterpretError, Outcome}; + +/// Represents a logical view of an infrastructure environment providing specific capabilities. +/// +/// A Topology acts as a self-contained "package" responsible for managing access +/// to its underlying resources and ensuring they are in a ready state before use. +/// It defines the contract for the capabilities it provides through implemented +/// capability traits (e.g., `HasK8sCapability`, `HasDnsServer`). +#[async_trait] +pub trait Topology: Send + Sync { + /// Returns a unique identifier or name for this specific topology instance. + /// This helps differentiate between multiple instances of potentially the same type. fn name(&self) -> &str; + + /// Ensures that the topology and its required underlying components or services + /// are ready to provide their declared capabilities. + /// + /// Implementations of this method MUST be idempotent. Subsequent calls after a + /// successful readiness check should ideally be cheap NO-OPs. + /// + /// This method encapsulates the logic for: + /// 1. **Checking Current State:** Assessing if the required resources/services are already running and configured. + /// 2. **Discovery:** Identifying the runtime environment (e.g., local Docker, AWS, existing cluster). + /// 3. **Initialization/Bootstrapping:** Performing necessary setup actions if not already ready. This might involve: + /// * Making API calls. + /// * Running external commands (e.g., `k3d`, `docker`). + /// * **Internal Orchestration:** For complex topologies, this method might manage dependencies on other sub-topologies, ensuring *their* `ensure_ready` is called first. Using nested `Maestros` to run setup `Scores` against these sub-topologies is the recommended pattern for non-trivial bootstrapping, allowing reuse of Harmony's core orchestration logic. + /// + /// # Returns + /// - `Ok(Outcome)`: Indicates the topology is now ready. The `Outcome` status might be `SUCCESS` if actions were taken, or `NOOP` if it was already ready. The message should provide context. + /// - `Err(TopologyError)`: Indicates the topology could not reach a ready state due to configuration issues, discovery failures, bootstrap errors, or unsupported environments. + async fn ensure_ready(&self) -> Result; } pub type IpAddress = IpAddr; From 6812d05849b0556524ef7ff2338b874bf25c0c17 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Mon, 14 Apr 2025 14:57:01 -0400 Subject: [PATCH 02/12] feat: Introduce K8sAnywhereTopology and refactor Kubernetes interactions This commit introduces a new topology, `K8sAnywhereTopology`, designed to handle Kubernetes deployments more flexibly. Key changes include: - Introduced `K8sAnywhereTopology` to encapsulate Kubernetes client management and configuration. - Refactored existing Kubernetes-related code to utilize the new topology. - Updated `OcK8sclient` to `K8sclient` across modules (k8s, lamp, deployment, resource) for consistency. - Ensured all relevant modules now interface with Kubernetes through the `K8sclient` trait. This change promotes a more modular and maintainable codebase for Kubernetes integrations within Harmony. --- harmony/src/domain/topology/ha_cluster.rs | 16 ++- .../domain/topology/{openshift.rs => k8s.rs} | 4 +- harmony/src/domain/topology/k8s_anywhere.rs | 119 ++++++++++++++++++ harmony/src/domain/topology/mod.rs | 4 +- harmony/src/domain/topology/network.rs | 6 +- harmony/src/modules/k8s/deployment.rs | 4 +- harmony/src/modules/k8s/resource.rs | 6 +- harmony/src/modules/lamp.rs | 4 +- 8 files changed, 145 insertions(+), 18 deletions(-) rename harmony/src/domain/topology/{openshift.rs => k8s.rs} (96%) create mode 100644 harmony/src/domain/topology/k8s_anywhere.rs diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 0e9230b..d2af961 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -3,6 +3,8 @@ use harmony_macros::ip; use harmony_types::net::MacAddress; use crate::executors::ExecutorError; +use crate::interpret::InterpretError; +use crate::interpret::Outcome; use super::DHCPStaticEntry; use super::DhcpServer; @@ -15,13 +17,13 @@ use super::IpAddress; use super::LoadBalancer; use super::LoadBalancerService; use super::LogicalHost; -use super::OcK8sclient; +use super::K8sclient; use super::Router; use super::TftpServer; use super::Topology; use super::Url; -use super::openshift::OpenshiftClient; +use super::k8s::K8sClient; use std::sync::Arc; #[derive(Debug, Clone)] @@ -40,16 +42,20 @@ pub struct HAClusterTopology { pub switch: Vec, } +#[async_trait] impl Topology for HAClusterTopology { fn name(&self) -> &str { todo!() } + async fn ensure_ready(&self) -> Result { + todo!("ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready.") + } } #[async_trait] -impl OcK8sclient for HAClusterTopology { - async fn oc_client(&self) -> Result, kube::Error> { - Ok(Arc::new(OpenshiftClient::try_default().await?)) +impl K8sclient for HAClusterTopology { + async fn k8s_client(&self) -> Result, kube::Error> { + Ok(Arc::new(K8sClient::try_default().await?)) } } diff --git a/harmony/src/domain/topology/openshift.rs b/harmony/src/domain/topology/k8s.rs similarity index 96% rename from harmony/src/domain/topology/openshift.rs rename to harmony/src/domain/topology/k8s.rs index 1790a06..ed345ee 100644 --- a/harmony/src/domain/topology/openshift.rs +++ b/harmony/src/domain/topology/k8s.rs @@ -2,11 +2,11 @@ use k8s_openapi::NamespaceResourceScope; use kube::{Api, Client, Error, Resource, api::PostParams}; use serde::de::DeserializeOwned; -pub struct OpenshiftClient { +pub struct K8sClient { client: Client, } -impl OpenshiftClient { +impl K8sClient { pub async fn try_default() -> Result { Ok(Self { client: Client::try_default().await?, diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs new file mode 100644 index 0000000..78e4a0e --- /dev/null +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -0,0 +1,119 @@ +use async_trait::async_trait; +use log::info; +use tokio::sync::OnceCell; + +use crate::interpret::{InterpretError, Outcome}; + +use super::{Topology, k8s::K8sClient}; + +struct K8sState { + client: K8sClient, + source: K8sSource, + message: String, +} + +enum K8sSource { + Existing, + K3d, + // TODO: Add variants for cloud providers like AwsEks, Gke, Aks +} + +pub struct K8sAnywhereTopology { + k8s_state: OnceCell>, +} + +impl K8sAnywhereTopology { + async fn try_load_default_kubeconfig(&self) -> Option { + todo!("Use kube-rs default behavior to load system kubeconfig"); + } + + async fn try_load_kubeconfig(&self, path: &str) -> Option { + todo!("Use kube-rs to load kubeconfig at path {path}"); + } + + async fn try_install_k3d(&self) -> Result { + todo!( + "Create Maestro with LocalDockerTopology or something along these lines and run a K3dInstallationScore on it" + ) + } + + async fn try_get_or_install_k8s_client(&self) -> Result, InterpretError> { + let k8s_anywhere_config = K8sAnywhereConfig { + kubeconfig: std::env::var("HARMONY_KUBECONFIG") + .ok() + .map(|v| v.to_string()), + use_system_kubeconfig: std::env::var("HARMONY_USE_SYSTEM_KUBECONFIG") + .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)), + }; + + if k8s_anywhere_config.use_system_kubeconfig { + match self.try_load_default_kubeconfig().await { + Some(client) => todo!(), + None => todo!(), + } + } + + if let Some(kubeconfig) = k8s_anywhere_config.kubeconfig { + match self.try_load_kubeconfig(&kubeconfig).await { + Some(client) => todo!(), + None => todo!(), + } + } + info!("No kubernetes configuration found"); + + if !k8s_anywhere_config.autoinstall { + info!( + "Harmony autoinstallation is not activated, do you wish to launch autoinstallation?" + ); + todo!("Prompt user"); + } + + match self.try_install_k3d().await { + Ok(client) => todo!(), + Err(_) => todo!(), + } + } +} + +struct K8sAnywhereConfig { + /// The path of the KUBECONFIG file that Harmony should use to interact with the Kubernetes + /// cluster + /// + /// Default : None + kubeconfig: Option, + + /// Whether to use the system KUBECONFIG, either the environment variable or the file in the + /// default or configured location + /// + /// Default : false + use_system_kubeconfig: bool, + + /// Whether to install automatically a kubernetes cluster + /// + /// When enabled, autoinstall will setup a K3D cluster on the localhost. https://k3d.io/stable/ + /// + /// Default: true + autoinstall: bool, +} + +#[async_trait] +impl Topology for K8sAnywhereTopology { + fn name(&self) -> &str { + todo!() + } + + async fn ensure_ready(&self) -> Result { + match self + .k8s_state + .get_or_try_init(|| self.try_get_or_install_k8s_client()) + .await? + { + Some(k8s_state) => Ok(Outcome::success(k8s_state.message.clone())), + None => Err(InterpretError::new( + "No K8s client could be found or installed".to_string(), + )), + } + } +} diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index 16525f5..29e12fc 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -1,8 +1,10 @@ mod ha_cluster; mod host_binding; mod http; +mod k8s_anywhere; +pub use k8s_anywhere::*; mod load_balancer; -pub mod openshift; +pub mod k8s; mod router; mod tftp; use async_trait::async_trait; diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index 13d1902..d4463ae 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -6,7 +6,7 @@ use serde::Serialize; use crate::executors::ExecutorError; -use super::{IpAddress, LogicalHost, openshift::OpenshiftClient}; +use super::{IpAddress, LogicalHost, k8s::K8sClient}; #[derive(Debug)] pub struct DHCPStaticEntry { @@ -42,8 +42,8 @@ pub struct NetworkDomain { pub name: String, } #[async_trait] -pub trait OcK8sclient: Send + Sync + std::fmt::Debug { - async fn oc_client(&self) -> Result, kube::Error>; +pub trait K8sclient: Send + Sync + std::fmt::Debug { + async fn k8s_client(&self) -> Result, kube::Error>; } #[async_trait] diff --git a/harmony/src/modules/k8s/deployment.rs b/harmony/src/modules/k8s/deployment.rs index cd2ad90..9e7178f 100644 --- a/harmony/src/modules/k8s/deployment.rs +++ b/harmony/src/modules/k8s/deployment.rs @@ -5,7 +5,7 @@ use serde_json::json; use crate::{ interpret::Interpret, score::Score, - topology::{OcK8sclient, Topology}, + topology::{K8sclient, Topology}, }; use super::resource::{K8sResourceInterpret, K8sResourceScore}; @@ -16,7 +16,7 @@ pub struct K8sDeploymentScore { pub image: String, } -impl Score for K8sDeploymentScore { +impl Score for K8sDeploymentScore { fn create_interpret(&self) -> Box> { let deployment: Deployment = serde_json::from_value(json!( { diff --git a/harmony/src/modules/k8s/resource.rs b/harmony/src/modules/k8s/resource.rs index 505c4a4..4e54be7 100644 --- a/harmony/src/modules/k8s/resource.rs +++ b/harmony/src/modules/k8s/resource.rs @@ -8,7 +8,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{OcK8sclient, Topology}, + topology::{K8sclient, Topology}, }; #[derive(Debug, Clone, Serialize)] @@ -63,7 +63,7 @@ impl< + Default + Send + Sync, - T: Topology + OcK8sclient, + T: Topology + K8sclient, > Interpret for K8sResourceInterpret where ::DynamicType: Default, @@ -74,7 +74,7 @@ where topology: &T, ) -> Result { topology - .oc_client() + .k8s_client() .await .expect("Environment should provide enough information to instanciate a client") .apply_namespaced(&self.score.resource) diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index ef7227c..55eefdd 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -9,7 +9,7 @@ use crate::{ inventory::Inventory, modules::k8s::deployment::K8sDeploymentScore, score::Score, - topology::{OcK8sclient, Topology, Url}, + topology::{K8sclient, Topology, Url}, }; #[derive(Debug, Clone, Serialize)] @@ -51,7 +51,7 @@ pub struct LAMPInterpret { } #[async_trait] -impl Interpret for LAMPInterpret { +impl Interpret for LAMPInterpret { async fn execute( &self, inventory: &Inventory, From 3f6f1fa0d45ceb806b60d68ec7c2938c674320a7 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 17 Apr 2025 09:55:33 -0400 Subject: [PATCH 03/12] wip: Implement basic K8sAnywhere setup with K3d support - Added initial K8sAnywhere topology and related modules. - Implemented a basic K3d installation score for cluster bootstrapping. - Introduced LocalhostTopology for local development and testing. - Added necessary module structure and dependencies. - Implemented user prompt for K3d installation confirmation. - Added basic error handling and logging. - Refactored existing code to improve modularity and maintainability. - Included necessary tests to ensure functionality. --- harmony/src/domain/topology/k8s_anywhere.rs | 46 ++++++++++++++++----- harmony/src/domain/topology/localhost.rs | 20 +++++++++ harmony/src/domain/topology/mod.rs | 2 + harmony/src/modules/k3d/install.rs | 25 +++++++++++ harmony/src/modules/k3d/mod.rs | 3 ++ harmony/src/modules/mod.rs | 1 + 6 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 harmony/src/domain/topology/localhost.rs create mode 100644 harmony/src/modules/k3d/install.rs create mode 100644 harmony/src/modules/k3d/mod.rs diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 78e4a0e..1db1aca 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -1,8 +1,12 @@ +use std::io; + use async_trait::async_trait; -use log::info; +use log::{info, warn}; use tokio::sync::OnceCell; -use crate::interpret::{InterpretError, Outcome}; +use crate::{ + interpret::{InterpretError, Outcome}, inventory::Inventory, maestro::Maestro, topology::LocalhostTopology +}; use super::{Topology, k8s::K8sClient}; @@ -13,8 +17,8 @@ struct K8sState { } enum K8sSource { - Existing, - K3d, + RemoteCluster, + LocalK3d, // TODO: Add variants for cloud providers like AwsEks, Gke, Aks } @@ -23,7 +27,7 @@ pub struct K8sAnywhereTopology { } impl K8sAnywhereTopology { - async fn try_load_default_kubeconfig(&self) -> Option { + async fn try_load_system_kubeconfig(&self) -> Option { todo!("Use kube-rs default behavior to load system kubeconfig"); } @@ -32,9 +36,12 @@ impl K8sAnywhereTopology { } async fn try_install_k3d(&self) -> Result { + let maestro = Maestro::new(Inventory::autoload(), LocalhostTopology::new()); + let k3d_score = K3DInstallationScore::default(); + maestro.interpret(Box::new(k3d_score)).await; todo!( "Create Maestro with LocalDockerTopology or something along these lines and run a K3dInstallationScore on it" - ) + ); } async fn try_get_or_install_k8s_client(&self) -> Result, InterpretError> { @@ -49,7 +56,7 @@ impl K8sAnywhereTopology { }; if k8s_anywhere_config.use_system_kubeconfig { - match self.try_load_default_kubeconfig().await { + match self.try_load_system_kubeconfig().await { Some(client) => todo!(), None => todo!(), } @@ -61,17 +68,36 @@ impl K8sAnywhereTopology { None => todo!(), } } + info!("No kubernetes configuration found"); if !k8s_anywhere_config.autoinstall { info!( - "Harmony autoinstallation is not activated, do you wish to launch autoinstallation?" + "Harmony autoinstallation is not activated, do you wish to launch autoinstallation? (y/N) : " ); - todo!("Prompt user"); + let mut input = String::new(); + + io::stdin() + .read_line(&mut input) + .expect("Failed to read line"); + + let input = input.trim(); + + if !input.eq_ignore_ascii_case("y") { + warn!( + "Installation cancelled, K8sAnywhere could not initialize a valid Kubernetes client" + ); + return Ok(None); + } } + info!("Starting K8sAnywhere installation"); match self.try_install_k3d().await { - Ok(client) => todo!(), + Ok(client) => Ok(Some(K8sState { + client, + source: K8sSource::LocalK3d, + message: "Successfully installed K3D cluster and acquired client".to_string(), + })), Err(_) => todo!(), } } diff --git a/harmony/src/domain/topology/localhost.rs b/harmony/src/domain/topology/localhost.rs new file mode 100644 index 0000000..19804e8 --- /dev/null +++ b/harmony/src/domain/topology/localhost.rs @@ -0,0 +1,20 @@ +use async_trait::async_trait; +use derive_new::new; + +use crate::interpret::{InterpretError, Outcome}; + +use super::Topology; + +#[derive(new)] +pub struct LocalhostTopology; + +#[async_trait] +impl Topology for LocalhostTopology { + fn name(&self) -> &str { + "LocalHostTopology" + } + + async fn ensure_ready(&self) -> Result { + Ok(Outcome::success("Localhost is Chuck Norris, always ready.".to_string())) + } +} diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index 29e12fc..b3e3779 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -2,6 +2,8 @@ mod ha_cluster; mod host_binding; mod http; mod k8s_anywhere; +mod localhost; +pub use localhost::*; pub use k8s_anywhere::*; mod load_balancer; pub mod k8s; diff --git a/harmony/src/modules/k3d/install.rs b/harmony/src/modules/k3d/install.rs new file mode 100644 index 0000000..386120f --- /dev/null +++ b/harmony/src/modules/k3d/install.rs @@ -0,0 +1,25 @@ +use serde::Serialize; + +use crate::{score::Score, topology::Topology}; + +#[derive(Debug, Clone, Serialize)] +pub struct K3DInstallationScore {} + +impl Score for K3DInstallationScore { + fn create_interpret(&self) -> Box> { + todo!(" + 1. Decide if I create a new crate for k3d management, especially to avoid the ocrtograb dependency + 2. Implement k3d management + 3. Find latest tag + 4. Download k3d to some path managed by harmony (or not?) + 5. Bootstrap cluster + 6. Get kubeconfig + 7. Load kubeconfig in k8s anywhere + 8. Complete k8sanywhere setup + ") + } + + fn name(&self) -> String { + todo!() + } +} diff --git a/harmony/src/modules/k3d/mod.rs b/harmony/src/modules/k3d/mod.rs new file mode 100644 index 0000000..d7db5d4 --- /dev/null +++ b/harmony/src/modules/k3d/mod.rs @@ -0,0 +1,3 @@ + +mod install; + diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index 8456867..60ddfd2 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -8,3 +8,4 @@ pub mod load_balancer; pub mod okd; pub mod opnsense; pub mod tftp; +mod k3d; From 847d84b46ff854c24bf7e6cbc9cbb4891631adec Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 17 Apr 2025 13:04:06 -0400 Subject: [PATCH 04/12] wip: Started work on k3d crate --- Cargo.lock | 231 +++++++++++++++++++- Cargo.toml | 1 + harmony/src/domain/topology/k8s_anywhere.rs | 2 +- harmony/src/modules/k3d/mod.rs | 2 +- harmony/src/modules/mod.rs | 2 +- k3d/Cargo.toml | 23 ++ k3d/src/lib.rs | 12 + 7 files changed, 262 insertions(+), 11 deletions(-) create mode 100644 k3d/Cargo.toml create mode 100644 k3d/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 28e9b2b..554bcd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "assert_cmd" version = "2.0.17" @@ -401,9 +407,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.36" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", "clap_derive", @@ -411,9 +417,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.36" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", @@ -706,15 +712,24 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "pem-rfc7468", "zeroize", ] +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive-new" version = "0.7.0" @@ -1214,8 +1229,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1867,6 +1884,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1935,6 +1962,33 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "k3d-rs" +version = "0.1.0" +dependencies = [ + "async-trait", + "env_logger", + "log", + "octocrab", + "pretty_assertions", + "tokio", +] + [[package]] name = "k8s-openapi" version = "0.24.0" @@ -2213,6 +2267,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -2262,6 +2322,46 @@ dependencies = [ "memchr", ] +[[package]] +name = "octocrab" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf799a9982a4d0b4b3fa15b4c1ff7daf5bd0597f46456744dcbb6ddc2e4c827" +dependencies = [ + "arc-swap", + "async-trait", + "base64 0.22.1", + "bytes", + "cfg-if", + "chrono", + "either", + "futures", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "jsonwebtoken", + "once_cell", + "percent-encoding", + "pin-project", + "secrecy", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "snafu", + "tokio", + "tower", + "tower-http", + "tracing", + "url", + "web-time", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -2542,6 +2642,26 @@ dependencies = [ "sha2", ] +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2636,6 +2756,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -3030,9 +3156,9 @@ dependencies = [ [[package]] name = "russh-sftp" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f08ed364d54b74d988c964b464a53a1916379f9441cfd10ca8fb264be1349842" +checksum = "3bb94393cafad0530145b8f626d8687f1ee1dedb93d7ba7740d6ae81868b13b5" dependencies = [ "bitflags 2.9.0", "bytes", @@ -3336,6 +3462,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_tokenstream" version = "0.2.2" @@ -3451,6 +3587,18 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.12", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -3466,6 +3614,27 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +[[package]] +name = "snafu" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "socket2" version = "0.5.9" @@ -3719,6 +3888,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -3836,10 +4036,13 @@ dependencies = [ "base64 0.22.1", "bitflags 2.9.0", "bytes", + "futures-util", "http 1.3.1", "http-body 1.0.1", + "iri-string", "mime", "pin-project-lite", + "tower", "tower-layer", "tower-service", "tracing", @@ -4010,6 +4213,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -4184,6 +4388,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 4ba83eb..1791b36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "opnsense-config", "opnsense-config-xml", "harmony_cli", + "k3d", ] [workspace.package] diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 1db1aca..f91d88b 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -5,7 +5,7 @@ use log::{info, warn}; use tokio::sync::OnceCell; use crate::{ - interpret::{InterpretError, Outcome}, inventory::Inventory, maestro::Maestro, topology::LocalhostTopology + interpret::{InterpretError, Outcome}, inventory::Inventory, maestro::Maestro, modules::k3d::K3DInstallationScore, topology::LocalhostTopology }; use super::{Topology, k8s::K8sClient}; diff --git a/harmony/src/modules/k3d/mod.rs b/harmony/src/modules/k3d/mod.rs index d7db5d4..2d243c0 100644 --- a/harmony/src/modules/k3d/mod.rs +++ b/harmony/src/modules/k3d/mod.rs @@ -1,3 +1,3 @@ - mod install; +pub use install::*; diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index 60ddfd2..9baa98a 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -8,4 +8,4 @@ pub mod load_balancer; pub mod okd; pub mod opnsense; pub mod tftp; -mod k3d; +pub mod k3d; diff --git a/k3d/Cargo.toml b/k3d/Cargo.toml new file mode 100644 index 0000000..8e5c781 --- /dev/null +++ b/k3d/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "k3d-rs" +edition = "2021" +version.workspace = true +readme.workspace = true +license.workspace = true + +[dependencies] +#serde = { version = "1.0.123", features = [ "derive" ] } +log = { workspace = true } +env_logger = { workspace = true } +#russh = { workspace = true } +#russh-keys = { workspace = true } +#thiserror = "1.0" +async-trait = { workspace = true } +tokio = { workspace = true } +octocrab = "0.44.0" +#serde_json = "1.0.133" +#tokio-util = { version = "0.7.13", features = [ "codec" ] } +#tokio-stream = "0.1.17" + +[dev-dependencies] +pretty_assertions = "1.4.1" diff --git a/k3d/src/lib.rs b/k3d/src/lib.rs new file mode 100644 index 0000000..3227138 --- /dev/null +++ b/k3d/src/lib.rs @@ -0,0 +1,12 @@ +use std::path::PathBuf; + +pub struct K3d { +} + +impl K3d { + pub async fn download_latest_release(&self) -> Result { + + } +} + + From 15785dd2190ab419cb6c06f615ad62d58628d742 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Fri, 18 Apr 2025 23:22:37 -0400 Subject: [PATCH 05/12] feat: download and install k3d latest release - Implemented functionality to fetch the latest k3d release tag from GitHub. - Added logic to determine the appropriate binary URL based on the current platform. - Implemented downloading and saving the binary to a specified directory. - Included unit tests to verify the download and installation process. - Added a `K3D_BIN_FILE_NAME` constant for clarity. - Added logging for better debugging. --- Cargo.lock | 232 ++++++++++++++++++++++++++++++-- Cargo.toml | 2 +- k3d/Cargo.toml | 13 +- k3d/src/downloadable_asset.rs | 243 ++++++++++++++++++++++++++++++++++ k3d/src/lib.rs | 154 ++++++++++++++++++++- 5 files changed, 631 insertions(+), 13 deletions(-) create mode 100644 k3d/src/downloadable_asset.rs diff --git a/Cargo.lock b/Cargo.lock index 554bcd2..d09e201 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,6 +184,12 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.4.0" @@ -1293,6 +1299,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "harmony" version = "0.1.0" @@ -1310,7 +1335,7 @@ dependencies = [ "log", "opnsense-config", "opnsense-config-xml", - "reqwest", + "reqwest 0.11.27", "russh", "rust-ipmi", "semver", @@ -1529,7 +1554,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -1552,6 +1577,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.9", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -1627,6 +1653,22 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.11" @@ -1983,10 +2025,15 @@ version = "0.1.0" dependencies = [ "async-trait", "env_logger", + "futures-util", "log", "octocrab", "pretty_assertions", + "regex", + "reqwest 0.12.15", + "sha2", "tokio", + "url", ] [[package]] @@ -2095,7 +2142,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7f0a8985e53d18c60dc82e7b5fa512fd194ea4c0d8bf1409b65cf44f8b0a8d9" dependencies = [ "log", - "reqwest", + "reqwest 0.11.27", "serde", "serde_derive", "serde_json", @@ -2976,11 +3023,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", - "hyper-tls", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -2994,7 +3041,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", "tokio-native-tls", "tower-service", @@ -3005,6 +3052,52 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.9", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls", + "hyper-tls 0.6.0", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 2.2.0", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "system-configuration 0.6.1", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -3780,6 +3873,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -3800,7 +3896,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", ] [[package]] @@ -3813,6 +3920,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -4378,6 +4495,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -4431,7 +4561,7 @@ dependencies = [ "windows-interface", "windows-link", "windows-result", - "windows-strings", + "windows-strings 0.4.0", ] [[package]] @@ -4462,6 +4592,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings 0.3.1", + "windows-targets 0.53.0", +] + [[package]] name = "windows-result" version = "0.3.2" @@ -4471,6 +4612,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-strings" version = "0.4.0" @@ -4531,13 +4681,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4550,6 +4716,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4562,6 +4734,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4574,12 +4752,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4592,6 +4782,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4604,6 +4800,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4616,6 +4818,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4628,6 +4836,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index 1791b36..7219d71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ 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"] } +tokio = { version = "1.40.0", features = ["io-std", "fs", "macros", "rt-multi-thread"] } cidr = "0.2.3" russh = "0.45.0" russh-keys = "0.45.0" diff --git a/k3d/Cargo.toml b/k3d/Cargo.toml index 8e5c781..633859d 100644 --- a/k3d/Cargo.toml +++ b/k3d/Cargo.toml @@ -15,9 +15,18 @@ env_logger = { workspace = true } async-trait = { workspace = true } tokio = { workspace = true } octocrab = "0.44.0" +regex = "1.11.1" +reqwest = { version = "0.12", features = ["stream"] } +#hyper-rustls = "0.27.5" +#hyper = { version = "1", features = [ "client" ] } +#hyper = { version = "1", features = ["full"] } +#http-body-util = "0.1" +#hyper-util = { version = "0.1", features = ["full"] } +url.workspace = true +sha2 = "0.10.8" +futures-util = "0.3.31" +#bytes = "1.10.1" #serde_json = "1.0.133" -#tokio-util = { version = "0.7.13", features = [ "codec" ] } -#tokio-stream = "0.1.17" [dev-dependencies] pretty_assertions = "1.4.1" diff --git a/k3d/src/downloadable_asset.rs b/k3d/src/downloadable_asset.rs new file mode 100644 index 0000000..cdf9bf2 --- /dev/null +++ b/k3d/src/downloadable_asset.rs @@ -0,0 +1,243 @@ +use futures_util::StreamExt; +use log::{debug, info, warn}; +use sha2::{Digest, Sha256}; +use std::io::Read; +use std::path::PathBuf; +use tokio::fs; +use tokio::fs::File; +use tokio::io::AsyncWriteExt; +use url::Url; + +#[derive(Debug)] +pub(crate) struct DownloadableAsset { + pub(crate) url: Url, + pub(crate) file_name: String, + pub(crate) checksum: String, +} + +impl DownloadableAsset { + fn verify_checksum(&self, file: PathBuf) -> bool { + if !file.exists() { + warn!("File does not exist: {:?}", file); + return false; + } + + let mut file = match std::fs::File::open(&file) { + Ok(file) => file, + Err(e) => { + warn!("Failed to open file for checksum verification: {:?}", e); + return false; + } + }; + + let mut hasher = Sha256::new(); + let mut buffer = [0; 1024 * 1024]; // 1MB buffer + + loop { + let bytes_read = match file.read(&mut buffer) { + Ok(0) => break, + Ok(n) => n, + Err(e) => { + warn!("Error reading file for checksum: {:?}", e); + return false; + } + }; + + hasher.update(&buffer[..bytes_read]); + } + + let result = hasher.finalize(); + let calculated_hash = format!("{:x}", result); + + debug!("Expected checksum: {}", self.checksum); + debug!("Calculated checksum: {}", calculated_hash); + + calculated_hash == self.checksum + } + + pub(crate) async fn download_to_path(&self, folder: PathBuf) -> Result { + if !folder.exists() { + fs::create_dir_all(&folder) + .await + .expect("Failed to create download directory"); + } + + let target_file_path = folder.join(&self.file_name); + debug!("Downloading to path: {:?}", target_file_path); + + if self.verify_checksum(target_file_path.clone()) { + debug!("File already exists with correct checksum, skipping download"); + return Ok(target_file_path); + } + + debug!("Downloading from URL: {}", self.url); + let client = reqwest::Client::new(); + let response = client + .get(self.url.clone()) + .send() + .await + .map_err(|e| format!("Failed to download file: {e}"))?; + + if !response.status().is_success() { + return Err(format!( + "Failed to download file, status: {}", + response.status() + )); + } + + let mut file = File::create(&target_file_path) + .await + .expect("Failed to create target file"); + + let mut stream = response.bytes_stream(); + while let Some(chunk_result) = stream.next().await { + let chunk = chunk_result.expect("Error while downloading file"); + file.write_all(&chunk) + .await + .expect("Failed to write data to file"); + } + + file.flush().await.expect("Failed to flush file"); + drop(file); + + if !self.verify_checksum(target_file_path.clone()) { + panic!("Downloaded file failed checksum verification"); + } + + info!( + "File downloaded and verified successfully: {}", + target_file_path.to_string_lossy() + ); + Ok(target_file_path) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use std::net::TcpListener; + use std::sync::OnceLock; + use std::thread; + + const BASE_TEST_PATH: &str = "/tmp/harmony-test-k3d-download"; + const TEST_SERVER_PORT: u16 = 18452; + const TEST_CONTENT: &str = "This is a test file."; + const TEST_CONTENT_HASH: &str = + "f29bc64a9d3732b4b9035125fdb3285f5b6455778edca72414671e0ca3b2e0de"; + + struct TestContext { + download_path: String, + domain: String, + } + + static TEST_SERVER: OnceLock<()> = OnceLock::new(); + + fn init_logs() { + let _ = env_logger::builder().try_init(); + } + + fn setup_test() -> TestContext { + init_logs(); + + TEST_SERVER.get_or_init(|| { + let listener = TcpListener::bind(format!("127.0.0.1:{}", TEST_SERVER_PORT)).unwrap(); + + thread::spawn(move || { + for stream in listener.incoming() { + thread::spawn(move || { + let mut stream = stream.expect("Stream opened correctly"); + let mut buffer = [0; 1024]; + let _ = stream.read(&mut buffer); + + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\n\r\n{}", + TEST_CONTENT.len(), + TEST_CONTENT + ); + + stream.write_all(response.as_bytes()).expect("Can write to stream"); + stream.flush().expect("Can flush stream"); + }); + } + }); + }); + + let test_id = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(); + let download_path = format!("{}/test_{}", BASE_TEST_PATH, test_id); + std::fs::create_dir_all(&download_path).unwrap(); + + assert!(wait_for_server_ready(1000), "Test server failed to start"); + + TestContext { + download_path, + domain: format!("127.0.0.1:{}", TEST_SERVER_PORT), + } + } + + fn wait_for_server_ready(timeout_ms: u64) -> bool { + let start = std::time::Instant::now(); + let timeout = std::time::Duration::from_millis(timeout_ms); + + while start.elapsed() < timeout { + if std::net::TcpStream::connect(format!("127.0.0.1:{}", TEST_SERVER_PORT)).is_ok() { + return true; + } + std::thread::sleep(std::time::Duration::from_millis(50)); + } + false + } + + #[tokio::test] + async fn test_download_to_path_success() { + let test = setup_test(); + + let asset = DownloadableAsset { + url: Url::parse(&format!("http://{}/test.txt", test.domain)).unwrap(), + file_name: "test.txt".to_string(), + checksum: TEST_CONTENT_HASH.to_string(), + }; + + let folder = PathBuf::from(&test.download_path); + let result = asset.download_to_path(folder).await.unwrap(); + + let downloaded_content = std::fs::read_to_string(result).unwrap(); + assert_eq!(downloaded_content, TEST_CONTENT); + } + + #[tokio::test] + async fn test_download_to_path_already_exists() { + let test = setup_test(); + let folder = PathBuf::from(&test.download_path); + + let asset = DownloadableAsset { + url: Url::parse(&format!("http://{}/test.txt", test.domain)).unwrap(), + file_name: "test.txt".to_string(), + checksum: TEST_CONTENT_HASH.to_string(), + }; + + let target_file_path = folder.join(&asset.file_name); + std::fs::write(&target_file_path, TEST_CONTENT).unwrap(); + + let result = asset.download_to_path(folder).await.unwrap(); + let content = std::fs::read_to_string(result).unwrap(); + assert_eq!(content, TEST_CONTENT); + } + + #[tokio::test] + async fn test_download_to_path_failure() { + let test = setup_test(); + + let asset = DownloadableAsset { + url: Url::parse("http://127.0.0.1:9999/test.txt").unwrap(), + file_name: "test.txt".to_string(), + checksum: "some_checksum".to_string(), + }; + + let result = asset.download_to_path(PathBuf::from(&test.download_path)).await; + assert!(result.is_err()); + } +} diff --git a/k3d/src/lib.rs b/k3d/src/lib.rs index 3227138..8e7fb72 100644 --- a/k3d/src/lib.rs +++ b/k3d/src/lib.rs @@ -1,12 +1,164 @@ +mod downloadable_asset; +use downloadable_asset::*; + +use log::{debug, info}; use std::path::PathBuf; +const K3D_BIN_FILE_NAME: &str = "k3d"; + pub struct K3d { + base_dir: PathBuf, } impl K3d { - pub async fn download_latest_release(&self) -> Result { + pub fn new(base_dir: PathBuf) -> Self { + Self { base_dir } + } + async fn get_binary_for_current_platform( + &self, + latest_release: octocrab::models::repos::Release, + ) -> DownloadableAsset { + let os = std::env::consts::OS; + let arch = std::env::consts::ARCH; + + debug!("Detecting platform: OS={}, ARCH={}", os, arch); + + // 2. Construct the binary name pattern based on platform + let binary_pattern = match (os, arch) { + ("linux", "x86") => "k3d-linux-386", + ("linux", "x86_64") => "k3d-linux-amd64", + ("linux", "arm") => "k3d-linux-arm", + ("linux", "aarch64") => "k3d-linux-arm64", + ("windows", "x86_64") => "k3d-windows-amd64.exe", + ("macos", "x86_64") => "k3d-darwin-amd64", + ("macos", "aarch64") => "k3d-darwin-arm64", + _ => panic!("Unsupported platform: {}-{}", os, arch), + }; + + debug!("Looking for binary matching pattern: {}", binary_pattern); + + // 3. Find the matching binary in release assets + let binary_asset = latest_release + .assets + .iter() + .find(|asset| asset.name == binary_pattern) + .unwrap_or_else(|| panic!("No matching binary found for {}", binary_pattern)); + + let binary_url = binary_asset.browser_download_url.clone(); + + // 4. Find and parse the checksums file + let checksums_asset = latest_release + .assets + .iter() + .find(|asset| asset.name == "checksums.txt") + .expect("Checksums file not found in release assets"); + + // 5. Download and parse checksums file + let checksums_url = checksums_asset.browser_download_url.clone(); + + let body = reqwest::get(checksums_url) + .await + .unwrap() + .text() + .await + .unwrap(); + println!("body: {body}"); + + // 6. Find the checksum for our binary + let checksum = body + .lines() + .find_map(|line| { + if line.ends_with(&binary_pattern) { + Some(line.split_whitespace().next().unwrap_or("").to_string()) + } else { + None + } + }) + .unwrap_or_else(|| panic!("Checksum not found for {}", binary_pattern)); + + debug!("Found binary at {} with checksum {}", binary_url, checksum); + + DownloadableAsset { + url: binary_url, + file_name: K3D_BIN_FILE_NAME.to_string(), + checksum, + } + } + + pub async fn download_latest_release(&self) -> Result { + let latest_release = self.get_latest_release_tag().await.unwrap(); + + let release_binary = self.get_binary_for_current_platform(latest_release).await; + info!("Foudn K3d binary to install : {release_binary:#?}"); + release_binary.download_to_path(self.base_dir.clone()).await + } + + // TODO : Make sure this will only find actual released versions, no prereleases or test + // builds + pub async fn get_latest_release_tag(&self) -> Result { + let octo = octocrab::instance(); + let latest_release = octo + .repos("k3d-io", "k3d") + .releases() + .get_latest() + .await + .map_err(|e| e.to_string())?; + // debug!("Got k3d releases {releases:#?}"); + println!("Got k3d first releases {latest_release:#?}"); + + Ok(latest_release) } } +#[cfg(test)] +mod test { + use regex::Regex; + use std::path::PathBuf; + use crate::{K3d, K3D_BIN_FILE_NAME}; + + #[tokio::test] + async fn k3d_latest_release_should_get_latest() { + let dir = get_clean_test_directory(); + + assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), false); + + let k3d = K3d::new(dir.clone()); + let latest_release = k3d.get_latest_release_tag().await.unwrap(); + + let tag_regex = Regex::new(r"^v\d+\.\d+\.\d+$").unwrap(); + assert!(tag_regex.is_match(&latest_release.tag_name)); + assert!(!latest_release.tag_name.is_empty()); + } + + #[tokio::test] + async fn k3d_download_latest_release_should_get_latest_bin() { + let dir = get_clean_test_directory(); + + assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), false); + + let k3d = K3d::new(dir.clone()); + let bin_file_path = k3d.download_latest_release().await.unwrap(); + assert_eq!(bin_file_path, dir.join(K3D_BIN_FILE_NAME)); + assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), true); + } + + fn get_clean_test_directory() -> PathBuf { + let dir = PathBuf::from("/tmp/harmony-k3d-test-dir"); + + if dir.exists() { + if let Err(e) = std::fs::remove_dir_all(&dir) { + // TODO sometimes this fails because of the race when running multiple tests at + // once + panic!("Failed to clean up test directory: {}", e); + } + } + + if let Err(e) = std::fs::create_dir_all(&dir) { + panic!("Failed to create test directory: {}", e); + } + + dir + } +} From 2229e9d7afb74f216452af884ff4b13c201c33d3 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Fri, 18 Apr 2025 23:36:39 -0400 Subject: [PATCH 06/12] chore: Cargo fmt --- harmony/src/domain/topology/ha_cluster.rs | 6 ++++-- harmony/src/domain/topology/k8s_anywhere.rs | 6 +++++- harmony/src/domain/topology/localhost.rs | 4 +++- harmony/src/domain/topology/mod.rs | 4 ++-- harmony/src/modules/k3d/mod.rs | 1 - harmony/src/modules/mod.rs | 2 +- k3d/src/downloadable_asset.rs | 16 +++++++++------- 7 files changed, 24 insertions(+), 15 deletions(-) diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index d2af961..a9bde4c 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -14,10 +14,10 @@ use super::DnsServer; use super::Firewall; use super::HttpServer; use super::IpAddress; +use super::K8sclient; use super::LoadBalancer; use super::LoadBalancerService; use super::LogicalHost; -use super::K8sclient; use super::Router; use super::TftpServer; @@ -48,7 +48,9 @@ impl Topology for HAClusterTopology { todo!() } async fn ensure_ready(&self) -> Result { - todo!("ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready.") + todo!( + "ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready." + ) } } diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index f91d88b..9eb4837 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -5,7 +5,11 @@ use log::{info, warn}; use tokio::sync::OnceCell; use crate::{ - interpret::{InterpretError, Outcome}, inventory::Inventory, maestro::Maestro, modules::k3d::K3DInstallationScore, topology::LocalhostTopology + interpret::{InterpretError, Outcome}, + inventory::Inventory, + maestro::Maestro, + modules::k3d::K3DInstallationScore, + topology::LocalhostTopology, }; use super::{Topology, k8s::K8sClient}; diff --git a/harmony/src/domain/topology/localhost.rs b/harmony/src/domain/topology/localhost.rs index 19804e8..dfa1c0b 100644 --- a/harmony/src/domain/topology/localhost.rs +++ b/harmony/src/domain/topology/localhost.rs @@ -15,6 +15,8 @@ impl Topology for LocalhostTopology { } async fn ensure_ready(&self) -> Result { - Ok(Outcome::success("Localhost is Chuck Norris, always ready.".to_string())) + Ok(Outcome::success( + "Localhost is Chuck Norris, always ready.".to_string(), + )) } } diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index b3e3779..e792227 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -3,10 +3,10 @@ mod host_binding; mod http; mod k8s_anywhere; mod localhost; -pub use localhost::*; pub use k8s_anywhere::*; -mod load_balancer; +pub use localhost::*; pub mod k8s; +mod load_balancer; mod router; mod tftp; use async_trait::async_trait; diff --git a/harmony/src/modules/k3d/mod.rs b/harmony/src/modules/k3d/mod.rs index 2d243c0..0dcdd5e 100644 --- a/harmony/src/modules/k3d/mod.rs +++ b/harmony/src/modules/k3d/mod.rs @@ -1,3 +1,2 @@ mod install; pub use install::*; - diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index 9baa98a..19d88d7 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -2,10 +2,10 @@ pub mod dhcp; pub mod dns; pub mod dummy; pub mod http; +pub mod k3d; pub mod k8s; pub mod lamp; pub mod load_balancer; pub mod okd; pub mod opnsense; pub mod tftp; -pub mod k3d; diff --git a/k3d/src/downloadable_asset.rs b/k3d/src/downloadable_asset.rs index cdf9bf2..53de329 100644 --- a/k3d/src/downloadable_asset.rs +++ b/k3d/src/downloadable_asset.rs @@ -142,20 +142,20 @@ mod tests { TEST_SERVER.get_or_init(|| { let listener = TcpListener::bind(format!("127.0.0.1:{}", TEST_SERVER_PORT)).unwrap(); - + thread::spawn(move || { for stream in listener.incoming() { thread::spawn(move || { let mut stream = stream.expect("Stream opened correctly"); let mut buffer = [0; 1024]; let _ = stream.read(&mut buffer); - + let response = format!( "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\n\r\n{}", TEST_CONTENT.len(), TEST_CONTENT ); - + stream.write_all(response.as_bytes()).expect("Can write to stream"); stream.flush().expect("Can flush stream"); }); @@ -203,7 +203,7 @@ mod tests { let folder = PathBuf::from(&test.download_path); let result = asset.download_to_path(folder).await.unwrap(); - + let downloaded_content = std::fs::read_to_string(result).unwrap(); assert_eq!(downloaded_content, TEST_CONTENT); } @@ -212,7 +212,7 @@ mod tests { async fn test_download_to_path_already_exists() { let test = setup_test(); let folder = PathBuf::from(&test.download_path); - + let asset = DownloadableAsset { url: Url::parse(&format!("http://{}/test.txt", test.domain)).unwrap(), file_name: "test.txt".to_string(), @@ -230,14 +230,16 @@ mod tests { #[tokio::test] async fn test_download_to_path_failure() { let test = setup_test(); - + let asset = DownloadableAsset { url: Url::parse("http://127.0.0.1:9999/test.txt").unwrap(), file_name: "test.txt".to_string(), checksum: "some_checksum".to_string(), }; - let result = asset.download_to_path(PathBuf::from(&test.download_path)).await; + let result = asset + .download_to_path(PathBuf::from(&test.download_path)) + .await; assert!(result.is_err()); } } From 83ba0e104498befc5ca63ea77575832d5c5fd76b Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Fri, 18 Apr 2025 23:45:40 -0400 Subject: [PATCH 07/12] fix: Initialize K3DInstallationScore correctly --- harmony/src/domain/topology/k8s_anywhere.rs | 4 ++-- harmony/src/modules/k3d/install.rs | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 9eb4837..2ba5a72 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -41,8 +41,8 @@ impl K8sAnywhereTopology { async fn try_install_k3d(&self) -> Result { let maestro = Maestro::new(Inventory::autoload(), LocalhostTopology::new()); - let k3d_score = K3DInstallationScore::default(); - maestro.interpret(Box::new(k3d_score)).await; + let k3d_score = K3DInstallationScore::new(); + maestro.interpret(Box::new(k3d_score)).await?; todo!( "Create Maestro with LocalDockerTopology or something along these lines and run a K3dInstallationScore on it" ); diff --git a/harmony/src/modules/k3d/install.rs b/harmony/src/modules/k3d/install.rs index 386120f..6cd2bf9 100644 --- a/harmony/src/modules/k3d/install.rs +++ b/harmony/src/modules/k3d/install.rs @@ -5,6 +5,12 @@ use crate::{score::Score, topology::Topology}; #[derive(Debug, Clone, Serialize)] pub struct K3DInstallationScore {} +impl K3DInstallationScore { + pub fn new() -> Self { + Self {} + } +} + impl Score for K3DInstallationScore { fn create_interpret(&self) -> Box> { todo!(" From 9e456bb4f592ff3880870e2522bbe153c009ea83 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Sat, 19 Apr 2025 15:06:05 -0400 Subject: [PATCH 08/12] chore: Refactor DownloadableAsset tests to use httptest instead of a local TcpListener --- Cargo.lock | 41 +++++++ k3d/Cargo.toml | 14 +-- k3d/src/downloadable_asset.rs | 208 ++++++++++++++++++++++------------ 3 files changed, 176 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d09e201..00a9b0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -568,6 +568,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crossterm" version = "0.25.0" @@ -1544,6 +1559,30 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "httptest" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde82de3ef9bd882493c6a5edbc3363ad928925b30ccecc0f2ddeb42601b3021" +dependencies = [ + "bstr", + "bytes", + "crossbeam-channel", + "form_urlencoded", + "futures", + "http 1.3.1", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", +] + [[package]] name = "hyper" version = "0.14.32" @@ -1581,6 +1620,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -2026,6 +2066,7 @@ dependencies = [ "async-trait", "env_logger", "futures-util", + "httptest", "log", "octocrab", "pretty_assertions", diff --git a/k3d/Cargo.toml b/k3d/Cargo.toml index 633859d..1124d75 100644 --- a/k3d/Cargo.toml +++ b/k3d/Cargo.toml @@ -6,27 +6,17 @@ readme.workspace = true license.workspace = true [dependencies] -#serde = { version = "1.0.123", features = [ "derive" ] } log = { workspace = true } -env_logger = { workspace = true } -#russh = { workspace = true } -#russh-keys = { workspace = true } -#thiserror = "1.0" async-trait = { workspace = true } tokio = { workspace = true } octocrab = "0.44.0" regex = "1.11.1" reqwest = { version = "0.12", features = ["stream"] } -#hyper-rustls = "0.27.5" -#hyper = { version = "1", features = [ "client" ] } -#hyper = { version = "1", features = ["full"] } -#http-body-util = "0.1" -#hyper-util = { version = "0.1", features = ["full"] } url.workspace = true sha2 = "0.10.8" futures-util = "0.3.31" -#bytes = "1.10.1" -#serde_json = "1.0.133" [dev-dependencies] +env_logger = { workspace = true } +httptest = "0.16.3" pretty_assertions = "1.4.1" diff --git a/k3d/src/downloadable_asset.rs b/k3d/src/downloadable_asset.rs index 53de329..ababc77 100644 --- a/k3d/src/downloadable_asset.rs +++ b/k3d/src/downloadable_asset.rs @@ -8,6 +8,33 @@ use tokio::fs::File; use tokio::io::AsyncWriteExt; use url::Url; +const CHECKSUM_FAILED_MSG: &str = "Downloaded file failed checksum verification"; + +/// Represents an asset that can be downloaded from a URL with checksum verification. +/// +/// This struct facilitates secure downloading of files from remote URLs by +/// verifying the integrity of the downloaded content using SHA-256 checksums. +/// It handles downloading the file, saving it to disk, and verifying the checksum matches +/// the expected value. +/// +/// # Examples +/// +/// ```compile_fail +/// # use url::Url; +/// # use std::path::PathBuf; +/// +/// # async fn example() -> Result<(), String> { +/// let asset = DownloadableAsset { +/// url: Url::parse("https://example.com/file.zip").unwrap(), +/// file_name: "file.zip".to_string(), +/// checksum: "a1b2c3d4e5f6...".to_string(), +/// }; +/// +/// let download_dir = PathBuf::from("/tmp/downloads"); +/// let file_path = asset.download_to_path(download_dir).await?; +/// # Ok(()) +/// # } +/// ``` #[derive(Debug)] pub(crate) struct DownloadableAsset { pub(crate) url: Url, @@ -55,6 +82,30 @@ impl DownloadableAsset { calculated_hash == self.checksum } + /// Downloads the asset to the specified directory, verifying its checksum. + /// + /// This function will: + /// 1. Create the target directory if it doesn't exist + /// 2. Check if the file already exists with the correct checksum + /// 3. If not, download the file from the URL + /// 4. Verify the downloaded file's checksum matches the expected value + /// + /// # Arguments + /// + /// * `folder` - The directory path where the file should be saved + /// + /// # Returns + /// + /// * `Ok(PathBuf)` - The path to the downloaded file on success + /// * `Err(String)` - A descriptive error message if the download or verification fails + /// + /// # Errors + /// + /// This function will return an error if: + /// - The network request fails + /// - The server responds with a non-success status code + /// - Writing to disk fails + /// - The checksum verification fails pub(crate) async fn download_to_path(&self, folder: PathBuf) -> Result { if !folder.exists() { fs::create_dir_all(&folder) @@ -101,7 +152,7 @@ impl DownloadableAsset { drop(file); if !self.verify_checksum(target_file_path.clone()) { - panic!("Downloaded file failed checksum verification"); + return Err(CHECKSUM_FAILED_MSG.to_string()); } info!( @@ -115,54 +166,20 @@ impl DownloadableAsset { #[cfg(test)] mod tests { use super::*; - use std::io::Write; - use std::net::TcpListener; - use std::sync::OnceLock; - use std::thread; + use httptest::{ + matchers::{self, request}, + responders, Expectation, Server, + }; const BASE_TEST_PATH: &str = "/tmp/harmony-test-k3d-download"; - const TEST_SERVER_PORT: u16 = 18452; const TEST_CONTENT: &str = "This is a test file."; const TEST_CONTENT_HASH: &str = "f29bc64a9d3732b4b9035125fdb3285f5b6455778edca72414671e0ca3b2e0de"; - struct TestContext { - download_path: String, - domain: String, - } - - static TEST_SERVER: OnceLock<()> = OnceLock::new(); - - fn init_logs() { + fn setup_test() -> (PathBuf, Server) { let _ = env_logger::builder().try_init(); - } - - fn setup_test() -> TestContext { - init_logs(); - - TEST_SERVER.get_or_init(|| { - let listener = TcpListener::bind(format!("127.0.0.1:{}", TEST_SERVER_PORT)).unwrap(); - - thread::spawn(move || { - for stream in listener.incoming() { - thread::spawn(move || { - let mut stream = stream.expect("Stream opened correctly"); - let mut buffer = [0; 1024]; - let _ = stream.read(&mut buffer); - - let response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\n\r\n{}", - TEST_CONTENT.len(), - TEST_CONTENT - ); - - stream.write_all(response.as_bytes()).expect("Can write to stream"); - stream.flush().expect("Can flush stream"); - }); - } - }); - }); + // Create unique test directory let test_id = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() @@ -170,51 +187,44 @@ mod tests { let download_path = format!("{}/test_{}", BASE_TEST_PATH, test_id); std::fs::create_dir_all(&download_path).unwrap(); - assert!(wait_for_server_ready(1000), "Test server failed to start"); - - TestContext { - download_path, - domain: format!("127.0.0.1:{}", TEST_SERVER_PORT), - } - } - - fn wait_for_server_ready(timeout_ms: u64) -> bool { - let start = std::time::Instant::now(); - let timeout = std::time::Duration::from_millis(timeout_ms); - - while start.elapsed() < timeout { - if std::net::TcpStream::connect(format!("127.0.0.1:{}", TEST_SERVER_PORT)).is_ok() { - return true; - } - std::thread::sleep(std::time::Duration::from_millis(50)); - } - false + (PathBuf::from(download_path), Server::run()) } #[tokio::test] async fn test_download_to_path_success() { - let test = setup_test(); + let (folder, server) = setup_test(); + + server.expect( + Expectation::matching(request::method_path("GET", "/test.txt")) + .respond_with(responders::status_code(200).body(TEST_CONTENT)), + ); let asset = DownloadableAsset { - url: Url::parse(&format!("http://{}/test.txt", test.domain)).unwrap(), + url: Url::parse(&server.url("/test.txt").to_string()).unwrap(), file_name: "test.txt".to_string(), checksum: TEST_CONTENT_HASH.to_string(), }; - let folder = PathBuf::from(&test.download_path); - let result = asset.download_to_path(folder).await.unwrap(); - + let result = asset + .download_to_path(folder.join("success")) + .await + .unwrap(); let downloaded_content = std::fs::read_to_string(result).unwrap(); assert_eq!(downloaded_content, TEST_CONTENT); } #[tokio::test] async fn test_download_to_path_already_exists() { - let test = setup_test(); - let folder = PathBuf::from(&test.download_path); + let (folder, server) = setup_test(); + + server.expect( + Expectation::matching(matchers::any()) + .times(0) + .respond_with(responders::status_code(200).body(TEST_CONTENT)), + ); let asset = DownloadableAsset { - url: Url::parse(&format!("http://{}/test.txt", test.domain)).unwrap(), + url: Url::parse(&server.url("/test.txt").to_string()).unwrap(), file_name: "test.txt".to_string(), checksum: TEST_CONTENT_HASH.to_string(), }; @@ -228,18 +238,66 @@ mod tests { } #[tokio::test] - async fn test_download_to_path_failure() { - let test = setup_test(); + async fn test_download_to_path_server_error() { + let (folder, server) = setup_test(); + + server.expect( + Expectation::matching(matchers::any()).respond_with(responders::status_code(404)), + ); let asset = DownloadableAsset { - url: Url::parse("http://127.0.0.1:9999/test.txt").unwrap(), + url: Url::parse(&server.url("/test.txt").to_string()).unwrap(), file_name: "test.txt".to_string(), - checksum: "some_checksum".to_string(), + checksum: TEST_CONTENT_HASH.to_string(), }; - let result = asset - .download_to_path(PathBuf::from(&test.download_path)) - .await; + let result = asset.download_to_path(folder.join("error")).await; assert!(result.is_err()); + assert!(result.unwrap_err().contains("status: 404")); + } + + #[tokio::test] + async fn test_download_to_path_checksum_failure() { + let (folder, server) = setup_test(); + + let invalid_content = "This is NOT the expected content"; + server.expect( + Expectation::matching(matchers::any()) + .respond_with(responders::status_code(200).body(invalid_content)), + ); + + let asset = DownloadableAsset { + url: Url::parse(&server.url("/test.txt").to_string()).unwrap(), + file_name: "test.txt".to_string(), + checksum: TEST_CONTENT_HASH.to_string(), + }; + + let join_handle = + tokio::spawn(async move { asset.download_to_path(folder.join("failure")).await }); + + assert_eq!( + join_handle.await.unwrap().err().unwrap(), + CHECKSUM_FAILED_MSG + ); + } + + #[tokio::test] + async fn test_download_with_specific_path_matcher() { + let (folder, server) = setup_test(); + + server.expect( + Expectation::matching(matchers::request::path("/specific/path.txt")) + .respond_with(responders::status_code(200).body(TEST_CONTENT)), + ); + + let asset = DownloadableAsset { + url: Url::parse(&server.url("/specific/path.txt").to_string()).unwrap(), + file_name: "path.txt".to_string(), + checksum: TEST_CONTENT_HASH.to_string(), + }; + + let result = asset.download_to_path(folder).await.unwrap(); + let downloaded_content = std::fs::read_to_string(result).unwrap(); + assert_eq!(downloaded_content, TEST_CONTENT); } } From 452ebc261481691771641e289f456397c3f6158f Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Sat, 19 Apr 2025 15:25:16 -0400 Subject: [PATCH 09/12] feat: add k3d installation interpret Adds a new interpret for k3d installation. This includes defining the `K3dInstallationInterpret` struct, implementing the `Interpret` trait for it, and adding the `K3dInstallation` variant to the `InterpretName` enum. The implementation currently contains `todo!()` placeholders for the actual logic. --- harmony/src/domain/interpret/mod.rs | 2 ++ harmony/src/modules/k3d/install.rs | 35 ++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/harmony/src/domain/interpret/mod.rs b/harmony/src/domain/interpret/mod.rs index 9cec988..0812ced 100644 --- a/harmony/src/domain/interpret/mod.rs +++ b/harmony/src/domain/interpret/mod.rs @@ -19,6 +19,7 @@ pub enum InterpretName { Dummy, Panic, OPNSense, + K3dInstallation, } impl std::fmt::Display for InterpretName { @@ -32,6 +33,7 @@ impl std::fmt::Display for InterpretName { InterpretName::Dummy => f.write_str("Dummy"), InterpretName::Panic => f.write_str("Panic"), InterpretName::OPNSense => f.write_str("OPNSense"), + InterpretName::K3dInstallation => f.write_str("K3dInstallation"), } } } diff --git a/harmony/src/modules/k3d/install.rs b/harmony/src/modules/k3d/install.rs index 6cd2bf9..0e0cec8 100644 --- a/harmony/src/modules/k3d/install.rs +++ b/harmony/src/modules/k3d/install.rs @@ -1,6 +1,13 @@ +use async_trait::async_trait; use serde::Serialize; -use crate::{score::Score, topology::Topology}; +use crate::{ + data::{Id, Version}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + score::Score, + topology::Topology, +}; #[derive(Debug, Clone, Serialize)] pub struct K3DInstallationScore {} @@ -29,3 +36,29 @@ impl Score for K3DInstallationScore { todo!() } } + +#[derive(Debug)] +struct K3dInstallationInterpret {} + +#[async_trait] +impl Interpret for K3dInstallationInterpret { + async fn execute( + &self, + inventory: &Inventory, + topology: &T, + ) -> Result { + todo!() + } + fn get_name(&self) -> InterpretName { + InterpretName::K3dInstallation + } + fn get_version(&self) -> Version { + todo!() + } + fn get_status(&self) -> InterpretStatus { + todo!() + } + fn get_children(&self) -> Vec { + todo!() + } +} From 0857aba039992437a687d14b82b03dd699d4bc1d Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 23 Apr 2025 10:15:51 -0400 Subject: [PATCH 10/12] Switch HAClusterTopology for K8sAnywhereTopology in lamp example --- examples/lamp/src/main.rs | 7 ++----- harmony_tui/src/lib.rs | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index feb8b4f..0887d04 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -1,9 +1,8 @@ use harmony::{ data::Version, - inventory::Inventory, maestro::Maestro, modules::lamp::{LAMPConfig, LAMPScore}, - topology::{HAClusterTopology, Url}, + topology::{K8sAnywhereTopology, Url}, }; #[tokio::main] @@ -18,9 +17,7 @@ async fn main() { }, }; - let inventory = Inventory::autoload(); - let topology = HAClusterTopology::autoload(); - let mut maestro = Maestro::new(inventory, topology); + let maestro = Maestro::::load_from_env(); maestro.register_all(vec![Box::new(lamp_stack)]); harmony_tui::init(maestro).await.unwrap(); } diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index c3a8a1a..908b057 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -51,7 +51,7 @@ pub mod tui { /// harmony_tui::init(maestro).await.unwrap(); /// } /// ``` -pub async fn init( +pub async fn init( maestro: Maestro, ) -> Result<(), Box> { HarmonyTUI::new(maestro).init().await @@ -63,12 +63,21 @@ pub struct HarmonyTUI { tui_state: TuiWidgetState, } -#[derive(Debug)] enum HarmonyTuiEvent { LaunchScore(Box>), } -impl HarmonyTUI { +impl std::fmt::Display for HarmonyTuiEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let output = match self { + HarmonyTuiEvent::LaunchScore(score) => format!("LaunchScore({})",score.name()), + }; + + f.write_str(&output) + } +} + +impl HarmonyTUI { pub fn new(maestro: Maestro) -> Self { let maestro = Arc::new(maestro); let (_handle, sender) = Self::start_channel(maestro.clone()); @@ -91,7 +100,7 @@ impl HarmonyTUI { let handle = tokio::spawn(async move { info!("Starting message channel receiver loop"); while let Some(event) = receiver.recv().await { - info!("Received event {event:#?}"); + info!("Received event {event}"); match event { HarmonyTuiEvent::LaunchScore(score_item) => { let maestro = maestro.clone(); From 45668638e1339ec2a9dd26ac9c5d15ec8f3cbf3c Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 23 Apr 2025 11:16:33 -0400 Subject: [PATCH 11/12] feat: TUI does not require Topology to implement Debug anymore --- examples/lamp/src/main.rs | 2 +- examples/tui/src/main.rs | 1 - harmony_tui/src/lib.rs | 4 ++-- harmony_tui/src/widget/score.rs | 16 ++++++++++++---- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index 0887d04..3075e31 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -17,7 +17,7 @@ async fn main() { }, }; - let maestro = Maestro::::load_from_env(); + let mut maestro = Maestro::::load_from_env(); maestro.register_all(vec![Box::new(lamp_stack)]); harmony_tui::init(maestro).await.unwrap(); } diff --git a/examples/tui/src/main.rs b/examples/tui/src/main.rs index 5ed607a..72f9aab 100644 --- a/examples/tui/src/main.rs +++ b/examples/tui/src/main.rs @@ -22,7 +22,6 @@ async fn main() { let topology = HAClusterTopology::autoload(); let mut maestro = Maestro::new(inventory, topology); - maestro.register_all(vec![ Box::new(SuccessScore {}), Box::new(ErrorScore {}), diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index 908b057..180d608 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -67,10 +67,10 @@ enum HarmonyTuiEvent { LaunchScore(Box>), } -impl std::fmt::Display for HarmonyTuiEvent { +impl std::fmt::Display for HarmonyTuiEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let output = match self { - HarmonyTuiEvent::LaunchScore(score) => format!("LaunchScore({})",score.name()), + HarmonyTuiEvent::LaunchScore(score) => format!("LaunchScore({})", score.name()), }; f.write_str(&output) diff --git a/harmony_tui/src/widget/score.rs b/harmony_tui/src/widget/score.rs index 3acb5c2..57a93fe 100644 --- a/harmony_tui/src/widget/score.rs +++ b/harmony_tui/src/widget/score.rs @@ -19,13 +19,21 @@ enum ExecutionState { CANCELED, } -#[derive(Debug)] struct Execution { state: ExecutionState, score: Box>, } -#[derive(Debug)] +impl std::fmt::Display for Execution { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "Execution of {} status {:?}", + self.score.name(), + self.state + )) + } +} + pub(crate) struct ScoreListWidget { list_state: Arc>, scores: Vec>>, @@ -34,7 +42,7 @@ pub(crate) struct ScoreListWidget { sender: mpsc::Sender>, } -impl ScoreListWidget { +impl ScoreListWidget { pub(crate) fn new( scores: Vec>>, sender: mpsc::Sender>, @@ -99,7 +107,7 @@ impl ScoreListWidget { match confirm { true => { execution.state = ExecutionState::RUNNING; - info!("Launch execution {:?}", execution); + info!("Launch execution {execution}"); self.sender .send(HarmonyTuiEvent::LaunchScore(execution.score.clone_box())) .await From 213fb25686ac0aeb8a4f29c24897285d33f78e46 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 23 Apr 2025 11:56:55 -0400 Subject: [PATCH 12/12] feat: Use inquire::Confirm instead of raw std::io::Read for K8sAnywhere installation confirmation prompt --- Cargo.lock | 1 + Cargo.toml | 1 + harmony/Cargo.toml | 1 + harmony/src/domain/topology/k8s_anywhere.rs | 17 ++++++----------- harmony_cli/Cargo.toml | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 00a9b0e..8020fb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1344,6 +1344,7 @@ dependencies = [ "harmony_macros", "harmony_types", "http 1.3.1", + "inquire", "k8s-openapi", "kube", "libredfish", diff --git a/Cargo.toml b/Cargo.toml index 7219d71..48fe426 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ 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" [workspace.dependencies.uuid] version = "1.11.0" diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index c5348d9..a5d53d1 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -30,3 +30,4 @@ k8s-openapi = { workspace = true } serde_yaml = { workspace = true } http = { workspace = true } serde-value = { workspace = true } +inquire.workspace = true diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 2ba5a72..b1d5390 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -1,6 +1,7 @@ use std::io; use async_trait::async_trait; +use inquire::Confirm; use log::{info, warn}; use tokio::sync::OnceCell; @@ -76,18 +77,12 @@ impl K8sAnywhereTopology { info!("No kubernetes configuration found"); if !k8s_anywhere_config.autoinstall { - info!( - "Harmony autoinstallation is not activated, do you wish to launch autoinstallation? (y/N) : " - ); - let mut input = String::new(); + let confirmation = Confirm::new( "Harmony autoinstallation is not activated, do you wish to launch autoinstallation? : ") + .with_default(false) + .prompt() + .expect("Unexpected prompt error"); - io::stdin() - .read_line(&mut input) - .expect("Failed to read line"); - - let input = input.trim(); - - if !input.eq_ignore_ascii_case("y") { + if !confirmation { warn!( "Installation cancelled, K8sAnywhere could not initialize a valid Kubernetes client" ); diff --git a/harmony_cli/Cargo.toml b/harmony_cli/Cargo.toml index ad681bd..9d92103 100644 --- a/harmony_cli/Cargo.toml +++ b/harmony_cli/Cargo.toml @@ -10,7 +10,7 @@ assert_cmd = "2.0.17" clap = { version = "4.5.35", features = ["derive"] } harmony = { path = "../harmony" } harmony_tui = { path = "../harmony_tui", optional = true } -inquire = "0.7.5" +inquire.workspace = true tokio.workspace = true