From 87f6afc24983a7b95f0085b1362613194915b914 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 30 Apr 2025 15:27:10 -0400 Subject: [PATCH] feat: add mariadb helm deployment to lamp interpreter - Adds a `deploy_database` function to the `LAMPInterpret` struct to deploy a MariaDB database using Helm. - Integrates `HelmCommand` trait requirement to the `LAMPInterpret` struct. - Introduces `HelmChartScore` to manage MariaDB deployment. - Adds namespace configuration for helm deployments. - Updates trait bounds for `LAMPInterpret` to include `HelmCommand`. - Implements `get_namespace` function to retrieve the namespace. --- Cargo.lock | 2 +- examples/lamp/Cargo.toml | 2 +- examples/lamp/src/main.rs | 2 +- harmony/src/modules/helm/chart.rs | 49 ++++++++++++++++++++++- harmony/src/modules/k8s/mod.rs | 1 + harmony/src/modules/k8s/namespace.rs | 46 ++++++++++++++++++++++ harmony/src/modules/lamp.rs | 59 ++++++++++++++++++++++------ opnsense-config/src/lib.rs | 6 +-- 8 files changed, 148 insertions(+), 19 deletions(-) create mode 100644 harmony/src/modules/k8s/namespace.rs diff --git a/Cargo.lock b/Cargo.lock index 76a6a96..776066d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1020,8 +1020,8 @@ dependencies = [ "cidr", "env_logger", "harmony", + "harmony_cli", "harmony_macros", - "harmony_tui", "harmony_types", "log", "tokio", diff --git a/examples/lamp/Cargo.toml b/examples/lamp/Cargo.toml index 902548e..a433e79 100644 --- a/examples/lamp/Cargo.toml +++ b/examples/lamp/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] harmony = { path = "../../harmony" } -harmony_tui = { path = "../../harmony_tui" } +harmony_cli = { path = "../../harmony_cli" } harmony_types = { path = "../../harmony_types" } cidr = { workspace = true } tokio = { workspace = true } diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index 27e5df6..9d51d0b 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -26,5 +26,5 @@ async fn main() { .await .unwrap(); maestro.register_all(vec![Box::new(lamp_stack)]); - harmony_tui::init(maestro).await.unwrap(); + harmony_cli::init(maestro, None).await.unwrap(); } diff --git a/harmony/src/modules/helm/chart.rs b/harmony/src/modules/helm/chart.rs index 00f7e5d..1be66f6 100644 --- a/harmony/src/modules/helm/chart.rs +++ b/harmony/src/modules/helm/chart.rs @@ -6,10 +6,12 @@ use crate::topology::{HelmCommand, Topology}; use async_trait::async_trait; use helm_wrapper_rs; use helm_wrapper_rs::blocking::{DefaultHelmExecutor, HelmExecutor}; +use log::info; pub use non_blank_string_rs::NonBlankString; use serde::Serialize; use std::collections::HashMap; use std::path::Path; +use std::str::FromStr; use temp_file::TempFile; #[derive(Debug, Clone, Serialize)] @@ -20,6 +22,10 @@ pub struct HelmChartScore { pub chart_version: Option, pub values_overrides: Option>, pub values_yaml: Option, + pub create_namespace: bool, + + /// Wether to run `helm upgrade --install` under the hood or only install when not present + pub install_only: bool, } impl Score for HelmChartScore { @@ -62,6 +68,47 @@ impl Interpret for HelmChartInterpret { }; let helm_executor = DefaultHelmExecutor::new(); + + let mut helm_options = Vec::new(); + if self.score.create_namespace { + helm_options.push(NonBlankString::from_str("--create-namespace").unwrap()); + } + + if self.score.install_only { + let chart_list = match helm_executor.list(Some(ns)) { + Ok(charts) => charts, + Err(e) => { + return Err(InterpretError::new(format!( + "Failed to list scores in namespace {:?} because of error : {}", + self.score.namespace, e + ))); + } + }; + + if chart_list + .iter() + .any(|item| item.name == self.score.release_name.to_string()) + { + info!( + "Release '{}' already exists in namespace '{}'. Skipping installation as install_only is true.", + self.score.release_name, ns + ); + + return Ok(Outcome::new( + InterpretStatus::SUCCESS, + format!( + "Helm Chart '{}' already installed to namespace {ns} and install_only=true", + self.score.release_name + ), + )); + } else { + info!( + "Release '{}' not found in namespace '{}'. Proceeding with installation.", + self.score.release_name, ns + ); + } + } + let res = helm_executor.install_or_upgrade( &ns, &self.score.release_name, @@ -69,7 +116,7 @@ impl Interpret for HelmChartInterpret { self.score.chart_version.as_ref(), self.score.values_overrides.as_ref(), yaml_path, - None, + Some(&helm_options), ); let status = match res { diff --git a/harmony/src/modules/k8s/mod.rs b/harmony/src/modules/k8s/mod.rs index df654fb..97e238f 100644 --- a/harmony/src/modules/k8s/mod.rs +++ b/harmony/src/modules/k8s/mod.rs @@ -1,2 +1,3 @@ pub mod deployment; +pub mod namespace; pub mod resource; diff --git a/harmony/src/modules/k8s/namespace.rs b/harmony/src/modules/k8s/namespace.rs new file mode 100644 index 0000000..aee87e3 --- /dev/null +++ b/harmony/src/modules/k8s/namespace.rs @@ -0,0 +1,46 @@ +use k8s_openapi::api::core::v1::Namespace; +use non_blank_string_rs::NonBlankString; +use serde::Serialize; +use serde_json::json; + +use crate::{ + interpret::Interpret, + score::Score, + topology::{K8sclient, Topology}, +}; + +#[derive(Debug, Clone, Serialize)] +pub struct K8sNamespaceScore { + pub name: Option, +} + +impl Score for K8sNamespaceScore { + fn create_interpret(&self) -> Box> { + let name = match &self.name { + Some(name) => name, + None => todo!( + "Return NoOp interpret when no namespace specified or something that makes sense" + ), + }; + let _namespace: Namespace = serde_json::from_value(json!( + { + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "name": name, + }, + } + )) + .unwrap(); + todo!( + "We currently only support namespaced ressources (see Scope = NamespaceResourceScope)" + ); + // Box::new(K8sResourceInterpret { + // score: K8sResourceScore::single(namespace.clone()), + // }) + } + + fn name(&self) -> String { + "K8sNamespaceScore".to_string() + } +} diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index f83ded2..5cb948f 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -1,9 +1,15 @@ +use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, WORKDIR}; +use dockerfile_builder::{Dockerfile, instruction_builder::EnvBuilder}; +use non_blank_string_rs::NonBlankString; +use std::fs; use std::path::{Path, PathBuf}; +use std::str::FromStr; use async_trait::async_trait; use log::info; use serde::Serialize; +use crate::topology::HelmCommand; use crate::{ data::{Id, Version}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, @@ -13,6 +19,8 @@ use crate::{ topology::{K8sclient, Topology, Url}, }; +use super::helm::chart::HelmChartScore; + #[derive(Debug, Clone, Serialize)] pub struct LAMPScore { pub name: String, @@ -36,10 +44,11 @@ impl Default for LAMPConfig { } } -impl Score for LAMPScore { +impl Score for LAMPScore { fn create_interpret(&self) -> Box> { Box::new(LAMPInterpret { score: self.clone(), + namespace: "harmony-lamp".to_string(), }) } @@ -51,10 +60,11 @@ impl Score for LAMPScore { #[derive(Debug)] pub struct LAMPInterpret { score: LAMPScore, + namespace: String, } #[async_trait] -impl Interpret for LAMPInterpret { +impl Interpret for LAMPInterpret { async fn execute( &self, inventory: &Inventory, @@ -70,18 +80,23 @@ impl Interpret for LAMPInterpret { }; info!("LAMP docker image built {image_name}"); + info!("Deploying database"); + self.deploy_database(inventory, topology).await?; + let deployment_score = K8sDeploymentScore { name: >::name(&self.score), image: image_name, }; - info!("LAMP deployment_score {deployment_score:?}"); - todo!(); deployment_score .create_interpret() .execute(inventory, topology) .await?; - todo!() + + info!("LAMP deployment_score {deployment_score:?}"); + todo!("1. Use HelmChartScore to deploy mariadb + 2. Use deploymentScore to deploy lamp docker container + 3. for remote clusters, push the image to some registry (use nationtech's for demos? push to the cluster's registry?)"); } fn get_name(&self) -> InterpretName { @@ -101,15 +116,31 @@ impl Interpret for LAMPInterpret { } } -use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, WORKDIR}; -use dockerfile_builder::{Dockerfile, instruction_builder::EnvBuilder}; -use std::fs; - impl LAMPInterpret { - pub fn build_dockerfile( + async fn deploy_database( &self, - score: &LAMPScore, - ) -> Result> { + inventory: &Inventory, + topology: &T, + ) -> Result { + let score = HelmChartScore { + namespace: self.get_namespace(), + release_name: NonBlankString::from_str(&format!("{}-database", self.score.name)) + .unwrap(), + chart_name: NonBlankString::from_str( + "oci://registry-1.docker.io/bitnamicharts/mariadb", + ) + .unwrap(), + chart_version: None, + values_overrides: None, + create_namespace: true, + install_only: true, + values_yaml: None, + }; + + score.create_interpret().execute(inventory, topology).await + } + + fn build_dockerfile(&self, score: &LAMPScore) -> Result> { let mut dockerfile = Dockerfile::new(); // Use the PHP version from the score to determine the base image @@ -260,4 +291,8 @@ opcache.fast_shutdown=1 Ok(image_name) } + + fn get_namespace(&self) -> Option { + Some(NonBlankString::from_str(&self.namespace).unwrap()) + } } diff --git a/opnsense-config/src/lib.rs b/opnsense-config/src/lib.rs index a497133..5953875 100644 --- a/opnsense-config/src/lib.rs +++ b/opnsense-config/src/lib.rs @@ -4,14 +4,14 @@ pub mod modules; pub use config::Config; pub use error::Error; -#[cfg(test)] -mod test { + +#[cfg(e2e_test)] +mod e2e_test { use opnsense_config_xml::StaticMap; use std::net::Ipv4Addr; use crate::Config; - #[cfg(opnsenseendtoend)] #[tokio::test] async fn test_public_sdk() { use pretty_assertions::assert_eq;