From 254f392cb5e44fbf0e269e1e9b770090aa676014 Mon Sep 17 00:00:00 2001 From: taha Date: Tue, 29 Apr 2025 16:09:04 +0000 Subject: [PATCH 1/8] feat(HelmScore): Add values yaml option to helm chart score (#23) Co-authored-by: tahahawa Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/23 --- Cargo.lock | 63 +++++++++++++++++-------------- harmony/Cargo.toml | 1 + harmony/src/modules/helm/chart.rs | 16 +++++++- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35c5d85..76a6a96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -356,9 +356,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.19" +version = "1.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" +checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" dependencies = [ "shlex", ] @@ -382,9 +382,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", @@ -519,7 +519,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "once_cell", "tiny-keccak", ] @@ -1289,9 +1289,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", @@ -1409,6 +1409,7 @@ dependencies = [ "serde-value", "serde_json", "serde_yaml", + "temp-file", "tokio", "url", "uuid", @@ -2064,9 +2065,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ad87c89110f55e4cd4dc2893a9790820206729eaf221555f742d540b0724a0" +checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6" dependencies = [ "jiff-static", "log", @@ -2077,9 +2078,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d076d5b64a7e2fe6f0743f02c43ca4a6725c0f904203bfe276a5b3e793103605" +checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254" dependencies = [ "proc-macro2", "quote", @@ -2238,9 +2239,9 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libm" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72" [[package]] name = "libredfish" @@ -2947,7 +2948,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.24", + "zerocopy 0.8.25", ] [[package]] @@ -3073,7 +3074,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -3121,7 +3122,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "libredox", "thiserror 2.0.12", ] @@ -3259,7 +3260,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -3806,9 +3807,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -3996,9 +3997,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -4079,6 +4080,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "temp-file" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5ff282c3f91797f0acb021f3af7fffa8a78601f0f2fd0a9f79ee7dcf9a9af9e" + [[package]] name = "tempfile" version = "3.19.1" @@ -4259,9 +4266,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -5096,11 +5103,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ - "zerocopy-derive 0.8.24", + "zerocopy-derive 0.8.25", ] [[package]] @@ -5116,9 +5123,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index a140a8b..959f2da 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -37,3 +37,4 @@ k3d-rs = { path = "../k3d" } directories = "6.0.0" lazy_static = "1.5.0" dockerfile_builder = "0.1.5" +temp-file = "0.1.9" diff --git a/harmony/src/modules/helm/chart.rs b/harmony/src/modules/helm/chart.rs index 35d6863..00f7e5d 100644 --- a/harmony/src/modules/helm/chart.rs +++ b/harmony/src/modules/helm/chart.rs @@ -9,6 +9,8 @@ use helm_wrapper_rs::blocking::{DefaultHelmExecutor, HelmExecutor}; pub use non_blank_string_rs::NonBlankString; use serde::Serialize; use std::collections::HashMap; +use std::path::Path; +use temp_file::TempFile; #[derive(Debug, Clone, Serialize)] pub struct HelmChartScore { @@ -17,6 +19,7 @@ pub struct HelmChartScore { pub chart_name: NonBlankString, pub chart_version: Option, pub values_overrides: Option>, + pub values_yaml: Option, } impl Score for HelmChartScore { @@ -48,6 +51,16 @@ impl Interpret for HelmChartInterpret { .namespace .as_ref() .unwrap_or_else(|| todo!("Get namespace from active kubernetes cluster")); + + let tf: TempFile; + let yaml_path: Option<&Path> = match self.score.values_yaml.as_ref() { + Some(yaml_str) => { + tf = temp_file::with_contents(yaml_str.as_bytes()); + Some(tf.path()) + } + None => None, + }; + let helm_executor = DefaultHelmExecutor::new(); let res = helm_executor.install_or_upgrade( &ns, @@ -55,9 +68,10 @@ impl Interpret for HelmChartInterpret { &self.score.chart_name, self.score.chart_version.as_ref(), self.score.values_overrides.as_ref(), - None, + yaml_path, None, ); + let status = match res { Ok(status) => status, Err(err) => return Err(InterpretError::new(err.to_string())), 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 2/8] 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; From bc2bd2f2f415dda5fd1e942339e360b090d17af1 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 30 Apr 2025 22:33:31 -0400 Subject: [PATCH 3/8] feat: push docker image to registry and deploy with full tag - Added functionality to tag and push the built Docker image to a specified registry. - Modified deployment score to use the full image tag (including registry and project). - Included error handling and logging for the `docker tag` and `docker push` commands. - Updated the `K8sDeploymentScore` struct to include a namespace field and environment variables for database credentials. - Added kebab-case conversion for deployment name and namespace. - Implemented a check_output function for better error reporting. --- Cargo.lock | 10 ++++ Cargo.toml | 1 + harmony/Cargo.toml | 27 +++++----- harmony/src/domain/config.rs | 4 ++ harmony/src/domain/topology/k8s.rs | 7 ++- harmony/src/modules/k8s/deployment.rs | 20 ++++--- harmony/src/modules/k8s/resource.rs | 6 ++- harmony/src/modules/lamp.rs | 77 +++++++++++++++++++++++++-- 8 files changed, 123 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 776066d..f84d847 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -524,6 +524,15 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1383,6 +1392,7 @@ version = "0.1.0" dependencies = [ "async-trait", "cidr", + "convert_case", "derive-new", "directories", "dockerfile_builder", diff --git a/Cargo.toml b/Cargo.toml index 48fe426..8dd08bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ serde_yaml = "0.9.34" serde-value = "0.7.0" http = "1.2.0" inquire = "0.7.5" +convert_case = "0.8.0" [workspace.dependencies.uuid] version = "1.11.0" diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index 959f2da..02a0ce7 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -13,23 +13,23 @@ rust-ipmi = "0.1.1" semver = "1.0.23" serde = { version = "1.0.209", features = ["derive"] } serde_json = "1.0.127" -tokio = { workspace = true } -derive-new = { workspace = true } -log = { workspace = true } -env_logger = { workspace = true } -async-trait = { workspace = true } -cidr = { workspace = true } +tokio.workspace = true +derive-new.workspace = true +log.workspace = true +env_logger.workspace = true +async-trait.workspace = true +cidr.workspace = true opnsense-config = { path = "../opnsense-config" } opnsense-config-xml = { path = "../opnsense-config-xml" } harmony_macros = { path = "../harmony_macros" } harmony_types = { path = "../harmony_types" } -uuid = { workspace = true } -url = { workspace = true } -kube = { workspace = true } -k8s-openapi = { workspace = true } -serde_yaml = { workspace = true } -http = { workspace = true } -serde-value = { workspace = true } +uuid.workspace = true +url.workspace = true +kube.workspace = true +k8s-openapi.workspace = true +serde_yaml.workspace = true +http.workspace = true +serde-value.workspace = true inquire.workspace = true helm-wrapper-rs = "0.4.0" non-blank-string-rs = "1.0.4" @@ -38,3 +38,4 @@ directories = "6.0.0" lazy_static = "1.5.0" dockerfile_builder = "0.1.5" temp-file = "0.1.9" +convert_case.workspace = true diff --git a/harmony/src/domain/config.rs b/harmony/src/domain/config.rs index 320e9a0..0fa059f 100644 --- a/harmony/src/domain/config.rs +++ b/harmony/src/domain/config.rs @@ -6,4 +6,8 @@ lazy_static! { .unwrap() .data_dir() .join("harmony"); + pub static ref REGISTRY_URL: String = std::env::var("HARMONY_REGISTRY_URL") + .unwrap_or_else(|_| "hub.nationtech.io".to_string()); + pub static ref REGISTRY_PROJECT: String = + std::env::var("HARMONY_REGISTRY_PROJECT").unwrap_or_else(|_| "harmony".to_string()); } diff --git a/harmony/src/domain/topology/k8s.rs b/harmony/src/domain/topology/k8s.rs index beecbf0..57b3668 100644 --- a/harmony/src/domain/topology/k8s.rs +++ b/harmony/src/domain/topology/k8s.rs @@ -38,7 +38,7 @@ impl K8sClient { Ok(result) } - pub async fn apply_namespaced(&self, resource: &Vec) -> Result + pub async fn apply_namespaced(&self, resource: &Vec, ns: Option<&str>) -> Result where K: Resource + Clone @@ -49,7 +49,10 @@ impl K8sClient { ::DynamicType: Default, { for r in resource.iter() { - let api: Api = Api::default_namespaced(self.client.clone()); + let api: Api = match ns { + Some(ns) => Api::namespaced(self.client.clone(), ns), + None => Api::default_namespaced(self.client.clone()), + }; api.create(&PostParams::default(), &r).await?; } todo!("") diff --git a/harmony/src/modules/k8s/deployment.rs b/harmony/src/modules/k8s/deployment.rs index 9e7178f..55f581f 100644 --- a/harmony/src/modules/k8s/deployment.rs +++ b/harmony/src/modules/k8s/deployment.rs @@ -1,4 +1,5 @@ -use k8s_openapi::api::apps::v1::Deployment; +use k8s_openapi::{DeepMerge, api::apps::v1::Deployment}; +use log::debug; use serde::Serialize; use serde_json::json; @@ -14,11 +15,13 @@ use super::resource::{K8sResourceInterpret, K8sResourceScore}; pub struct K8sDeploymentScore { pub name: String, pub image: String, + pub namespace: Option, + pub env_vars: serde_json::Value, } impl Score for K8sDeploymentScore { fn create_interpret(&self) -> Box> { - let deployment: Deployment = serde_json::from_value(json!( + let deployment = json!( { "metadata": { "name": self.name @@ -38,18 +41,21 @@ impl Score for K8sDeploymentScore { "spec": { "containers": [ { - "image": self.image, - "name": self.image + "image": self.image, + "name": self.name, + "imagePullPolicy": "IfNotPresent", + "env": self.env_vars, } ] } } } } - )) - .unwrap(); + ); + + let deployment: Deployment = serde_json::from_value(deployment).unwrap(); Box::new(K8sResourceInterpret { - score: K8sResourceScore::single(deployment.clone()), + score: K8sResourceScore::single(deployment.clone(), self.namespace.clone()), }) } diff --git a/harmony/src/modules/k8s/resource.rs b/harmony/src/modules/k8s/resource.rs index 4e54be7..6880292 100644 --- a/harmony/src/modules/k8s/resource.rs +++ b/harmony/src/modules/k8s/resource.rs @@ -14,12 +14,14 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct K8sResourceScore { pub resource: Vec, + pub namespace: Option, } impl K8sResourceScore { - pub fn single(resource: K) -> Self { + pub fn single(resource: K, namespace: Option) -> Self { Self { resource: vec![resource], + namespace, } } } @@ -77,7 +79,7 @@ where .k8s_client() .await .expect("Environment should provide enough information to instanciate a client") - .apply_namespaced(&self.score.resource) + .apply_namespaced(&self.score.resource, self.score.namespace.as_deref()) .await?; Ok(Outcome::success( diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index 5cb948f..2110904 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -1,14 +1,17 @@ +use convert_case::{Case, Casing}; 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 serde_json::json; use std::fs; use std::path::{Path, PathBuf}; use std::str::FromStr; use async_trait::async_trait; -use log::info; +use log::{debug, info}; use serde::Serialize; +use crate::config::{REGISTRY_PROJECT, REGISTRY_URL}; use crate::topology::HelmCommand; use crate::{ data::{Id, Version}, @@ -80,22 +83,49 @@ impl Interpret for LAMPInterpret { }; info!("LAMP docker image built {image_name}"); + let remote_name = match self.push_docker_image(&image_name) { + Ok(remote_name) => remote_name, + Err(e) => { + return Err(InterpretError::new(format!( + "Could not push docker image {e}" + ))); + } + }; + info!("LAMP docker image pushed to {remote_name}"); + info!("Deploying database"); self.deploy_database(inventory, topology).await?; + let base_name = self.score.name.to_case(Case::Kebab); + let secret_name = format!("{}-database-mariadb", base_name); + let deployment_score = K8sDeploymentScore { - name: >::name(&self.score), - image: image_name, + name: >::name(&self.score).to_case(Case::Kebab), + image: remote_name, + namespace: self.get_namespace().map(|nbs| nbs.to_string()), + env_vars: json!([ + { + "name": "MYSQL_PASSWORD", + "valueFrom": { + "secretKeyRef": { + "name": secret_name, + "key": "mariadb-root-password" + } + } + }, + ]), }; + info!("Deploying score {deployment_score:#?}"); + deployment_score .create_interpret() .execute(inventory, topology) .await?; info!("LAMP deployment_score {deployment_score:?}"); - todo!("1. Use HelmChartScore to deploy mariadb - 2. Use deploymentScore to deploy lamp docker container + todo!("1. [x] Use HelmChartScore to deploy mariadb + 2. [x] 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?)"); } @@ -258,6 +288,43 @@ opcache.fast_shutdown=1 Ok(dockerfile_path) } + fn check_output( + &self, + output: &std::process::Output, + msg: &str, + ) -> Result<(), Box> { + if !output.status.success() { + return Err(format!("{msg}: {}", String::from_utf8_lossy(&output.stderr)).into()); + } + Ok(()) + } + + fn push_docker_image(&self, image_name: &str) -> Result> { + let full_tag = format!("{}/{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT, &image_name); + let output = std::process::Command::new("docker") + .args(["tag", image_name, &full_tag]) + .output()?; + self.check_output(&output, "Tagging docker image failed")?; + + debug!( + "docker tag output {} {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + + let output = std::process::Command::new("docker") + .args(["push", &full_tag]) + .output()?; + self.check_output(&output, "Pushing docker image failed")?; + debug!( + "docker push output {} {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + + Ok(full_tag) + } + pub fn build_docker_image(&self) -> Result> { info!("Generating Dockerfile"); let dockerfile = self.build_dockerfile(&self.score)?; From c879ca143fbb94a93d9051e29ceeed15603f08d2 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 30 Apr 2025 23:36:12 -0400 Subject: [PATCH 4/8] feat: Add comments explaining a bit of what harmony does in the lamp demo --- examples/lamp/src/main.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index 9d51d0b..41adfb4 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -8,17 +8,30 @@ use harmony::{ #[tokio::main] async fn main() { - // let _ = env_logger::Builder::from_default_env().filter_level(log::LevelFilter::Info).try_init(); + // This here is the whole configuration to + // - setup a local K3D cluster + // - Build a docker image with the PHP project builtin and production grade settings + // - Deploy a mariadb database using a production grade helm chart + // - Deploy the new container using a kubernetes deployment + // - Configure networking between the PHP container and the database + // - Provision a public route and an SSL certificate automatically on production environments + // + // Enjoy :) let lamp_stack = LAMPScore { name: "harmony-lamp-demo".to_string(), domain: Url::Url(url::Url::parse("https://lampdemo.harmony.nationtech.io").unwrap()), php_version: Version::from("8.4.4").unwrap(), + // This config can be extended as needed for more complicated configurations config: LAMPConfig { project_root: "./php".into(), ..Default::default() }, }; + // You can choose the type of Topology you want, we suggest starting with the + // K8sAnywhereTopology as it is the most automatic one that enables you to easily deploy + // locally, to development environment from a CI, to staging, and to production with settings + // that automatically adapt to each environment grade. let mut maestro = Maestro::::initialize( Inventory::autoload(), K8sAnywhereTopology::new(), @@ -26,5 +39,7 @@ async fn main() { .await .unwrap(); maestro.register_all(vec![Box::new(lamp_stack)]); + // Here we bootstrap the CLI, this gives some nice features if you need them harmony_cli::init(maestro, None).await.unwrap(); } +// That's it, end of the infra as code. From 1c3669cb47e9a04edccef1d6500cc62f485afc8a Mon Sep 17 00:00:00 2001 From: Willem Date: Fri, 2 May 2025 11:56:27 -0400 Subject: [PATCH 5/8] chore: added default mariadb size and pass env variables to php app --- harmony/src/modules/lamp.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index 2110904..7d2b28d 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -3,6 +3,7 @@ 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 serde_json::json; +use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -17,8 +18,8 @@ use crate::{ data::{Id, Version}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, - modules::k8s::deployment::K8sDeploymentScore, - score::Score, +modules::k8s::deployment::K8sDeploymentScore, +score::Score, topology::{K8sclient, Topology, Url}, }; @@ -36,6 +37,7 @@ pub struct LAMPScore { pub struct LAMPConfig { pub project_root: PathBuf, pub ssl_enabled: bool, + pub database_size: String, } impl Default for LAMPConfig { @@ -43,6 +45,7 @@ impl Default for LAMPConfig { LAMPConfig { project_root: Path::new("./src").to_path_buf(), ssl_enabled: true, + database_size: "2Gi".to_string(), } } } @@ -113,6 +116,10 @@ impl Interpret for LAMPInterpret { } } }, + { + "name": "MYSQL_HOST", + "value": secret_name + }, ]), }; @@ -152,6 +159,11 @@ impl LAMPInterpret { inventory: &Inventory, topology: &T, ) -> Result { + let mut mariadb_overrides = HashMap::new(); + mariadb_overrides.insert( + NonBlankString::from_str("primary.persistence.size").unwrap(), + self.score.config.database_size.clone(), + ); let score = HelmChartScore { namespace: self.get_namespace(), release_name: NonBlankString::from_str(&format!("{}-database", self.score.name)) @@ -161,7 +173,7 @@ impl LAMPInterpret { ) .unwrap(), chart_version: None, - values_overrides: None, + values_overrides: Some(mariadb_overrides), create_namespace: true, install_only: true, values_yaml: None, @@ -257,6 +269,13 @@ opcache.fast_shutdown=1 sed -i 's/ServerSignature On/ServerSignature Off/' /etc/apache2/conf-enabled/security.conf" )); + // Set env vars + dockerfile.push(RUN::from( + "echo 'PassEnv MYSQL_PASSWORD' >> /etc/apache2/sites-available/000-default.conf \ + && echo 'PassEnv MYSQL_USER' >> /etc/apache2/sites-available/000-default.conf \ + && echo 'PassEnv MYSQL_HOST' >> /etc/apache2/sites-available/000-default.conf", + )); + // Create a dedicated user for running Apache dockerfile.push(RUN::from( "groupadd -g 1000 appuser && \ From a7ba9be486299485637f297c0b472c4a7c8746b3 Mon Sep 17 00:00:00 2001 From: Willem Date: Fri, 2 May 2025 12:03:18 -0400 Subject: [PATCH 6/8] feat:php program to fill pvc and report database usage --- examples/lamp/php/index.php | 84 ++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/examples/lamp/php/index.php b/examples/lamp/php/index.php index 6cf1a50..471f6a2 100644 --- a/examples/lamp/php/index.php +++ b/examples/lamp/php/index.php @@ -1,3 +1,85 @@ PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, +]; + +try { + $pdo = new PDO($dsn, $user, $pass, $options); + $pdo->exec("CREATE DATABASE IF NOT EXISTS `$db`"); + $pdo->exec("USE `$db`"); + $pdo->exec(" + CREATE TABLE IF NOT EXISTS filler ( + id INT AUTO_INCREMENT PRIMARY KEY, + data LONGBLOB + ) + "); +} catch (\PDOException $e) { + die("❌ DB connection failed: " . $e->getMessage()); +} + +function getDbStats($pdo, $db) { + $stmt = $pdo->query(" + SELECT + ROUND(SUM(data_length + index_length) / 1024 / 1024 / 1024, 2) AS total_size_gb, + SUM(table_rows) AS total_rows + FROM information_schema.tables + WHERE table_schema = '$db' + "); + $result = $stmt->fetch(); + $sizeGb = $result['total_size_gb'] ?? '0'; + $rows = $result['total_rows'] ?? '0'; + $avgMb = ($rows > 0) ? round(($sizeGb * 1024) / $rows, 2) : 0; + return [$sizeGb, $rows, $avgMb]; +} + +list($dbSize, $rowCount, $avgRowMb) = getDbStats($pdo, $db); + +$message = ''; + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['fill'])) { + $iterations = 1024; + $data = str_repeat(random_bytes(1024), 1024); // 1MB + $stmt = $pdo->prepare("INSERT INTO filler (data) VALUES (:data)"); + + for ($i = 0; $i < $iterations; $i++) { + $stmt->execute([':data' => $data]); + } + + list($dbSize, $rowCount, $avgRowMb) = getDbStats($pdo, $db); + + $message = "

✅ 1GB inserted into MariaDB successfully.

"; +} ?> + + + + + MariaDB Filler + + +

MariaDB Storage Filler

+ +
    +
  • 📦 MariaDB Used Size: GB
  • +
  • 📊 Total Rows:
  • +
  • 📐 Average Row Size: MB
  • +
+ +
+ +
+ + + From e1133ea114c5e8f1daf1d167f435d3917f47e49e Mon Sep 17 00:00:00 2001 From: Willem Date: Fri, 2 May 2025 15:02:50 -0400 Subject: [PATCH 7/8] use default database_size None in LampConfig to default to value from helm chart --- harmony/src/modules/lamp.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index 7d2b28d..47d6ca9 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -18,8 +18,8 @@ use crate::{ data::{Id, Version}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, -modules::k8s::deployment::K8sDeploymentScore, -score::Score, + modules::k8s::deployment::K8sDeploymentScore, + score::Score, topology::{K8sclient, Topology, Url}, }; @@ -37,7 +37,7 @@ pub struct LAMPScore { pub struct LAMPConfig { pub project_root: PathBuf, pub ssl_enabled: bool, - pub database_size: String, + pub database_size: Option, } impl Default for LAMPConfig { @@ -45,7 +45,7 @@ impl Default for LAMPConfig { LAMPConfig { project_root: Path::new("./src").to_path_buf(), ssl_enabled: true, - database_size: "2Gi".to_string(), + database_size: None, } } } @@ -159,11 +159,13 @@ impl LAMPInterpret { inventory: &Inventory, topology: &T, ) -> Result { - let mut mariadb_overrides = HashMap::new(); - mariadb_overrides.insert( - NonBlankString::from_str("primary.persistence.size").unwrap(), - self.score.config.database_size.clone(), - ); + let mut values_overrides = HashMap::new(); + if let Some(database_size) = self.score.config.database_size.clone() { + values_overrides.insert( + NonBlankString::from_str("primary.persistence.size").unwrap(), + database_size, + ); + } let score = HelmChartScore { namespace: self.get_namespace(), release_name: NonBlankString::from_str(&format!("{}-database", self.score.name)) @@ -173,7 +175,7 @@ impl LAMPInterpret { ) .unwrap(), chart_version: None, - values_overrides: Some(mariadb_overrides), + values_overrides: Some(values_overrides), create_namespace: true, install_only: true, values_yaml: None, @@ -181,7 +183,6 @@ impl LAMPInterpret { score.create_interpret().execute(inventory, topology).await } - fn build_dockerfile(&self, score: &LAMPScore) -> Result> { let mut dockerfile = Dockerfile::new(); From 78fffcd725d3c5f38feee4d68a1d99eed946b731 Mon Sep 17 00:00:00 2001 From: Willem Date: Fri, 2 May 2025 15:07:39 -0400 Subject: [PATCH 8/8] fix: specified 2Gi db size from LAMPconfig --- examples/lamp/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index 41adfb4..1aaca90 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -24,6 +24,7 @@ async fn main() { // This config can be extended as needed for more complicated configurations config: LAMPConfig { project_root: "./php".into(), + database_size: format!("2Gi").into(), ..Default::default() }, };