forked from NationTech/harmony
feat: implement k3d cluster management
- Adds functionality to download, install, and manage k3d clusters. - Includes methods for downloading the latest release, creating clusters, and verifying cluster existence. - Implements `ensure_k3d_installed`, `get_latest_release_tag`, `download_latest_release`, `is_k3d_installed`, `verify_cluster_exists`, `create_cluster` and `create_kubernetes_client`. - Provides a `get_client` method to access the Kubernetes client. - Includes unit tests for download and installation. - Adds handling for different operating systems. - Improves error handling and logging. - Introduces a `K3d` struct to encapsulate k3d cluster management logic. - Adds the ability to specify the cluster name during K3d initialization.
This commit is contained in:
@@ -15,6 +15,7 @@ reqwest = { version = "0.12", features = ["stream"] }
|
||||
url.workspace = true
|
||||
sha2 = "0.10.8"
|
||||
futures-util = "0.3.31"
|
||||
kube.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = { workspace = true }
|
||||
|
||||
221
k3d/src/lib.rs
221
k3d/src/lib.rs
@@ -1,18 +1,23 @@
|
||||
mod downloadable_asset;
|
||||
use downloadable_asset::*;
|
||||
|
||||
use log::{debug, info};
|
||||
use kube::Client;
|
||||
use log::{debug, info, warn};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const K3D_BIN_FILE_NAME: &str = "k3d";
|
||||
|
||||
pub struct K3d {
|
||||
base_dir: PathBuf,
|
||||
cluster_name: Option<String>,
|
||||
}
|
||||
|
||||
impl K3d {
|
||||
pub fn new(base_dir: PathBuf) -> Self {
|
||||
Self { base_dir }
|
||||
pub fn new(base_dir: PathBuf, cluster_name: Option<String>) -> Self {
|
||||
Self {
|
||||
base_dir,
|
||||
cluster_name,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_binary_for_current_platform(
|
||||
@@ -24,7 +29,6 @@ impl K3d {
|
||||
|
||||
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",
|
||||
@@ -38,7 +42,6 @@ impl K3d {
|
||||
|
||||
debug!("Looking for binary matching pattern: {}", binary_pattern);
|
||||
|
||||
// 3. Find the matching binary in release assets
|
||||
let binary_asset = latest_release
|
||||
.assets
|
||||
.iter()
|
||||
@@ -47,14 +50,12 @@ impl K3d {
|
||||
|
||||
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)
|
||||
@@ -65,7 +66,6 @@ impl K3d {
|
||||
.unwrap();
|
||||
println!("body: {body}");
|
||||
|
||||
// 6. Find the checksum for our binary
|
||||
let checksum = body
|
||||
.lines()
|
||||
.find_map(|line| {
|
||||
@@ -109,6 +109,207 @@ impl K3d {
|
||||
|
||||
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)]
|
||||
@@ -124,7 +325,7 @@ mod test {
|
||||
|
||||
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 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);
|
||||
|
||||
let k3d = K3d::new(dir.clone());
|
||||
let k3d = K3d::new(dir.clone(), None);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user