Merge pull request 'feat: add mariadb helm deployment to lamp interpreter' (#26) from feat/lampDatabase into master

Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/26
Reviewed-by: wjro <wrolleman@nationtech.io>
This commit is contained in:
johnride 2025-04-30 20:03:13 +00:00
commit 28978299c9
8 changed files with 148 additions and 19 deletions

2
Cargo.lock generated
View File

@ -1020,8 +1020,8 @@ dependencies = [
"cidr",
"env_logger",
"harmony",
"harmony_cli",
"harmony_macros",
"harmony_tui",
"harmony_types",
"log",
"tokio",

View File

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

View File

@ -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();
}

View File

@ -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<NonBlankString>,
pub values_overrides: Option<HashMap<NonBlankString, String>>,
pub values_yaml: Option<String>,
pub create_namespace: bool,
/// Wether to run `helm upgrade --install` under the hood or only install when not present
pub install_only: bool,
}
impl<T: Topology + HelmCommand> Score<T> for HelmChartScore {
@ -62,6 +68,47 @@ impl<T: Topology + HelmCommand> Interpret<T> 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<T: Topology + HelmCommand> Interpret<T> for HelmChartInterpret {
self.score.chart_version.as_ref(),
self.score.values_overrides.as_ref(),
yaml_path,
None,
Some(&helm_options),
);
let status = match res {

View File

@ -1,2 +1,3 @@
pub mod deployment;
pub mod namespace;
pub mod resource;

View File

@ -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<NonBlankString>,
}
impl<T: Topology + K8sclient> Score<T> for K8sNamespaceScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
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()
}
}

View File

@ -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<T: Topology + K8sclient> Score<T> for LAMPScore {
impl<T: Topology + K8sclient + HelmCommand> Score<T> for LAMPScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(LAMPInterpret {
score: self.clone(),
namespace: "harmony-lamp".to_string(),
})
}
@ -51,10 +60,11 @@ impl<T: Topology + K8sclient> Score<T> for LAMPScore {
#[derive(Debug)]
pub struct LAMPInterpret {
score: LAMPScore,
namespace: String,
}
#[async_trait]
impl<T: Topology + K8sclient> Interpret<T> for LAMPInterpret {
impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for LAMPInterpret {
async fn execute(
&self,
inventory: &Inventory,
@ -70,18 +80,23 @@ impl<T: Topology + K8sclient> Interpret<T> for LAMPInterpret {
};
info!("LAMP docker image built {image_name}");
info!("Deploying database");
self.deploy_database(inventory, topology).await?;
let deployment_score = K8sDeploymentScore {
name: <LAMPScore as Score<T>>::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<T: Topology + K8sclient> Interpret<T> 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<T: Topology + K8sclient + HelmCommand>(
&self,
score: &LAMPScore,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
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<PathBuf, Box<dyn std::error::Error>> {
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<NonBlankString> {
Some(NonBlankString::from_str(&self.namespace).unwrap())
}
}

View File

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