feat: implement k3d cluster management #20
							
								
								
									
										52
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										52
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -795,6 +795,27 @@ dependencies = [ | |||||||
|  "subtle", |  "subtle", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "directories" | ||||||
|  | version = "6.0.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" | ||||||
|  | dependencies = [ | ||||||
|  |  "dirs-sys", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "dirs-sys" | ||||||
|  | version = "0.5.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" | ||||||
|  | dependencies = [ | ||||||
|  |  "libc", | ||||||
|  |  "option-ext", | ||||||
|  |  "redox_users", | ||||||
|  |  "windows-sys 0.59.0", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "displaydoc" | name = "displaydoc" | ||||||
| version = "0.2.5" | version = "0.2.5" | ||||||
| @ -1341,14 +1362,17 @@ dependencies = [ | |||||||
|  "async-trait", |  "async-trait", | ||||||
|  "cidr", |  "cidr", | ||||||
|  "derive-new", |  "derive-new", | ||||||
|  |  "directories", | ||||||
|  "env_logger", |  "env_logger", | ||||||
|  "harmony_macros", |  "harmony_macros", | ||||||
|  "harmony_types", |  "harmony_types", | ||||||
|  "helm-wrapper-rs", |  "helm-wrapper-rs", | ||||||
|  "http 1.3.1", |  "http 1.3.1", | ||||||
|  "inquire", |  "inquire", | ||||||
|  |  "k3d-rs", | ||||||
|  "k8s-openapi", |  "k8s-openapi", | ||||||
|  "kube", |  "kube", | ||||||
|  |  "lazy_static", | ||||||
|  "libredfish", |  "libredfish", | ||||||
|  "log", |  "log", | ||||||
|  "non-blank-string-rs", |  "non-blank-string-rs", | ||||||
| @ -2085,6 +2109,7 @@ dependencies = [ | |||||||
|  "env_logger", |  "env_logger", | ||||||
|  "futures-util", |  "futures-util", | ||||||
|  "httptest", |  "httptest", | ||||||
|  |  "kube", | ||||||
|  "log", |  "log", | ||||||
|  "octocrab", |  "octocrab", | ||||||
|  "pretty_assertions", |  "pretty_assertions", | ||||||
| @ -2207,6 +2232,16 @@ dependencies = [ | |||||||
|  "serde_json", |  "serde_json", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "libredox" | ||||||
|  | version = "0.1.3" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" | ||||||
|  | dependencies = [ | ||||||
|  |  "bitflags 2.9.0", | ||||||
|  |  "libc", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "linux-raw-sys" | name = "linux-raw-sys" | ||||||
| version = "0.4.15" | version = "0.4.15" | ||||||
| @ -2572,6 +2607,12 @@ dependencies = [ | |||||||
|  "yaserde_derive", |  "yaserde_derive", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "option-ext" | ||||||
|  | version = "0.2.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "ordered-float" | name = "ordered-float" | ||||||
| version = "2.10.1" | version = "2.10.1" | ||||||
| @ -3051,6 +3092,17 @@ dependencies = [ | |||||||
|  "bitflags 2.9.0", |  "bitflags 2.9.0", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "redox_users" | ||||||
|  | version = "0.5.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" | ||||||
|  | dependencies = [ | ||||||
|  |  "getrandom 0.2.15", | ||||||
|  |  "libredox", | ||||||
|  |  "thiserror 2.0.12", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "regex" | name = "regex" | ||||||
| version = "1.11.1" | version = "1.11.1" | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| use harmony::{ | use harmony::{ | ||||||
|     data::Version, |     data::Version, | ||||||
|  |     inventory::Inventory, | ||||||
|     maestro::Maestro, |     maestro::Maestro, | ||||||
|     modules::lamp::{LAMPConfig, LAMPScore}, |     modules::lamp::{LAMPConfig, LAMPScore}, | ||||||
|     topology::{K8sAnywhereTopology, Url}, |     topology::{K8sAnywhereTopology, Url}, | ||||||
| @ -17,7 +18,12 @@ async fn main() { | |||||||
|         }, |         }, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     let mut maestro = Maestro::<K8sAnywhereTopology>::load_from_env(); |     let mut maestro = Maestro::<K8sAnywhereTopology>::initialize( | ||||||
|  |         Inventory::autoload(), | ||||||
|  |         K8sAnywhereTopology::new(), | ||||||
|  |     ) | ||||||
|  |     .await | ||||||
|  |     .unwrap(); | ||||||
|     maestro.register_all(vec![Box::new(lamp_stack)]); |     maestro.register_all(vec![Box::new(lamp_stack)]); | ||||||
|     harmony_tui::init(maestro).await.unwrap(); |     harmony_tui::init(maestro).await.unwrap(); | ||||||
| } | } | ||||||
|  | |||||||
| @ -33,3 +33,6 @@ serde-value = { workspace = true } | |||||||
| inquire.workspace = true | inquire.workspace = true | ||||||
| helm-wrapper-rs = "0.4.0" | helm-wrapper-rs = "0.4.0" | ||||||
| non-blank-string-rs = "1.0.4" | non-blank-string-rs = "1.0.4" | ||||||
|  | k3d-rs = { path = "../k3d" } | ||||||
|  | directories = "6.0.0" | ||||||
|  | lazy_static = "1.5.0" | ||||||
|  | |||||||
							
								
								
									
										9
									
								
								harmony/src/domain/config.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								harmony/src/domain/config.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | |||||||
|  | use lazy_static::lazy_static; | ||||||
|  | use std::path::PathBuf; | ||||||
|  | 
 | ||||||
|  | lazy_static! { | ||||||
|  |     pub static ref HARMONY_CONFIG_DIR: PathBuf = directories::BaseDirs::new() | ||||||
|  |         .unwrap() | ||||||
|  |         .data_dir() | ||||||
|  |         .join("harmony"); | ||||||
|  | } | ||||||
|  | |||||||
| @ -52,27 +52,6 @@ impl<T: Topology> Maestro<T> { | |||||||
|         Ok(outcome) |         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
 |  | ||||||
|     // locally (lazily, if not installed yet, when the first execution occurs) and use that as a topology
 |  | ||||||
|     // So, by default, the inventory is a single host that the binary is running on, and the
 |  | ||||||
|     // topology is a single node k3s
 |  | ||||||
|     //
 |  | ||||||
|     // By default :
 |  | ||||||
|     // - Linux => k3s
 |  | ||||||
|     // - macos, windows => docker compose
 |  | ||||||
|     //
 |  | ||||||
|     // To run more complex cases like OKDHACluster, either provide the default target in the
 |  | ||||||
|     // harmony infrastructure as code or as an environment variable
 |  | ||||||
|     pub fn load_from_env() -> Self { |  | ||||||
|         // Load env var HARMONY_TOPOLOGY
 |  | ||||||
|         match std::env::var("HARMONY_TOPOLOGY") { |  | ||||||
|             Ok(_) => todo!(), |  | ||||||
|             Err(_) => todo!(), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn register_all(&mut self, mut scores: ScoreVec<T>) { |     pub fn register_all(&mut self, mut scores: ScoreVec<T>) { | ||||||
|         let mut score_mut = self.scores.write().expect("Should acquire lock"); |         let mut score_mut = self.scores.write().expect("Should acquire lock"); | ||||||
|         score_mut.append(&mut scores); |         score_mut.append(&mut scores); | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | pub mod config; | ||||||
| pub mod data; | pub mod data; | ||||||
| pub mod executors; | pub mod executors; | ||||||
| pub mod filter; | pub mod filter; | ||||||
|  | |||||||
| @ -1,7 +1,9 @@ | |||||||
|  | use derive_new::new; | ||||||
| use k8s_openapi::NamespaceResourceScope; | use k8s_openapi::NamespaceResourceScope; | ||||||
| use kube::{Api, Client, Error, Resource, api::PostParams}; | use kube::{Api, Client, Error, Resource, api::PostParams}; | ||||||
| use serde::de::DeserializeOwned; | use serde::de::DeserializeOwned; | ||||||
| 
 | 
 | ||||||
|  | #[derive(new)] | ||||||
| pub struct K8sClient { | pub struct K8sClient { | ||||||
|     client: Client, |     client: Client, | ||||||
| } | } | ||||||
|  | |||||||
| @ -61,13 +61,15 @@ impl K8sAnywhereTopology { | |||||||
|         todo!("Use kube-rs to load kubeconfig at path {path}"); |         todo!("Use kube-rs to load kubeconfig at path {path}"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async fn try_install_k3d(&self) -> Result<K8sClient, InterpretError> { |     fn get_k3d_installation_score(&self) -> K3DInstallationScore { | ||||||
|  |         K3DInstallationScore::default() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn try_install_k3d(&self) -> Result<(), InterpretError> { | ||||||
|         let maestro = Maestro::initialize(Inventory::autoload(), LocalhostTopology::new()).await?; |         let maestro = Maestro::initialize(Inventory::autoload(), LocalhostTopology::new()).await?; | ||||||
|         let k3d_score = K3DInstallationScore::new(); |         let k3d_score = self.get_k3d_installation_score(); | ||||||
|         maestro.interpret(Box::new(k3d_score)).await?; |         maestro.interpret(Box::new(k3d_score)).await?; | ||||||
|         todo!( |         Ok(()) | ||||||
|             "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<Option<K8sState>, InterpretError> { |     async fn try_get_or_install_k8s_client(&self) -> Result<Option<K8sState>, InterpretError> { | ||||||
| @ -112,9 +114,14 @@ impl K8sAnywhereTopology { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         info!("Starting K8sAnywhere installation"); |         info!("Starting K8sAnywhere installation"); | ||||||
|         match self.try_install_k3d().await { |         self.try_install_k3d().await?; | ||||||
|  |         let k3d_score = self.get_k3d_installation_score(); | ||||||
|  |         match k3d_rs::K3d::new(k3d_score.installation_path, Some(k3d_score.cluster_name)) | ||||||
|  |             .get_client() | ||||||
|  |             .await | ||||||
|  |         { | ||||||
|             Ok(client) => Ok(Some(K8sState { |             Ok(client) => Ok(Some(K8sState { | ||||||
|                 _client: client, |                 _client: K8sClient::new(client), | ||||||
|                 _source: K8sSource::LocalK3d, |                 _source: K8sSource::LocalK3d, | ||||||
|                 message: "Successfully installed K3D cluster and acquired client".to_string(), |                 message: "Successfully installed K3D cluster and acquired client".to_string(), | ||||||
|             })), |             })), | ||||||
|  | |||||||
| @ -1,7 +1,11 @@ | |||||||
|  | use std::path::PathBuf; | ||||||
|  | 
 | ||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
|  | use log::info; | ||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| 
 | 
 | ||||||
| use crate::{ | use crate::{ | ||||||
|  |     config::HARMONY_CONFIG_DIR, | ||||||
|     data::{Id, Version}, |     data::{Id, Version}, | ||||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, |     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||||
|     inventory::Inventory, |     inventory::Inventory, | ||||||
| @ -10,26 +14,25 @@ use crate::{ | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Clone, Serialize)] | #[derive(Debug, Clone, Serialize)] | ||||||
| pub struct K3DInstallationScore {} | pub struct K3DInstallationScore { | ||||||
|  |     pub installation_path: PathBuf, | ||||||
|  |     pub cluster_name: String, | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| impl K3DInstallationScore { | impl Default for K3DInstallationScore { | ||||||
|     pub fn new() -> Self { |     fn default() -> Self { | ||||||
|         Self {} |         Self { | ||||||
|  |             installation_path: HARMONY_CONFIG_DIR.join("k3d"), | ||||||
|  |             cluster_name: "harmony".to_string(), | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl<T: Topology> Score<T> for K3DInstallationScore { | impl<T: Topology> Score<T> for K3DInstallationScore { | ||||||
|     fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { |     fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { | ||||||
|         todo!(" |         Box::new(K3dInstallationInterpret { | ||||||
|         1. Decide if I create a new crate for k3d management, especially to avoid the ocrtograb dependency |             score: self.clone(), | ||||||
|         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 { |     fn name(&self) -> String { | ||||||
| @ -38,7 +41,9 @@ impl<T: Topology> Score<T> for K3DInstallationScore { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| pub struct K3dInstallationInterpret {} | pub struct K3dInstallationInterpret { | ||||||
|  |     score: K3DInstallationScore, | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl<T: Topology> Interpret<T> for K3dInstallationInterpret { | impl<T: Topology> Interpret<T> for K3dInstallationInterpret { | ||||||
| @ -47,7 +52,20 @@ impl<T: Topology> Interpret<T> for K3dInstallationInterpret { | |||||||
|         _inventory: &Inventory, |         _inventory: &Inventory, | ||||||
|         _topology: &T, |         _topology: &T, | ||||||
|     ) -> Result<Outcome, InterpretError> { |     ) -> Result<Outcome, InterpretError> { | ||||||
|         todo!() |         let k3d = k3d_rs::K3d::new( | ||||||
|  |             self.score.installation_path.clone(), | ||||||
|  |             Some(self.score.cluster_name.clone()), | ||||||
|  |         ); | ||||||
|  |         match k3d.ensure_installed().await { | ||||||
|  |             Ok(_client) => { | ||||||
|  |                 let msg = format!("k3d cluster {} is installed ", self.score.cluster_name); | ||||||
|  |                 info!("{msg}"); | ||||||
|  |                 Ok(Outcome::success(msg)) | ||||||
|  |             } | ||||||
|  |             Err(msg) => Err(InterpretError::new(format!( | ||||||
|  |                 "K3dInstallationInterpret failed to ensure k3d is installed : {msg}" | ||||||
|  |             ))), | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|     fn get_name(&self) -> InterpretName { |     fn get_name(&self) -> InterpretName { | ||||||
|         InterpretName::K3dInstallation |         InterpretName::K3dInstallation | ||||||
|  | |||||||
| @ -99,7 +99,7 @@ pub async fn init<T: Topology + Send + Sync + 'static>( | |||||||
|         return Err("Not compiled with interactive support".into()); |         return Err("Not compiled with interactive support".into()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     env_logger::builder().init(); |     let _ = env_logger::builder().try_init(); | ||||||
| 
 | 
 | ||||||
|     let scores_vec = maestro_scores_filter(&maestro, args.all, args.filter, args.number); |     let scores_vec = maestro_scores_filter(&maestro, args.all, args.filter, args.number); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -15,6 +15,7 @@ reqwest = { version = "0.12", features = ["stream"]  } | |||||||
| url.workspace = true | url.workspace = true | ||||||
| sha2 = "0.10.8" | sha2 = "0.10.8" | ||||||
| futures-util = "0.3.31" | futures-util = "0.3.31" | ||||||
|  | kube.workspace = true | ||||||
| 
 | 
 | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| env_logger = { workspace = true } | env_logger = { workspace = true } | ||||||
|  | |||||||
							
								
								
									
										221
									
								
								k3d/src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										221
									
								
								k3d/src/lib.rs
									
									
									
									
									
								
							| @ -1,18 +1,23 @@ | |||||||
| mod downloadable_asset; | mod downloadable_asset; | ||||||
| use downloadable_asset::*; | use downloadable_asset::*; | ||||||
| 
 | 
 | ||||||
| use log::{debug, info}; | use kube::Client; | ||||||
|  | use log::{debug, info, warn}; | ||||||
| use std::path::PathBuf; | use std::path::PathBuf; | ||||||
| 
 | 
 | ||||||
| const K3D_BIN_FILE_NAME: &str = "k3d"; | const K3D_BIN_FILE_NAME: &str = "k3d"; | ||||||
| 
 | 
 | ||||||
| pub struct K3d { | pub struct K3d { | ||||||
|     base_dir: PathBuf, |     base_dir: PathBuf, | ||||||
|  |     cluster_name: Option<String>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl K3d { | impl K3d { | ||||||
|     pub fn new(base_dir: PathBuf) -> Self { |     pub fn new(base_dir: PathBuf, cluster_name: Option<String>) -> Self { | ||||||
|         Self { base_dir } |         Self { | ||||||
|  |             base_dir, | ||||||
|  |             cluster_name, | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async fn get_binary_for_current_platform( |     async fn get_binary_for_current_platform( | ||||||
| @ -24,7 +29,6 @@ impl K3d { | |||||||
| 
 | 
 | ||||||
|         debug!("Detecting platform: OS={}, ARCH={}", os, arch); |         debug!("Detecting platform: OS={}, ARCH={}", os, arch); | ||||||
| 
 | 
 | ||||||
|         // 2. Construct the binary name pattern based on platform
 |  | ||||||
|         let binary_pattern = match (os, arch) { |         let binary_pattern = match (os, arch) { | ||||||
|             ("linux", "x86") => "k3d-linux-386", |             ("linux", "x86") => "k3d-linux-386", | ||||||
|             ("linux", "x86_64") => "k3d-linux-amd64", |             ("linux", "x86_64") => "k3d-linux-amd64", | ||||||
| @ -38,7 +42,6 @@ impl K3d { | |||||||
| 
 | 
 | ||||||
|         debug!("Looking for binary matching pattern: {}", binary_pattern); |         debug!("Looking for binary matching pattern: {}", binary_pattern); | ||||||
| 
 | 
 | ||||||
|         // 3. Find the matching binary in release assets
 |  | ||||||
|         let binary_asset = latest_release |         let binary_asset = latest_release | ||||||
|             .assets |             .assets | ||||||
|             .iter() |             .iter() | ||||||
| @ -47,14 +50,12 @@ impl K3d { | |||||||
| 
 | 
 | ||||||
|         let binary_url = binary_asset.browser_download_url.clone(); |         let binary_url = binary_asset.browser_download_url.clone(); | ||||||
| 
 | 
 | ||||||
|         // 4. Find and parse the checksums file
 |  | ||||||
|         let checksums_asset = latest_release |         let checksums_asset = latest_release | ||||||
|             .assets |             .assets | ||||||
|             .iter() |             .iter() | ||||||
|             .find(|asset| asset.name == "checksums.txt") |             .find(|asset| asset.name == "checksums.txt") | ||||||
|             .expect("Checksums file not found in release assets"); |             .expect("Checksums file not found in release assets"); | ||||||
| 
 | 
 | ||||||
|         // 5. Download and parse checksums file
 |  | ||||||
|         let checksums_url = checksums_asset.browser_download_url.clone(); |         let checksums_url = checksums_asset.browser_download_url.clone(); | ||||||
| 
 | 
 | ||||||
|         let body = reqwest::get(checksums_url) |         let body = reqwest::get(checksums_url) | ||||||
| @ -65,7 +66,6 @@ impl K3d { | |||||||
|             .unwrap(); |             .unwrap(); | ||||||
|         println!("body: {body}"); |         println!("body: {body}"); | ||||||
| 
 | 
 | ||||||
|         // 6. Find the checksum for our binary
 |  | ||||||
|         let checksum = body |         let checksum = body | ||||||
|             .lines() |             .lines() | ||||||
|             .find_map(|line| { |             .find_map(|line| { | ||||||
| @ -109,6 +109,207 @@ impl K3d { | |||||||
| 
 | 
 | ||||||
|         Ok(latest_release) |         Ok(latest_release) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     /// Checks if k3d binary exists and is executable
 | ||||||
|  |     ///
 | ||||||
|  |     /// Verifies that:
 | ||||||
|  |     /// 1. The k3d binary exists in the base directory
 | ||||||
|  |     /// 2. It has proper executable permissions (on Unix systems)
 | ||||||
|  |     /// 3. It responds correctly to a simple command (`k3d --version`)
 | ||||||
|  |     pub fn is_installed(&self) -> bool { | ||||||
|  |         let binary_path = self.base_dir.join(K3D_BIN_FILE_NAME); | ||||||
|  | 
 | ||||||
|  |         if !binary_path.exists() { | ||||||
|  |             debug!("K3d binary not found at {:?}", binary_path); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if !self.ensure_binary_executable(&binary_path) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         self.can_execute_binary_check(&binary_path) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Verifies if the specified cluster is already running
 | ||||||
|  |     ///
 | ||||||
|  |     /// Executes `k3d cluster list <cluster_name>` and checks for a successful response,
 | ||||||
|  |     /// indicating that the cluster exists and is registered with k3d.
 | ||||||
|  |     pub fn is_cluster_initialized(&self) -> bool { | ||||||
|  |         let cluster_name = match &self.cluster_name { | ||||||
|  |             Some(name) => name, | ||||||
|  |             None => { | ||||||
|  |                 debug!("No cluster name specified, can't verify if cluster is initialized"); | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         let binary_path = self.base_dir.join(K3D_BIN_FILE_NAME); | ||||||
|  |         if !binary_path.exists() { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         self.verify_cluster_exists(&binary_path, cluster_name) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Creates a new k3d cluster with the specified name
 | ||||||
|  |     ///
 | ||||||
|  |     /// This method:
 | ||||||
|  |     /// 1. Creates a new k3d cluster using `k3d cluster create <cluster_name>`
 | ||||||
|  |     /// 2. Waits for the cluster to initialize
 | ||||||
|  |     /// 3. Returns a configured Kubernetes client connected to the cluster
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Returns
 | ||||||
|  |     /// - `Ok(Client)` - Successfully created cluster and connected client
 | ||||||
|  |     /// - `Err(String)` - Error message detailing what went wrong
 | ||||||
|  |     pub async fn initialize_cluster(&self) -> Result<Client, String> { | ||||||
|  |         let cluster_name = match &self.cluster_name { | ||||||
|  |             Some(name) => name, | ||||||
|  |             None => return Err("No cluster name specified for initialization".to_string()), | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         let binary_path = self.base_dir.join(K3D_BIN_FILE_NAME); | ||||||
|  |         if !binary_path.exists() { | ||||||
|  |             return Err(format!("K3d binary not found at {:?}", binary_path)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         info!("Initializing k3d cluster '{}'", cluster_name); | ||||||
|  | 
 | ||||||
|  |         self.create_cluster(&binary_path, cluster_name)?; | ||||||
|  |         self.create_kubernetes_client().await | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Ensures k3d is installed and the cluster is initialized
 | ||||||
|  |     ///
 | ||||||
|  |     /// This method provides a complete setup flow:
 | ||||||
|  |     /// 1. Checks if k3d is installed, downloads and installs it if needed
 | ||||||
|  |     /// 2. Verifies if the specified cluster exists, creates it if not
 | ||||||
|  |     /// 3. Returns a Kubernetes client connected to the cluster
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Returns
 | ||||||
|  |     /// - `Ok(Client)` - Successfully ensured k3d and cluster are ready
 | ||||||
|  |     /// - `Err(String)` - Error message if any step failed
 | ||||||
|  |     pub async fn ensure_installed(&self) -> Result<Client, String> { | ||||||
|  |         if !self.is_installed() { | ||||||
|  |             info!("K3d is not installed, downloading latest release"); | ||||||
|  |             self.download_latest_release() | ||||||
|  |                 .await | ||||||
|  |                 .map_err(|e| format!("Failed to download k3d: {}", e))?; | ||||||
|  | 
 | ||||||
|  |             if !self.is_installed() { | ||||||
|  |                 return Err("Failed to install k3d properly".to_string()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if !self.is_cluster_initialized() { | ||||||
|  |             info!("Cluster is not initialized, initializing now"); | ||||||
|  |             return self.initialize_cluster().await; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         info!("K3d and cluster are already properly set up"); | ||||||
|  |         self.create_kubernetes_client().await | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Private helper methods
 | ||||||
|  | 
 | ||||||
|  |     #[cfg(not(target_os = "windows"))] | ||||||
|  |     fn ensure_binary_executable(&self, binary_path: &PathBuf) -> bool { | ||||||
|  |         use std::os::unix::fs::PermissionsExt; | ||||||
|  | 
 | ||||||
|  |         let mut perms = match std::fs::metadata(binary_path) { | ||||||
|  |             Ok(metadata) => metadata.permissions(), | ||||||
|  |             Err(e) => { | ||||||
|  |                 debug!("Failed to get binary metadata: {}", e); | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         perms.set_mode(0o755); | ||||||
|  | 
 | ||||||
|  |         if let Err(e) = std::fs::set_permissions(binary_path, perms) { | ||||||
|  |             debug!("Failed to set executable permissions on k3d binary: {}", e); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[cfg(target_os = "windows")] | ||||||
|  |     fn ensure_binary_executable(&self, _binary_path: &PathBuf) -> bool { | ||||||
|  |         // Windows doesn't use executable file permissions
 | ||||||
|  |         true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn can_execute_binary_check(&self, binary_path: &PathBuf) -> bool { | ||||||
|  |         match std::process::Command::new(binary_path) | ||||||
|  |             .arg("--version") | ||||||
|  |             .output() | ||||||
|  |         { | ||||||
|  |             Ok(output) => { | ||||||
|  |                 if output.status.success() { | ||||||
|  |                     debug!("K3d binary is installed and working"); | ||||||
|  |                     true | ||||||
|  |                 } else { | ||||||
|  |                     debug!("K3d binary check failed: {:?}", output); | ||||||
|  |                     false | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Err(e) => { | ||||||
|  |                 debug!("Failed to execute K3d binary: {}", e); | ||||||
|  |                 false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn verify_cluster_exists(&self, binary_path: &PathBuf, cluster_name: &str) -> bool { | ||||||
|  |         match std::process::Command::new(binary_path) | ||||||
|  |             .args(["cluster", "list", cluster_name, "--no-headers"]) | ||||||
|  |             .output() | ||||||
|  |         { | ||||||
|  |             Ok(output) => { | ||||||
|  |                 if output.status.success() && !output.stdout.is_empty() { | ||||||
|  |                     debug!("Cluster '{}' is initialized", cluster_name); | ||||||
|  |                     true | ||||||
|  |                 } else { | ||||||
|  |                     debug!("Cluster '{}' is not initialized", cluster_name); | ||||||
|  |                     false | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Err(e) => { | ||||||
|  |                 debug!("Failed to check cluster initialization: {}", e); | ||||||
|  |                 false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn create_cluster(&self, binary_path: &PathBuf, cluster_name: &str) -> Result<(), String> { | ||||||
|  |         let output = std::process::Command::new(binary_path) | ||||||
|  |             .args(["cluster", "create", cluster_name]) | ||||||
|  |             .output() | ||||||
|  |             .map_err(|e| format!("Failed to execute k3d command: {}", e))?; | ||||||
|  | 
 | ||||||
|  |         if !output.status.success() { | ||||||
|  |             let stderr = String::from_utf8_lossy(&output.stderr); | ||||||
|  |             return Err(format!("Failed to create cluster: {}", stderr)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         info!("Successfully created k3d cluster '{}'", cluster_name); | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn create_kubernetes_client(&self) -> Result<Client, String> { | ||||||
|  |         warn!("TODO this method is way too dumb, it should make sure that the client is connected to the k3d cluster actually represented by this instance, not just any default  client"); | ||||||
|  |         Client::try_default() | ||||||
|  |             .await | ||||||
|  |             .map_err(|e| format!("Failed to create Kubernetes client: {}", e)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub async fn get_client(&self) -> Result<Client, String> { | ||||||
|  |         match self.is_cluster_initialized() { | ||||||
|  |             true => Ok(self.create_kubernetes_client().await?), | ||||||
|  |             false => Err("Cannot get client! Cluster not initialized yet".to_string()), | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| @ -124,7 +325,7 @@ mod test { | |||||||
| 
 | 
 | ||||||
|         assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), false); |         assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), false); | ||||||
| 
 | 
 | ||||||
|         let k3d = K3d::new(dir.clone()); |         let k3d = K3d::new(dir.clone(), None); | ||||||
|         let latest_release = k3d.get_latest_release_tag().await.unwrap(); |         let latest_release = k3d.get_latest_release_tag().await.unwrap(); | ||||||
| 
 | 
 | ||||||
|         let tag_regex = Regex::new(r"^v\d+\.\d+\.\d+$").unwrap(); |         let tag_regex = Regex::new(r"^v\d+\.\d+\.\d+$").unwrap(); | ||||||
| @ -138,7 +339,7 @@ mod test { | |||||||
| 
 | 
 | ||||||
|         assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), false); |         assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), false); | ||||||
| 
 | 
 | ||||||
|         let k3d = K3d::new(dir.clone()); |         let k3d = K3d::new(dir.clone(), None); | ||||||
|         let bin_file_path = k3d.download_latest_release().await.unwrap(); |         let bin_file_path = k3d.download_latest_release().await.unwrap(); | ||||||
|         assert_eq!(bin_file_path, dir.join(K3D_BIN_FILE_NAME)); |         assert_eq!(bin_file_path, dir.join(K3D_BIN_FILE_NAME)); | ||||||
|         assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), true); |         assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), true); | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	
this is cool