Compare commits
4 Commits
feat/harmo
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d6a869639 | |||
| ff1ab1813e | |||
| eb9f0b3356 | |||
| 7cb5237fdd |
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -3010,6 +3010,13 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "harmony_i18n"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "harmony_inventory_agent"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -16,6 +16,7 @@ members = [
|
||||
"harmony_inventory_agent",
|
||||
"harmony_secret_derive",
|
||||
"harmony_secret",
|
||||
"harmony_i18n",
|
||||
"harmony_config_derive",
|
||||
"harmony_config",
|
||||
"brocade",
|
||||
|
||||
@@ -27,6 +27,7 @@ async fn main() {
|
||||
};
|
||||
let application = Arc::new(RustWebapp {
|
||||
name: "example-monitoring".to_string(),
|
||||
version: "0.1.0".to_string(),
|
||||
dns: "example-monitoring.harmony.mcd".to_string(),
|
||||
project_root: PathBuf::from("./examples/rust/webapp"),
|
||||
framework: Some(RustWebFramework::Leptos),
|
||||
|
||||
@@ -16,6 +16,7 @@ use harmony_types::{k8s_name::K8sName, net::Url};
|
||||
async fn main() {
|
||||
let application = Arc::new(RustWebapp {
|
||||
name: "test-rhob-monitoring".to_string(),
|
||||
version: "0.1.0".to_string(),
|
||||
dns: "test-rhob-monitoring.harmony.mcd".to_string(),
|
||||
project_root: PathBuf::from("./webapp"), // Relative from 'harmony-path' param
|
||||
framework: Some(RustWebFramework::Leptos),
|
||||
|
||||
@@ -20,6 +20,7 @@ use harmony_types::k8s_name::K8sName;
|
||||
async fn main() {
|
||||
let application = Arc::new(RustWebapp {
|
||||
name: "harmony-example-rust-webapp".to_string(),
|
||||
version: "0.1.0".to_string(),
|
||||
dns: "harmony-example-rust-webapp.harmony.mcd".to_string(),
|
||||
project_root: PathBuf::from("./webapp"),
|
||||
framework: Some(RustWebFramework::Leptos),
|
||||
|
||||
@@ -17,6 +17,7 @@ use std::{path::PathBuf, sync::Arc};
|
||||
async fn main() {
|
||||
let application = Arc::new(RustWebapp {
|
||||
name: "harmony-example-tryrust".to_string(),
|
||||
version: "0.1.0".to_string(),
|
||||
dns: "tryrust.example.harmony.mcd".to_string(),
|
||||
project_root: PathBuf::from("./tryrust.org"), // <== Project root, in this case it is a
|
||||
// submodule
|
||||
|
||||
@@ -32,6 +32,7 @@ pub enum InterpretName {
|
||||
K8sPrometheusCrdAlerting,
|
||||
CephRemoveOsd,
|
||||
DiscoverInventoryAgent,
|
||||
DeployInventoryAgent,
|
||||
CephClusterHealth,
|
||||
Custom(&'static str),
|
||||
RHOBAlerting,
|
||||
@@ -64,6 +65,7 @@ impl std::fmt::Display for InterpretName {
|
||||
InterpretName::K8sPrometheusCrdAlerting => f.write_str("K8sPrometheusCrdAlerting"),
|
||||
InterpretName::CephRemoveOsd => f.write_str("CephRemoveOsd"),
|
||||
InterpretName::DiscoverInventoryAgent => f.write_str("DiscoverInventoryAgent"),
|
||||
InterpretName::DeployInventoryAgent => f.write_str("DeployInventoryAgent"),
|
||||
InterpretName::CephClusterHealth => f.write_str("CephClusterHealth"),
|
||||
InterpretName::Custom(name) => f.write_str(name),
|
||||
InterpretName::RHOBAlerting => f.write_str("RHOBAlerting"),
|
||||
|
||||
@@ -57,6 +57,7 @@ pub enum RustWebFramework {
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct RustWebapp {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
/// The path to the root of the Rust project to be containerized.
|
||||
pub project_root: PathBuf,
|
||||
pub service_port: u32,
|
||||
@@ -465,6 +466,7 @@ impl RustWebapp {
|
||||
|
||||
let app_name = &self.name;
|
||||
let service_port = self.service_port;
|
||||
let chart_version = &self.version;
|
||||
// Create Chart.yaml
|
||||
let chart_yaml = format!(
|
||||
r#"
|
||||
@@ -472,7 +474,7 @@ apiVersion: v2
|
||||
name: {chart_name}
|
||||
description: A Helm chart for the {app_name} web application.
|
||||
type: application
|
||||
version: 0.2.1
|
||||
version: {chart_version}
|
||||
appVersion: "{image_tag}"
|
||||
"#,
|
||||
);
|
||||
|
||||
@@ -4,12 +4,24 @@ use std::net::Ipv4Addr;
|
||||
|
||||
use cidr::{Ipv4Cidr, Ipv4Inet};
|
||||
pub use discovery::*;
|
||||
use tokio::time::{Duration, timeout};
|
||||
use k8s_openapi::api::{
|
||||
apps::v1::{DaemonSet, DaemonSetSpec},
|
||||
core::v1::{
|
||||
Container, EnvVar, Namespace, PodSpec, PodTemplateSpec, ResourceRequirements, SecurityContext,
|
||||
ServiceAccount, Toleration,
|
||||
},
|
||||
rbac::v1::{PolicyRule, Role, RoleBinding, RoleRef, Subject},
|
||||
};
|
||||
use k8s_openapi::apimachinery::pkg::api::resource::Quantity;
|
||||
use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
|
||||
use kube::api::ObjectMeta;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use harmony_inventory_agent::local_presence::DiscoveryEvent;
|
||||
use log::{debug, info, trace};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::{
|
||||
data::Version,
|
||||
@@ -17,8 +29,9 @@ use crate::{
|
||||
infra::inventory::InventoryRepositoryFactory,
|
||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||
inventory::Inventory,
|
||||
modules::k8s::resource::K8sResourceScore,
|
||||
score::Score,
|
||||
topology::Topology,
|
||||
topology::{K8sclient, Topology},
|
||||
};
|
||||
use harmony_types::id::Id;
|
||||
|
||||
@@ -290,3 +303,208 @@ impl DiscoverInventoryAgentInterpret {
|
||||
info!("CIDR discovery completed");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeployInventoryAgentScore {
|
||||
pub image: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for DeployInventoryAgentScore {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
image: Some("hub.nationtech.io/harmony/harmony_inventory_agent:latest".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Topology + K8sclient> Score<T> for DeployInventoryAgentScore {
|
||||
fn name(&self) -> String {
|
||||
"DeployInventoryAgentScore".to_string()
|
||||
}
|
||||
|
||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||
Box::new(DeployInventoryAgentInterpret {
|
||||
score: self.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DeployInventoryAgentInterpret {
|
||||
score: DeployInventoryAgentScore,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Topology + K8sclient> Interpret<T> for DeployInventoryAgentInterpret {
|
||||
async fn execute(
|
||||
&self,
|
||||
_inventory: &Inventory,
|
||||
topology: &T,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
let namespace_name = "harmony-inventory-agent".to_string();
|
||||
let image = self.score.image.as_ref().unwrap();
|
||||
|
||||
let mut ns_labels = BTreeMap::new();
|
||||
ns_labels.insert("pod-security.kubernetes.io/enforce".to_string(), "privileged".to_string());
|
||||
ns_labels.insert("pod-security.kubernetes.io/audit".to_string(), "privileged".to_string());
|
||||
ns_labels.insert("pod-security.kubernetes.io/warn".to_string(), "privileged".to_string());
|
||||
|
||||
let namespace = Namespace {
|
||||
metadata: ObjectMeta {
|
||||
name: Some(namespace_name.clone()),
|
||||
labels: Some(ns_labels),
|
||||
..ObjectMeta::default()
|
||||
},
|
||||
..Namespace::default()
|
||||
};
|
||||
|
||||
let service_account_name = "harmony-inventory-agent".to_string();
|
||||
let service_account = ServiceAccount {
|
||||
metadata: ObjectMeta {
|
||||
name: Some(service_account_name.clone()),
|
||||
namespace: Some(namespace_name.clone()),
|
||||
..ObjectMeta::default()
|
||||
},
|
||||
..ServiceAccount::default()
|
||||
};
|
||||
|
||||
let role = Role {
|
||||
metadata: ObjectMeta {
|
||||
name: Some("use-privileged-scc".to_string()),
|
||||
namespace: Some(namespace_name.clone()),
|
||||
..ObjectMeta::default()
|
||||
},
|
||||
rules: Some(vec![PolicyRule {
|
||||
api_groups: Some(vec!["security.openshift.io".to_string()]),
|
||||
resources: Some(vec!["securitycontextconstraints".to_string()]),
|
||||
resource_names: Some(vec!["privileged".to_string()]),
|
||||
verbs: vec!["use".to_string()],
|
||||
..PolicyRule::default()
|
||||
}]),
|
||||
..Role::default()
|
||||
};
|
||||
|
||||
let role_binding = RoleBinding {
|
||||
metadata: ObjectMeta {
|
||||
name: Some("use-privileged-scc".to_string()),
|
||||
namespace: Some(namespace_name.clone()),
|
||||
..ObjectMeta::default()
|
||||
},
|
||||
subjects: Some(vec![Subject {
|
||||
kind: "ServiceAccount".to_string(),
|
||||
name: service_account_name.clone(),
|
||||
namespace: Some(namespace_name.clone()),
|
||||
..Subject::default()
|
||||
}]),
|
||||
role_ref: RoleRef {
|
||||
api_group: "rbac.authorization.k8s.io".to_string(),
|
||||
kind: "Role".to_string(),
|
||||
name: "use-privileged-scc".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let mut daemonset_labels = BTreeMap::new();
|
||||
daemonset_labels.insert("app".to_string(), "harmony-inventory-agent".to_string());
|
||||
|
||||
let daemon_set = DaemonSet {
|
||||
metadata: ObjectMeta {
|
||||
name: Some("harmony-inventory-agent".to_string()),
|
||||
namespace: Some(namespace_name.clone()),
|
||||
labels: Some(daemonset_labels.clone()),
|
||||
..ObjectMeta::default()
|
||||
},
|
||||
spec: Some(DaemonSetSpec {
|
||||
selector: LabelSelector {
|
||||
match_labels: Some(daemonset_labels.clone()),
|
||||
..LabelSelector::default()
|
||||
},
|
||||
template: PodTemplateSpec {
|
||||
metadata: Some(ObjectMeta {
|
||||
labels: Some(daemonset_labels),
|
||||
..ObjectMeta::default()
|
||||
}),
|
||||
spec: Some(PodSpec {
|
||||
service_account_name: Some(service_account_name.clone()),
|
||||
host_network: Some(true),
|
||||
dns_policy: Some("ClusterFirstWithHostNet".to_string()),
|
||||
tolerations: Some(vec![Toleration {
|
||||
key: Some("node-role.kubernetes.io/master".to_string()),
|
||||
operator: Some("Exists".to_string()),
|
||||
effect: Some("NoSchedule".to_string()),
|
||||
..Toleration::default()
|
||||
}]),
|
||||
containers: vec![Container {
|
||||
name: "inventory-agent".to_string(),
|
||||
image: Some(image.to_string()),
|
||||
image_pull_policy: Some("Always".to_string()),
|
||||
env: Some(vec![EnvVar {
|
||||
name: "RUST_LOG".to_string(),
|
||||
value: Some("harmony_inventory_agent=trace,info".to_string()),
|
||||
..EnvVar::default()
|
||||
}]),
|
||||
resources: Some(ResourceRequirements {
|
||||
limits: Some({
|
||||
let mut limits = BTreeMap::new();
|
||||
limits.insert("cpu".to_string(), Quantity("200m".to_string()));
|
||||
limits.insert("memory".to_string(), Quantity("256Mi".to_string()));
|
||||
limits
|
||||
}),
|
||||
requests: Some({
|
||||
let mut requests = BTreeMap::new();
|
||||
requests.insert("cpu".to_string(), Quantity("100m".to_string()));
|
||||
requests.insert("memory".to_string(), Quantity("128Mi".to_string()));
|
||||
requests
|
||||
}),
|
||||
..ResourceRequirements::default()
|
||||
}),
|
||||
security_context: Some(SecurityContext {
|
||||
privileged: Some(true),
|
||||
..SecurityContext::default()
|
||||
}),
|
||||
..Container::default()
|
||||
}],
|
||||
..PodSpec::default()
|
||||
}),
|
||||
},
|
||||
..DaemonSetSpec::default()
|
||||
}),
|
||||
..DaemonSet::default()
|
||||
};
|
||||
|
||||
K8sResourceScore::single(namespace, None)
|
||||
.interpret(_inventory, topology)
|
||||
.await?;
|
||||
K8sResourceScore::single(service_account, Some(namespace_name.clone()))
|
||||
.interpret(_inventory, topology)
|
||||
.await?;
|
||||
K8sResourceScore::single(role, Some(namespace_name.clone()))
|
||||
.interpret(_inventory, topology)
|
||||
.await?;
|
||||
K8sResourceScore::single(role_binding, Some(namespace_name.clone()))
|
||||
.interpret(_inventory, topology)
|
||||
.await?;
|
||||
K8sResourceScore::single(daemon_set, Some(namespace_name.clone()))
|
||||
.interpret(_inventory, topology)
|
||||
.await?;
|
||||
|
||||
Ok(Outcome::success(
|
||||
"Harmony inventory agent successfully deployed".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn get_name(&self) -> InterpretName {
|
||||
InterpretName::DeployInventoryAgent
|
||||
}
|
||||
|
||||
fn get_version(&self) -> Version {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_status(&self) -> InterpretStatus {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_children(&self) -> Vec<Id> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
12
harmony_i18n/Cargo.toml
Normal file
12
harmony_i18n/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "harmony_i18n"
|
||||
edition = "2024"
|
||||
version.workspace = true
|
||||
readme.workspace = true
|
||||
license.workspace = true
|
||||
description = "Minimal compile-time i18n with user-defined languages"
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
186
harmony_i18n/src/lib.rs
Normal file
186
harmony_i18n/src/lib.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub trait Language: Clone + Copy + PartialEq + Eq + Send + Sync + 'static {
|
||||
fn code(&self) -> &'static str;
|
||||
|
||||
fn all() -> &'static [Self]
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
pub trait Translations<L: Language>:
|
||||
Sized + Clone + Copy + PartialEq + Send + Sync + 'static
|
||||
{
|
||||
fn for_lang(lang: L) -> Self;
|
||||
|
||||
fn for_code(code: &str) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
for lang in L::all() {
|
||||
if lang.code() == code {
|
||||
return Some(Self::for_lang(*lang));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TranslationsRef<T, L: Language> {
|
||||
translations: T,
|
||||
_lang: PhantomData<L>,
|
||||
}
|
||||
|
||||
impl<T, L: Language> TranslationsRef<T, L> {
|
||||
pub fn new(translations: T) -> Self {
|
||||
Self {
|
||||
translations,
|
||||
_lang: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self) -> &T {
|
||||
&self.translations
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! translations {
|
||||
(
|
||||
$(#[$struct_attr:meta])*
|
||||
$vis:vis struct $name:ident<$lang_type:ty> {
|
||||
$($field:ident: $ty:ty,)*
|
||||
}
|
||||
$($lang_variant:ident: { $($tfield:ident: $translation:expr,)* },)+
|
||||
) => {
|
||||
$(#[$struct_attr])*
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
$vis struct $name {
|
||||
$(pub $field: $ty,)*
|
||||
}
|
||||
|
||||
impl $crate::Translations<$lang_type> for $name {
|
||||
fn for_lang(lang: $lang_type) -> Self {
|
||||
match lang {
|
||||
$(
|
||||
<$lang_type>::$lang_variant => Self {
|
||||
$($tfield: $translation,)*
|
||||
},
|
||||
)+
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! define_language {
|
||||
(
|
||||
$(#[$enum_attr:meta])*
|
||||
$vis:vis enum $name:ident {
|
||||
$(
|
||||
$(#[$variant_attr:meta])*
|
||||
$variant:ident = $code:expr,
|
||||
)+
|
||||
}
|
||||
) => {
|
||||
$(#[$enum_attr])*
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
|
||||
$vis enum $name {
|
||||
$(
|
||||
$(#[$variant_attr])*
|
||||
$variant,
|
||||
)+
|
||||
}
|
||||
|
||||
impl $crate::Language for $name {
|
||||
fn code(&self) -> &'static str {
|
||||
match self {
|
||||
$(
|
||||
Self::$variant => $code,
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
fn all() -> &'static [Self] {
|
||||
&[
|
||||
$(
|
||||
Self::$variant,
|
||||
)+
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl $name {
|
||||
pub fn from_code(code: &str) -> Option<Self> {
|
||||
Self::all().iter().find(|l| l.code() == code).copied()
|
||||
}
|
||||
|
||||
pub fn toggle(&self) -> Self {
|
||||
let all = Self::all();
|
||||
let idx = all.iter().position(|l| l == self).unwrap_or(0);
|
||||
all[(idx + 1) % all.len()]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
define_language! {
|
||||
#[derive(Default)]
|
||||
pub enum Lang {
|
||||
#[default]
|
||||
En = "en",
|
||||
Fr = "fr",
|
||||
}
|
||||
}
|
||||
|
||||
translations! {
|
||||
pub struct TestTexts<Lang> {
|
||||
greeting: &'static str,
|
||||
farewell: &'static str,
|
||||
}
|
||||
En: {
|
||||
greeting: "Hello",
|
||||
farewell: "Goodbye",
|
||||
},
|
||||
Fr: {
|
||||
greeting: "Bonjour",
|
||||
farewell: "Au revoir",
|
||||
},
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_language_enum() {
|
||||
assert_eq!(Lang::En.code(), "en");
|
||||
assert_eq!(Lang::Fr.code(), "fr");
|
||||
assert!(Lang::from_code("en").is_some());
|
||||
assert!(Lang::from_code("de").is_none());
|
||||
assert_eq!(Lang::En.toggle(), Lang::Fr);
|
||||
assert_eq!(Lang::Fr.toggle(), Lang::En);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_translations_for_lang() {
|
||||
let en = TestTexts::for_lang(Lang::En);
|
||||
assert_eq!(en.greeting, "Hello");
|
||||
assert_eq!(en.farewell, "Goodbye");
|
||||
|
||||
let fr = TestTexts::for_lang(Lang::Fr);
|
||||
assert_eq!(fr.greeting, "Bonjour");
|
||||
assert_eq!(fr.farewell, "Au revoir");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_for_code() {
|
||||
let texts = TestTexts::for_code("fr");
|
||||
assert!(texts.is_some());
|
||||
assert_eq!(texts.unwrap().greeting, "Bonjour");
|
||||
|
||||
let none = TestTexts::for_code("de");
|
||||
assert!(none.is_none());
|
||||
}
|
||||
}
|
||||
@@ -183,7 +183,7 @@ impl OpenbaoSecretStore {
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
fs::write(path, token)?;
|
||||
fs::write(path, serde_json::to_string(token)?.as_bytes())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user