diff --git a/Cargo.lock b/Cargo.lock
index 35c5d85..f84d847 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,11 +519,20 @@ 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",
]
+[[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"
@@ -1020,8 +1029,8 @@ dependencies = [
"cidr",
"env_logger",
"harmony",
+ "harmony_cli",
"harmony_macros",
- "harmony_tui",
"harmony_types",
"log",
"tokio",
@@ -1289,9 +1298,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",
@@ -1383,6 +1392,7 @@ version = "0.1.0"
dependencies = [
"async-trait",
"cidr",
+ "convert_case",
"derive-new",
"directories",
"dockerfile_builder",
@@ -1409,6 +1419,7 @@ dependencies = [
"serde-value",
"serde_json",
"serde_yaml",
+ "temp-file",
"tokio",
"url",
"uuid",
@@ -2064,9 +2075,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 +2088,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 +2249,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 +2958,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 +3084,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 +3132,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 +3270,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
- "getrandom 0.2.15",
+ "getrandom 0.2.16",
"libc",
"untrusted",
"windows-sys 0.52.0",
@@ -3806,9 +3817,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 +4007,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 +4090,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 +4276,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 +5113,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 +5133,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/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/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/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
+ = $message ?>
+
+ - 📦 MariaDB Used Size: = $dbSize ?> GB
+ - 📊 Total Rows: = $rowCount ?>
+ - 📐 Average Row Size: = $avgRowMb ?> MB
+
+
+
+
+
+
diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs
index 27e5df6..1aaca90 100644
--- a/examples/lamp/src/main.rs
+++ b/examples/lamp/src/main.rs
@@ -8,17 +8,31 @@ 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(),
+ database_size: format!("2Gi").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 +40,7 @@ async fn main() {
.await
.unwrap();
maestro.register_all(vec![Box::new(lamp_stack)]);
- harmony_tui::init(maestro).await.unwrap();
+ // 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.
diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml
index a140a8b..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"
@@ -37,3 +37,5 @@ k3d-rs = { path = "../k3d" }
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/helm/chart.rs b/harmony/src/modules/helm/chart.rs
index 35d6863..1be66f6 100644
--- a/harmony/src/modules/helm/chart.rs
+++ b/harmony/src/modules/helm/chart.rs
@@ -6,9 +6,13 @@ 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)]
pub struct HelmChartScore {
@@ -17,6 +21,11 @@ pub struct HelmChartScore {
pub chart_name: NonBlankString,
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 {
@@ -48,16 +57,68 @@ 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 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,
&self.score.chart_name,
self.score.chart_version.as_ref(),
self.score.values_overrides.as_ref(),
- None,
- None,
+ yaml_path,
+ Some(&helm_options),
);
+
let status = match res {
Ok(status) => status,
Err(err) => return Err(InterpretError::new(err.to_string())),
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/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/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 f83ded2..47d6ca9 100644
--- a/harmony/src/modules/lamp.rs
+++ b/harmony/src/modules/lamp.rs
@@ -1,9 +1,19 @@
+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::collections::HashMap;
+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},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
@@ -13,6 +23,8 @@ use crate::{
topology::{K8sclient, Topology, Url},
};
+use super::helm::chart::HelmChartScore;
+
#[derive(Debug, Clone, Serialize)]
pub struct LAMPScore {
pub name: String,
@@ -25,6 +37,7 @@ pub struct LAMPScore {
pub struct LAMPConfig {
pub project_root: PathBuf,
pub ssl_enabled: bool,
+ pub database_size: Option,
}
impl Default for LAMPConfig {
@@ -32,14 +45,16 @@ impl Default for LAMPConfig {
LAMPConfig {
project_root: Path::new("./src").to_path_buf(),
ssl_enabled: true,
+ database_size: None,
}
}
}
-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 +66,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 +86,54 @@ 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"
+ }
+ }
+ },
+ {
+ "name": "MYSQL_HOST",
+ "value": secret_name
+ },
+ ]),
};
- info!("LAMP deployment_score {deployment_score:?}");
- todo!();
+ info!("Deploying score {deployment_score:#?}");
+
deployment_score
.create_interpret()
.execute(inventory, topology)
.await?;
- todo!()
+
+ info!("LAMP deployment_score {deployment_score:?}");
+ 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?)");
}
fn get_name(&self) -> InterpretName {
@@ -101,15 +153,37 @@ 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 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))
+ .unwrap(),
+ chart_name: NonBlankString::from_str(
+ "oci://registry-1.docker.io/bitnamicharts/mariadb",
+ )
+ .unwrap(),
+ chart_version: None,
+ values_overrides: Some(values_overrides),
+ 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
@@ -196,6 +270,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 && \
@@ -227,6 +308,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)?;
@@ -260,4 +378,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;