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:
2025-04-24 17:36:01 -04:00
parent d307893f15
commit fbcd3e4f7f
12 changed files with 335 additions and 56 deletions

View File

@@ -33,3 +33,6 @@ serde-value = { workspace = true }
inquire.workspace = true
helm-wrapper-rs = "0.4.0"
non-blank-string-rs = "1.0.4"
k3d-rs = { path = "../k3d" }
directories = "6.0.0"
lazy_static = "1.5.0"

View 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");
}

View File

@@ -52,27 +52,6 @@ impl<T: Topology> Maestro<T> {
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>) {
let mut score_mut = self.scores.write().expect("Should acquire lock");
score_mut.append(&mut scores);

View File

@@ -1,3 +1,4 @@
pub mod config;
pub mod data;
pub mod executors;
pub mod filter;

View File

@@ -1,7 +1,9 @@
use derive_new::new;
use k8s_openapi::NamespaceResourceScope;
use kube::{Api, Client, Error, Resource, api::PostParams};
use serde::de::DeserializeOwned;
#[derive(new)]
pub struct K8sClient {
client: Client,
}

View File

@@ -61,13 +61,15 @@ impl K8sAnywhereTopology {
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 k3d_score = K3DInstallationScore::new();
let k3d_score = self.get_k3d_installation_score();
maestro.interpret(Box::new(k3d_score)).await?;
todo!(
"Create Maestro with LocalDockerTopology or something along these lines and run a K3dInstallationScore on it"
);
Ok(())
}
async fn try_get_or_install_k8s_client(&self) -> Result<Option<K8sState>, InterpretError> {
@@ -112,9 +114,14 @@ impl K8sAnywhereTopology {
}
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 {
_client: client,
_client: K8sClient::new(client),
_source: K8sSource::LocalK3d,
message: "Successfully installed K3D cluster and acquired client".to_string(),
})),

View File

@@ -1,7 +1,11 @@
use std::path::PathBuf;
use async_trait::async_trait;
use log::info;
use serde::Serialize;
use crate::{
config::HARMONY_CONFIG_DIR,
data::{Id, Version},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory,
@@ -10,26 +14,25 @@ use crate::{
};
#[derive(Debug, Clone, Serialize)]
pub struct K3DInstallationScore {}
pub struct K3DInstallationScore {
pub installation_path: PathBuf,
pub cluster_name: String,
}
impl K3DInstallationScore {
pub fn new() -> Self {
Self {}
impl Default for K3DInstallationScore {
fn default() -> Self {
Self {
installation_path: HARMONY_CONFIG_DIR.join("k3d"),
cluster_name: "harmony".to_string(),
}
}
}
impl<T: Topology> Score<T> for K3DInstallationScore {
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
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
")
Box::new(K3dInstallationInterpret {
score: self.clone(),
})
}
fn name(&self) -> String {
@@ -38,7 +41,9 @@ impl<T: Topology> Score<T> for K3DInstallationScore {
}
#[derive(Debug)]
pub struct K3dInstallationInterpret {}
pub struct K3dInstallationInterpret {
score: K3DInstallationScore,
}
#[async_trait]
impl<T: Topology> Interpret<T> for K3dInstallationInterpret {
@@ -47,7 +52,20 @@ impl<T: Topology> Interpret<T> for K3dInstallationInterpret {
_inventory: &Inventory,
_topology: &T,
) -> 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 {
InterpretName::K3dInstallation