diff --git a/harmony/src/domain/topology/k8s.rs b/harmony/src/domain/topology/k8s.rs index 57b3668..c3ad628 100644 --- a/harmony/src/domain/topology/k8s.rs +++ b/harmony/src/domain/topology/k8s.rs @@ -1,6 +1,11 @@ use derive_new::new; use k8s_openapi::NamespaceResourceScope; -use kube::{Api, Client, Error, Resource, api::PostParams}; +use kube::{ + Api, Client, Config, Error, Resource, + api::PostParams, + config::{KubeConfigOptions, Kubeconfig}, +}; +use log::error; use serde::de::DeserializeOwned; #[derive(new)] @@ -57,4 +62,22 @@ impl K8sClient { } todo!("") } + + pub(crate) async fn from_kubeconfig(path: &str) -> Option { + let k = match Kubeconfig::read_from(path) { + Ok(k) => k, + Err(e) => { + error!("Failed to load kubeconfig from {path} : {e}"); + return None; + } + }; + Some(K8sClient::new( + Client::try_from( + Config::from_custom_kubeconfig(k, &KubeConfigOptions::default()) + .await + .unwrap(), + ) + .unwrap(), + )) + } } diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 54b6012..375d887 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -17,12 +17,13 @@ use super::{HelmCommand, K8sclient, Topology, k8s::K8sClient}; struct K8sState { client: Arc, - _source: K8sSource, + source: K8sSource, message: String, } enum K8sSource { LocalK3d, + Kubeconfig, } pub struct K8sAnywhereTopology { @@ -75,7 +76,7 @@ impl K8sAnywhereTopology { } async fn try_load_kubeconfig(&self, path: &str) -> Option { - todo!("Use kube-rs to load kubeconfig at path {path}"); + K8sClient::from_kubeconfig(path).await } fn get_k3d_installation_score(&self) -> K3DInstallationScore { @@ -109,8 +110,18 @@ impl K8sAnywhereTopology { if let Some(kubeconfig) = k8s_anywhere_config.kubeconfig { match self.try_load_kubeconfig(&kubeconfig).await { - Some(_client) => todo!(), - None => todo!(), + Some(client) => { + return Ok(Some(K8sState { + client: Arc::new(client), + source: K8sSource::Kubeconfig, + message: format!("Loaded k8s client from kubeconfig {kubeconfig}"), + })); + } + None => { + return Err(InterpretError::new(format!( + "Failed to load kubeconfig from {kubeconfig}" + ))); + } } } @@ -142,7 +153,7 @@ impl K8sAnywhereTopology { let state = match k3d.get_client().await { Ok(client) => K8sState { client: Arc::new(K8sClient::new(client)), - _source: K8sSource::LocalK3d, + source: K8sSource::LocalK3d, message: "Successfully installed K3D cluster and acquired client".to_string(), }, Err(_) => todo!(), diff --git a/harmony/src/modules/cert_manager/helm.rs b/harmony/src/modules/cert_manager/helm.rs new file mode 100644 index 0000000..9a2521c --- /dev/null +++ b/harmony/src/modules/cert_manager/helm.rs @@ -0,0 +1,46 @@ +use std::{collections::HashMap, str::FromStr}; + +use non_blank_string_rs::NonBlankString; +use serde::Serialize; +use url::Url; + +use crate::{ + modules::helm::chart::{HelmChartScore, HelmRepository}, + score::Score, + topology::{HelmCommand, Topology}, +}; + +#[derive(Debug, Serialize, Clone)] +pub struct CertManagerHelmScore {} + +impl Score for CertManagerHelmScore { + fn create_interpret(&self) -> Box> { + let mut values_overrides = HashMap::new(); + values_overrides.insert( + NonBlankString::from_str("crds.enabled").unwrap(), + "true".to_string(), + ); + let values_overrides = Some(values_overrides); + + HelmChartScore { + namespace: Some(NonBlankString::from_str("cert-manager").unwrap()), + release_name: NonBlankString::from_str("cert-manager").unwrap(), + chart_name: NonBlankString::from_str("jetstack/cert-manager").unwrap(), + chart_version: None, + values_overrides, + values_yaml: None, + create_namespace: true, + install_only: true, + repository: Some(HelmRepository::new( + "jetstack".to_string(), + Url::parse("https://charts.jetstack.io").unwrap(), + true, + )), + } + .create_interpret() + } + + fn name(&self) -> String { + format!("CertManagerHelmScore") + } +} diff --git a/harmony/src/modules/cert_manager/mod.rs b/harmony/src/modules/cert_manager/mod.rs new file mode 100644 index 0000000..8fd309a --- /dev/null +++ b/harmony/src/modules/cert_manager/mod.rs @@ -0,0 +1,2 @@ +mod helm; +pub use helm::*; diff --git a/harmony/src/modules/helm/chart.rs b/harmony/src/modules/helm/chart.rs index 1be66f6..be15a15 100644 --- a/harmony/src/modules/helm/chart.rs +++ b/harmony/src/modules/helm/chart.rs @@ -6,13 +6,31 @@ use crate::topology::{HelmCommand, Topology}; use async_trait::async_trait; use helm_wrapper_rs; use helm_wrapper_rs::blocking::{DefaultHelmExecutor, HelmExecutor}; -use log::info; +use log::{debug, error, info, warn}; pub use non_blank_string_rs::NonBlankString; use serde::Serialize; use std::collections::HashMap; use std::path::Path; +use std::process::{Command, Output, Stdio}; use std::str::FromStr; use temp_file::TempFile; +use url::Url; + +#[derive(Debug, Clone, Serialize)] +pub struct HelmRepository { + name: String, + url: Url, + force_update: bool, +} +impl HelmRepository { + pub(crate) fn new(name: String, url: Url, force_update: bool) -> Self { + Self { + name, + url, + force_update, + } + } +} #[derive(Debug, Clone, Serialize)] pub struct HelmChartScore { @@ -26,6 +44,7 @@ pub struct HelmChartScore { /// Wether to run `helm upgrade --install` under the hood or only install when not present pub install_only: bool, + pub repository: Option, } impl Score for HelmChartScore { @@ -44,6 +63,77 @@ impl Score for HelmChartScore { pub struct HelmChartInterpret { pub score: HelmChartScore, } +impl HelmChartInterpret { + fn add_repo(&self) -> Result<(), InterpretError> { + let repo = match &self.score.repository { + Some(repo) => repo, + None => { + info!("No Helm repository specified in the score. Skipping repository setup."); + return Ok(()); + } + }; + info!( + "Ensuring Helm repository exists: Name='{}', URL='{}', ForceUpdate={}", + repo.name, repo.url, repo.force_update + ); + + let mut add_args = vec!["repo", "add", &repo.name, repo.url.as_str()]; + if repo.force_update { + add_args.push("--force-update"); + } + + let add_output = run_helm_command(&add_args)?; + let full_output = format!( + "{}\n{}", + String::from_utf8_lossy(&add_output.stdout), + String::from_utf8_lossy(&add_output.stderr) + ); + + match add_output.status.success() { + true => { + return Ok(()); + } + false => { + return Err(InterpretError::new(format!( + "Failed to add helm repository!\n{full_output}" + ))); + } + } + } +} + +fn run_helm_command(args: &[&str]) -> Result { + let command_str = format!("helm {}", args.join(" ")); + debug!("Running Helm command: `{}`", command_str); + + let output = Command::new("helm") + .args(args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .map_err(|e| { + InterpretError::new(format!( + "Failed to execute helm command '{}': {}. Is helm installed and in PATH?", + command_str, e + )) + })?; + + if !output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + warn!( + "Helm command `{}` failed with status: {}\nStdout:\n{}\nStderr:\n{}", + command_str, output.status, stdout, stderr + ); + } else { + debug!( + "Helm command `{}` finished successfully. Status: {}", + command_str, output.status + ); + } + + Ok(output) +} #[async_trait] impl Interpret for HelmChartInterpret { @@ -67,6 +157,8 @@ impl Interpret for HelmChartInterpret { None => None, }; + self.add_repo()?; + let helm_executor = DefaultHelmExecutor::new(); let mut helm_options = Vec::new(); diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index 47d6ca9..560673f 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -179,6 +179,7 @@ impl LAMPInterpret { create_namespace: true, install_only: true, values_yaml: None, + repository: None, }; score.create_interpret().execute(inventory, topology).await diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index 6a615c5..6faeb00 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -1,3 +1,4 @@ +pub mod cert_manager; pub mod dhcp; pub mod dns; pub mod dummy;