Merge remote-tracking branch 'origin/master' into monitoring-alerting

This commit is contained in:
2025-05-06 10:03:27 -04:00
15 changed files with 440 additions and 77 deletions

View File

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

View File

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

View File

@@ -38,7 +38,7 @@ impl K8sClient {
Ok(result)
}
pub async fn apply_namespaced<K>(&self, resource: &Vec<K>) -> Result<K, Error>
pub async fn apply_namespaced<K>(&self, resource: &Vec<K>, ns: Option<&str>) -> Result<K, Error>
where
K: Resource<Scope = NamespaceResourceScope>
+ Clone
@@ -49,7 +49,10 @@ impl K8sClient {
<K as kube::Resource>::DynamicType: Default,
{
for r in resource.iter() {
let api: Api<K> = Api::default_namespaced(self.client.clone());
let api: Api<K> = match ns {
Some(ns) => Api::namespaced(self.client.clone(), ns),
None => Api::default_namespaced(self.client.clone()),
};
api.create(&PostParams::default(), &r).await?;
}
todo!("")

View File

@@ -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<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 {
@@ -48,16 +57,68 @@ impl<T: Topology + HelmCommand> Interpret<T> 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())),

View File

@@ -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<String>,
pub env_vars: serde_json::Value,
}
impl<T: Topology + K8sclient> Score<T> for K8sDeploymentScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
let deployment: Deployment = serde_json::from_value(json!(
let deployment = json!(
{
"metadata": {
"name": self.name
@@ -38,18 +41,21 @@ impl<T: Topology + K8sclient> Score<T> 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()),
})
}

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

@@ -14,12 +14,14 @@ use crate::{
#[derive(Debug, Clone, Serialize)]
pub struct K8sResourceScore<K: Resource + std::fmt::Debug> {
pub resource: Vec<K>,
pub namespace: Option<String>,
}
impl<K: Resource + std::fmt::Debug> K8sResourceScore<K> {
pub fn single(resource: K) -> Self {
pub fn single(resource: K, namespace: Option<String>) -> 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(

View File

@@ -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<String>,
}
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<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 +66,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 +86,54 @@ impl<T: Topology + K8sclient> Interpret<T> 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: <LAMPScore as Score<T>>::name(&self.score),
image: image_name,
name: <LAMPScore as Score<T>>::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<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 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<PathBuf, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<String, Box<dyn std::error::Error>> {
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<String, Box<dyn std::error::Error>> {
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<NonBlankString> {
Some(NonBlankString::from_str(&self.namespace).unwrap())
}
}