Compare commits

..

1 Commits

Author SHA1 Message Date
82d1f87ff8 fix: stop swallowing non-404 errors in ResourceBundle::delete
Previously all errors were silently discarded when deleting bundle
resources. Now only 404 (Not Found) is ignored; other errors propagate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 00:09:28 -04:00
13 changed files with 9 additions and 444 deletions

7
Cargo.lock generated
View File

@@ -3010,13 +3010,6 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "harmony_i18n"
version = "0.1.0"
dependencies = [
"serde",
]
[[package]]
name = "harmony_inventory_agent"
version = "0.1.0"

View File

@@ -16,7 +16,6 @@ members = [
"harmony_inventory_agent",
"harmony_secret_derive",
"harmony_secret",
"harmony_i18n",
"harmony_config_derive",
"harmony_config",
"brocade",

View File

@@ -27,7 +27,6 @@ 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),

View File

@@ -16,7 +16,6 @@ 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),

View File

@@ -20,7 +20,6 @@ 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),

View File

@@ -17,7 +17,6 @@ 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

View File

@@ -52,7 +52,7 @@
//! }
//! ```
use kube::{Error, Resource, ResourceExt, api::DynamicObject};
use kube::{Error, Resource, ResourceExt, api::DynamicObject, core::ErrorResponse};
use serde::Serialize;
use serde_json;
@@ -117,16 +117,13 @@ impl ResourceBundle {
/// Delete all resources in this bundle from the cluster.
/// Resources are deleted in reverse order to respect dependencies.
pub async fn delete(&self, client: &K8sClient) -> Result<(), Error> {
// FIXME delete all in parallel and retry using kube::client::retry::RetryPolicy
for res in self.resources.iter().rev() {
let api = client.get_api_for_dynamic_object(res, res.namespace().as_deref())?;
let name = res.name_any();
// FIXME this swallows all errors. Swallowing a 404 is ok but other errors must be
// handled properly (such as retrying). A normal error case is when we delete a
// resource bundle with dependencies between various resources. Such as a pod with a
// dependency on a ClusterRoleBinding. Trying to delete the ClusterRoleBinding first
// is expected to fail
let _ = api.delete(&name, &kube::api::DeleteParams::default()).await;
match api.delete(&name, &kube::api::DeleteParams::default()).await {
Ok(_) | Err(Error::Api(ErrorResponse { code: 404, .. })) => {}
Err(e) => return Err(e),
}
}
Ok(())
}

View File

@@ -32,7 +32,6 @@ pub enum InterpretName {
K8sPrometheusCrdAlerting,
CephRemoveOsd,
DiscoverInventoryAgent,
DeployInventoryAgent,
CephClusterHealth,
Custom(&'static str),
RHOBAlerting,
@@ -65,7 +64,6 @@ 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"),

View File

@@ -57,7 +57,6 @@ 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,
@@ -466,7 +465,6 @@ 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#"
@@ -474,7 +472,7 @@ apiVersion: v2
name: {chart_name}
description: A Helm chart for the {app_name} web application.
type: application
version: {chart_version}
version: 0.2.1
appVersion: "{image_tag}"
"#,
);

View File

@@ -4,24 +4,12 @@ use std::net::Ipv4Addr;
use cidr::{Ipv4Cidr, Ipv4Inet};
pub use discovery::*;
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 tokio::time::{Duration, timeout};
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,
@@ -29,9 +17,8 @@ use crate::{
infra::inventory::InventoryRepositoryFactory,
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory,
modules::k8s::resource::K8sResourceScore,
score::Score,
topology::{K8sclient, Topology},
topology::Topology,
};
use harmony_types::id::Id;
@@ -303,208 +290,3 @@ 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!()
}
}

View File

@@ -1,12 +0,0 @@
[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]

View File

@@ -1,186 +0,0 @@
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());
}
}

View File

@@ -183,7 +183,7 @@ impl OpenbaoSecretStore {
}
#[cfg(not(unix))]
{
fs::write(path, serde_json::to_string(token)?.as_bytes())?;
fs::write(path, token)?;
}
Ok(())
}