fix(k8s_anywhere): Ensure k3d cluster is started before use

- Refactor k3d cluster management to explicitly start the cluster.
- Introduce `start_cluster` function to ensure cluster is running before operations.
- Improve error handling and logging during cluster startup.
- Update `create_cluster` and other related functions to utilize the new startup mechanism.
- Enhance reliability and prevent potential issues caused by an uninitialized cluster.
- Add `run_k3d_command` to handle k3d commands with logging and error handling.
This commit is contained in:
Jean-Gabriel Gill-Couture 2025-04-25 11:32:02 -04:00
parent 23971ecd7c
commit 22752960f9
5 changed files with 81 additions and 30 deletions

View File

@ -8,6 +8,7 @@ use harmony::{
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// let _ = env_logger::Builder::from_default_env().filter_level(log::LevelFilter::Info).try_init();
let lamp_stack = LAMPScore { let lamp_stack = LAMPScore {
name: "harmony-lamp-demo".to_string(), name: "harmony-lamp-demo".to_string(),
domain: Url::Url(url::Url::parse("https://lampdemo.harmony.nationtech.io").unwrap()), domain: Url::Url(url::Url::parse("https://lampdemo.harmony.nationtech.io").unwrap()),

View File

@ -116,17 +116,22 @@ impl K8sAnywhereTopology {
info!("Starting K8sAnywhere installation"); info!("Starting K8sAnywhere installation");
self.try_install_k3d().await?; self.try_install_k3d().await?;
let k3d_score = self.get_k3d_installation_score(); let k3d_score = self.get_k3d_installation_score();
match k3d_rs::K3d::new(k3d_score.installation_path, Some(k3d_score.cluster_name)) // I feel like having to rely on the k3d_rs crate here is a smell
.get_client() // I think we should have a way to interact more deeply with scores/interpret. Maybe the
.await // K3DInstallationScore should expose a method to get_client ? Not too sure what would be a
{ // good implementation due to the stateful nature of the k3d thing. Which is why I went
Ok(client) => Ok(Some(K8sState { // with this solution for now
let k3d = k3d_rs::K3d::new(k3d_score.installation_path, Some(k3d_score.cluster_name));
let state = match k3d.get_client().await {
Ok(client) => K8sState {
_client: K8sClient::new(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(),
})), },
Err(_) => todo!(), Err(_) => todo!(),
} };
Ok(Some(state))
} }
} }
@ -154,7 +159,7 @@ struct K8sAnywhereConfig {
#[async_trait] #[async_trait]
impl Topology for K8sAnywhereTopology { impl Topology for K8sAnywhereTopology {
fn name(&self) -> &str { fn name(&self) -> &str {
todo!() "K8sAnywhereTopology"
} }
async fn ensure_ready(&self) -> Result<Outcome, InterpretError> { async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {

View File

@ -117,7 +117,7 @@ impl K3d {
/// 2. It has proper executable permissions (on Unix systems) /// 2. It has proper executable permissions (on Unix systems)
/// 3. It responds correctly to a simple command (`k3d --version`) /// 3. It responds correctly to a simple command (`k3d --version`)
pub fn is_installed(&self) -> bool { pub fn is_installed(&self) -> bool {
let binary_path = self.base_dir.join(K3D_BIN_FILE_NAME); let binary_path = self.get_k3d_binary_path();
if !binary_path.exists() { if !binary_path.exists() {
debug!("K3d binary not found at {:?}", binary_path); debug!("K3d binary not found at {:?}", binary_path);
@ -131,15 +131,15 @@ impl K3d {
self.can_execute_binary_check(&binary_path) self.can_execute_binary_check(&binary_path)
} }
/// Verifies if the specified cluster is already running /// Verifies if the specified cluster is already created
/// ///
/// Executes `k3d cluster list <cluster_name>` and checks for a successful response, /// Executes `k3d cluster list <cluster_name>` and checks for a successful response,
/// indicating that the cluster exists and is registered with k3d. /// indicating that the cluster exists and is registered with k3d.
pub fn is_cluster_initialized(&self) -> bool { pub fn is_cluster_initialized(&self) -> bool {
let cluster_name = match &self.cluster_name { let cluster_name = match self.get_cluster_name() {
Some(name) => name, Ok(name) => name,
None => { Err(_) => {
debug!("No cluster name specified, can't verify if cluster is initialized"); debug!("Could not get cluster name, can't verify if cluster is initialized");
return false; return false;
} }
}; };
@ -152,6 +152,13 @@ impl K3d {
self.verify_cluster_exists(&binary_path, cluster_name) self.verify_cluster_exists(&binary_path, cluster_name)
} }
fn get_cluster_name(&self) -> Result<&String, String> {
match &self.cluster_name {
Some(name) => Ok(name),
None => Err("No cluster name available".to_string()),
}
}
/// Creates a new k3d cluster with the specified name /// Creates a new k3d cluster with the specified name
/// ///
/// This method: /// This method:
@ -163,22 +170,29 @@ impl K3d {
/// - `Ok(Client)` - Successfully created cluster and connected client /// - `Ok(Client)` - Successfully created cluster and connected client
/// - `Err(String)` - Error message detailing what went wrong /// - `Err(String)` - Error message detailing what went wrong
pub async fn initialize_cluster(&self) -> Result<Client, String> { pub async fn initialize_cluster(&self) -> Result<Client, String> {
let cluster_name = match &self.cluster_name { let cluster_name = match self.get_cluster_name() {
Some(name) => name, Ok(name) => name,
None => return Err("No cluster name specified for initialization".to_string()), Err(_) => return Err("Could not get cluster_name, cannot initialize".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); info!("Initializing k3d cluster '{}'", cluster_name);
self.create_cluster(&binary_path, cluster_name)?; self.create_cluster(cluster_name)?;
self.create_kubernetes_client().await self.create_kubernetes_client().await
} }
fn get_k3d_binary_path(&self) -> PathBuf {
self.base_dir.join(K3D_BIN_FILE_NAME)
}
fn get_k3d_binary(&self) -> Result<PathBuf, String> {
let path = self.get_k3d_binary_path();
if !path.exists() {
return Err(format!("K3d binary not found at {:?}", path));
}
Ok(path)
}
/// Ensures k3d is installed and the cluster is initialized /// Ensures k3d is installed and the cluster is initialized
/// ///
/// This method provides a complete setup flow: /// This method provides a complete setup flow:
@ -206,6 +220,8 @@ impl K3d {
return self.initialize_cluster().await; return self.initialize_cluster().await;
} }
self.start_cluster().await?;
info!("K3d and cluster are already properly set up"); info!("K3d and cluster are already properly set up");
self.create_kubernetes_client().await self.create_kubernetes_client().await
} }
@ -282,11 +298,27 @@ impl K3d {
} }
} }
fn create_cluster(&self, binary_path: &PathBuf, cluster_name: &str) -> Result<(), String> { pub fn run_k3d_command<I, S>(&self, args: I) -> Result<std::process::Output, String>
let output = std::process::Command::new(binary_path) where
.args(["cluster", "create", cluster_name]) I: IntoIterator<Item = S>,
.output() S: AsRef<std::ffi::OsStr>,
.map_err(|e| format!("Failed to execute k3d command: {}", e))?; {
let binary_path = self.get_k3d_binary()?;
let output = std::process::Command::new(binary_path).args(args).output();
match output {
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
debug!("stderr : {}", stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
debug!("stdout : {}", stdout);
Ok(output)
}
Err(e) => Err(format!("Failed to execute k3d command: {}", e)),
}
}
fn create_cluster(&self, cluster_name: &str) -> Result<(), String> {
let output = self.run_k3d_command(["cluster", "create", cluster_name])?;
if !output.status.success() { if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr); let stderr = String::from_utf8_lossy(&output.stderr);
@ -310,6 +342,19 @@ impl K3d {
false => Err("Cannot get client! Cluster not initialized yet".to_string()), false => Err("Cannot get client! Cluster not initialized yet".to_string()),
} }
} }
async fn start_cluster(&self) -> Result<(), String> {
let cluster_name = self.get_cluster_name()?;
let output = self.run_k3d_command(["cluster", "start", cluster_name])?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Failed to start cluster: {}", stderr));
}
info!("Successfully started k3d cluster '{}'", cluster_name);
Ok(())
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -23,7 +23,7 @@ pub struct Config {
} }
impl Serialize for Config { impl Serialize for Config {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where where
S: serde::Serializer, S: serde::Serializer,
{ {

View File

@ -10,11 +10,11 @@ mod test {
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
use crate::Config; use crate::Config;
use pretty_assertions::assert_eq;
#[cfg(opnsenseendtoend)] #[cfg(opnsenseendtoend)]
#[tokio::test] #[tokio::test]
async fn test_public_sdk() { async fn test_public_sdk() {
use pretty_assertions::assert_eq;
let mac = "11:22:33:44:55:66"; let mac = "11:22:33:44:55:66";
let ip = Ipv4Addr::new(10, 100, 8, 200); let ip = Ipv4Addr::new(10, 100, 8, 200);
let hostname = "test_hostname"; let hostname = "test_hostname";