feat: LAMP stack and Monitoring stack now work on OKD, we just have to manually set a few serviceaccounts to privileged scc until we find a better solution

This commit is contained in:
Jean-Gabriel Gill-Couture 2025-05-06 22:13:38 -04:00
parent c2d1cb9b76
commit 51724d0e55
10 changed files with 52 additions and 238 deletions

View File

@ -2,7 +2,10 @@ use harmony::{
data::Version, data::Version,
inventory::Inventory, inventory::Inventory,
maestro::Maestro, maestro::Maestro,
modules::lamp::{LAMPConfig, LAMPScore}, modules::{
lamp::{LAMPConfig, LAMPScore},
monitoring::monitoring_alerting::MonitoringAlertingStackScore,
},
topology::{K8sAnywhereTopology, Url}, topology::{K8sAnywhereTopology, Url},
}; };
@ -24,7 +27,7 @@ async fn main() {
// This config can be extended as needed for more complicated configurations // This config can be extended as needed for more complicated configurations
config: LAMPConfig { config: LAMPConfig {
project_root: "./php".into(), project_root: "./php".into(),
database_size: format!("2Gi").into(), database_size: format!("4Gi").into(),
..Default::default() ..Default::default()
}, },
}; };
@ -39,7 +42,11 @@ async fn main() {
) )
.await .await
.unwrap(); .unwrap();
maestro.register_all(vec![Box::new(lamp_stack)]);
let monitoring_stack_score =
MonitoringAlertingStackScore::new_with_ns(&lamp_stack.config.namespace);
maestro.register_all(vec![Box::new(lamp_stack), Box::new(monitoring_stack_score)]);
// Here we bootstrap the CLI, this gives some nice features if you need them // Here we bootstrap the CLI, this gives some nice features if you need them
harmony_cli::init(maestro, None).await.unwrap(); harmony_cli::init(maestro, None).await.unwrap();
} }

View File

@ -92,9 +92,7 @@ impl K8sAnywhereTopology {
async fn try_get_or_install_k8s_client(&self) -> Result<Option<K8sState>, InterpretError> { async fn try_get_or_install_k8s_client(&self) -> Result<Option<K8sState>, InterpretError> {
let k8s_anywhere_config = K8sAnywhereConfig { let k8s_anywhere_config = K8sAnywhereConfig {
kubeconfig: std::env::var("HARMONY_KUBECONFIG") kubeconfig: std::env::var("KUBECONFIG").ok().map(|v| v.to_string()),
.ok()
.map(|v| v.to_string()),
use_system_kubeconfig: std::env::var("HARMONY_USE_SYSTEM_KUBECONFIG") use_system_kubeconfig: std::env::var("HARMONY_USE_SYSTEM_KUBECONFIG")
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)), .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
autoinstall: std::env::var("HARMONY_AUTOINSTALL") autoinstall: std::env::var("HARMONY_AUTOINSTALL")

View File

@ -1,4 +1,3 @@
pub mod monitoring_alerting;
mod ha_cluster; mod ha_cluster;
mod host_binding; mod host_binding;
mod http; mod http;

View File

@ -1,108 +0,0 @@
use std::sync::Arc;
use log::warn;
use tokio::sync::OnceCell;
use k8s_openapi::api::core::v1::Pod;
use kube::{
Client,
api::{Api, ListParams},
};
use async_trait::async_trait;
use crate::{
interpret::{InterpretError, Outcome},
inventory::Inventory,
maestro::Maestro,
modules::monitoring::monitoring_alerting::MonitoringAlertingStackScore,
score::Score,
};
use super::{HelmCommand, K8sAnywhereTopology, Topology, k8s::K8sClient};
#[derive(Clone, Debug)]
struct MonitoringState {
message: String,
}
#[derive(Debug)]
pub struct MonitoringAlertingTopology {
monitoring_state: OnceCell<Option<MonitoringState>>,
}
impl MonitoringAlertingTopology {
pub fn new() -> Self {
Self {
monitoring_state: OnceCell::new(),
}
}
async fn get_monitoring_state(&self) -> Result<Option<MonitoringState>, InterpretError> {
let client = Client::try_default()
.await
.map_err(|e| InterpretError::new(format!("Kubernetes client error: {}", e)))?;
for ns in &["monitoring", "openshift-monitoring"] {
let pods: Api<Pod> = Api::namespaced(client.clone(), ns);
//TODO hardcoding the label is a problem
//check all pods are ready
let lp = ListParams::default().labels("app.kubernetes.io/name=prometheus");
match pods.list(&lp).await {
Ok(pod_list) => {
for p in pod_list.items {
if let Some(status) = p.status {
if let Some(conditions) = status.conditions {
if conditions
.iter()
.any(|c| c.type_ == "Ready" && c.status == "True")
{
return Ok(Some(MonitoringState {
message: format!(
"Prometheus is ready in namespace: {}",
ns
),
}));
}
}
}
}
}
Err(e) => {
warn!("Failed to query pods in ns {}: {}", ns, e);
}
}
}
Ok(None)
}
}
impl<T: Topology> Clone for Box<dyn Score<T>> {
fn clone(&self) -> Box<dyn Score<T>> {
self.clone_box()
}
}
#[async_trait]
impl Topology for MonitoringAlertingTopology {
fn name(&self) -> &str {
"MonitoringAlertingTopology"
}
async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {
if let Some(state) = self.get_monitoring_state().await? {
// Monitoring stack is already ready — stop app.
println!("{}", state.message);
std::process::exit(0);
}
// Monitoring not found — proceed with installation.
Ok(Outcome::success(
"Monitoring stack installation started.".to_string(),
))
}
}
impl HelmCommand for MonitoringAlertingTopology {}

View File

@ -6,7 +6,7 @@ use crate::topology::{HelmCommand, Topology};
use async_trait::async_trait; use async_trait::async_trait;
use helm_wrapper_rs; use helm_wrapper_rs;
use helm_wrapper_rs::blocking::{DefaultHelmExecutor, HelmExecutor}; use helm_wrapper_rs::blocking::{DefaultHelmExecutor, HelmExecutor};
use log::{debug, error, info, warn}; use log::{debug, info, warn};
pub use non_blank_string_rs::NonBlankString; pub use non_blank_string_rs::NonBlankString;
use serde::Serialize; use serde::Serialize;
use std::collections::HashMap; use std::collections::HashMap;
@ -104,6 +104,7 @@ impl HelmChartInterpret {
fn run_helm_command(args: &[&str]) -> Result<Output, InterpretError> { fn run_helm_command(args: &[&str]) -> Result<Output, InterpretError> {
let command_str = format!("helm {}", args.join(" ")); let command_str = format!("helm {}", args.join(" "));
debug!("Got KUBECONFIG: `{}`", std::env::var("KUBECONFIG").unwrap());
debug!("Running Helm command: `{}`", command_str); debug!("Running Helm command: `{}`", command_str);
let output = Command::new("helm") let output = Command::new("helm")
@ -159,7 +160,13 @@ impl<T: Topology + HelmCommand> Interpret<T> for HelmChartInterpret {
self.add_repo()?; self.add_repo()?;
let helm_executor = DefaultHelmExecutor::new(); let helm_executor = DefaultHelmExecutor::new_with_opts(
&NonBlankString::from_str("helm").unwrap(),
None,
900,
false,
false,
);
let mut helm_options = Vec::new(); let mut helm_options = Vec::new();
if self.score.create_namespace { if self.score.create_namespace {

View File

@ -42,7 +42,7 @@ impl<T: Topology + K8sclient> Score<T> for K8sDeploymentScore {
{ {
"image": self.image, "image": self.image,
"name": self.name, "name": self.name,
"imagePullPolicy": "IfNotPresent", "imagePullPolicy": "Always",
"env": self.env_vars, "env": self.env_vars,
} }
] ]

View File

@ -38,6 +38,7 @@ pub struct LAMPConfig {
pub project_root: PathBuf, pub project_root: PathBuf,
pub ssl_enabled: bool, pub ssl_enabled: bool,
pub database_size: Option<String>, pub database_size: Option<String>,
pub namespace: String,
} }
impl Default for LAMPConfig { impl Default for LAMPConfig {
@ -46,6 +47,7 @@ impl Default for LAMPConfig {
project_root: Path::new("./src").to_path_buf(), project_root: Path::new("./src").to_path_buf(),
ssl_enabled: true, ssl_enabled: true,
database_size: None, database_size: None,
namespace: "harmony-lamp".to_string(),
} }
} }
} }
@ -54,7 +56,6 @@ impl<T: Topology + K8sclient + HelmCommand> Score<T> for LAMPScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(LAMPInterpret { Box::new(LAMPInterpret {
score: self.clone(), score: self.clone(),
namespace: "harmony-lamp".to_string(),
}) })
} }
@ -66,7 +67,6 @@ impl<T: Topology + K8sclient + HelmCommand> Score<T> for LAMPScore {
#[derive(Debug)] #[derive(Debug)]
pub struct LAMPInterpret { pub struct LAMPInterpret {
score: LAMPScore, score: LAMPScore,
namespace: String,
} }
#[async_trait] #[async_trait]
@ -132,7 +132,9 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for LAMPInterpret {
info!("LAMP deployment_score {deployment_score:?}"); info!("LAMP deployment_score {deployment_score:?}");
Ok(Outcome::success("Successfully deployed LAMP Stack!".to_string())) Ok(Outcome::success(
"Successfully deployed LAMP Stack!".to_string(),
))
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {
@ -164,6 +166,10 @@ impl LAMPInterpret {
NonBlankString::from_str("primary.persistence.size").unwrap(), NonBlankString::from_str("primary.persistence.size").unwrap(),
database_size, database_size,
); );
values_overrides.insert(
NonBlankString::from_str("auth.rootPassword").unwrap(),
"mariadb-changethis".to_string(),
);
} }
let score = HelmChartScore { let score = HelmChartScore {
namespace: self.get_namespace(), namespace: self.get_namespace(),
@ -176,7 +182,7 @@ impl LAMPInterpret {
chart_version: None, chart_version: None,
values_overrides: Some(values_overrides), values_overrides: Some(values_overrides),
create_namespace: true, create_namespace: true,
install_only: true, install_only: false,
values_yaml: None, values_yaml: None,
repository: None, repository: None,
}; };
@ -231,6 +237,9 @@ impl LAMPInterpret {
opcache", opcache",
)); ));
dockerfile.push(RUN::from(r#"sed -i 's/VirtualHost \*:80/VirtualHost *:8080/' /etc/apache2/sites-available/000-default.conf && \
sed -i 's/^Listen 80$/Listen 8080/' /etc/apache2/ports.conf"#));
// Copy PHP configuration // Copy PHP configuration
dockerfile.push(RUN::from("mkdir -p /usr/local/etc/php/conf.d/")); dockerfile.push(RUN::from("mkdir -p /usr/local/etc/php/conf.d/"));
@ -296,7 +305,7 @@ opcache.fast_shutdown=1
dockerfile.push(RUN::from("chown -R appuser:appuser /var/www/html")); dockerfile.push(RUN::from("chown -R appuser:appuser /var/www/html"));
// Expose Apache port // Expose Apache port
dockerfile.push(EXPOSE::from("80/tcp")); dockerfile.push(EXPOSE::from("8080/tcp"));
// Set the default command // Set the default command
dockerfile.push(CMD::from("apache2-foreground")); dockerfile.push(CMD::from("apache2-foreground"));
@ -380,6 +389,6 @@ opcache.fast_shutdown=1
} }
fn get_namespace(&self) -> Option<NonBlankString> { fn get_namespace(&self) -> Option<NonBlankString> {
Some(NonBlankString::from_str(&self.namespace).unwrap()) Some(NonBlankString::from_str(&self.score.config.namespace).unwrap())
} }
} }

View File

@ -9,7 +9,7 @@ pub mod k3d;
pub mod k8s; pub mod k8s;
pub mod lamp; pub mod lamp;
pub mod load_balancer; pub mod load_balancer;
pub mod monitoring;
pub mod okd; pub mod okd;
pub mod opnsense; pub mod opnsense;
pub mod tftp; pub mod tftp;
pub mod monitoring;

View File

@ -1,3 +1,2 @@
pub mod monitoring_alerting;
mod kube_prometheus; mod kube_prometheus;
pub mod monitoring_alerting;

View File

@ -1,35 +1,27 @@
use async_trait::async_trait;
use log::info;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
data::{Id, Version}, interpret::Interpret,
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, modules::helm::chart::HelmChartScore,
inventory::Inventory, score::Score,
maestro::Maestro, topology::{HelmCommand, Topology},
score::{CloneBoxScore, Score},
topology::{HelmCommand, Topology, monitoring_alerting::MonitoringAlertingTopology},
}; };
use super::kube_prometheus::kube_prometheus_score; use super::kube_prometheus::kube_prometheus_score;
#[derive(Debug)] #[derive(Debug, Clone, Serialize)]
pub struct MonitoringAlertingStackScore { pub struct MonitoringAlertingStackScore {
//TODO add documenation to explain why its here // TODO Support other components in our monitoring/alerting stack instead of a single helm
//keeps it open for the end user to specify which stack they want // chart
//if it isnt default kube-prometheus pub monitoring_stack: HelmChartScore,
pub monitoring_stack: Vec<Box<dyn Score<MonitoringAlertingTopology>>>,
pub namespace: String, pub namespace: String,
} }
impl MonitoringAlertingStackScore { impl MonitoringAlertingStackScore {
pub fn new( pub fn new_with_ns(ns: &str) -> Self {
monitoring_stack: Vec<Box<dyn Score<MonitoringAlertingTopology>>>,
namespace: String,
) -> Self {
Self { Self {
monitoring_stack, monitoring_stack: kube_prometheus_score(ns),
namespace, namespace: ns.to_string(),
} }
} }
} }
@ -38,107 +30,18 @@ impl Default for MonitoringAlertingStackScore {
fn default() -> Self { fn default() -> Self {
let ns = "monitoring"; let ns = "monitoring";
Self { Self {
monitoring_stack: vec![Box::new(kube_prometheus_score(ns))], monitoring_stack: kube_prometheus_score(ns),
namespace: ns.to_string(), namespace: ns.to_string(),
} }
} }
} }
impl Clone for MonitoringAlertingStackScore {
fn clone(&self) -> Self {
Self {
monitoring_stack: self
.monitoring_stack
.iter()
.map(|s| s.clone_box())
.collect(),
namespace: self.namespace.clone(),
}
}
}
impl Serialize for MonitoringAlertingStackScore { impl<T: Topology + HelmCommand> Score<T> for MonitoringAlertingStackScore {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut s = serializer.serialize_struct("MonitoringAlertingStackScore", 1)?;
let monitoring_values: Vec<_> = self
.monitoring_stack
.iter()
.map(|m| m.serialize())
.collect();
s.serialize_field("monitoring", &monitoring_values)?;
s.end()
}
}
impl<T:Topology> Score<T> for MonitoringAlertingStackScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(MonitoringAlertingStackInterpret { self.monitoring_stack.create_interpret()
score: MonitoringAlertingStackScore {
monitoring_stack: self
.monitoring_stack
.iter()
.map(|s| s.clone_box())
.collect(),
namespace: self.namespace.clone(),
},
})
} }
fn name(&self) -> String { fn name(&self) -> String {
format!("MonitoringAlertingStackScore") format!("MonitoringAlertingStackScore")
} }
} }
#[derive(Debug)]
struct MonitoringAlertingStackInterpret {
pub score: MonitoringAlertingStackScore,
}
#[async_trait]
impl <T: Topology> Interpret<T> for MonitoringAlertingStackInterpret {
async fn execute(
&self,
_inventory: &Inventory,
_topology: &T,
) -> Result<Outcome, InterpretError> {
let inventory = Inventory::autoload();
let topology = MonitoringAlertingTopology::new();
let maestro = match Maestro::initialize(inventory, topology).await {
Ok(m) => m,
Err(e) => {
println!("failed to initialize Maestro: {}", e);
std::process::exit(1);
}
};
let scores_vec = self.score.monitoring_stack.clone();
for s in scores_vec{
info!("Running: {}", s.name());
maestro.interpret(s).await?;
}
Ok(Outcome::success(format!(
"monitoring stack installed in {} namespace",
self.score.namespace
)))
}
fn get_name(&self) -> InterpretName {
todo!()
}
fn get_version(&self) -> Version {
todo!()
}
fn get_status(&self) -> InterpretStatus {
todo!()
}
fn get_children(&self) -> Vec<Id> {
todo!()
}
}