Files
harmony/harmony-k8s/src/bundle.rs
Jean-Gabriel Gill-Couture 50f62b6437 chore: warning sweep — auto-fix pass + scoped allows for generated code
Workspace warning count: 408 → 105.

Three buckets cleared:

* Auto-fixable (`cargo fix` + `cargo clippy --fix`): unused imports
  removed, unused variables prefixed with `_`, deprecated method
  calls updated. Applied across harmony, harmony-k8s, harmony-agent,
  harmony_inventory_agent, the fleet/ workspace, and ~15 examples.
* Generated code (opnsense-api/src/generated/): 269 snake_case
  warnings + ~10 unreachable-pattern warnings come from
  CamelCase-preserving bindings to OPNsense's HAProxy/Caddy XML
  schemas. Scoped a single `#[allow(non_snake_case,
  unreachable_patterns)]` at `pub mod generated;` rather than
  fighting the codegen — renaming would break serde round-trips
  and the codegen would regenerate them anyway.
* opnsense-codegen parser's defensive `let...else` guards on
  `XmlNode` (currently single-variant): file-level
  `#![allow(irrefutable_let_patterns)]` with a comment explaining
  why we keep the `else` arms (they re-arm if the IR grows a
  second variant).

`harmony_inventory_agent::local_presence::{DiscoveryEvent,
discover_agents}` re-exports were stripped twice by the auto-fix
passes (consumers live in another crate, so the local crate looks
"unused" to lint). Anchored with explicit `pub use` + an
`#[allow(unused_imports)]` annotation noting why.

All 151 harmony lib tests still pass. Remaining ~105 warnings are
mostly real dead code in non-fleet modules + a handful of
unused-imports/variables clippy couldn't auto-resolve; cleared in
the next pass.
2026-05-06 22:51:44 -04:00

140 lines
4.9 KiB
Rust

//! Resource Bundle Pattern Implementation
//!
//! This module implements the Resource Bundle pattern for managing groups of
//! Kubernetes resources that form a logical unit of work.
//!
//! ## Purpose
//!
//! The ResourceBundle pattern addresses the need to manage ephemeral privileged
//! pods along with their platform-specific security requirements (e.g., OpenShift
//! Security Context Constraints).
//!
//! ## Use Cases
//!
//! - Writing files to node filesystems (e.g., NetworkManager configurations for
//! network bonding as described in ADR-019)
//! - Running privileged commands on nodes (e.g., reboots, system configuration)
//!
//! ## Benefits
//!
//! - **Separation of Concerns**: Client code doesn't need to know about
//! platform-specific RBAC requirements
//! - **Atomic Operations**: Resources are applied and deleted as a unit
//! - **Clean Abstractions**: Privileged operations are encapsulated in bundles
//! rather than scattered throughout client methods
//!
//! ## Example
//!
//! ```
//! use harmony_k8s::{K8sClient, helper};
//! use harmony_k8s::KubernetesDistribution;
//!
//! async fn write_network_config(client: &K8sClient, node: &str) {
//! // Create a bundle with platform-specific RBAC
//! let bundle = helper::build_privileged_bundle(
//! helper::PrivilegedPodConfig {
//! name: "network-config".to_string(),
//! namespace: "default".to_string(),
//! node_name: node.to_string(),
//! // ... other config
//! ..Default::default()
//! },
//! &KubernetesDistribution::OpenshiftFamily,
//! );
//!
//! // Apply all resources (RBAC + Pod) atomically
//! bundle.apply(client).await.unwrap();
//!
//! // ... wait for completion ...
//!
//! // Cleanup all resources
//! bundle.delete(client).await.unwrap();
//! }
//! ```
use kube::{Error, Resource, ResourceExt, api::DynamicObject};
use serde::Serialize;
use serde_json;
use crate::K8sClient;
/// A ResourceBundle represents a logical unit of work consisting of multiple
/// Kubernetes resources that should be applied or deleted together.
///
/// This pattern is useful for managing ephemeral privileged pods along with
/// their required RBAC bindings (e.g., OpenShift SCC bindings).
#[derive(Debug)]
pub struct ResourceBundle {
pub resources: Vec<DynamicObject>,
}
impl Default for ResourceBundle {
fn default() -> Self {
Self::new()
}
}
impl ResourceBundle {
pub fn new() -> Self {
Self {
resources: Vec::new(),
}
}
/// Add a Kubernetes resource to this bundle.
/// The resource is converted to a DynamicObject for generic handling.
pub fn add<K>(&mut self, resource: K)
where
K: Resource + Serialize,
<K as Resource>::DynamicType: Default,
{
// Convert the typed resource to JSON, then to DynamicObject
let json = serde_json::to_value(&resource).expect("Failed to serialize resource");
let mut obj: DynamicObject =
serde_json::from_value(json).expect("Failed to convert to DynamicObject");
// Ensure type metadata is set
if obj.types.is_none() {
let api_version = Default::default();
let kind = Default::default();
let gvk = K::api_version(&api_version);
let kind = K::kind(&kind);
obj.types = Some(kube::api::TypeMeta {
api_version: gvk.to_string(),
kind: kind.to_string(),
});
}
self.resources.push(obj);
}
/// Apply all resources in this bundle to the cluster.
/// Resources are applied in the order they were added.
pub async fn apply(&self, client: &K8sClient) -> Result<(), Error> {
for res in &self.resources {
let namespace = res.namespace();
client
.apply_dynamic(res, namespace.as_deref(), true)
.await?;
}
Ok(())
}
/// 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;
}
Ok(())
}
}