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.
This commit is contained in:
Jean-Gabriel Gill-Couture 2025-04-14 14:57:01 -04:00
parent 027114c48c
commit 6812d05849
8 changed files with 145 additions and 18 deletions

View File

@ -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<LogicalHost>,
}
#[async_trait]
impl Topology for HAClusterTopology {
fn name(&self) -> &str {
todo!()
}
async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {
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<Arc<OpenshiftClient>, kube::Error> {
Ok(Arc::new(OpenshiftClient::try_default().await?))
impl K8sclient for HAClusterTopology {
async fn k8s_client(&self) -> Result<Arc<K8sClient>, kube::Error> {
Ok(Arc::new(K8sClient::try_default().await?))
}
}

View File

@ -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<Self, Error> {
Ok(Self {
client: Client::try_default().await?,

View File

@ -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<Option<K8sState>>,
}
impl K8sAnywhereTopology {
async fn try_load_default_kubeconfig(&self) -> Option<K8sClient> {
todo!("Use kube-rs default behavior to load system kubeconfig");
}
async fn try_load_kubeconfig(&self, path: &str) -> Option<K8sClient> {
todo!("Use kube-rs to load kubeconfig at path {path}");
}
async fn try_install_k3d(&self) -> Result<K8sClient, InterpretError> {
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<Option<K8sState>, 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<String>,
/// 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<Outcome, InterpretError> {
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(),
)),
}
}
}

View File

@ -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;

View File

@ -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<Arc<OpenshiftClient>, kube::Error>;
pub trait K8sclient: Send + Sync + std::fmt::Debug {
async fn k8s_client(&self) -> Result<Arc<K8sClient>, kube::Error>;
}
#[async_trait]

View File

@ -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<T: Topology + OcK8sclient> Score<T> for K8sDeploymentScore {
impl<T: Topology + K8sclient> Score<T> for K8sDeploymentScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
let deployment: Deployment = serde_json::from_value(json!(
{

View File

@ -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<T> for K8sResourceInterpret<K>
where
<K as kube::Resource>::DynamicType: Default,
@ -74,7 +74,7 @@ where
topology: &T,
) -> Result<Outcome, InterpretError> {
topology
.oc_client()
.k8s_client()
.await
.expect("Environment should provide enough information to instanciate a client")
.apply_namespaced(&self.score.resource)

View File

@ -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<T: Topology + OcK8sclient> Interpret<T> for LAMPInterpret {
impl<T: Topology + K8sclient> Interpret<T> for LAMPInterpret {
async fn execute(
&self,
inventory: &Inventory,