From 989f407502704cefb6fe6e36447392a0d0eb0e77 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Sun, 23 Feb 2025 13:33:47 -0500 Subject: [PATCH 01/62] docs(adr): add ADR for using iPXE with chaining for architecture independence Adopt iPXE as the primary bootloader with chaining to support BIOS and UEFI architectures, enabling dynamic boot configurations, advanced network booting, and diskless machine management. This introduces a dependency on iPXE but offers significant benefits in flexibility and configuration simplicity. --- adr/004-ipxe.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 adr/004-ipxe.md diff --git a/adr/004-ipxe.md b/adr/004-ipxe.md new file mode 100644 index 0000000..1f76de6 --- /dev/null +++ b/adr/004-ipxe.md @@ -0,0 +1,19 @@ +# ADR: Use iPXE as the Primary Bootloader with Chaining for Architecture Independence + +**Status:** Implemented + +**Context:** +Harmony requires a flexible and unified bootloader solution to handle both BIOS and UEFI architectures. We need support for dynamic boot configurations, advanced network booting capabilities, and the ability to manage diskless machines. + +**Decision:** +Adopt iPXE as the primary bootloader. For BIOS and UEFI clients, use chaining to load iPXE, ensuring all clients boot into a common iPXE environment. + +**Consequences:** +- **Benefits:** + - Single configuration file for all architectures. + - Enables dynamic and scripted boot processes. + - Supports booting over various protocols (HTTP, HTTPS, iSCSI, SAN, etc.). + - Allows diskless machines with networked root filesystems. +- **Trade-offs:** + - Adds a dependency on iPXE. + - Requires proper configuration and maintenance of iPXE. From 7f56d4d65491e351e6fcb915e98ef54fb5b80360 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 6 Mar 2025 12:40:44 -0500 Subject: [PATCH 02/62] feat(adr/006-secret-management): propose using Keycloak for secret management Introduce Architecture Decision Record (ADR) outlining the use of Keycloak as a secret management solution. The document details the context, considerations, decision workflow, rationale for choosing Keycloak over alternatives, and potential consequences including benefits and challenges. --- adr/006-secret-management.md | 87 ++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 adr/006-secret-management.md diff --git a/adr/006-secret-management.md b/adr/006-secret-management.md new file mode 100644 index 0000000..35423ec --- /dev/null +++ b/adr/006-secret-management.md @@ -0,0 +1,87 @@ +# Architecture Decision Record: Keycloak for Secret Management + +## Status + +Proposed + +### TODO : +Before accepting this proposal we need to run a POC to validate this potential issue : + +**Keycloak Misuse**: Using Keycloak primarily as a secrets manager is inappropriate, as it's designed for identity and access management (IAM), not secrets management. This creates scalability and functionality limitations. + +## Context + +Our infrastructure orchestration requires a robust secret management system as part of our automation project to support secure transitions from development to production environments. Key considerations include: + +1. **User lifecycle management**: In enterprise settings, developers and administrators frequently join and leave projects, creating security risks if their access to secrets isn't properly revoked. + +2. **Security limitations with file-based solutions**: Traditional encrypted file-based approaches (like SOPS) present challenges with user revocation, as departed users can retain local copies of encrypted files and continue accessing secrets indefinitely. + +3. **Authentication integration**: Our solution needs to integrate with existing identity providers to leverage centralized authentication systems already in place. + +4. **Diverse user base**: Our solution must work well for both enterprise teams and small organizations/individual developers without imposing excessive complexity. + +5. **Operational simplicity**: The solution should minimize the cognitive overhead required to manage secrets securely. + +## Decision + +We will implement Keycloak as our secret management solution with the following workflow: + +1. Projects will contain only configuration metadata indicating where secrets are stored and which identity provider to use. + +2. When a developer runs the project locally, they will be automatically prompted to authenticate via SSO (browser-based or TOTP). + +3. Upon successful authentication, the application will use the developer's credentials to retrieve appropriate secrets from the centralized Keycloak server. + +4. When a developer leaves the organization, their SSO access is revoked, automatically removing their ability to access secrets. + +For smaller organizations and individual developers, we will provide a fully managed Keycloak instance with: +- A free tier supporting a limited number of secrets or API calls +- Simplified setup that hides complexity through our automation tooling +- Potential for paid tiers as usage scales + +### Why keycloack + +We wanted a solution that met these criterias + +- Fully open source +- Mature +- Integrates with various SSO providers +- Supports secrets +- As easy as possible to deploy on our existing K8s infrastructure +- Supports any kind of secret, not just K8s + +Other considered options : + +- Vault : not fully pen source +- SOPS : no SSO integration, makes user lifecycle harder +- AWS Secrets : vendor lock-in and cost +- Bitwarden : SSO feature not fully open source + +## Consequences + +### Positive + +1. **Improved security posture**: Secret access is tied directly to centralized identity management, reducing the risk of leaked credentials from former team members. + +2. **Simplified developer onboarding**: New team members can immediately access appropriate secrets without manual sharing of encrypted files or keys. + +3. **Transparent authentication**: SSO integration creates a seamless experience that leverages existing organizational authentication systems. + +4. **Centralized audit capability**: All secret access can be logged and monitored in a single location. + +5. **Scalable for different organization sizes**: The managed service option makes enterprise-grade secret management accessible to smaller teams. + +### Challenges + +1. **Network dependency**: Developers require network connectivity to access secrets, which could complicate offline development scenarios. + +2. **Operational overhead**: While hidden from most users, we will need to maintain a reliable managed Keycloak service. + +3. **Migration complexity**: Existing projects using file-based secret solutions will require migration assistance. + +4. **Potential for clipboard leakage**: While more difficult than with file-based solutions, determined users could still manually copy secrets they've legitimately accessed. + +5. **Service availability concerns**: Dependency on the centralized secret service creates a potential single point of failure. + +6. **Implementation complexity**: Integrating with various SSO providers and creating a seamless developer experience will require significant initial engineering effort. From 2950235d23892921c41d780ec9053928db4c7be4 Mon Sep 17 00:00:00 2001 From: johnride Date: Mon, 10 Mar 2025 04:31:06 +0000 Subject: [PATCH 03/62] Actualiser adr/006-secret-management.md --- adr/006-secret-management.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adr/006-secret-management.md b/adr/006-secret-management.md index 35423ec..370d6db 100644 --- a/adr/006-secret-management.md +++ b/adr/006-secret-management.md @@ -4,7 +4,7 @@ Proposed -### TODO : +### TODO [#3](https://git.nationtech.io/NationTech/harmony/issues/3): Before accepting this proposal we need to run a POC to validate this potential issue : **Keycloak Misuse**: Using Keycloak primarily as a secrets manager is inappropriate, as it's designed for identity and access management (IAM), not secrets management. This creates scalability and functionality limitations. From fe42ebd347957018a617fd8a57dbe9fbc0bf8dad Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Mon, 10 Mar 2025 00:35:22 -0400 Subject: [PATCH 04/62] feat(adr): add architecture decision record for interactive project setup Add an Architecture Decision Record (ADR) outlining the approach to integrate LAMP projects into Harmony's automated delivery pipeline using either Score Spec or a custom Rust DSL. A decision will have to be made between the two in the short term to decide which we will implement first. The ADR details the benefits and consequences of each option, focusing on providing a seamless transition for developers while leveraging Harmony's enterprise-grade features. --- adr/005-interactive-project.md | 78 ++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 adr/005-interactive-project.md diff --git a/adr/005-interactive-project.md b/adr/005-interactive-project.md new file mode 100644 index 0000000..9b5abc5 --- /dev/null +++ b/adr/005-interactive-project.md @@ -0,0 +1,78 @@ +# Architecture Decision Record: Interactive project setup for automated delivery pipeline of various codebases + +## Status + +Draft + +## Context + +Many categories of developers, of which we will focus on LAMP (Linux Apache, MySQL, PHP) developers at first, are underserved by modern delivery tools. + +Most of these projects are developed with a small team, small budget, but still are mission critical to their users. + +We believe that Harmony, with its end-to-end infrastructure orchestration approach, enables relatively easy integration for this category of projects in a modern delivery pipeline that is opinionated enough that the development team is not overwhelmed by choices, but also flexible enough to allow them to deploy their application according to their habits. This inclues local development, managed dedicated servers, virtualized environments, manual dashboards like CPanel, cloud providers, etc. + +To enable this, we need to provide an easy way for developers to step on to the harmony pipeline without disrupting their workflow. + +This ADR will outline the approach taken to go from a LAMP project to be standalone, to a LAMP project using harmony that can benefit from all the enterprise grade features of our opinionated delivery pipeline including : + +- Automated environment provisionning (local, staging, uat, prod) +- Infrastructure optimized for the delivery stage + - Production with automated backups +- Automated domain names for early stages, configured domain name for production +- SSL certificates +- Secret management +- SSO integration +- IDP, IDS security +- Monitoring, logging +- Artifact registry +- Automated deployment and rollback +- Dependency management (databases, configuration, scripts) + +## Decision + +# Option 1 : Score spec + +To simplify onboarding of existing projects, we decided to integrate with Score Spec for the following reasons : + +- It is a CNCF project, which helps a lot with adoption and community building +- It already supports important targets for us including docker-compose and k8s +- It provides a way to define the application's infrastructure at the correct level of abstraction for us to deploy it anywhere -- that is the goal of the score-spec project +- Once we evolve, we can simply have a score compatible provider that allows any project with a score spec to be deployed on the harmony stack +- Score was built with enterprise use-cases in mind : Humanitec platform engineering customers + + +## Consequences + +### Positive + +- Score Community is growing, using harmony will be very easy for them + +### Negative + +- Score is not that big yet, and mostly used by Humanitec's clients (I guess), which is a hard to penetrate environment + +# Option 2 : Custom Rust DSL + +We decided to develop a rust based DSL. Even though this means people will be afraid of "Rust", we believe the numerous advantages are worth the risk. + +The main selection criterias are : + +- Robustness : the application/infrastructure definition should not be fragile to typos or versioning. Rusts robust dependency management (cargo) and type safety are best in class for robustness +- Flexibility : Writing the definition in a standard programming language empowers users to easily leverage the internals of harmony to adapt the code to their needs. +- Extensibility : Once again, a standard programming language enables easily importing a configuration, or multiple configurations, create reusable bits, and build upon the different components to really take control over a complex multi-project deployment without going crazy because of a typo in a yaml definition that changed 4 years ago + +## Consequences + +### Positive + +- Complete control over the syntax and semantics of the DSL, tailored specifically to our needs. +- Potential for better performance optimizations as we can implement exactly what is required without additional abstractions. + +### Negative + +- Higher initial development cost due to building a new language from scratch. +- Steeper learning curve for developers who need to use the DSL. +- Lack of an existing community and ecosystem, which could slow down adoption. +- Increased maintenance overhead as the DSL needs to be updated and supported internally. + From fbc18d2faddb045ee2a17533fcda43c4ca8936d5 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Mon, 10 Mar 2025 15:18:40 -0400 Subject: [PATCH 05/62] feat(harmony): add lamp server module and refactor dhcpd tests Implement LAMP server module with basic configuration. Refactor and remove commented out Dhcpd struct and associated tests in opnsense/xml_utils. Ensure codebase adheres to best practices and maintainability standards. --- harmony/src/domain/maestro/mod.rs | 2 +- harmony/src/domain/topology/mod.rs | 2 +- harmony/src/modules/lamp.rs | 70 +++++++++++++++++++++++ harmony/src/modules/mod.rs | 1 + opnsense-config-xml/src/data/dhcpd.rs | 81 --------------------------- 5 files changed, 73 insertions(+), 83 deletions(-) create mode 100644 harmony/src/modules/lamp.rs diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index ed28173..46ec8da 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -3,7 +3,7 @@ use std::sync::{Arc, RwLock}; use log::info; use super::{ - interpret::{Interpret, InterpretError, Outcome}, + interpret::{InterpretError, Outcome}, inventory::Inventory, score::Score, topology::HAClusterTopology, diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index 81dd36e..438a3dd 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -14,7 +14,7 @@ pub use http::*; pub use network::*; pub use tftp::*; -use std::{net::IpAddr, sync::Arc}; +use std::net::IpAddr; pub type IpAddress = IpAddr; diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs new file mode 100644 index 0000000..b0aa94f --- /dev/null +++ b/harmony/src/modules/lamp.rs @@ -0,0 +1,70 @@ +use async_trait::async_trait; + +use crate::{ + data::{Id, Version}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + modules::k8s::deployment::K8sDeploymentScore, + score::Score, + topology::HAClusterTopology, +}; + +#[derive(Debug, Clone)] +pub struct LAMPScore { + pub name: String, +} + +impl Score for LAMPScore { + fn create_interpret(&self) -> Box { + todo!() + } + + fn name(&self) -> String { + "LampScore".to_string() + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +#[derive(Debug)] +pub struct LAMPInterpret { + score: LAMPScore, +} + +#[async_trait] +impl Interpret for LAMPInterpret { + async fn execute( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + ) -> Result { + let deployment_score = K8sDeploymentScore { + name: self.score.name.clone(), + image: "local_image".to_string(), + }; + + deployment_score + .create_interpret() + .execute(inventory, topology) + .await?; + todo!() + } + + fn get_name(&self) -> InterpretName { + todo!() + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + todo!() + } +} diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index c181375..51e164f 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -7,3 +7,4 @@ pub mod load_balancer; pub mod okd; pub mod opnsense; pub mod tftp; +pub mod lamp; diff --git a/opnsense-config-xml/src/data/dhcpd.rs b/opnsense-config-xml/src/data/dhcpd.rs index 6e694b5..5b06610 100644 --- a/opnsense-config-xml/src/data/dhcpd.rs +++ b/opnsense-config-xml/src/data/dhcpd.rs @@ -4,13 +4,6 @@ use yaserde::MaybeString; use super::opnsense::{NumberOption, Range, StaticMap}; -// #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] -// #[yaserde(rename = "dhcpd")] -// pub struct Dhcpd { -// #[yaserde(rename = "lan")] -// pub lan: DhcpInterface, -// } - #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct DhcpInterface { pub enable: Option, @@ -42,77 +35,3 @@ pub struct DhcpRange { #[yaserde(rename = "to")] pub to: String, } - -#[cfg(test)] -mod test { - use crate::xml_utils::to_xml_str; - - use pretty_assertions::assert_eq; - - #[test] - fn dhcpd_should_deserialize_serialize_identical() { - let dhcpd: Dhcpd = - yaserde::de::from_str(SERIALIZED_DHCPD).expect("Deserialize Dhcpd failed"); - - assert_eq!( - to_xml_str(&dhcpd).expect("Serialize Dhcpd failed"), - SERIALIZED_DHCPD - ); - } - - const SERIALIZED_DHCPD: &str = " - - - 1 - 192.168.20.1 - somedomain.yourlocal.mcd - hmac-md5 - - - - - 192.168.20.50 - 192.168.20.200 - - - 192.168.20.1 - - - 55:55:55:55:55:1c - 192.168.20.160 - somehost983 - someservire8 - - - - - - 55:55:55:55:55:1c - 192.168.20.155 - somehost893 - - - - - - 55:55:55:55:55:1c - 192.168.20.165 - somehost893 - - - - - - - 55:55:55:55:55:1c - 192.168.20.50 - hostswitch2 - switch-2 (bottom) - - - - - - -\n"; -} From 7291db7ca30dd82ede15b769e64b9f802934b2ca Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Mon, 10 Mar 2025 17:04:35 -0400 Subject: [PATCH 06/62] feat(example/lamp): add LAMPScore and configuration support - Introduce `LAMPScore` struct with additional fields: `domain`, `config`, and `php_version`. - Define default implementation for `LAMPConfig`. - Update `Url` enum to use `Url(url::Url)` instead of `Remote(url::Url)`. - Adjust references in `HttpServer` and `TftpServer` implementations. - Modify `Interpret` trait implementation to use `name()` method from `LAMPScore`. --- Cargo.lock | 15 +++++++++++++++ examples/lamp/Cargo.toml | 18 ++++++++++++++++++ examples/lamp/php/index.php | 3 +++ examples/lamp/src/main.rs | 21 +++++++++++++++++++++ harmony/src/domain/maestro/mod.rs | 21 +++++++++++++++++++++ harmony/src/domain/topology/mod.rs | 4 ++-- harmony/src/infra/opnsense/http.rs | 2 +- harmony/src/infra/opnsense/tftp.rs | 2 +- harmony/src/modules/lamp.rs | 24 ++++++++++++++++++++++-- 9 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 examples/lamp/Cargo.toml create mode 100644 examples/lamp/php/index.php create mode 100644 examples/lamp/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index fb8f3a4..2cad7e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -815,6 +815,21 @@ dependencies = [ "url", ] +[[package]] +name = "example-lamp" +version = "0.1.0" +dependencies = [ + "cidr", + "env_logger", + "harmony", + "harmony_macros", + "harmony_tui", + "harmony_types", + "log", + "tokio", + "url", +] + [[package]] name = "example-nanodc" version = "0.1.0" diff --git a/examples/lamp/Cargo.toml b/examples/lamp/Cargo.toml new file mode 100644 index 0000000..902548e --- /dev/null +++ b/examples/lamp/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "example-lamp" +edition = "2024" +version.workspace = true +readme.workspace = true +license.workspace = true +publish = false + +[dependencies] +harmony = { path = "../../harmony" } +harmony_tui = { path = "../../harmony_tui" } +harmony_types = { path = "../../harmony_types" } +cidr = { workspace = true } +tokio = { workspace = true } +harmony_macros = { path = "../../harmony_macros" } +log = { workspace = true } +env_logger = { workspace = true } +url = { workspace = true } diff --git a/examples/lamp/php/index.php b/examples/lamp/php/index.php new file mode 100644 index 0000000..6cf1a50 --- /dev/null +++ b/examples/lamp/php/index.php @@ -0,0 +1,3 @@ + diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs new file mode 100644 index 0000000..dad2673 --- /dev/null +++ b/examples/lamp/src/main.rs @@ -0,0 +1,21 @@ +use harmony::{ + data::Version, + maestro::Maestro, + modules::lamp::{LAMPConfig, LAMPScore}, + topology::Url, +}; + +#[tokio::main] +async fn main() { + let lamp_stack = LAMPScore { + name: "harmony-lamp-demo".to_string(), + domain: Url::Url(url::Url::parse("https://lampdemo.harmony.nationtech.io").unwrap()), + php_version: Version::from("8.4.4").unwrap(), + config: LAMPConfig { + project_root: "./php".into(), + ..Default::default() + }, + }; + + Maestro::load_from_env().interpret(Box::new(lamp_stack)).await.unwrap(); +} diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index 46ec8da..9f92ec5 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -26,6 +26,27 @@ impl Maestro { } } + // Load the inventory and inventory from environment. + // This function is able to discover the context that it is running in, such as k8s clusters, aws cloud, linux host, etc. + // When the HARMONY_TOPOLOGY environment variable is not set, it will default to install k3s + // locally (lazily, if not installed yet, when the first execution occurs) and use that as a topology + // So, by default, the inventory is a single host that the binary is running on, and the + // topology is a single node k3s + // + // By default : + // - Linux => k3s + // - macos, windows => docker compose + // + // To run more complex cases like OKDHACluster, either provide the default target in the + // harmony infrastructure as code or as an environment variable + pub fn load_from_env() -> Self { + // Load env var HARMONY_TOPOLOGY + match std::env::var("HARMONY_TOPOLOGY") { + Ok(_) => todo!(), + Err(_) => todo!(), + } + } + pub fn start(&mut self) { info!("Starting Maestro"); } diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index 438a3dd..9d5663c 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -21,14 +21,14 @@ pub type IpAddress = IpAddr; #[derive(Debug, Clone)] pub enum Url { LocalFolder(String), - Remote(url::Url), + Url(url::Url), } impl std::fmt::Display for Url { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Url::LocalFolder(path) => write!(f, "{}", path), - Url::Remote(url) => write!(f, "{}", url), + Url::Url(url) => write!(f, "{}", url), } } } diff --git a/harmony/src/infra/opnsense/http.rs b/harmony/src/infra/opnsense/http.rs index 3c33bba..a06fe5b 100644 --- a/harmony/src/infra/opnsense/http.rs +++ b/harmony/src/infra/opnsense/http.rs @@ -22,7 +22,7 @@ impl HttpServer for OPNSenseFirewall { .await .map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?; } - Url::Remote(_url) => todo!(), + Url::Url(_url) => todo!(), } Ok(()) } diff --git a/harmony/src/infra/opnsense/tftp.rs b/harmony/src/infra/opnsense/tftp.rs index 3f1156e..6978150 100644 --- a/harmony/src/infra/opnsense/tftp.rs +++ b/harmony/src/infra/opnsense/tftp.rs @@ -22,7 +22,7 @@ impl TftpServer for OPNSenseFirewall { .await .map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?; } - Url::Remote(url) => todo!("This url is not supported yet {url}"), + Url::Url(url) => todo!("This url is not supported yet {url}"), } Ok(()) } diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index b0aa94f..e6b7612 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -1,3 +1,5 @@ +use std::path::{Path, PathBuf}; + use async_trait::async_trait; use crate::{ @@ -6,12 +8,30 @@ use crate::{ inventory::Inventory, modules::k8s::deployment::K8sDeploymentScore, score::Score, - topology::HAClusterTopology, + topology::{HAClusterTopology, Url}, }; #[derive(Debug, Clone)] pub struct LAMPScore { pub name: String, + pub domain: Url, + pub config: LAMPConfig, + pub php_version: Version, +} + +#[derive(Debug, Clone)] +pub struct LAMPConfig { + pub project_root: PathBuf, + pub ssl_enabled: bool, +} + +impl Default for LAMPConfig { + fn default() -> Self { + LAMPConfig { + project_root: Path::new("./src").to_path_buf(), + ssl_enabled: true, + } + } } impl Score for LAMPScore { @@ -41,7 +61,7 @@ impl Interpret for LAMPInterpret { topology: &HAClusterTopology, ) -> Result { let deployment_score = K8sDeploymentScore { - name: self.score.name.clone(), + name: self.score.name(), image: "local_image".to_string(), }; From 35fcc295aaf67549f2e431a9d5e96a3ff4b031a1 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Mon, 17 Mar 2025 15:48:14 -0400 Subject: [PATCH 07/62] ADR: Default runtime for harmony workloads This ADR proposes to use k3s as a default runtime on linux and k3d on other platforms supporting docker --- adr/006-secret-management.md | 1 + adr/007-default-runtime.md | 68 ++++++++++++++++++++++++++++++++++++ examples/lamp/src/main.rs | 5 ++- 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 adr/007-default-runtime.md diff --git a/adr/006-secret-management.md b/adr/006-secret-management.md index 35423ec..592c7de 100644 --- a/adr/006-secret-management.md +++ b/adr/006-secret-management.md @@ -5,6 +5,7 @@ Proposed ### TODO : + Before accepting this proposal we need to run a POC to validate this potential issue : **Keycloak Misuse**: Using Keycloak primarily as a secrets manager is inappropriate, as it's designed for identity and access management (IAM), not secrets management. This creates scalability and functionality limitations. diff --git a/adr/007-default-runtime.md b/adr/007-default-runtime.md new file mode 100644 index 0000000..21781ce --- /dev/null +++ b/adr/007-default-runtime.md @@ -0,0 +1,68 @@ +# Architecture Decision Record: Default Runtime for Managed Workloads** + +## Status + +Proposed + +## Context + +Our infrastructure orchestrator manages workloads that require a runtime environment for execution. + +**Requirements** + +- Cross platform +- Supports running complex workloads (similar level of complexity to kubernetes) +- Easy to install and setup +- Low footprint +- Well maintained +- K8s compatibility a big plus + +When a user launches a project orchestrated by harmony it should run. End of story. + +This means that we need to be able to automatically install a runtime appropriate for our core use cases. As our context is mostly enterprise applications deployed on various flavors of kubernetes clusters, it is natural to move towards lightweight k8s distributions such as k3s, microk8s, minikube, etc. + +Now, the main use case where we want it to "just work" is when a developer (or user) clones a repository or downloads an application and wants to launch it locally with zero setup. This means that there is no container runtime installed, no kubernetes cluster, no assumption of libraries or tools installed. + +## Decision + +We selected **k3s as the default runtime** for managed workloads on Linux platforms and **k3d on Windows and MacOS**. Other platforms that do not support either k3s or docker are deemed out of scope. + +## Rationale + +- **Lightweight and Easy to Install:** K3s is designed to be a lightweight Kubernetes distribution, making it easy to install and run on resource-constrained environments, which aligns with our "zero setup" goal. K3d provides a similar experience for MacOS and Windows, leveraging Docker to create lightweight k3s clusters. +- **Kubernetes Compatibility:** K3s is a certified Kubernetes distribution, ensuring compatibility with standard Kubernetes manifests and tools. This allows users to easily migrate workloads between our platform and other Kubernetes environments. K3d also provides this compatibility by running k3s in Docker containers. +- **Low Resource Consumption:** K3s has a small footprint, minimizing the resource overhead on the host system. This is crucial for local development environments where resources are often limited. K3d shares this advantage as it runs k3s in containers, allowing for efficient resource utilization. +- **Well-Maintained:** Rancher (now SUSE) actively maintains K3s, providing regular updates and security patches. This ensures the stability and security of our platform. K3d benefits from the active k3s community and its own dedicated maintainers. +- **Suitable for Edge Computing:** While not our primary focus *now*, K3s's design makes it suitable for edge computing scenarios, providing a potential future expansion path. +- **Single Binary:** K3s is packaged as a single binary, simplifying installation and management. +- **Docker Dependency on Windows/MacOS acceptable:** While not ideal, the ubiquity of Docker Desktop on these platforms makes k3d a reasonable choice, as it leverages an existing dependency. + +## Consequences + +### Positive + +- **Simplified User Experience:** Users can quickly launch projects without needing to install and configure a complex runtime environment. +- **Increased Developer Productivity:** Developers can focus on writing code rather than managing infrastructure. +- **Consistent Environment:** Ensures a consistent runtime environment across different development machines. +- **Reduced Support Burden:** Automating runtime setup reduces the support burden on the platform team. +- **Future-Proofing:** Kubernetes compatibility provides a clear migration path to other Kubernetes environments. +- **Enables local testing that mirrors production:** By using a k8s distribution locally, developers can test how their application will behave in a production k8s cluster. + +### Negative + +- **Platform Dependency:** K3s is primarily designed for Linux, requiring a different solution (k3d) for Windows and macOS. This adds complexity to our platform. +- **Docker Dependency on Windows/MacOS:** k3d relies on Docker being installed. Users without Docker will need to install it. This could be a barrier to entry for some users. +- **Potential Resource Conflicts:** While K3s is lightweight, it still consumes resources. Running multiple K3s instances (especially via k3d) can lead to resource contention on developer machines. +- **Security Considerations:** As with any runtime environment, K3s and k3d introduce potential security vulnerabilities. We need to ensure that we keep them up-to-date with the latest security patches. +- **Abstraction Leakage:** While we aim for a "zero setup" experience, users may still need to understand basic Kubernetes concepts to troubleshoot issues. + +## Alternatives considered + +- **Minikube:** A popular option, but can be more resource-intensive than K3s. Also, the driver model can be complex to manage across different operating systems. +- **MicroK8s:** Another lightweight Kubernetes distribution, but K3s has a stronger focus on simplicity and ease of use. +- **Docker Compose:** Simpler than Kubernetes, but lacks the orchestration capabilities needed for complex workloads. Also, doesn't align with our Kubernetes-centric approach. +- **Podman Desktop:** An increasingly popular alternative to Docker Desktop, but k3d does not fully support it yet. +- **Kind (Kubernetes in Docker):** Similar to k3d, but specifically designed for testing Kubernetes itself, rather than running general workloads. +- **Relying on User-Provided Runtime:** This would require users to install and configure their own runtime environment, which goes against our "zero setup" goal. Increases support burden and leads to inconsistent environments. +- **Virtual Machines (VMs):** Using VMs would provide isolation, but would be much more resource-intensive and slower to start than container-based solutions. + diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index dad2673..1643511 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -17,5 +17,8 @@ async fn main() { }, }; - Maestro::load_from_env().interpret(Box::new(lamp_stack)).await.unwrap(); + Maestro::load_from_env() + .interpret(Box::new(lamp_stack)) + .await + .unwrap(); } From 3d6f646460cf10c1c62fc1bea6422dd24b0bee74 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Mon, 17 Mar 2025 16:20:52 -0400 Subject: [PATCH 08/62] feat(adr/007-default-runtime.md): add future work section discussing WASM potential Add a new section to the ADR document outlining potential future work with WebAssembly (WASM) as an alternative runtime, comparing it to Java's bytecode and JVM model, highlighting potential benefits in observability, heap allocation, and garbage collection. Note current maturity limitations compared to our target customer base. --- adr/007-default-runtime.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/adr/007-default-runtime.md b/adr/007-default-runtime.md index 21781ce..8515ffd 100644 --- a/adr/007-default-runtime.md +++ b/adr/007-default-runtime.md @@ -66,3 +66,14 @@ We selected **k3s as the default runtime** for managed workloads on Linux platfo - **Relying on User-Provided Runtime:** This would require users to install and configure their own runtime environment, which goes against our "zero setup" goal. Increases support burden and leads to inconsistent environments. - **Virtual Machines (VMs):** Using VMs would provide isolation, but would be much more resource-intensive and slower to start than container-based solutions. +## Future work + +There is a hype cycle around WASM that holds some ground : WASM to replace containers makes sense in many ways. + +To me, it feels just like Java with bytecode. If you can compile to bytecode, all you need to run your program is a JVM. Now, as long as you can compile to WASM, you can run your program on any WASM runtime. + +This feels like a sensible iteration over both managing containers, that are just bundling a bunch of libraries to make sure your program has everything it needs. This brings major benefits that used to come with the JVM such as observability into heap allocation, garbage collection, etc. + +The benefits won't be the exact same but it is reasonable to consider that this simplification and unification of runtime from containers to WASM will bring very significant improvements to the entire software delivery lifecycle. + +Down the road, it will be interesting to consider WASM, but the maturity versus our target customer base is not there yet. From 2433c02de9aaacccd40250b93449a252fc0dd7e2 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Mon, 17 Mar 2025 22:36:07 -0400 Subject: [PATCH 09/62] feat: select k3d for cross-platform Kubernetes development Select k3d as the primary solution for running Kubernetes clusters on Windows and macOS, alongside native k3s on Linux, to achieve a consistent "zero setup" experience across platforms while considering resource usage, complexity, and long-term alternatives like WebAssembly. --- adr/007-default-runtime.md | 94 ++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 54 deletions(-) diff --git a/adr/007-default-runtime.md b/adr/007-default-runtime.md index 8515ffd..c1032d2 100644 --- a/adr/007-default-runtime.md +++ b/adr/007-default-runtime.md @@ -1,79 +1,65 @@ -# Architecture Decision Record: Default Runtime for Managed Workloads** +## Architecture Decision Record: Default Runtime for Managed Workloads -## Status +### Status Proposed -## Context +### Context -Our infrastructure orchestrator manages workloads that require a runtime environment for execution. +Our infrastructure orchestrator manages workloads requiring a Kubernetes-compatible runtime environment. **Requirements** -- Cross platform -- Supports running complex workloads (similar level of complexity to kubernetes) -- Easy to install and setup -- Low footprint -- Well maintained -- K8s compatibility a big plus +- Cross-platform (Linux, Windows, macOS) +- Kubernetes compatibility +- Lightweight, easy setup with minimal dependencies +- Clean host teardown and minimal residue +- Well-maintained and actively supported -When a user launches a project orchestrated by harmony it should run. End of story. +### Decision -This means that we need to be able to automatically install a runtime appropriate for our core use cases. As our context is mostly enterprise applications deployed on various flavors of kubernetes clusters, it is natural to move towards lightweight k8s distributions such as k3s, microk8s, minikube, etc. +We select **k3d (k3s in Docker)** as our default runtime environment across all supported platforms (Linux, Windows, macOS). -Now, the main use case where we want it to "just work" is when a developer (or user) clones a repository or downloads an application and wants to launch it locally with zero setup. This means that there is no container runtime installed, no kubernetes cluster, no assumption of libraries or tools installed. +### Rationale -## Decision +- **Consistency Across Platforms:** + One solution for all platforms simplifies development, supports documentation, and reduces complexity. -We selected **k3s as the default runtime** for managed workloads on Linux platforms and **k3d on Windows and MacOS**. Other platforms that do not support either k3s or docker are deemed out of scope. +- **Simplified Setup and Teardown:** + k3d runs Kubernetes clusters in Docker containers, allowing quick setup, teardown, and minimal host residue. -## Rationale +- **Leveraging Existing Container Ecosystem:** + Docker/container runtimes are widely adopted, making their presence and familiarity common among users. -- **Lightweight and Easy to Install:** K3s is designed to be a lightweight Kubernetes distribution, making it easy to install and run on resource-constrained environments, which aligns with our "zero setup" goal. K3d provides a similar experience for MacOS and Windows, leveraging Docker to create lightweight k3s clusters. -- **Kubernetes Compatibility:** K3s is a certified Kubernetes distribution, ensuring compatibility with standard Kubernetes manifests and tools. This allows users to easily migrate workloads between our platform and other Kubernetes environments. K3d also provides this compatibility by running k3s in Docker containers. -- **Low Resource Consumption:** K3s has a small footprint, minimizing the resource overhead on the host system. This is crucial for local development environments where resources are often limited. K3d shares this advantage as it runs k3s in containers, allowing for efficient resource utilization. -- **Well-Maintained:** Rancher (now SUSE) actively maintains K3s, providing regular updates and security patches. This ensures the stability and security of our platform. K3d benefits from the active k3s community and its own dedicated maintainers. -- **Suitable for Edge Computing:** While not our primary focus *now*, K3s's design makes it suitable for edge computing scenarios, providing a potential future expansion path. -- **Single Binary:** K3s is packaged as a single binary, simplifying installation and management. -- **Docker Dependency on Windows/MacOS acceptable:** While not ideal, the ubiquity of Docker Desktop on these platforms makes k3d a reasonable choice, as it leverages an existing dependency. +- **Kubernetes Compatibility:** + k3s (within k3d) is fully Kubernetes-certified, ensuring compatibility with standard Kubernetes tools and manifests. -## Consequences +- **Active Maintenance and Community:** + k3d and k3s both have active communities and are well-maintained. -### Positive +### Consequences -- **Simplified User Experience:** Users can quickly launch projects without needing to install and configure a complex runtime environment. -- **Increased Developer Productivity:** Developers can focus on writing code rather than managing infrastructure. -- **Consistent Environment:** Ensures a consistent runtime environment across different development machines. -- **Reduced Support Burden:** Automating runtime setup reduces the support burden on the platform team. -- **Future-Proofing:** Kubernetes compatibility provides a clear migration path to other Kubernetes environments. -- **Enables local testing that mirrors production:** By using a k8s distribution locally, developers can test how their application will behave in a production k8s cluster. +#### Positive -### Negative +- **Uniform User Experience:** Users have a consistent setup experience across all platforms. +- **Reduced Support Overhead:** Standardizing runtime simplifies support, documentation, and troubleshooting. +- **Clean Isolation:** Containerization allows developers to easily clean up clusters without affecting host systems. +- **Facilitates Multi-Cluster Development:** Easy creation and management of multiple clusters concurrently. -- **Platform Dependency:** K3s is primarily designed for Linux, requiring a different solution (k3d) for Windows and macOS. This adds complexity to our platform. -- **Docker Dependency on Windows/MacOS:** k3d relies on Docker being installed. Users without Docker will need to install it. This could be a barrier to entry for some users. -- **Potential Resource Conflicts:** While K3s is lightweight, it still consumes resources. Running multiple K3s instances (especially via k3d) can lead to resource contention on developer machines. -- **Security Considerations:** As with any runtime environment, K3s and k3d introduce potential security vulnerabilities. We need to ensure that we keep them up-to-date with the latest security patches. -- **Abstraction Leakage:** While we aim for a "zero setup" experience, users may still need to understand basic Kubernetes concepts to troubleshoot issues. +#### Negative -## Alternatives considered +- **Docker Dependency:** Requires Docker (or compatible runtime) on all platforms. +- **Potential Overhead:** Slight performance/resource overhead compared to native k3s. +- **Docker Licensing Considerations:** Enterprise licensing of Docker Desktop could introduce additional considerations. -- **Minikube:** A popular option, but can be more resource-intensive than K3s. Also, the driver model can be complex to manage across different operating systems. -- **MicroK8s:** Another lightweight Kubernetes distribution, but K3s has a stronger focus on simplicity and ease of use. -- **Docker Compose:** Simpler than Kubernetes, but lacks the orchestration capabilities needed for complex workloads. Also, doesn't align with our Kubernetes-centric approach. -- **Podman Desktop:** An increasingly popular alternative to Docker Desktop, but k3d does not fully support it yet. -- **Kind (Kubernetes in Docker):** Similar to k3d, but specifically designed for testing Kubernetes itself, rather than running general workloads. -- **Relying on User-Provided Runtime:** This would require users to install and configure their own runtime environment, which goes against our "zero setup" goal. Increases support burden and leads to inconsistent environments. -- **Virtual Machines (VMs):** Using VMs would provide isolation, but would be much more resource-intensive and slower to start than container-based solutions. +### Alternatives Considered -## Future work +- **Native k3s (Linux) / k3d (Windows/macOS):** Original proposal. Rejected for greater simplicity and consistency. +- **Minikube, MicroK8s, Kind:** Rejected due to complexity, resource usage, or narrower use-case focus. +- **Docker Compose, Podman Desktop:** Rejected due to lack of orchestration or current limited k3d compatibility. -There is a hype cycle around WASM that holds some ground : WASM to replace containers makes sense in many ways. +### Future Work -To me, it feels just like Java with bytecode. If you can compile to bytecode, all you need to run your program is a JVM. Now, as long as you can compile to WASM, you can run your program on any WASM runtime. - -This feels like a sensible iteration over both managing containers, that are just bundling a bunch of libraries to make sure your program has everything it needs. This brings major benefits that used to come with the JVM such as observability into heap allocation, garbage collection, etc. - -The benefits won't be the exact same but it is reasonable to consider that this simplification and unification of runtime from containers to WASM will bring very significant improvements to the entire software delivery lifecycle. - -Down the road, it will be interesting to consider WASM, but the maturity versus our target customer base is not there yet. +- Evaluate Podman Desktop or other container runtimes to avoid Docker dependency. +- Continuously monitor k3d maturity and stability. +- Investigate WebAssembly (WASM) runtimes as emerging alternatives for containerized workloads. From 3962238f0de2c96a9b901cb6f8e2b9f7eedad8bf Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Tue, 25 Mar 2025 08:03:45 -0400 Subject: [PATCH 10/62] spike: Working on abstractions, Topology, Score, Capability, Maestro for strong type safety and nice UX/DX --- examples/topology/Cargo.toml | 10 + examples/topology/src/main.rs | 332 +++++++++++++++++++++++++ examples/topology/src/main_claudev1.rs | 323 ++++++++++++++++++++++++ examples/topology/src/main_right.rs | 129 ++++++++++ examples/topology/src/main_v1.rs | 155 ++++++++++++ 5 files changed, 949 insertions(+) create mode 100644 examples/topology/Cargo.toml create mode 100644 examples/topology/src/main.rs create mode 100644 examples/topology/src/main_claudev1.rs create mode 100644 examples/topology/src/main_right.rs create mode 100644 examples/topology/src/main_v1.rs diff --git a/examples/topology/Cargo.toml b/examples/topology/Cargo.toml new file mode 100644 index 0000000..96740a9 --- /dev/null +++ b/examples/topology/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "example-topology" +edition = "2024" +version.workspace = true +readme.workspace = true +license.workspace = true +publish = false + +[dependencies] +rand.workspace = true diff --git a/examples/topology/src/main.rs b/examples/topology/src/main.rs new file mode 100644 index 0000000..f3091c4 --- /dev/null +++ b/examples/topology/src/main.rs @@ -0,0 +1,332 @@ +use std::process::Command; +use rand::Rng; // Add rand dependency + +// ===== Capability Traits ===== + +/// Base trait for all capabilities +pub trait Capability {} + +/// Capability for executing shell commands on a host +pub trait CommandCapability: Capability { + fn execute_command(&self, command: &str, args: &[&str]) -> Result; +} + +/// Capability for interacting with a Kubernetes cluster +pub trait KubernetesCapability: Capability { + fn apply_manifest(&self, manifest: &str) -> Result<(), String>; + fn get_resource(&self, resource_type: &str, name: &str) -> Result; +} + +// ===== Topology Traits ===== + +/// Base trait for all topologies +pub trait Topology { + // Base topology methods that don't depend on capabilities + fn name(&self) -> &str; +} + +// ===== Score Traits ===== + +/// Generic Score trait with an associated Capability type +pub trait Score { + fn apply(&self, topology: &T) -> Result<(), String>; + fn name(&self) -> &str; +} + +// ===== Concrete Topologies ===== + +/// A topology representing a Linux host +pub struct LinuxHostTopology { + name: String, + host: String, +} + +// Implement the base Capability trait for LinuxHostTopology +impl Capability for LinuxHostTopology {} + +impl LinuxHostTopology { + pub fn new(name: String, host: String) -> Self { + Self { name, host } + } +} + +impl Topology for LinuxHostTopology { + fn name(&self) -> &str { + &self.name + } +} + +impl CommandCapability for LinuxHostTopology { + fn execute_command(&self, command: &str, args: &[&str]) -> Result { + println!("Executing on {}: {} {:?}", self.host, command, args); + // In a real implementation, this would SSH to the host and execute the command + let output = Command::new(command) + .args(args) + .output() + .map_err(|e| e.to_string())?; + + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + Err(String::from_utf8_lossy(&output.stderr).to_string()) + } + } +} + +/// A topology representing a K3D Kubernetes cluster +pub struct K3DTopology { + name: String, + linux_host: LinuxHostTopology, + cluster_name: String, +} + +// Implement the base Capability trait for K3DTopology +impl Capability for K3DTopology {} + +impl K3DTopology { + pub fn new(name: String, linux_host: LinuxHostTopology, cluster_name: String) -> Self { + Self { + name, + linux_host, + cluster_name, + } + } +} + +impl Topology for K3DTopology { + fn name(&self) -> &str { + &self.name + } +} + +impl CommandCapability for K3DTopology { + fn execute_command(&self, command: &str, args: &[&str]) -> Result { + // Delegate to the underlying Linux host + self.linux_host.execute_command(command, args) + } +} + +impl KubernetesCapability for K3DTopology { + fn apply_manifest(&self, manifest: &str) -> Result<(), String> { + println!("Applying manifest to K3D cluster '{}'", self.cluster_name); + // Write manifest to a temporary file + //let temp_file = format!("/tmp/manifest-{}.yaml", rand::thread_rng().gen::()); + let temp_file = format!("/tmp/manifest-TODO_RANDOM_NUMBER.yaml"); + + // Use the linux_host directly to avoid capability trait bounds + self.linux_host.execute_command("bash", &["-c", &format!("cat > {}", temp_file)])?; + + // Apply with kubectl + self.linux_host.execute_command( + "kubectl", + &["--context", &format!("k3d-{}", self.cluster_name), "apply", "-f", &temp_file] + )?; + + Ok(()) + } + + fn get_resource(&self, resource_type: &str, name: &str) -> Result { + println!("Getting resource {}/{} from K3D cluster '{}'", resource_type, name, self.cluster_name); + self.linux_host.execute_command( + "kubectl", + &[ + "--context", + &format!("k3d-{}", self.cluster_name), + "get", + resource_type, + name, + "-o", + "yaml", + ] + ) + } +} + +// ===== Concrete Scores ===== + +/// A score that executes commands on a topology +pub struct CommandScore { + name: String, + command: String, + args: Vec, +} + +impl CommandScore { + pub fn new(name: String, command: String, args: Vec) -> Self { + Self { name, command, args } + } +} + +impl Score for CommandScore +where + T: Topology + CommandCapability +{ + fn apply(&self, topology: &T) -> Result<(), String> { + println!("Applying CommandScore '{}' to topology '{}'", self.name, topology.name()); + let args_refs: Vec<&str> = self.args.iter().map(|s| s.as_str()).collect(); + topology.execute_command(&self.command, &args_refs)?; + Ok(()) + } + + fn name(&self) -> &str { + &self.name + } +} + +/// A score that applies Kubernetes resources to a topology +pub struct K8sResourceScore { + name: String, + manifest: String, +} + +impl K8sResourceScore { + pub fn new(name: String, manifest: String) -> Self { + Self { name, manifest } + } +} + +impl Score for K8sResourceScore +where + T: Topology + KubernetesCapability +{ + fn apply(&self, topology: &T) -> Result<(), String> { + println!("Applying K8sResourceScore '{}' to topology '{}'", self.name, topology.name()); + topology.apply_manifest(&self.manifest) + } + + fn name(&self) -> &str { + &self.name + } +} + +// ===== Maestro Orchestrator ===== + +/// Type-safe orchestrator that enforces capability requirements at compile time +pub struct Maestro { + topology: T, + scores: Vec>>, +} + +/// A trait object wrapper that hides the specific Score type but preserves its +/// capability requirements +trait ScoreWrapper { + fn apply(&self, topology: &T) -> Result<(), String>; + fn name(&self) -> &str; +} + +/// Implementation of ScoreWrapper for any Score that works with topology T +impl ScoreWrapper for S +where + T: Topology, + S: Score + 'static +{ + fn apply(&self, topology: &T) -> Result<(), String> { + >::apply(self, topology) + } + + fn name(&self) -> &str { + >::name(self) + } +} + +impl Maestro { + pub fn new(topology: T) -> Self { + Self { + topology, + scores: Vec::new(), + } + } + + /// Register a score that is compatible with this topology's capabilities + pub fn register_score(&mut self, score: S) + where + S: Score + 'static + { + println!("Registering score '{}' for topology '{}'", score.name(), self.topology.name()); + self.scores.push(Box::new(score)); + } + + /// Apply all registered scores to the topology + pub fn orchestrate(&self) -> Result<(), String> { + println!("Orchestrating topology '{}'", self.topology.name()); + for score in &self.scores { + score.apply(&self.topology)?; + } + Ok(()) + } +} + +// ===== Example Usage ===== + +fn main() { + // Create a Linux host topology + let linux_host = LinuxHostTopology::new( + "dev-machine".to_string(), + "localhost".to_string() + ); + + // Create a maestro for the Linux host + let mut linux_maestro = Maestro::new(linux_host); + + // Register a command score that works with any topology having CommandCapability + linux_maestro.register_score(CommandScore::new( + "check-disk".to_string(), + "df".to_string(), + vec!["-h".to_string()] + )); + + // This would fail to compile if we tried to register a K8sResourceScore + // because LinuxHostTopology doesn't implement KubernetesCapability + // linux_maestro.register_score(K8sResourceScore::new(...)); + + // Create a K3D topology which has both Command and Kubernetes capabilities + let k3d_host = LinuxHostTopology::new( + "k3d-host".to_string(), + "localhost".to_string() + ); + + let k3d_topology = K3DTopology::new( + "dev-cluster".to_string(), + k3d_host, + "devcluster".to_string() + ); + + // Create a maestro for the K3D topology + let mut k3d_maestro = Maestro::new(k3d_topology); + + // We can register both command scores and kubernetes scores + k3d_maestro.register_score(CommandScore::new( + "check-nodes".to_string(), + "kubectl".to_string(), + vec!["get".to_string(), "nodes".to_string()] + )); + + k3d_maestro.register_score(K8sResourceScore::new( + "deploy-nginx".to_string(), + r#" + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx + spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:latest + ports: + - containerPort: 80 + "#.to_string() + )); + + // Orchestrate both topologies + linux_maestro.orchestrate().unwrap(); + k3d_maestro.orchestrate().unwrap(); +} diff --git a/examples/topology/src/main_claudev1.rs b/examples/topology/src/main_claudev1.rs new file mode 100644 index 0000000..480fa2c --- /dev/null +++ b/examples/topology/src/main_claudev1.rs @@ -0,0 +1,323 @@ +use std::marker::PhantomData; +use std::process::Command; + +// ===== Capability Traits ===== + +/// Base trait for all capabilities +pub trait Capability {} + +/// Capability for executing shell commands on a host +pub trait CommandCapability: Capability { + fn execute_command(&self, command: &str, args: &[&str]) -> Result; +} + +/// Capability for interacting with a Kubernetes cluster +pub trait KubernetesCapability: Capability { + fn apply_manifest(&self, manifest: &str) -> Result<(), String>; + fn get_resource(&self, resource_type: &str, name: &str) -> Result; +} + +// ===== Topology Traits ===== + +/// Base trait for all topologies +pub trait Topology { + // Base topology methods that don't depend on capabilities + fn name(&self) -> &str; +} + +// ===== Score Traits ===== + +/// Generic Score trait with an associated Capability type +pub trait Score { + fn apply(&self, topology: &T) -> Result<(), String>; + fn name(&self) -> &str; +} + +// ===== Concrete Topologies ===== + +/// A topology representing a Linux host +pub struct LinuxHostTopology { + name: String, + host: String, +} + +impl LinuxHostTopology { + pub fn new(name: String, host: String) -> Self { + Self { name, host } + } +} + +impl Topology for LinuxHostTopology { + fn name(&self) -> &str { + &self.name + } +} + +impl CommandCapability for LinuxHostTopology { + fn execute_command(&self, command: &str, args: &[&str]) -> Result { + println!("Executing on {}: {} {:?}", self.host, command, args); + // In a real implementation, this would SSH to the host and execute the command + let output = Command::new(command) + .args(args) + .output() + .map_err(|e| e.to_string())?; + + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + Err(String::from_utf8_lossy(&output.stderr).to_string()) + } + } +} + +/// A topology representing a K3D Kubernetes cluster +pub struct K3DTopology { + name: String, + linux_host: LinuxHostTopology, + cluster_name: String, +} + +impl K3DTopology { + pub fn new(name: String, linux_host: LinuxHostTopology, cluster_name: String) -> Self { + Self { + name, + linux_host, + cluster_name, + } + } +} + +impl Topology for K3DTopology { + fn name(&self) -> &str { + &self.name + } +} + +impl CommandCapability for K3DTopology { + fn execute_command(&self, command: &str, args: &[&str]) -> Result { + // Delegate to the underlying Linux host + self.linux_host.execute_command(command, args) + } +} + +impl KubernetesCapability for K3DTopology { + fn apply_manifest(&self, manifest: &str) -> Result<(), String> { + println!("Applying manifest to K3D cluster '{}'", self.cluster_name); + // Write manifest to a temporary file + let temp_file = format!("/tmp/manifest-{}.yaml", rand::random::()); + self.execute_command("bash", &["-c", &format!("cat > {}", temp_file)])?; + + // Apply with kubectl + self.execute_command( + "kubectl", + &["--context", &format!("k3d-{}", self.cluster_name), "apply", "-f", &temp_file] + )?; + + Ok(()) + } + + fn get_resource(&self, resource_type: &str, name: &str) -> Result { + println!("Getting resource {}/{} from K3D cluster '{}'", resource_type, name, self.cluster_name); + self.execute_command( + "kubectl", + &[ + "--context", + &format!("k3d-{}", self.cluster_name), + "get", + resource_type, + name, + "-o", + "yaml", + ] + ) + } +} + +// ===== Concrete Scores ===== + +/// A score that executes commands on a topology +pub struct CommandScore { + name: String, + command: String, + args: Vec, +} + +impl CommandScore { + pub fn new(name: String, command: String, args: Vec) -> Self { + Self { name, command, args } + } +} + +impl Score for CommandScore +where + T: Topology + CommandCapability +{ + fn apply(&self, topology: &T) -> Result<(), String> { + println!("Applying CommandScore '{}' to topology '{}'", self.name, topology.name()); + let args_refs: Vec<&str> = self.args.iter().map(|s| s.as_str()).collect(); + topology.execute_command(&self.command, &args_refs)?; + Ok(()) + } + + fn name(&self) -> &str { + &self.name + } +} + +/// A score that applies Kubernetes resources to a topology +pub struct K8sResourceScore { + name: String, + manifest: String, +} + +impl K8sResourceScore { + pub fn new(name: String, manifest: String) -> Self { + Self { name, manifest } + } +} + +impl Score for K8sResourceScore +where + T: Topology + KubernetesCapability +{ + fn apply(&self, topology: &T) -> Result<(), String> { + println!("Applying K8sResourceScore '{}' to topology '{}'", self.name, topology.name()); + topology.apply_manifest(&self.manifest) + } + + fn name(&self) -> &str { + &self.name + } +} + +// ===== Maestro Orchestrator ===== + +/// Type-safe orchestrator that enforces capability requirements at compile time +pub struct Maestro { + topology: T, + scores: Vec>>, +} + +/// A trait object wrapper that hides the specific Score type but preserves its +/// capability requirements +trait ScoreWrapper { + fn apply(&self, topology: &T) -> Result<(), String>; + fn name(&self) -> &str; +} + +/// Implementation of ScoreWrapper for any Score that works with topology T +impl ScoreWrapper for S +where + T: Topology, + S: Score + 'static +{ + fn apply(&self, topology: &T) -> Result<(), String> { + >::apply(self, topology) + } + + fn name(&self) -> &str { + >::name(self) + } +} + +impl Maestro { + pub fn new(topology: T) -> Self { + Self { + topology, + scores: Vec::new(), + } + } + + /// Register a score that is compatible with this topology's capabilities + pub fn register_score(&mut self, score: S) + where + S: Score + 'static + { + println!("Registering score '{}' for topology '{}'", score.name(), self.topology.name()); + self.scores.push(Box::new(score)); + } + + /// Apply all registered scores to the topology + pub fn orchestrate(&self) -> Result<(), String> { + println!("Orchestrating topology '{}'", self.topology.name()); + for score in &self.scores { + score.apply(&self.topology)?; + } + Ok(()) + } +} + +// ===== Example Usage ===== + +fn main() { + // Create a Linux host topology + let linux_host = LinuxHostTopology::new( + "dev-machine".to_string(), + "localhost".to_string() + ); + + // Create a maestro for the Linux host + let mut linux_maestro = Maestro::new(linux_host); + + // Register a command score that works with any topology having CommandCapability + linux_maestro.register_score(CommandScore::new( + "check-disk".to_string(), + "df".to_string(), + vec!["-h".to_string()] + )); + + // This would fail to compile if we tried to register a K8sResourceScore + // because LinuxHostTopology doesn't implement KubernetesCapability + // linux_maestro.register_score(K8sResourceScore::new(...)); + + // Create a K3D topology which has both Command and Kubernetes capabilities + let k3d_host = LinuxHostTopology::new( + "k3d-host".to_string(), + "localhost".to_string() + ); + + let k3d_topology = K3DTopology::new( + "dev-cluster".to_string(), + k3d_host, + "devcluster".to_string() + ); + + // Create a maestro for the K3D topology + let mut k3d_maestro = Maestro::new(k3d_topology); + + // We can register both command scores and kubernetes scores + k3d_maestro.register_score(CommandScore::new( + "check-nodes".to_string(), + "kubectl".to_string(), + vec!["get".to_string(), "nodes".to_string()] + )); + + k3d_maestro.register_score(K8sResourceScore::new( + "deploy-nginx".to_string(), + r#" + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx + spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:latest + ports: + - containerPort: 80 + "#.to_string() + )); + + // Orchestrate both topologies + linux_maestro.orchestrate().unwrap(); + k3d_maestro.orchestrate().unwrap(); +} diff --git a/examples/topology/src/main_right.rs b/examples/topology/src/main_right.rs new file mode 100644 index 0000000..baa6c6c --- /dev/null +++ b/examples/topology/src/main_right.rs @@ -0,0 +1,129 @@ +use std::marker::PhantomData; + +// Capability Trait Hierarchy +pub trait Capability {} + +// Specific Capability Traits +pub trait ShellAccess: Capability {} +pub trait ContainerRuntime: Capability {} +pub trait KubernetesAccess: Capability {} +pub trait FileSystemAccess: Capability {} + +// Topology Trait - Defines the core interface for infrastructure topologies +pub trait Topology { + type Capabilities: Capability; + + fn name(&self) -> &str; +} + +// Score Trait - Defines the core interface for infrastructure transformation +pub trait Score { + type RequiredCapabilities: Capability; + type OutputTopology: Topology; + + fn apply(&self, topology: T) -> Result; +} + +// Linux Host Topology +pub struct LinuxHostTopology; + +impl Topology for LinuxHostTopology { + type Capabilities = dyn ShellAccess + FileSystemAccess; + + fn name(&self) -> &str { + "Linux Host" + } +} + +impl ShellAccess for LinuxHostTopology {} +impl FileSystemAccess for LinuxHostTopology {} + +// K3D Topology +pub struct K3DTopology; + +impl Topology for K3DTopology { + type Capabilities = dyn ContainerRuntime + KubernetesAccess + ShellAccess; + + fn name(&self) -> &str { + "K3D Kubernetes Cluster" + } +} + +impl ContainerRuntime for K3DTopology {} +impl KubernetesAccess for K3DTopology {} +impl ShellAccess for K3DTopology {} + +// Command Score - A score that requires shell access +pub struct CommandScore { + command: String, +} + +impl Score for CommandScore { + type RequiredCapabilities = dyn ShellAccess; + type OutputTopology = LinuxHostTopology; + + fn apply(&self, _topology: T) -> Result + where + T: ShellAccess + { + // Simulate command execution + println!("Executing command: {}", self.command); + Ok(LinuxHostTopology) + } +} + +// Kubernetes Resource Score +pub struct K8sResourceScore { + resource_definition: String, +} + +impl Score for K8sResourceScore { + type RequiredCapabilities = dyn KubernetesAccess; + type OutputTopology = K3DTopology; + + fn apply(&self, _topology: T) -> Result + where + T: dyn KubernetesAccess + { + // Simulate Kubernetes resource application + println!("Applying K8s resource: {}", self.resource_definition); + Ok(K3DTopology) + } +} + +// Maestro - The orchestration coordinator +pub struct Maestro; + +impl Maestro { + // Type-safe score application + pub fn apply_score(topology: T, score: S) -> Result + where + T: Topology, + S: Score, + T: S::RequiredCapabilities + { + score.apply(topology) + } +} + +fn main() { + // Example usage demonstrating type-driven design + let linux_host = LinuxHostTopology; + let k3d_cluster = K3DTopology; + + // Command score on Linux host + let command_score = CommandScore { + command: "echo 'Hello, World!'".to_string(), + }; + + let result = Maestro::apply_score(linux_host, command_score) + .expect("Command score application failed"); + + // K8s resource score on K3D cluster + let k8s_score = K8sResourceScore { + resource_definition: "apiVersion: v1\nkind: Pod\n...".to_string(), + }; + + let k8s_result = Maestro::apply_score(k3d_cluster, k8s_score) + .expect("K8s resource score application failed"); +} diff --git a/examples/topology/src/main_v1.rs b/examples/topology/src/main_v1.rs new file mode 100644 index 0000000..3ae3b11 --- /dev/null +++ b/examples/topology/src/main_v1.rs @@ -0,0 +1,155 @@ +mod main_right; +mod main_claude; +// Capability Traits + +trait Capability {} + +trait LinuxOperations: Capability { + fn execute_command(&self, command: &str) -> Result; +} + +trait KubernetesOperations: Capability { + fn create_resource(&self, resource: &str) -> Result; + fn delete_resource(&self, resource: &str) -> Result; +} + +// Topology Implementations + +struct LinuxHostTopology; + +impl LinuxOperations for LinuxHostTopology { + fn execute_command(&self, command: &str) -> Result { + // Implementation for executing commands on a Linux host + Ok(format!("Executed command: {}", command)) + } +} + +impl Capability for LinuxHostTopology {} + +struct K3DTopology; + +impl KubernetesOperations for K3DTopology { + fn create_resource(&self, resource: &str) -> Result { + // Implementation for creating Kubernetes resources in K3D + Ok(format!("Created resource: {}", resource)) + } + + fn delete_resource(&self, resource: &str) -> Result { + // Implementation for deleting Kubernetes resources in K3D + Ok(format!("Deleted resource: {}", resource)) + } +} + +impl Capability for K3DTopology {} + +// Score Implementations + +struct K8sResourceScore { + resource: String, +} + +impl Score for K8sResourceScore +where + T: KubernetesOperations, +{ + fn execute(&self, topology: &T) -> Result { + topology.create_resource(&self.resource) + } +} + +struct CommandScore { + command: String, +} + +impl Score for CommandScore +where + T: LinuxOperations + 'static, +{ + fn execute(&self, topology: &T) -> Result { + topology.execute_command(&self.command) + } +} + +// Score Trait + +trait Score +where + T: Capability + 'static, +{ + fn execute(&self, topology: &T) -> Result; +} + +// Maestro Implementation + +struct Maestro { + scores: Vec>>>, +} + +impl Maestro { + fn new() -> Self { + Maestro { scores: Vec::new() } + } + + fn register_score(&mut self, score: Box) + where + T: Score> + 'static, + { + self.scores.push(Box::new(score)); + } + + fn execute_scores(&self, topology: &T) -> Result, String> + where + T: Capability + 'static, + { + let mut results = Vec::new(); + for score in &self.scores { + if let Some(score) = score.as_any().downcast_ref::>>() { + results.push(score.execute(topology)?); + } + } + Ok(results) + } +} + +// Helper trait for downcasting + +trait AsAny { + fn as_any(&self) -> &dyn std::any::Any; +} + +impl AsAny for T { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +// Main Function + +fn main() { + let mut maestro = Maestro::new(); + + let k8s_score = K8sResourceScore { + resource: "deployment.yaml".to_string(), + }; + maestro.register_score(k8s_score); + + let command_score = CommandScore { + command: "ls -l".to_string(), + }; + maestro.register_score(command_score); + + let linux_topology = LinuxHostTopology; + let k3d_topology = K3DTopology; + + let linux_results = maestro.execute_scores(&linux_topology).unwrap(); + println!("Linux Topology Results:"); + for result in linux_results { + println!("{}", result); + } + + let k3d_results = maestro.execute_scores(&k3d_topology).unwrap(); + println!("K3D Topology Results:"); + for result in k3d_results { + println!("{}", result); + } +} From d7897f29c4479c9bb7f622e2d44aa4d83817efdd Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 26 Mar 2025 16:39:11 -0400 Subject: [PATCH 11/62] feat(orchestration): introduce Interpret trait and refactor score application Refactor the orchestration process to use an `Interpret` trait instead of directly applying scores. This change introduces a more flexible and extensible design for executing commands associated with different types of topologies. The `CommandScore` and `K8sResourceScore` now implement this trait, providing a clear separation between score definition and execution logic. Update the `Maestro::orchestrate` method to compile scores into interpreters before executing them against their respective topologies. --- examples/topology/src/main.rs | 227 ++++++++++++++++------------------ 1 file changed, 105 insertions(+), 122 deletions(-) diff --git a/examples/topology/src/main.rs b/examples/topology/src/main.rs index f3091c4..308cdd3 100644 --- a/examples/topology/src/main.rs +++ b/examples/topology/src/main.rs @@ -1,47 +1,31 @@ +use rand::Rng; use std::process::Command; -use rand::Rng; // Add rand dependency -// ===== Capability Traits ===== - -/// Base trait for all capabilities pub trait Capability {} -/// Capability for executing shell commands on a host pub trait CommandCapability: Capability { fn execute_command(&self, command: &str, args: &[&str]) -> Result; } -/// Capability for interacting with a Kubernetes cluster pub trait KubernetesCapability: Capability { fn apply_manifest(&self, manifest: &str) -> Result<(), String>; fn get_resource(&self, resource_type: &str, name: &str) -> Result; } -// ===== Topology Traits ===== - -/// Base trait for all topologies pub trait Topology { - // Base topology methods that don't depend on capabilities fn name(&self) -> &str; } -// ===== Score Traits ===== - -/// Generic Score trait with an associated Capability type pub trait Score { - fn apply(&self, topology: &T) -> Result<(), String>; + fn compile(&self) -> Result>, String>; fn name(&self) -> &str; } -// ===== Concrete Topologies ===== - -/// A topology representing a Linux host pub struct LinuxHostTopology { name: String, host: String, } -// Implement the base Capability trait for LinuxHostTopology impl Capability for LinuxHostTopology {} impl LinuxHostTopology { @@ -64,7 +48,7 @@ impl CommandCapability for LinuxHostTopology { .args(args) .output() .map_err(|e| e.to_string())?; - + if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } else { @@ -73,19 +57,17 @@ impl CommandCapability for LinuxHostTopology { } } -/// A topology representing a K3D Kubernetes cluster pub struct K3DTopology { name: String, linux_host: LinuxHostTopology, cluster_name: String, } -// Implement the base Capability trait for K3DTopology impl Capability for K3DTopology {} impl K3DTopology { pub fn new(name: String, linux_host: LinuxHostTopology, cluster_name: String) -> Self { - Self { + Self { name, linux_host, cluster_name, @@ -101,7 +83,6 @@ impl Topology for K3DTopology { impl CommandCapability for K3DTopology { fn execute_command(&self, command: &str, args: &[&str]) -> Result { - // Delegate to the underlying Linux host self.linux_host.execute_command(command, args) } } @@ -112,39 +93,40 @@ impl KubernetesCapability for K3DTopology { // Write manifest to a temporary file //let temp_file = format!("/tmp/manifest-{}.yaml", rand::thread_rng().gen::()); let temp_file = format!("/tmp/manifest-TODO_RANDOM_NUMBER.yaml"); - + // Use the linux_host directly to avoid capability trait bounds - self.linux_host.execute_command("bash", &["-c", &format!("cat > {}", temp_file)])?; - + self.linux_host + .execute_command("bash", &["-c", &format!("cat > {}", temp_file)])?; + // Apply with kubectl - self.linux_host.execute_command( - "kubectl", - &["--context", &format!("k3d-{}", self.cluster_name), "apply", "-f", &temp_file] - )?; - + self.linux_host.execute_command("kubectl", &[ + "--context", + &format!("k3d-{}", self.cluster_name), + "apply", + "-f", + &temp_file, + ])?; + Ok(()) } fn get_resource(&self, resource_type: &str, name: &str) -> Result { - println!("Getting resource {}/{} from K3D cluster '{}'", resource_type, name, self.cluster_name); - self.linux_host.execute_command( - "kubectl", - &[ - "--context", - &format!("k3d-{}", self.cluster_name), - "get", - resource_type, - name, - "-o", - "yaml", - ] - ) + println!( + "Getting resource {}/{} from K3D cluster '{}'", + resource_type, name, self.cluster_name + ); + self.linux_host.execute_command("kubectl", &[ + "--context", + &format!("k3d-{}", self.cluster_name), + "get", + resource_type, + name, + "-o", + "yaml", + ]) } } -// ===== Concrete Scores ===== - -/// A score that executes commands on a topology pub struct CommandScore { name: String, command: String, @@ -153,19 +135,35 @@ pub struct CommandScore { impl CommandScore { pub fn new(name: String, command: String, args: Vec) -> Self { - Self { name, command, args } + Self { + name, + command, + args, + } } } -impl Score for CommandScore -where - T: Topology + CommandCapability +pub trait Interpret { + fn execute(&self, topology: &T) -> Result; +} + +struct CommandInterpret; + +impl Interpret for CommandInterpret +where + T: Topology + CommandCapability, { - fn apply(&self, topology: &T) -> Result<(), String> { - println!("Applying CommandScore '{}' to topology '{}'", self.name, topology.name()); - let args_refs: Vec<&str> = self.args.iter().map(|s| s.as_str()).collect(); - topology.execute_command(&self.command, &args_refs)?; - Ok(()) + fn execute(&self, topology: &T) -> Result { + todo!() + } +} + +impl Score for CommandScore +where + T: Topology + CommandCapability, +{ + fn compile(&self) -> Result>, String> { + Ok(Box::new(CommandInterpret {})) } fn name(&self) -> &str { @@ -173,7 +171,8 @@ where } } -/// A score that applies Kubernetes resources to a topology + +#[derive(Clone)] pub struct K8sResourceScore { name: String, manifest: String, @@ -185,13 +184,24 @@ impl K8sResourceScore { } } -impl Score for K8sResourceScore -where - T: Topology + KubernetesCapability +struct K8sResourceInterpret { + score: K8sResourceScore, +} + +impl Interpret for K8sResourceInterpret { + fn execute(&self, topology: &T) -> Result { + todo!() + } +} + +impl Score for K8sResourceScore +where + T: Topology + KubernetesCapability, { - fn apply(&self, topology: &T) -> Result<(), String> { - println!("Applying K8sResourceScore '{}' to topology '{}'", self.name, topology.name()); - topology.apply_manifest(&self.manifest) + fn compile(&self) -> Result + 'static)>, String> { + Ok(Box::new(K8sResourceInterpret { + score: self.clone(), + })) } fn name(&self) -> &str { @@ -199,35 +209,11 @@ where } } -// ===== Maestro Orchestrator ===== - -/// Type-safe orchestrator that enforces capability requirements at compile time pub struct Maestro { topology: T, - scores: Vec>>, + scores: Vec>>, } -/// A trait object wrapper that hides the specific Score type but preserves its -/// capability requirements -trait ScoreWrapper { - fn apply(&self, topology: &T) -> Result<(), String>; - fn name(&self) -> &str; -} - -/// Implementation of ScoreWrapper for any Score that works with topology T -impl ScoreWrapper for S -where - T: Topology, - S: Score + 'static -{ - fn apply(&self, topology: &T) -> Result<(), String> { - >::apply(self, topology) - } - - fn name(&self) -> &str { - >::name(self) - } -} impl Maestro { pub fn new(topology: T) -> Self { @@ -237,70 +223,66 @@ impl Maestro { } } - /// Register a score that is compatible with this topology's capabilities - pub fn register_score(&mut self, score: S) - where - S: Score + 'static + pub fn register_score(&mut self, score: S) + where + S: Score + 'static, { - println!("Registering score '{}' for topology '{}'", score.name(), self.topology.name()); + println!( + "Registering score '{}' for topology '{}'", + score.name(), + self.topology.name() + ); self.scores.push(Box::new(score)); } - /// Apply all registered scores to the topology pub fn orchestrate(&self) -> Result<(), String> { println!("Orchestrating topology '{}'", self.topology.name()); for score in &self.scores { - score.apply(&self.topology)?; + let interpret = score.compile()?; + interpret.execute(&self.topology)?; } Ok(()) } } -// ===== Example Usage ===== - fn main() { - // Create a Linux host topology - let linux_host = LinuxHostTopology::new( - "dev-machine".to_string(), - "localhost".to_string() - ); - - // Create a maestro for the Linux host + let linux_host = LinuxHostTopology::new("dev-machine".to_string(), "localhost".to_string()); + let mut linux_maestro = Maestro::new(linux_host); - - // Register a command score that works with any topology having CommandCapability + linux_maestro.register_score(CommandScore::new( "check-disk".to_string(), "df".to_string(), - vec!["-h".to_string()] + vec!["-h".to_string()], )); - + linux_maestro.orchestrate().unwrap(); + // This would fail to compile if we tried to register a K8sResourceScore // because LinuxHostTopology doesn't implement KubernetesCapability - // linux_maestro.register_score(K8sResourceScore::new(...)); - + //linux_maestro.register_score(K8sResourceScore::new( + // "...".to_string(), + // "...".to_string(), + //)); + // Create a K3D topology which has both Command and Kubernetes capabilities - let k3d_host = LinuxHostTopology::new( - "k3d-host".to_string(), - "localhost".to_string() - ); - + let k3d_host = LinuxHostTopology::new("k3d-host".to_string(), "localhost".to_string()); + let k3d_topology = K3DTopology::new( "dev-cluster".to_string(), k3d_host, - "devcluster".to_string() + "devcluster".to_string(), ); - + // Create a maestro for the K3D topology let mut k3d_maestro = Maestro::new(k3d_topology); - + // We can register both command scores and kubernetes scores k3d_maestro.register_score(CommandScore::new( "check-nodes".to_string(), "kubectl".to_string(), - vec!["get".to_string(), "nodes".to_string()] + vec!["get".to_string(), "nodes".to_string()], )); - + k3d_maestro.register_score(K8sResourceScore::new( "deploy-nginx".to_string(), r#" @@ -323,9 +305,10 @@ fn main() { image: nginx:latest ports: - containerPort: 80 - "#.to_string() + "# + .to_string(), )); - + // Orchestrate both topologies linux_maestro.orchestrate().unwrap(); k3d_maestro.orchestrate().unwrap(); From fda007f014b575b20fb4772004f156337128709a Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 26 Mar 2025 23:10:51 -0400 Subject: [PATCH 12/62] feat(topology): generalize Score and Interpret implementations with topology traits Refactor various `Score` and `Interpret` implementations to utilize generic `Topology` traits, removing hardcoded dependencies on `HAClusterTopology`. This enhancement allows for more flexible and extensible code, accommodating different types of network topologies. --- harmony/src/domain/interpret/mod.rs | 11 +++--- harmony/src/domain/maestro/mod.rs | 23 +++++++------ harmony/src/domain/score.rs | 7 ++-- harmony/src/domain/topology/ha_cluster.rs | 16 +++++++-- harmony/src/domain/topology/mod.rs | 6 ++++ harmony/src/domain/topology/network.rs | 17 +++++----- harmony/src/modules/dhcp.rs | 29 ++++++---------- harmony/src/modules/dns.rs | 34 +++++++------------ harmony/src/modules/dummy.rs | 34 ++++++------------- harmony/src/modules/http.rs | 15 +++----- harmony/src/modules/k8s/deployment.rs | 10 ++---- harmony/src/modules/k8s/resource.rs | 16 ++++----- harmony/src/modules/lamp.rs | 16 ++++----- harmony/src/modules/load_balancer.rs | 31 ++++++----------- harmony/src/modules/okd/bootstrap_dhcp.rs | 10 ++---- .../modules/okd/bootstrap_load_balancer.rs | 11 ++---- harmony/src/modules/okd/dhcp.rs | 10 ++---- harmony/src/modules/okd/dns.rs | 10 ++---- harmony/src/modules/okd/load_balancer.rs | 11 ++---- harmony/src/modules/opnsense/shell.rs | 14 +++----- harmony/src/modules/opnsense/upgrade.rs | 9 ++--- harmony/src/modules/tftp.rs | 25 ++++++-------- 22 files changed, 148 insertions(+), 217 deletions(-) diff --git a/harmony/src/domain/interpret/mod.rs b/harmony/src/domain/interpret/mod.rs index 731d663..0268ffd 100644 --- a/harmony/src/domain/interpret/mod.rs +++ b/harmony/src/domain/interpret/mod.rs @@ -7,7 +7,7 @@ use super::{ data::{Id, Version}, executors::ExecutorError, inventory::Inventory, - topology::HAClusterTopology, + topology::Topology, }; pub enum InterpretName { @@ -37,12 +37,9 @@ impl std::fmt::Display for InterpretName { } #[async_trait] -pub trait Interpret: std::fmt::Debug + Send { - async fn execute( - &self, - inventory: &Inventory, - topology: &HAClusterTopology, - ) -> Result; +pub trait Interpret: std::fmt::Debug + Send { + async fn execute(&self, inventory: &Inventory, topology: &T) + -> Result; fn get_name(&self) -> InterpretName; fn get_version(&self) -> Version; fn get_status(&self) -> InterpretStatus; diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index 9f92ec5..ea4ff26 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -6,19 +6,19 @@ use super::{ interpret::{InterpretError, Outcome}, inventory::Inventory, score::Score, - topology::HAClusterTopology, + topology::Topology, }; -type ScoreVec = Vec>; +type ScoreVec = Vec>>; -pub struct Maestro { +pub struct Maestro { inventory: Inventory, - topology: HAClusterTopology, - scores: Arc>, + topology: T, + scores: Arc>>, } -impl Maestro { - pub fn new(inventory: Inventory, topology: HAClusterTopology) -> Self { +impl Maestro { + pub fn new(inventory: Inventory, topology: T) -> Self { Self { inventory, topology, @@ -51,12 +51,15 @@ impl Maestro { info!("Starting Maestro"); } - pub fn register_all(&mut self, mut scores: ScoreVec) { + pub fn register_all(&mut self, mut scores: ScoreVec) { let mut score_mut = self.scores.write().expect("Should acquire lock"); score_mut.append(&mut scores); } - pub async fn interpret(&self, score: Box) -> Result { + pub async fn interpret(&self, score: S) -> Result + where + S: Score, + { info!("Running score {score:?}"); let interpret = score.create_interpret(); info!("Launching interpret {interpret:?}"); @@ -65,7 +68,7 @@ impl Maestro { result } - pub fn scores(&self) -> Arc> { + pub fn scores(&self) -> Arc>> { self.scores.clone() } } diff --git a/harmony/src/domain/score.rs b/harmony/src/domain/score.rs index 7b2c790..672c1e5 100644 --- a/harmony/src/domain/score.rs +++ b/harmony/src/domain/score.rs @@ -1,7 +1,6 @@ -use super::interpret::Interpret; +use super::{interpret::Interpret, topology::Topology}; -pub trait Score: std::fmt::Debug + Send + Sync { - fn create_interpret(&self) -> Box; +pub trait Score: std::fmt::Debug + Send + Sync { + fn create_interpret(&self) -> Box>; fn name(&self) -> String; - fn clone_box(&self) -> Box; } diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index ba7c063..0750b71 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -15,9 +15,11 @@ use super::IpAddress; use super::LoadBalancer; use super::LoadBalancerService; use super::LogicalHost; +use super::OcK8sclient; use super::Router; use super::TftpServer; +use super::Topology; use super::Url; use super::openshift::OpenshiftClient; use std::sync::Arc; @@ -38,11 +40,20 @@ pub struct HAClusterTopology { pub switch: Vec, } -impl HAClusterTopology { - pub async fn oc_client(&self) -> Result, kube::Error> { +impl Topology for HAClusterTopology { + fn name(&self) -> &str { + todo!() + } +} + +#[async_trait] +impl OcK8sclient for HAClusterTopology { + async fn oc_client(&self) -> Result, kube::Error> { Ok(Arc::new(OpenshiftClient::try_default().await?)) } +} +impl HAClusterTopology { pub fn autoload() -> Self { let dummy_infra = Arc::new(DummyInfra {}); let dummy_host = LogicalHost { @@ -67,6 +78,7 @@ impl HAClusterTopology { } } +#[derive(Debug)] struct DummyInfra; const UNIMPLEMENTED_DUMMY_INFRA: &str = "This is a dummy infrastructure, no operation is supported"; diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index 9d5663c..e1c7f7c 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -16,6 +16,12 @@ pub use tftp::*; use std::net::IpAddr; +pub trait Topology { + fn name(&self) -> &str; +} + +pub trait Capability {} + pub type IpAddress = IpAddr; #[derive(Debug, Clone)] diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index f45c87d..523db2f 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -1,11 +1,11 @@ -use std::{net::Ipv4Addr, str::FromStr}; +use std::{net::Ipv4Addr, str::FromStr, sync::Arc}; use async_trait::async_trait; use harmony_types::net::MacAddress; use crate::executors::ExecutorError; -use super::{IpAddress, LogicalHost}; +use super::{openshift::OpenshiftClient, IpAddress, LogicalHost}; #[derive(Debug)] pub struct DHCPStaticEntry { @@ -40,9 +40,14 @@ impl std::fmt::Debug for dyn Firewall { pub struct NetworkDomain { pub name: String, } +#[async_trait] +pub trait OcK8sclient: Send + Sync + std::fmt::Debug { + async fn oc_client(&self) -> Result, kube::Error>; +} + #[async_trait] -pub trait DhcpServer: Send + Sync { +pub trait DhcpServer: Send + Sync + std::fmt::Debug { async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError>; async fn remove_static_mapping(&self, mac: &MacAddress) -> Result<(), ExecutorError>; async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)>; @@ -53,12 +58,6 @@ pub trait DhcpServer: Send + Sync { async fn commit_config(&self) -> Result<(), ExecutorError>; } -impl std::fmt::Debug for dyn DhcpServer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("DhcpServer {}", self.get_ip())) - } -} - #[async_trait] pub trait DnsServer: Send + Sync { async fn register_dhcp_leases(&self, register: bool) -> Result<(), ExecutorError>; diff --git a/harmony/src/modules/dhcp.rs b/harmony/src/modules/dhcp.rs index 604d624..bd332c1 100644 --- a/harmony/src/modules/dhcp.rs +++ b/harmony/src/modules/dhcp.rs @@ -8,7 +8,7 @@ use crate::{ domain::{data::Version, interpret::InterpretStatus}, interpret::{Interpret, InterpretError, InterpretName, Outcome}, inventory::Inventory, - topology::{DHCPStaticEntry, HAClusterTopology, HostBinding, IpAddress}, + topology::{DHCPStaticEntry, DhcpServer, HAClusterTopology, HostBinding, IpAddress, Topology}, }; use crate::domain::score::Score; @@ -20,18 +20,14 @@ pub struct DhcpScore { pub boot_filename: Option, } -impl Score for DhcpScore { - fn create_interpret(&self) -> Box { +impl Score for DhcpScore { + fn create_interpret(&self) -> Box> { Box::new(DhcpInterpret::new(self.clone())) } fn name(&self) -> String { "DhcpScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } // https://docs.opnsense.org/manual/dhcp.html#advanced-settings @@ -52,10 +48,10 @@ impl DhcpInterpret { status: InterpretStatus::QUEUED, } } - async fn add_static_entries( + async fn add_static_entries( &self, _inventory: &Inventory, - topology: &HAClusterTopology, + dhcp_server: &D, ) -> Result { let dhcp_entries: Vec = self .score @@ -78,7 +74,6 @@ impl DhcpInterpret { .collect(); info!("DHCPStaticEntry : {:?}", dhcp_entries); - let dhcp_server = Arc::new(topology.dhcp_server.clone()); info!("DHCP server : {:?}", dhcp_server); let number_new_entries = dhcp_entries.len(); @@ -96,14 +91,13 @@ impl DhcpInterpret { )) } - async fn set_pxe_options( + async fn set_pxe_options( &self, _inventory: &Inventory, - topology: &HAClusterTopology, + dhcp_server: &D, ) -> Result { let next_server_outcome = match self.score.next_server { Some(next_server) => { - let dhcp_server = Arc::new(topology.dhcp_server.clone()); dhcp_server.set_next_server(next_server).await?; Outcome::new( InterpretStatus::SUCCESS, @@ -115,7 +109,6 @@ impl DhcpInterpret { let boot_filename_outcome = match &self.score.boot_filename { Some(boot_filename) => { - let dhcp_server = Arc::new(topology.dhcp_server.clone()); dhcp_server.set_boot_filename(&boot_filename).await?; Outcome::new( InterpretStatus::SUCCESS, @@ -142,7 +135,7 @@ impl DhcpInterpret { } #[async_trait] -impl Interpret for DhcpInterpret { +impl Interpret for DhcpInterpret { fn get_name(&self) -> InterpretName { InterpretName::OPNSenseDHCP } @@ -162,15 +155,15 @@ impl Interpret for DhcpInterpret { async fn execute( &self, inventory: &Inventory, - topology: &HAClusterTopology, + topology: &T, ) -> Result { - info!("Executing {} on inventory {inventory:?}", self.get_name()); + info!("Executing DhcpInterpret on inventory {inventory:?}"); self.set_pxe_options(inventory, topology).await?; self.add_static_entries(inventory, topology).await?; - topology.dhcp_server.commit_config().await?; + topology.commit_config().await?; Ok(Outcome::new( InterpretStatus::SUCCESS, diff --git a/harmony/src/modules/dns.rs b/harmony/src/modules/dns.rs index 79c8870..1002a35 100644 --- a/harmony/src/modules/dns.rs +++ b/harmony/src/modules/dns.rs @@ -7,7 +7,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{DnsRecord, HAClusterTopology}, + topology::{DnsRecord, DnsServer, HAClusterTopology, Topology}, }; #[derive(Debug, new, Clone)] @@ -16,18 +16,14 @@ pub struct DnsScore { register_dhcp_leases: Option, } -impl Score for DnsScore { - fn create_interpret(&self) -> Box { +impl Score for DnsScore { + fn create_interpret(&self) -> Box> { Box::new(DnsInterpret::new(self.clone())) } fn name(&self) -> String { "DnsScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } // https://docs.opnsense.org/manual/dhcp.html#advanced-settings @@ -48,12 +44,11 @@ impl DnsInterpret { status: InterpretStatus::QUEUED, } } - async fn serve_dhcp_entries( + async fn serve_dhcp_entries( &self, _inventory: &Inventory, - topology: &HAClusterTopology, + dns: &T, ) -> Result { - let dns = topology.dns_server.clone(); if let Some(register) = self.score.register_dhcp_leases { dns.register_dhcp_leases(register).await?; } @@ -64,15 +59,12 @@ impl DnsInterpret { )) } - async fn ensure_hosts_registered( + async fn ensure_hosts_registered( &self, - topology: &HAClusterTopology, + dns_server: &D, ) -> Result { let entries = &self.score.dns_entries; - topology - .dns_server - .ensure_hosts_registered(entries.clone()) - .await?; + dns_server.ensure_hosts_registered(entries.clone()).await?; Ok(Outcome::new( InterpretStatus::SUCCESS, @@ -85,7 +77,7 @@ impl DnsInterpret { } #[async_trait] -impl Interpret for DnsInterpret { +impl Interpret for DnsInterpret { fn get_name(&self) -> InterpretName { InterpretName::OPNSenseDns } @@ -105,14 +97,14 @@ impl Interpret for DnsInterpret { async fn execute( &self, inventory: &Inventory, - topology: &HAClusterTopology, + topology: &T, ) -> Result { - info!("Executing {} on inventory {inventory:?}", self.get_name()); + info!("Executing {} on inventory {inventory:?}", >::get_name(self)); self.serve_dhcp_entries(inventory, topology).await?; - self.ensure_hosts_registered(&topology).await?; + self.ensure_hosts_registered(topology).await?; - topology.dns_server.commit_config().await?; + topology.commit_config().await?; Ok(Outcome::new( InterpretStatus::SUCCESS, diff --git a/harmony/src/modules/dummy.rs b/harmony/src/modules/dummy.rs index 0d2c327..99e2f12 100644 --- a/harmony/src/modules/dummy.rs +++ b/harmony/src/modules/dummy.rs @@ -5,7 +5,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::HAClusterTopology, + topology::{HAClusterTopology, Topology}, }; /// Score that always errors. This is only useful for development/testing purposes. It does nothing @@ -13,8 +13,8 @@ use crate::{ #[derive(Debug, Clone)] pub struct ErrorScore; -impl Score for ErrorScore { - fn create_interpret(&self) -> Box { +impl Score for ErrorScore { + fn create_interpret(&self) -> Box> { Box::new(DummyInterpret { result: Err(InterpretError::new("Error Score default error".to_string())), status: InterpretStatus::QUEUED, @@ -24,10 +24,6 @@ impl Score for ErrorScore { fn name(&self) -> String { "ErrorScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } /// Score that always succeeds. This is only useful for development/testing purposes. It does nothing @@ -35,8 +31,8 @@ impl Score for ErrorScore { #[derive(Debug, Clone)] pub struct SuccessScore; -impl Score for SuccessScore { - fn create_interpret(&self) -> Box { +impl Score for SuccessScore { + fn create_interpret(&self) -> Box> { Box::new(DummyInterpret { result: Ok(Outcome::success("SuccessScore default success".to_string())), status: InterpretStatus::QUEUED, @@ -46,10 +42,6 @@ impl Score for SuccessScore { fn name(&self) -> String { "SuccessScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } /// An interpret that only returns the result it is given when built. It does nothing else. Only @@ -61,7 +53,7 @@ struct DummyInterpret { } #[async_trait] -impl Interpret for DummyInterpret { +impl Interpret for DummyInterpret { fn get_name(&self) -> InterpretName { InterpretName::Dummy } @@ -81,7 +73,7 @@ impl Interpret for DummyInterpret { async fn execute( &self, _inventory: &Inventory, - _topology: &HAClusterTopology, + _topology: &T, ) -> Result { self.result.clone() } @@ -92,18 +84,14 @@ impl Interpret for DummyInterpret { #[derive(Debug, Clone)] pub struct PanicScore; -impl Score for PanicScore { - fn create_interpret(&self) -> Box { +impl Score for PanicScore { + fn create_interpret(&self) -> Box> { Box::new(PanicInterpret {}) } fn name(&self) -> String { "PanicScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } /// An interpret that always panics when executed. Useful for development/testing purposes. @@ -111,7 +99,7 @@ impl Score for PanicScore { struct PanicInterpret; #[async_trait] -impl Interpret for PanicInterpret { +impl Interpret for PanicInterpret { fn get_name(&self) -> InterpretName { InterpretName::Panic } @@ -131,7 +119,7 @@ impl Interpret for PanicInterpret { async fn execute( &self, _inventory: &Inventory, - _topology: &HAClusterTopology, + _topology: &T, ) -> Result { panic!("Panic interpret always panics when executed") } diff --git a/harmony/src/modules/http.rs b/harmony/src/modules/http.rs index 51eed3b..1d6df51 100644 --- a/harmony/src/modules/http.rs +++ b/harmony/src/modules/http.rs @@ -6,7 +6,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{HAClusterTopology, Url}, + topology::{HAClusterTopology, HttpServer, Topology, Url}, }; #[derive(Debug, new, Clone)] @@ -14,18 +14,14 @@ pub struct HttpScore { files_to_serve: Url, } -impl Score for HttpScore { - fn create_interpret(&self) -> Box { +impl Score for HttpScore { + fn create_interpret(&self) -> Box> { Box::new(HttpInterpret::new(self.clone())) } fn name(&self) -> String { "HttpScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } #[derive(Debug, new, Clone)] @@ -34,13 +30,12 @@ pub struct HttpInterpret { } #[async_trait] -impl Interpret for HttpInterpret { +impl Interpret for HttpInterpret { async fn execute( &self, _inventory: &Inventory, - topology: &HAClusterTopology, + http_server: &T, ) -> Result { - let http_server = &topology.http_server; http_server.ensure_initialized().await?; // http_server.set_ip(topology.router.get_gateway()).await?; http_server.serve_files(&self.score.files_to_serve).await?; diff --git a/harmony/src/modules/k8s/deployment.rs b/harmony/src/modules/k8s/deployment.rs index de93e3a..4528ed6 100644 --- a/harmony/src/modules/k8s/deployment.rs +++ b/harmony/src/modules/k8s/deployment.rs @@ -1,7 +1,7 @@ use k8s_openapi::api::apps::v1::Deployment; use serde_json::json; -use crate::{interpret::Interpret, score::Score}; +use crate::{interpret::Interpret, score::Score, topology::{OcK8sclient, Topology}}; use super::resource::{K8sResourceInterpret, K8sResourceScore}; @@ -11,8 +11,8 @@ pub struct K8sDeploymentScore { pub image: String, } -impl Score for K8sDeploymentScore { - fn create_interpret(&self) -> Box { +impl Score for K8sDeploymentScore { + fn create_interpret(&self) -> Box> { let deployment: Deployment = serde_json::from_value(json!( { "metadata": { @@ -51,8 +51,4 @@ impl Score for K8sDeploymentScore { fn name(&self) -> String { "K8sDeploymentScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } diff --git a/harmony/src/modules/k8s/resource.rs b/harmony/src/modules/k8s/resource.rs index cf45be8..878da22 100644 --- a/harmony/src/modules/k8s/resource.rs +++ b/harmony/src/modules/k8s/resource.rs @@ -8,7 +8,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::HAClusterTopology, + topology::{HAClusterTopology, OcK8sclient, Topology}, }; #[derive(Debug, Clone)] @@ -34,21 +34,18 @@ impl< + 'static + Send + Clone, -> Score for K8sResourceScore + T: Topology, +> Score for K8sResourceScore where ::DynamicType: Default, { - fn create_interpret(&self) -> Box { + fn create_interpret(&self) -> Box> { todo!() } fn name(&self) -> String { "K8sResourceScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } #[derive(Debug)] @@ -66,14 +63,15 @@ impl< + Default + Send + Sync, -> Interpret for K8sResourceInterpret + T: Topology + OcK8sclient, +> Interpret for K8sResourceInterpret where ::DynamicType: Default, { async fn execute( &self, _inventory: &Inventory, - topology: &HAClusterTopology, + topology: &T, ) -> Result { topology .oc_client() diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index e6b7612..d7cd495 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -8,7 +8,7 @@ use crate::{ inventory::Inventory, modules::k8s::deployment::K8sDeploymentScore, score::Score, - topology::{HAClusterTopology, Url}, + topology::{HAClusterTopology, OcK8sclient, Topology, Url}, }; #[derive(Debug, Clone)] @@ -34,18 +34,14 @@ impl Default for LAMPConfig { } } -impl Score for LAMPScore { - fn create_interpret(&self) -> Box { +impl Score for LAMPScore { + fn create_interpret(&self) -> Box> { todo!() } fn name(&self) -> String { "LampScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } #[derive(Debug)] @@ -54,14 +50,14 @@ pub struct LAMPInterpret { } #[async_trait] -impl Interpret for LAMPInterpret { +impl Interpret for LAMPInterpret { async fn execute( &self, inventory: &Inventory, - topology: &HAClusterTopology, + topology: &T, ) -> Result { let deployment_score = K8sDeploymentScore { - name: self.score.name(), + name: >::name(&self.score), image: "local_image".to_string(), }; diff --git a/harmony/src/modules/load_balancer.rs b/harmony/src/modules/load_balancer.rs index 75318ca..5358e84 100644 --- a/harmony/src/modules/load_balancer.rs +++ b/harmony/src/modules/load_balancer.rs @@ -6,7 +6,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{HAClusterTopology, LoadBalancerService}, + topology::{HAClusterTopology, LoadBalancer, LoadBalancerService, Topology}, }; #[derive(Debug, Clone)] @@ -19,18 +19,14 @@ pub struct LoadBalancerScore { // uuid? } -impl Score for LoadBalancerScore { - fn create_interpret(&self) -> Box { +impl Score for LoadBalancerScore { + fn create_interpret(&self) -> Box> { Box::new(LoadBalancerInterpret::new(self.clone())) } fn name(&self) -> String { "LoadBalancerScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } #[derive(Debug)] @@ -51,37 +47,32 @@ impl LoadBalancerInterpret { } #[async_trait] -impl Interpret for LoadBalancerInterpret { +impl Interpret for LoadBalancerInterpret { async fn execute( &self, _inventory: &Inventory, - topology: &HAClusterTopology, + load_balancer: &T, ) -> Result { info!( "Making sure Load Balancer is initialized: {:?}", - topology.load_balancer.ensure_initialized().await? + load_balancer.ensure_initialized().await? ); for service in self.score.public_services.iter() { info!("Ensuring service exists {service:?}"); - topology - .load_balancer - .ensure_service_exists(service) - .await?; + + load_balancer.ensure_service_exists(service).await?; } for service in self.score.private_services.iter() { info!("Ensuring private service exists {service:?}"); - topology - .load_balancer - .ensure_service_exists(service) - .await?; + load_balancer.ensure_service_exists(service).await?; } info!("Applying load balancer configuration"); - topology.load_balancer.commit_config().await?; + load_balancer.commit_config().await?; info!("Making a full reload and restart of haproxy"); - topology.load_balancer.reload_restart().await?; + load_balancer.reload_restart().await?; Ok(Outcome::success(format!( "Load balancer successfully configured {} services", self.score.public_services.len() + self.score.private_services.len() diff --git a/harmony/src/modules/okd/bootstrap_dhcp.rs b/harmony/src/modules/okd/bootstrap_dhcp.rs index 4f5c6ee..ebb0a9d 100644 --- a/harmony/src/modules/okd/bootstrap_dhcp.rs +++ b/harmony/src/modules/okd/bootstrap_dhcp.rs @@ -3,7 +3,7 @@ use crate::{ inventory::Inventory, modules::dhcp::DhcpScore, score::Score, - topology::{HAClusterTopology, HostBinding}, + topology::{DhcpServer, HAClusterTopology, HostBinding, Topology}, }; #[derive(Debug, Clone)] @@ -46,16 +46,12 @@ impl OKDBootstrapDhcpScore { } } -impl Score for OKDBootstrapDhcpScore { - fn create_interpret(&self) -> Box { +impl Score for OKDBootstrapDhcpScore { + fn create_interpret(&self) -> Box> { self.dhcp_score.create_interpret() } fn name(&self) -> String { "OKDBootstrapDhcpScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } diff --git a/harmony/src/modules/okd/bootstrap_load_balancer.rs b/harmony/src/modules/okd/bootstrap_load_balancer.rs index 4c29026..d983eed 100644 --- a/harmony/src/modules/okd/bootstrap_load_balancer.rs +++ b/harmony/src/modules/okd/bootstrap_load_balancer.rs @@ -5,8 +5,7 @@ use crate::{ modules::load_balancer::LoadBalancerScore, score::Score, topology::{ - BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, - LoadBalancerService, + BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancer, LoadBalancerService, Topology }, }; @@ -69,16 +68,12 @@ impl OKDBootstrapLoadBalancerScore { } } -impl Score for OKDBootstrapLoadBalancerScore { - fn create_interpret(&self) -> Box { +impl Score for OKDBootstrapLoadBalancerScore { + fn create_interpret(&self) -> Box> { self.load_balancer_score.create_interpret() } fn name(&self) -> String { "OKDBootstrapLoadBalancerScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } diff --git a/harmony/src/modules/okd/dhcp.rs b/harmony/src/modules/okd/dhcp.rs index c976f8d..e3e0e09 100644 --- a/harmony/src/modules/okd/dhcp.rs +++ b/harmony/src/modules/okd/dhcp.rs @@ -3,7 +3,7 @@ use crate::{ inventory::Inventory, modules::dhcp::DhcpScore, score::Score, - topology::{HAClusterTopology, HostBinding}, + topology::{DhcpServer, HAClusterTopology, HostBinding, Topology}, }; #[derive(Debug, Clone)] @@ -38,16 +38,12 @@ impl OKDDhcpScore { } } -impl Score for OKDDhcpScore { - fn create_interpret(&self) -> Box { +impl Score for OKDDhcpScore { + fn create_interpret(&self) -> Box> { self.dhcp_score.create_interpret() } fn name(&self) -> String { "OKDDhcpScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } diff --git a/harmony/src/modules/okd/dns.rs b/harmony/src/modules/okd/dns.rs index f4e7f2c..34c2dc2 100644 --- a/harmony/src/modules/okd/dns.rs +++ b/harmony/src/modules/okd/dns.rs @@ -2,7 +2,7 @@ use crate::{ interpret::Interpret, modules::dns::DnsScore, score::Score, - topology::{DnsRecord, DnsRecordType, HAClusterTopology}, + topology::{DnsRecord, DnsRecordType, DnsServer, HAClusterTopology, Topology}, }; #[derive(Debug, Clone)] @@ -40,16 +40,12 @@ impl OKDDnsScore { } } -impl Score for OKDDnsScore { - fn create_interpret(&self) -> Box { +impl Score for OKDDnsScore { + fn create_interpret(&self) -> Box> { self.dns_score.create_interpret() } fn name(&self) -> String { "OKDDnsScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } diff --git a/harmony/src/modules/okd/load_balancer.rs b/harmony/src/modules/okd/load_balancer.rs index 38a5d04..c3b5264 100644 --- a/harmony/src/modules/okd/load_balancer.rs +++ b/harmony/src/modules/okd/load_balancer.rs @@ -5,8 +5,7 @@ use crate::{ modules::load_balancer::LoadBalancerScore, score::Score, topology::{ - BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, - LoadBalancerService, + BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancer, LoadBalancerService, Topology }, }; @@ -80,16 +79,12 @@ impl OKDLoadBalancerScore { } } -impl Score for OKDLoadBalancerScore { - fn create_interpret(&self) -> Box { +impl Score for OKDLoadBalancerScore { + fn create_interpret(&self) -> Box> { self.load_balancer_score.create_interpret() } fn name(&self) -> String { "OKDLoadBalancerScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } diff --git a/harmony/src/modules/opnsense/shell.rs b/harmony/src/modules/opnsense/shell.rs index 00fd131..f4cecad 100644 --- a/harmony/src/modules/opnsense/shell.rs +++ b/harmony/src/modules/opnsense/shell.rs @@ -8,7 +8,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::HAClusterTopology, + topology::{HAClusterTopology, Topology}, }; #[derive(Debug, Clone)] @@ -17,8 +17,8 @@ pub struct OPNsenseShellCommandScore { pub command: String, } -impl Score for OPNsenseShellCommandScore { - fn create_interpret(&self) -> Box { +impl Score for OPNsenseShellCommandScore { + fn create_interpret(&self) -> Box> { Box::new(OPNsenseShellInterpret { status: InterpretStatus::QUEUED, score: self.clone(), @@ -28,10 +28,6 @@ impl Score for OPNsenseShellCommandScore { fn name(&self) -> String { "OPNSenseShellCommandScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } #[derive(Debug)] @@ -41,11 +37,11 @@ pub struct OPNsenseShellInterpret { } #[async_trait] -impl Interpret for OPNsenseShellInterpret { +impl Interpret for OPNsenseShellInterpret { async fn execute( &self, _inventory: &Inventory, - _topology: &HAClusterTopology, + _topology: &T, ) -> Result { let output = self .score diff --git a/harmony/src/modules/opnsense/upgrade.rs b/harmony/src/modules/opnsense/upgrade.rs index 6b0637d..06d373f 100644 --- a/harmony/src/modules/opnsense/upgrade.rs +++ b/harmony/src/modules/opnsense/upgrade.rs @@ -5,6 +5,7 @@ use tokio::sync::RwLock; use crate::{ interpret::{Interpret, InterpretStatus}, score::Score, + topology::Topology, }; use super::{OPNsenseShellCommandScore, OPNsenseShellInterpret}; @@ -14,8 +15,8 @@ pub struct OPNSenseLaunchUpgrade { pub opnsense: Arc>, } -impl Score for OPNSenseLaunchUpgrade { - fn create_interpret(&self) -> Box { +impl Score for OPNSenseLaunchUpgrade { + fn create_interpret(&self) -> Box> { let score = OPNsenseShellCommandScore { opnsense: self.opnsense.clone(), command: "/usr/local/opnsense/scripts/firmware/update.sh".to_string(), @@ -30,8 +31,4 @@ impl Score for OPNSenseLaunchUpgrade { fn name(&self) -> String { "OPNSenseLaunchUpgrade".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } diff --git a/harmony/src/modules/tftp.rs b/harmony/src/modules/tftp.rs index a7c2167..504459a 100644 --- a/harmony/src/modules/tftp.rs +++ b/harmony/src/modules/tftp.rs @@ -6,7 +6,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{HAClusterTopology, Url}, + topology::{HAClusterTopology, Router, TftpServer, Topology, Url}, }; #[derive(Debug, new, Clone)] @@ -14,18 +14,14 @@ pub struct TftpScore { files_to_serve: Url, } -impl Score for TftpScore { - fn create_interpret(&self) -> Box { +impl Score for TftpScore { + fn create_interpret(&self) -> Box> { Box::new(TftpInterpret::new(self.clone())) } fn name(&self) -> String { "TftpScore".to_string() } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } } #[derive(Debug, new, Clone)] @@ -34,18 +30,17 @@ pub struct TftpInterpret { } #[async_trait] -impl Interpret for TftpInterpret { +impl Interpret for TftpInterpret { async fn execute( &self, _inventory: &Inventory, - topology: &HAClusterTopology, + topology: &T, ) -> Result { - let tftp_server = &topology.tftp_server; - tftp_server.ensure_initialized().await?; - tftp_server.set_ip(topology.router.get_gateway()).await?; - tftp_server.serve_files(&self.score.files_to_serve).await?; - tftp_server.commit_config().await?; - tftp_server.reload_restart().await?; + topology.ensure_initialized().await?; + topology.set_ip(topology.get_gateway()).await?; + topology.serve_files(&self.score.files_to_serve).await?; + topology.commit_config().await?; + topology.reload_restart().await?; Ok(Outcome::success(format!( "TFTP Server running and serving files from {}", self.score.files_to_serve From 6e9bf3a4beb9f34aa1ec8fd4915c01b0080eec7b Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Mon, 31 Mar 2025 15:02:41 -0400 Subject: [PATCH 13/62] Add more Topology samples with various architectures --- examples/topology/src/main.rs | 439 ++++++++----------- examples/topology/src/main_claude37_2.rs | 314 ++++++++++++++ examples/topology/src/main_gemini25pro.rs | 369 ++++++++++++++++ examples/topology/src/main_geminifail.rs | 492 ++++++++++++++++++++++ 4 files changed, 1353 insertions(+), 261 deletions(-) create mode 100644 examples/topology/src/main_claude37_2.rs create mode 100644 examples/topology/src/main_gemini25pro.rs create mode 100644 examples/topology/src/main_geminifail.rs diff --git a/examples/topology/src/main.rs b/examples/topology/src/main.rs index 308cdd3..8fc305d 100644 --- a/examples/topology/src/main.rs +++ b/examples/topology/src/main.rs @@ -1,315 +1,232 @@ -use rand::Rng; -use std::process::Command; +// Basic traits from your example +trait Topology {} -pub trait Capability {} - -pub trait CommandCapability: Capability { - fn execute_command(&self, command: &str, args: &[&str]) -> Result; +trait Score: Clone + std::fmt::Debug { + fn get_interpret(&self) -> Box>; + fn name(&self) -> String; } -pub trait KubernetesCapability: Capability { - fn apply_manifest(&self, manifest: &str) -> Result<(), String>; - fn get_resource(&self, resource_type: &str, name: &str) -> Result; +trait Interpret { + fn execute(&self); } -pub trait Topology { - fn name(&self) -> &str; +struct Maestro { + topology: T } -pub trait Score { - fn compile(&self) -> Result>, String>; - fn name(&self) -> &str; -} - -pub struct LinuxHostTopology { - name: String, - host: String, -} - -impl Capability for LinuxHostTopology {} - -impl LinuxHostTopology { - pub fn new(name: String, host: String) -> Self { - Self { name, host } +impl Maestro { + pub fn new(topology: T) -> Self { + Maestro { topology } + } + + pub fn register_score(&self, score: S) { + println!("Registering score: {}", score.name()); + } + + pub fn execute_score(&self, score: S) { + println!("Executing score: {}", score.name()); + score.get_interpret::().execute(); } } -impl Topology for LinuxHostTopology { - fn name(&self) -> &str { - &self.name +// Capability traits - these are used to enforce requirements +trait CommandExecution { + fn execute_command(&self, command: &[String]) -> Result; +} + +trait FileSystem { + fn read_file(&self, path: &str) -> Result; + fn write_file(&self, path: &str, content: &str) -> Result<(), String>; +} + +// A concrete topology implementation +#[derive(Clone, Debug)] +struct LinuxHostTopology { + hostname: String, +} + +impl Topology for LinuxHostTopology {} + +// Implement the capabilities for LinuxHostTopology +impl CommandExecution for LinuxHostTopology { + fn execute_command(&self, command: &[String]) -> Result { + println!("Executing command on {}: {:?}", self.hostname, command); + // In a real implementation, this would use std::process::Command + Ok(format!("Command executed successfully on {}", self.hostname)) } } -impl CommandCapability for LinuxHostTopology { - fn execute_command(&self, command: &str, args: &[&str]) -> Result { - println!("Executing on {}: {} {:?}", self.host, command, args); - // In a real implementation, this would SSH to the host and execute the command - let output = Command::new(command) - .args(args) - .output() - .map_err(|e| e.to_string())?; - - if output.status.success() { - Ok(String::from_utf8_lossy(&output.stdout).to_string()) - } else { - Err(String::from_utf8_lossy(&output.stderr).to_string()) - } +impl FileSystem for LinuxHostTopology { + fn read_file(&self, path: &str) -> Result { + println!("Reading file {} on {}", path, self.hostname); + Ok(format!("Content of {} on {}", path, self.hostname)) } -} - -pub struct K3DTopology { - name: String, - linux_host: LinuxHostTopology, - cluster_name: String, -} - -impl Capability for K3DTopology {} - -impl K3DTopology { - pub fn new(name: String, linux_host: LinuxHostTopology, cluster_name: String) -> Self { - Self { - name, - linux_host, - cluster_name, - } - } -} - -impl Topology for K3DTopology { - fn name(&self) -> &str { - &self.name - } -} - -impl CommandCapability for K3DTopology { - fn execute_command(&self, command: &str, args: &[&str]) -> Result { - self.linux_host.execute_command(command, args) - } -} - -impl KubernetesCapability for K3DTopology { - fn apply_manifest(&self, manifest: &str) -> Result<(), String> { - println!("Applying manifest to K3D cluster '{}'", self.cluster_name); - // Write manifest to a temporary file - //let temp_file = format!("/tmp/manifest-{}.yaml", rand::thread_rng().gen::()); - let temp_file = format!("/tmp/manifest-TODO_RANDOM_NUMBER.yaml"); - - // Use the linux_host directly to avoid capability trait bounds - self.linux_host - .execute_command("bash", &["-c", &format!("cat > {}", temp_file)])?; - - // Apply with kubectl - self.linux_host.execute_command("kubectl", &[ - "--context", - &format!("k3d-{}", self.cluster_name), - "apply", - "-f", - &temp_file, - ])?; + fn write_file(&self, path: &str, content: &str) -> Result<(), String> { + println!("Writing to file {} on {}: {}", path, self.hostname, content); Ok(()) } +} - fn get_resource(&self, resource_type: &str, name: &str) -> Result { - println!( - "Getting resource {}/{} from K3D cluster '{}'", - resource_type, name, self.cluster_name - ); - self.linux_host.execute_command("kubectl", &[ - "--context", - &format!("k3d-{}", self.cluster_name), - "get", - resource_type, - name, - "-o", - "yaml", - ]) +// Another topology that doesn't support command execution +#[derive(Clone, Debug)] +struct BareMetalTopology { + device_id: String, +} + +impl Topology for BareMetalTopology {} + +impl FileSystem for BareMetalTopology { + fn read_file(&self, path: &str) -> Result { + println!("Reading file {} on device {}", path, self.device_id); + Ok(format!("Content of {} on device {}", path, self.device_id)) + } + + fn write_file(&self, path: &str, content: &str) -> Result<(), String> { + println!("Writing to file {} on device {}: {}", path, self.device_id, content); + Ok(()) } } -pub struct CommandScore { +// CommandScore implementation +#[derive(Clone, Debug)] +struct CommandScore { name: String, - command: String, args: Vec, } impl CommandScore { - pub fn new(name: String, command: String, args: Vec) -> Self { - Self { - name, - command, - args, + pub fn new(name: String, args: Vec) -> Self { + CommandScore { name, args } + } +} + +impl Score for CommandScore { + fn get_interpret(&self) -> Box> { + // This is the key part: we constrain T to implement CommandExecution + // If T doesn't implement CommandExecution, this will fail to compile + Box::new(CommandInterpret::::new(self.clone())) + } + + fn name(&self) -> String { + self.name.clone() + } +} + +// CommandInterpret implementation +struct CommandInterpret { + score: CommandScore, + _marker: std::marker::PhantomData, +} + +impl CommandInterpret { + pub fn new(score: CommandScore) -> Self { + CommandInterpret { + score, + _marker: std::marker::PhantomData, } } } -pub trait Interpret { - fn execute(&self, topology: &T) -> Result; -} - -struct CommandInterpret; - -impl Interpret for CommandInterpret -where - T: Topology + CommandCapability, -{ - fn execute(&self, topology: &T) -> Result { - todo!() +impl Interpret for CommandInterpret { + fn execute(&self) { + println!("Command interpret is executing: {:?}", self.score.args); + // In a real implementation, you would call the topology's execute_command method + // topology.execute_command(&self.score.args); } } -impl Score for CommandScore -where - T: Topology + CommandCapability, -{ - fn compile(&self) -> Result>, String> { - Ok(Box::new(CommandInterpret {})) - } - - fn name(&self) -> &str { - &self.name - } -} - - -#[derive(Clone)] -pub struct K8sResourceScore { +// FileScore implementation - a different type of score that requires FileSystem capability +#[derive(Clone, Debug)] +struct FileScore { name: String, - manifest: String, + path: String, + content: Option, } -impl K8sResourceScore { - pub fn new(name: String, manifest: String) -> Self { - Self { name, manifest } +impl FileScore { + pub fn new_read(name: String, path: String) -> Self { + FileScore { name, path, content: None } + } + + pub fn new_write(name: String, path: String, content: String) -> Self { + FileScore { name, path, content: Some(content) } } } -struct K8sResourceInterpret { - score: K8sResourceScore, -} - -impl Interpret for K8sResourceInterpret { - fn execute(&self, topology: &T) -> Result { - todo!() +impl Score for FileScore { + fn get_interpret(&self) -> Box> { + // This constrains T to implement FileSystem + Box::new(FileInterpret::::new(self.clone())) + } + + fn name(&self) -> String { + self.name.clone() } } -impl Score for K8sResourceScore -where - T: Topology + KubernetesCapability, -{ - fn compile(&self) -> Result + 'static)>, String> { - Ok(Box::new(K8sResourceInterpret { - score: self.clone(), - })) - } - - fn name(&self) -> &str { - &self.name - } +// FileInterpret implementation +struct FileInterpret { + score: FileScore, + _marker: std::marker::PhantomData, } -pub struct Maestro { - topology: T, - scores: Vec>>, -} - - -impl Maestro { - pub fn new(topology: T) -> Self { - Self { - topology, - scores: Vec::new(), +impl FileInterpret { + pub fn new(score: FileScore) -> Self { + FileInterpret { + score, + _marker: std::marker::PhantomData, } } +} - pub fn register_score(&mut self, score: S) - where - S: Score + 'static, - { - println!( - "Registering score '{}' for topology '{}'", - score.name(), - self.topology.name() - ); - self.scores.push(Box::new(score)); - } - - pub fn orchestrate(&self) -> Result<(), String> { - println!("Orchestrating topology '{}'", self.topology.name()); - for score in &self.scores { - let interpret = score.compile()?; - interpret.execute(&self.topology)?; +impl Interpret for FileInterpret { + fn execute(&self) { + match &self.score.content { + Some(content) => { + println!("File interpret is writing to {}: {}", self.score.path, content); + // In a real implementation: topology.write_file(&self.score.path, content); + }, + None => { + println!("File interpret is reading from {}", self.score.path); + // In a real implementation: let content = topology.read_file(&self.score.path); + } } - Ok(()) } } fn main() { - let linux_host = LinuxHostTopology::new("dev-machine".to_string(), "localhost".to_string()); - - let mut linux_maestro = Maestro::new(linux_host); - - linux_maestro.register_score(CommandScore::new( - "check-disk".to_string(), - "df".to_string(), - vec!["-h".to_string()], - )); - linux_maestro.orchestrate().unwrap(); - - // This would fail to compile if we tried to register a K8sResourceScore - // because LinuxHostTopology doesn't implement KubernetesCapability - //linux_maestro.register_score(K8sResourceScore::new( - // "...".to_string(), - // "...".to_string(), - //)); - - // Create a K3D topology which has both Command and Kubernetes capabilities - let k3d_host = LinuxHostTopology::new("k3d-host".to_string(), "localhost".to_string()); - - let k3d_topology = K3DTopology::new( - "dev-cluster".to_string(), - k3d_host, - "devcluster".to_string(), + // Create our topologies + let linux = LinuxHostTopology { hostname: "server1.example.com".to_string() }; + let bare_metal = BareMetalTopology { device_id: "device001".to_string() }; + + // Create our maestros + let linux_maestro = Maestro::new(linux); + let bare_metal_maestro = Maestro::new(bare_metal); + + // Create scores + let command_score = CommandScore::new( + "List Files".to_string(), + vec!["ls".to_string(), "-la".to_string()] ); - - // Create a maestro for the K3D topology - let mut k3d_maestro = Maestro::new(k3d_topology); - - // We can register both command scores and kubernetes scores - k3d_maestro.register_score(CommandScore::new( - "check-nodes".to_string(), - "kubectl".to_string(), - vec!["get".to_string(), "nodes".to_string()], - )); - - k3d_maestro.register_score(K8sResourceScore::new( - "deploy-nginx".to_string(), - r#" - apiVersion: apps/v1 - kind: Deployment - metadata: - name: nginx - spec: - replicas: 1 - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:latest - ports: - - containerPort: 80 - "# - .to_string(), - )); - - // Orchestrate both topologies - linux_maestro.orchestrate().unwrap(); - k3d_maestro.orchestrate().unwrap(); + + let file_read_score = FileScore::new_read( + "Read Config".to_string(), + "/etc/config.json".to_string() + ); + + // This will work because LinuxHostTopology implements CommandExecution + linux_maestro.execute_score(command_score.clone()); + + // This will work because LinuxHostTopology implements FileSystem + linux_maestro.execute_score(file_read_score.clone()); + + // This will work because BareMetalTopology implements FileSystem + bare_metal_maestro.execute_score(file_read_score); + + // This would NOT compile because BareMetalTopology doesn't implement CommandExecution: + // bare_metal_maestro.execute_score(command_score); + // The error would occur at compile time, ensuring type safety + + println!("All scores executed successfully!"); } diff --git a/examples/topology/src/main_claude37_2.rs b/examples/topology/src/main_claude37_2.rs new file mode 100644 index 0000000..d1b7896 --- /dev/null +++ b/examples/topology/src/main_claude37_2.rs @@ -0,0 +1,314 @@ +mod main_gemini25pro; +use std::process::Command; + +pub trait Capability {} + +pub trait CommandCapability: Capability { + fn execute_command(&self, command: &str, args: &[&str]) -> Result; +} + +pub trait KubernetesCapability: Capability { + fn apply_manifest(&self, manifest: &str) -> Result<(), String>; + fn get_resource(&self, resource_type: &str, name: &str) -> Result; +} + +pub trait Topology { + fn name(&self) -> &str; +} + +pub trait Score { + fn compile(&self) -> Result>, String>; + fn name(&self) -> &str; +} + +pub struct LinuxHostTopology { + name: String, + host: String, +} + +impl Capability for LinuxHostTopology {} + +impl LinuxHostTopology { + pub fn new(name: String, host: String) -> Self { + Self { name, host } + } +} + +impl Topology for LinuxHostTopology { + fn name(&self) -> &str { + &self.name + } +} + +impl CommandCapability for LinuxHostTopology { + fn execute_command(&self, command: &str, args: &[&str]) -> Result { + println!("Executing on {}: {} {:?}", self.host, command, args); + // In a real implementation, this would SSH to the host and execute the command + let output = Command::new(command) + .args(args) + .output() + .map_err(|e| e.to_string())?; + + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + Err(String::from_utf8_lossy(&output.stderr).to_string()) + } + } +} + +pub struct K3DTopology { + name: String, + linux_host: LinuxHostTopology, + cluster_name: String, +} + +impl Capability for K3DTopology {} + +impl K3DTopology { + pub fn new(name: String, linux_host: LinuxHostTopology, cluster_name: String) -> Self { + Self { + name, + linux_host, + cluster_name, + } + } +} + +impl Topology for K3DTopology { + fn name(&self) -> &str { + &self.name + } +} + +impl CommandCapability for K3DTopology { + fn execute_command(&self, command: &str, args: &[&str]) -> Result { + self.linux_host.execute_command(command, args) + } +} + +impl KubernetesCapability for K3DTopology { + fn apply_manifest(&self, manifest: &str) -> Result<(), String> { + println!("Applying manifest to K3D cluster '{}'", self.cluster_name); + // Write manifest to a temporary file + let temp_file = format!("/tmp/manifest-harmony-temp.yaml"); + + // Use the linux_host directly to avoid capability trait bounds + self.linux_host + .execute_command("bash", &["-c", &format!("cat > {}", temp_file)])?; + + // Apply with kubectl + self.linux_host.execute_command("kubectl", &[ + "--context", + &format!("k3d-{}", self.cluster_name), + "apply", + "-f", + &temp_file, + ])?; + + Ok(()) + } + + fn get_resource(&self, resource_type: &str, name: &str) -> Result { + println!( + "Getting resource {}/{} from K3D cluster '{}'", + resource_type, name, self.cluster_name + ); + self.linux_host.execute_command("kubectl", &[ + "--context", + &format!("k3d-{}", self.cluster_name), + "get", + resource_type, + name, + "-o", + "yaml", + ]) + } +} + +pub struct CommandScore { + name: String, + command: String, + args: Vec, +} + +impl CommandScore { + pub fn new(name: String, command: String, args: Vec) -> Self { + Self { + name, + command, + args, + } + } +} + +pub trait Interpret { + fn execute(&self, topology: &T) -> Result; +} + +struct CommandInterpret; + +impl Interpret for CommandInterpret +where + T: Topology + CommandCapability, +{ + fn execute(&self, topology: &T) -> Result { + todo!() + } +} + +impl Score for CommandScore +where + T: Topology + CommandCapability, +{ + fn compile(&self) -> Result>, String> { + Ok(Box::new(CommandInterpret {})) + } + + fn name(&self) -> &str { + &self.name + } +} + + +#[derive(Clone)] +pub struct K8sResourceScore { + name: String, + manifest: String, +} + +impl K8sResourceScore { + pub fn new(name: String, manifest: String) -> Self { + Self { name, manifest } + } +} + +struct K8sResourceInterpret { + score: K8sResourceScore, +} + +impl Interpret for K8sResourceInterpret { + fn execute(&self, topology: &T) -> Result { + todo!() + } +} + +impl Score for K8sResourceScore +where + T: Topology + KubernetesCapability, +{ + fn compile(&self) -> Result + 'static)>, String> { + Ok(Box::new(K8sResourceInterpret { + score: self.clone(), + })) + } + + fn name(&self) -> &str { + &self.name + } +} + +pub struct Maestro { + topology: T, + scores: Vec>>, +} + + +impl Maestro { + pub fn new(topology: T) -> Self { + Self { + topology, + scores: Vec::new(), + } + } + + pub fn register_score(&mut self, score: S) + where + S: Score + 'static, + { + println!( + "Registering score '{}' for topology '{}'", + score.name(), + self.topology.name() + ); + self.scores.push(Box::new(score)); + } + + pub fn orchestrate(&self) -> Result<(), String> { + println!("Orchestrating topology '{}'", self.topology.name()); + for score in &self.scores { + let interpret = score.compile()?; + interpret.execute(&self.topology)?; + } + Ok(()) + } +} + +fn main() { + let linux_host = LinuxHostTopology::new("dev-machine".to_string(), "localhost".to_string()); + + let mut linux_maestro = Maestro::new(linux_host); + + linux_maestro.register_score(CommandScore::new( + "check-disk".to_string(), + "df".to_string(), + vec!["-h".to_string()], + )); + linux_maestro.orchestrate().unwrap(); + + // This would fail to compile if we tried to register a K8sResourceScore + // because LinuxHostTopology doesn't implement KubernetesCapability + //linux_maestro.register_score(K8sResourceScore::new( + // "...".to_string(), + // "...".to_string(), + //)); + + // Create a K3D topology which has both Command and Kubernetes capabilities + let k3d_host = LinuxHostTopology::new("k3d-host".to_string(), "localhost".to_string()); + + let k3d_topology = K3DTopology::new( + "dev-cluster".to_string(), + k3d_host, + "devcluster".to_string(), + ); + + // Create a maestro for the K3D topology + let mut k3d_maestro = Maestro::new(k3d_topology); + + // We can register both command scores and kubernetes scores + k3d_maestro.register_score(CommandScore::new( + "check-nodes".to_string(), + "kubectl".to_string(), + vec!["get".to_string(), "nodes".to_string()], + )); + + k3d_maestro.register_score(K8sResourceScore::new( + "deploy-nginx".to_string(), + r#" + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx + spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:latest + ports: + - containerPort: 80 + "# + .to_string(), + )); + + // Orchestrate both topologies + linux_maestro.orchestrate().unwrap(); + k3d_maestro.orchestrate().unwrap(); +} diff --git a/examples/topology/src/main_gemini25pro.rs b/examples/topology/src/main_gemini25pro.rs new file mode 100644 index 0000000..d173d83 --- /dev/null +++ b/examples/topology/src/main_gemini25pro.rs @@ -0,0 +1,369 @@ +// Import necessary items (though for this example, few are needed beyond std) +use std::fmt; + +// --- Error Handling --- +// A simple error type for demonstration purposes. In a real app, use `thiserror` or `anyhow`. +#[derive(Debug)] +enum OrchestrationError { + CommandFailed(String), + KubeClientError(String), + TopologySetupFailed(String), +} + +impl fmt::Display for OrchestrationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + OrchestrationError::CommandFailed(e) => write!(f, "Command execution failed: {}", e), + OrchestrationError::KubeClientError(e) => write!(f, "Kubernetes client error: {}", e), + OrchestrationError::TopologySetupFailed(e) => write!(f, "Topology setup failed: {}", e), + } + } +} + +impl std::error::Error for OrchestrationError {} + +// Define a common Result type +type Result = std::result::Result>; + +// --- 1. Capability Specification (as Traits) --- + +/// Capability trait representing the ability to run Linux commands. +/// This follows the "Parse, Don't Validate" idea implicitly - if you have an object +/// implementing this, you know you *can* run commands, no need to check later. +trait LinuxOperations { + fn run_command(&self, command: &str) -> Result; +} + +/// A mock Kubernetes client trait for demonstration. +trait KubeClient { + fn apply_manifest(&self, manifest: &str) -> Result<()>; + fn get_pods(&self, namespace: &str) -> Result>; +} + +/// Mock implementation of a KubeClient. +struct MockKubeClient { + cluster_name: String, +} + +impl KubeClient for MockKubeClient { + fn apply_manifest(&self, manifest: &str) -> Result<()> { + println!( + "[{}] Applying Kubernetes manifest:\n---\n{}\n---", + self.cluster_name, manifest + ); + // Simulate success or failure + if manifest.contains("invalid") { + Err(Box::new(OrchestrationError::KubeClientError( + "Invalid manifest content".into(), + ))) + } else { + Ok(()) + } + } + fn get_pods(&self, namespace: &str) -> Result> { + println!( + "[{}] Getting pods in namespace '{}'", + self.cluster_name, namespace + ); + Ok(vec![ + format!("pod-a-12345-{}-{}", namespace, self.cluster_name), + format!("pod-b-67890-{}-{}", namespace, self.cluster_name), + ]) + } +} + +/// Capability trait representing access to a Kubernetes cluster. +/// This follows Rust Embedded WG's "Zero-Cost Abstractions" - the trait itself +/// adds no runtime overhead, only compile-time structure. +trait KubernetesCluster { + // Provides access to a Kubernetes client instance. + // Using `impl Trait` in return position for flexibility. + fn get_kube_client(&self) -> Result; +} + +// --- 2. Topology Implementations --- +// Topologies implement the capabilities they provide. + +/// Represents a basic Linux host. +#[derive(Debug, Clone)] +struct LinuxHostTopology { + hostname: String, + // In a real scenario: SSH connection details, etc. +} + +impl LinuxHostTopology { + fn new(hostname: &str) -> Self { + println!("Initializing LinuxHostTopology for {}", hostname); + Self { + hostname: hostname.to_string(), + } + } +} + +// LinuxHostTopology provides LinuxOperations capability. +impl LinuxOperations for LinuxHostTopology { + fn run_command(&self, command: &str) -> Result { + println!("[{}] Running command: '{}'", self.hostname, command); + // Simulate command execution (e.g., via SSH) + if command.starts_with("fail") { + Err(Box::new(OrchestrationError::CommandFailed(format!( + "Command '{}' failed", + command + )))) + } else { + Ok(format!("Output of '{}' on {}", command, self.hostname)) + } + } +} + +/// Represents a K3D (Kubernetes in Docker) cluster running on a host. +#[derive(Debug, Clone)] +struct K3DTopology { + cluster_name: String, + host_os: String, // Example: might implicitly run commands on the underlying host + // In a real scenario: Kubeconfig path, Docker client, etc. +} + +impl K3DTopology { + fn new(cluster_name: &str) -> Self { + println!("Initializing K3DTopology for cluster {}", cluster_name); + Self { + cluster_name: cluster_name.to_string(), + host_os: "Linux".to_string(), // Assume k3d runs on Linux for this example + } + } +} + +// K3DTopology provides KubernetesCluster capability. +impl KubernetesCluster for K3DTopology { + fn get_kube_client(&self) -> Result { + println!("[{}] Creating mock Kubernetes client", self.cluster_name); + // In a real scenario, this would initialize a client using kubeconfig etc. + Ok(MockKubeClient { + cluster_name: self.cluster_name.clone(), + }) + } +} + +// K3DTopology *also* provides LinuxOperations (e.g., for running commands inside nodes or on the host managing k3d). +impl LinuxOperations for K3DTopology { + fn run_command(&self, command: &str) -> Result { + println!( + "[{} on {} host] Running command: '{}'", + self.cluster_name, self.host_os, command + ); + // Simulate command execution (maybe `docker exec` or similar) + if command.starts_with("fail") { + Err(Box::new(OrchestrationError::CommandFailed(format!( + "Command '{}' failed within k3d context", + command + )))) + } else { + Ok(format!( + "Output of '{}' within k3d cluster {}", + command, self.cluster_name + )) + } + } +} + +// --- 3. Score Implementations --- +// Scores require capabilities via trait bounds on their execution logic. + +/// Base trait for identifying scores. Could be empty or hold metadata. +trait Score { + fn name(&self) -> &'static str; + // We don't put execute here, as its signature depends on required capabilities. +} + +/// A score that runs a shell command on a Linux host. +#[derive(Debug)] +struct CommandScore { + command: String, +} + +impl Score for CommandScore { + fn name(&self) -> &'static str { + "CommandScore" + } +} + +impl CommandScore { + fn new(command: &str) -> Self { + Self { + command: command.to_string(), + } + } + + /// Execute method is generic over T, but requires T implements LinuxOperations. + /// This follows the "Scores as Polymorphic Functions" idea. + fn execute(&self, topology: &T) -> Result<()> { + println!("Executing Score: {}", Score::name(self)); + let output = topology.run_command(&self.command)?; + println!("Command Score Output: {}", output); + Ok(()) + } +} + +/// A score that applies a Kubernetes resource manifest. +#[derive(Debug)] +struct K8sResourceScore { + manifest_path: String, // Path or content +} + +impl Score for K8sResourceScore { + fn name(&self) -> &'static str { + "K8sResourceScore" + } +} + +impl K8sResourceScore { + fn new(manifest_path: &str) -> Self { + Self { + manifest_path: manifest_path.to_string(), + } + } + + /// Execute method requires T implements KubernetesCluster. + fn execute(&self, topology: &T) -> Result<()> { + println!("Executing Score: {}", Score::name(self)); + let client = topology.get_kube_client()?; + let manifest_content = format!( + "apiVersion: v1\nkind: Pod\nmetadata:\n name: my-pod-from-{}", + self.manifest_path + ); // Simulate reading file + client.apply_manifest(&manifest_content)?; + println!( + "K8s Resource Score applied manifest: {}", + self.manifest_path + ); + Ok(()) + } +} + +// --- 4. Maestro (The Orchestrator) --- + +// This version of Maestro uses a helper trait (`ScoreRunner`) to enable +// storing heterogeneous scores while preserving compile-time checks. + +/// A helper trait to erase the specific capability requirements *after* +/// the compiler has verified them, allowing storage in a Vec. +/// The verification happens in the blanket impls below. +trait ScoreRunner { + // T is the concrete Topology type + fn run(&self, topology: &T) -> Result<()>; + fn name(&self) -> &'static str; +} + +// Blanket implementation: A CommandScore can be run on any Topology T +// *if and only if* T implements LinuxOperations. +// The compiler checks this bound when `add_score` is called. +impl ScoreRunner for CommandScore { + fn run(&self, topology: &T) -> Result<()> { + self.execute(topology) // Call the capability-specific execute method + } + fn name(&self) -> &'static str { + Score::name(self) + } +} + +// Blanket implementation: A K8sResourceScore can be run on any Topology T +// *if and only if* T implements KubernetesCluster. +impl ScoreRunner for K8sResourceScore { + fn run(&self, topology: &T) -> Result<()> { + self.execute(topology) // Call the capability-specific execute method + } + fn name(&self) -> &'static str { + Score::name(self) + } +} + +/// The Maestro orchestrator, strongly typed to a specific Topology `T`. +struct Maestro { + topology: T, + // Stores type-erased runners, but addition is type-safe. + scores: Vec>>, +} + +impl Maestro { + /// Creates a new Maestro instance bound to a specific topology. + fn new(topology: T) -> Self { + println!("Maestro initialized."); + Maestro { + topology, + scores: Vec::new(), + } + } + + /// Adds a score to the Maestro. + /// **Compile-time check happens here!** + /// The `S: ScoreRunner` bound ensures that the score `S` provides an + /// implementation of `ScoreRunner` *for the specific topology type `T`*. + /// The blanket impls above ensure this is only possible if `T` has the + /// required capabilities for `S`. + /// This directly follows the "Theoretical Example: The Compiler as an Ally". + fn add_score(&mut self, score: S) + where + S: Score + ScoreRunner + 'static, // S must be runnable on *this* T + { + println!("Registering score: {}", Score::name(&score)); + self.scores.push(Box::new(score)); + } + + /// Runs all registered scores sequentially on the topology. + fn run_all(&self) -> Vec> { + println!("\n--- Running all scores ---"); + self.scores + .iter() + .map(|score_runner| { + println!("---"); + let result = score_runner.run(&self.topology); + match &result { + Ok(_) => println!("Score '{}' completed successfully.", score_runner.name()), + Err(e) => eprintln!("Score '{}' failed: {}", score_runner.name(), e), + } + result + }) + .collect() + } +} + +// --- 5. Example Usage --- + +fn main() { + println!("=== Scenario 1: Linux Host Topology ==="); + let linux_host = LinuxHostTopology::new("server1.example.com"); + let mut maestro_linux = Maestro::new(linux_host); + + // Add scores compatible with LinuxHostTopology (which has LinuxOperations) + maestro_linux.add_score(CommandScore::new("uname -a")); + maestro_linux.add_score(CommandScore::new("ls -l /tmp")); + + // *** Compile-time Error Example *** + // Try adding a score that requires KubernetesCluster capability. + // This line WILL NOT COMPILE because LinuxHostTopology does not implement KubernetesCluster, + // therefore K8sResourceScore does not implement ScoreRunner. + // maestro_linux.add_score(K8sResourceScore::new("my-app.yaml")); + // Uncomment the line above to see the compiler error! The error message will + // likely point to the `ScoreRunner` bound not being satisfied + // for `K8sResourceScore`. + + let results_linux = maestro_linux.run_all(); + println!("\nLinux Host Results: {:?}", results_linux); + + println!("\n=== Scenario 2: K3D Topology ==="); + let k3d_cluster = K3DTopology::new("dev-cluster"); + let mut maestro_k3d = Maestro::new(k3d_cluster); + + // Add scores compatible with K3DTopology (which has LinuxOperations AND KubernetesCluster) + maestro_k3d.add_score(CommandScore::new("pwd")); // Uses LinuxOperations + maestro_k3d.add_score(K8sResourceScore::new("nginx-deployment.yaml")); // Uses KubernetesCluster + maestro_k3d.add_score(K8sResourceScore::new("invalid-service.yaml")); // Test error case + maestro_k3d.add_score(CommandScore::new("fail please")); // Test error case + + let results_k3d = maestro_k3d.run_all(); + println!("\nK3D Cluster Results: {:?}", results_k3d); + + println!("\n=== Compile-Time Safety Demonstrated ==="); + println!("(Check the commented-out line in the code for the compile error example)"); +} diff --git a/examples/topology/src/main_geminifail.rs b/examples/topology/src/main_geminifail.rs new file mode 100644 index 0000000..938d976 --- /dev/null +++ b/examples/topology/src/main_geminifail.rs @@ -0,0 +1,492 @@ +use std::any::Any; +use std::fmt::Debug; +use std::process::Command; +pub trait Capability {} + +pub trait CommandCapability: Capability { + fn execute_command(&self, command: &str, args: &Vec) -> Result; +} + +pub trait KubernetesCapability: Capability { + fn apply_manifest(&self, manifest: &str) -> Result<(), String>; + fn get_resource(&self, resource_type: &str, name: &str) -> Result; +} + +pub trait Topology { + fn name(&self) -> &str; +} + +pub trait Interpret { + fn execute(&self, topology: &T) -> Result; +} + +// --- Score Definition Structs (Concrete) --- +// CommandScore struct remains the same +#[derive(Debug, Clone)] // Added Debug/Clone for easier handling +pub struct CommandScore { + name: String, + command: String, + args: Vec, +} + +impl CommandScore { + pub fn new(name: String, command: String, args: Vec) -> Self { + Self { name, command, args } + } +} + +// K8sResourceScore struct remains the same +#[derive(Debug, Clone)] +pub struct K8sResourceScore { + name: String, + manifest: String, +} + +impl K8sResourceScore { + pub fn new(name: String, manifest: String) -> Self { + Self { name, manifest } + } +} + + +// --- Metadata / Base Score Trait (Non-Generic) --- +// Trait for common info and enabling downcasting later if needed +pub trait ScoreDefinition: Debug + Send + Sync { + fn name(&self) -> &str; + // Method to allow downcasting + fn as_any(&self) -> &dyn Any; + // Optional: Could add methods for description, parameters etc. + // fn description(&self) -> &str; + + // Optional but potentially useful: A way to clone the definition + fn box_clone(&self) -> Box; +} + +// Implement Clone for Box +impl Clone for Box { + fn clone(&self) -> Self { + self.box_clone() + } +} + +// Implement ScoreDefinition for your concrete score types +impl ScoreDefinition for CommandScore { + fn name(&self) -> &str { + &self.name + } + fn as_any(&self) -> &dyn Any { + self + } + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +impl ScoreDefinition for K8sResourceScore { + fn name(&self) -> &str { + &self.name + } + fn as_any(&self) -> &dyn Any { + self + } + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} + + +// --- Score Compatibility Trait (Generic over T) --- +// This remains largely the same, ensuring compile-time checks +pub trait Score: ScoreDefinition { + // No need for name() here, it's in ScoreDefinition + fn compile(&self) -> Result>, String>; +} + +// --- Implementations of Score (Crucial Link) --- + +// CommandScore implements Score for any T with CommandCapability +impl Score for CommandScore +where + T: Topology + CommandCapability + 'static, // Added 'static bound often needed for Box + // Self: ScoreDefinition // This bound is implicit now +{ + fn compile(&self) -> Result>, String> { + // Pass necessary data from self to CommandInterpret + Ok(Box::new(CommandInterpret { + command: self.command.clone(), + args: self.args.clone(), + })) + } +} + +// K8sResourceScore implements Score for any T with KubernetesCapability +impl Score for K8sResourceScore +where + T: Topology + KubernetesCapability + 'static, + // Self: ScoreDefinition +{ + fn compile(&self) -> Result>, String> { + Ok(Box::new(K8sResourceInterpret { + manifest: self.manifest.clone(), // Pass needed data + })) + } +} + + +// --- Interpret Implementations --- +// Need to hold the actual data now + +struct CommandInterpret { + command: String, + args: Vec, // Or owned Strings if lifetime is tricky +} + +impl<'a, T> Interpret for CommandInterpret +where + T: Topology + CommandCapability, +{ + fn execute(&self, topology: &T) -> Result { + // Now uses data stored in self + topology.execute_command(&self.command, &self.args) + } +} + +struct K8sResourceInterpret { + manifest: String, +} + +impl Interpret for K8sResourceInterpret { + fn execute(&self, topology: &T) -> Result { + topology.apply_manifest(&self.manifest)?; + // apply_manifest returns Result<(), String>, adapt if needed + Ok(format!("Applied manifest for {}", topology.name())) // Example success message + } +} + +// --- Maestro --- +// Maestro remains almost identical, leveraging the Score bound +pub struct Maestro { + topology: T, + // Stores Score trait objects, ensuring compatibility + scores: Vec>>, +} + +impl Maestro { // Often need T: 'static here + pub fn new(topology: T) -> Self { + Self { + topology, + scores: Vec::new(), + } + } + + // This method signature is key - it takes a concrete S + // and the compiler checks if S implements Score + pub fn register_score(&mut self, score: S) -> Result<(), String> + where + S: Score + ScoreDefinition + Clone + 'static, // Ensure S is a Score for *this* T + // We might need S: Clone if we want to store Box::new(score) + // Alternatively, accept Box and try to downcast/wrap + { + println!( + "Registering score '{}' for topology '{}'", + score.name(), + self.topology.name() + ); + // The compiler has already guaranteed that S implements Score + // We need to box it as dyn Score + self.scores.push(Box::new(score)); + Ok(()) + } + + // Alternative registration if you have Box + pub fn register_score_definition(&mut self, score_def: Box) -> Result<(), String> + where + T: Topology + CommandCapability + KubernetesCapability + 'static, // Example: list all needed caps here, or use generics + downcasting + { + println!( + "Attempting to register score '{}' for topology '{}'", + score_def.name(), + self.topology.name() + ); + + // Downcast to check concrete type and then check compatibility + if let Some(cs) = score_def.as_any().downcast_ref::() { + // Check if T satisfies CommandScore's requirements (CommandCapability) + // This check is somewhat manual or needs restructuring if we avoid listing all caps + // A simpler way is to just try to create the Box> + let boxed_score: Box> = Box::new(cs.clone()); // This relies on the blanket impls + self.scores.push(boxed_score); + Ok(()) + } else if let Some(ks) = score_def.as_any().downcast_ref::() { + // Check if T satisfies K8sResourceScore's requirements (KubernetesCapability) + let boxed_score: Box> = Box::new(ks.clone()); + self.scores.push(boxed_score); + Ok(()) + } else { + Err(format!("Score '{}' is of an unknown type or incompatible", score_def.name())) + } + // This downcasting approach in Maestro slightly undermines the full compile-time + // check unless designed carefully. The generic `register_score>` is safer. + } + + + pub fn orchestrate(&self) -> Result<(), String> { + println!("Orchestrating topology '{}'", self.topology.name()); + for score in &self.scores { + println!("Compiling score '{}'", score.name()); // Use name() from ScoreDefinition + let interpret = score.compile()?; + println!("Executing score '{}'", score.name()); + interpret.execute(&self.topology)?; + } + Ok(()) + } +} + +// --- TUI Example --- +struct ScoreItem { + // Holds the definition/metadata, NOT the Score trait object + definition: Box, +} + +struct HarmonyTui { + // List of available score *definitions* + available_scores: Vec, + // Example: Maybe maps topology names to Maestros + // maestros: HashMap>, // Storing Maestros generically is another challenge! +} + +impl HarmonyTui { + fn new() -> Self { + HarmonyTui { available_scores: vec![] } + } + + fn add_available_score(&mut self, score_def: Box) { + self.available_scores.push(ScoreItem { definition: score_def }); + } + + fn display_scores(&self) { + println!("Available Scores:"); + for (i, item) in self.available_scores.iter().enumerate() { + println!("{}: {}", i, item.definition.name()); + } + } + + fn execute_score(&self, score: ScoreItem) { + score.definition. + + } + + // Example: Function to add a selected score to a specific Maestro + // This function would need access to the Maestros and handle the types + fn add_selected_score_to_maestro( + &self, + score_index: usize, + maestro: &mut Maestro + ) -> Result<(), String> + where + T: Topology + CommandCapability + KubernetesCapability + 'static, // Adjust bounds as needed + { + let score_item = self.available_scores.get(score_index) + .ok_or("Invalid score index")?; + + // We have Box, need to add to Maestro + // Easiest is to downcast and call the generic register_score + + if let Some(cs) = score_item.definition.as_any().downcast_ref::() { + // Compiler checks if CommandScore: Score via register_score's bound + maestro.register_score(cs.clone())?; + Ok(()) + } else if let Some(ks) = score_item.definition.as_any().downcast_ref::() { + // Compiler checks if K8sResourceScore: Score via register_score's bound + maestro.register_score(ks.clone())?; + Ok(()) + } else { + Err(format!("Cannot add score '{}': Unknown type or check Maestro compatibility", score_item.definition.name())) + } + } +} + +pub struct K3DTopology { + name: String, + linux_host: LinuxHostTopology, + cluster_name: String, +} + +impl Capability for K3DTopology {} + +impl K3DTopology { + pub fn new(name: String, linux_host: LinuxHostTopology, cluster_name: String) -> Self { + Self { + name, + linux_host, + cluster_name, + } + } +} + +impl Topology for K3DTopology { + fn name(&self) -> &str { + &self.name + } +} + +impl CommandCapability for K3DTopology { + fn execute_command(&self, command: &str, args: &Vec) -> Result { + self.linux_host.execute_command(command, args) + } +} + +impl KubernetesCapability for K3DTopology { + fn apply_manifest(&self, manifest: &str) -> Result<(), String> { + println!("Applying manifest to K3D cluster '{}'", self.cluster_name); + // Write manifest to a temporary file + let temp_file = format!("/tmp/manifest-harmony-temp.yaml"); + + // Use the linux_host directly to avoid capability trait bounds + self.linux_host + .execute_command("bash", &Vec::from(["-c".to_string(), format!("cat > {}", temp_file)]))?; + + // Apply with kubectl + self.linux_host.execute_command("kubectl", &Vec::from([ + "--context".to_string(), + format!("k3d-{}", self.cluster_name), + "apply".to_string(), + "-f".to_string(), + temp_file.to_string(), + ]))?; + + Ok(()) + } + + fn get_resource(&self, resource_type: &str, name: &str) -> Result { + println!( + "Getting resource {}/{} from K3D cluster '{}'", + resource_type, name, self.cluster_name + ); + self.linux_host.execute_command("kubectl", &Vec::from([ + "--context".to_string(), + format!("k3d-{}", self.cluster_name), + "get".to_string(), + resource_type.to_string(), + name.to_string(), + "-o".to_string(), + "yaml".to_string(), + ])) + } +} + + +pub struct LinuxHostTopology { + name: String, + host: String, +} +impl Capability for LinuxHostTopology {} + +impl LinuxHostTopology { + pub fn new(name: String, host: String) -> Self { + Self { name, host } + } +} + +impl Topology for LinuxHostTopology { + fn name(&self) -> &str { + &self.name + } +} + +impl CommandCapability for LinuxHostTopology { + fn execute_command(&self, command: &str, args: &Vec) -> Result { + println!("Executing on {}: {} {:?}", self.host, command, args); + // In a real implementation, this would SSH to the host and execute the command + let output = Command::new(command) + .args(args) + .output() + .map_err(|e| e.to_string())?; + + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + Err(String::from_utf8_lossy(&output.stderr).to_string()) + } + } +} + + + +// --- Main Function Adapated --- +fn main() { + // --- Linux Host --- + let linux_host = LinuxHostTopology::new("dev-machine".to_string(), "localhost".to_string()); + let mut linux_maestro = Maestro::new(linux_host); + + let df_score = CommandScore::new( + "check-disk".to_string(), + "df".to_string(), + vec!["-h".to_string()], + ); + + // Registration uses the generic method, compiler checks CommandScore: Score + linux_maestro.register_score(df_score.clone()).unwrap(); // clone needed if df_score used later + + // --- K3D Host --- + let k3d_host = LinuxHostTopology::new("k3d-host".to_string(), "localhost".to_string()); + let k3d_topology = K3DTopology::new( + "dev-cluster".to_string(), + k3d_host, + "devcluster".to_string(), + ); + let mut k3d_maestro = Maestro::new(k3d_topology); + + let nodes_score = CommandScore::new( + "check-nodes".to_string(), + "kubectl".to_string(), + vec!["get".to_string(), "nodes".to_string()], + ); + let nginx_score = K8sResourceScore::new( + "deploy-nginx".to_string(), + // ... manifest string ... + r#"..."#.to_string(), + ); + + // Compiler checks CommandScore: Score + k3d_maestro.register_score(nodes_score.clone()).unwrap(); + // Compiler checks K8sResourceScore: Score + k3d_maestro.register_score(nginx_score.clone()).unwrap(); + + + // --- TUI Example Usage --- + let mut tui = HarmonyTui::new(); + // Add score *definitions* to the TUI + tui.add_available_score(Box::new(df_score)); + tui.add_available_score(Box::new(nodes_score)); + tui.add_available_score(Box::new(nginx_score)); + + tui.display_scores(); + + // Simulate user selecting score 0 (check-disk) and adding to linux_maestro + match tui.add_selected_score_to_maestro(0, &mut linux_maestro) { + Ok(_) => println!("Successfully registered check-disk to linux_maestro via TUI selection"), + Err(e) => println!("Failed: {}", e), // Should succeed + } + + // Simulate user selecting score 2 (deploy-nginx) and adding to linux_maestro + match tui.add_selected_score_to_maestro(2, &mut linux_maestro) { + Ok(_) => println!("Successfully registered deploy-nginx to linux_maestro via TUI selection"), // Should fail! + Err(e) => println!("Correctly failed to add deploy-nginx to linux_maestro: {}", e), + // The failure happens inside add_selected_score_to_maestro because the + // maestro.register_score(ks.clone()) call fails the trait bound check + // K8sResourceScore: Score is false. + } + + // Simulate user selecting score 2 (deploy-nginx) and adding to k3d_maestro + match tui.add_selected_score_to_maestro(2, &mut k3d_maestro) { + Ok(_) => println!("Successfully registered deploy-nginx to k3d_maestro via TUI selection"), // Should succeed + Err(e) => println!("Failed: {}", e), + } + + // --- Orchestration --- + println!("\n--- Orchestrating Linux Maestro ---"); + linux_maestro.orchestrate().unwrap(); + println!("\n--- Orchestrating K3D Maestro ---"); + k3d_maestro.orchestrate().unwrap(); +} From f7dc15cbf0d84b5dc9bf57aef85418fdf8a44559 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Mon, 31 Mar 2025 15:07:16 -0400 Subject: [PATCH 14/62] refactor(topology): remove unused HAClusterTopology import Remove the unnecessary `HAClusterTopology` import from multiple modules to clean up dependencies and reduce clutter. This change does not affect functionality as `HAClusterTopology` is no longer required in these files. --- harmony/src/domain/maestro/mod.rs | 2 +- harmony/src/modules/dhcp.rs | 3 +-- harmony/src/modules/dns.rs | 2 +- harmony/src/modules/dummy.rs | 2 +- harmony/src/modules/http.rs | 2 +- harmony/src/modules/k8s/resource.rs | 2 +- harmony/src/modules/lamp.rs | 2 +- harmony/src/modules/load_balancer.rs | 2 +- harmony/src/modules/okd/upgrade.rs | 2 +- harmony/src/modules/opnsense/shell.rs | 2 +- harmony/src/modules/tftp.rs | 2 +- 11 files changed, 11 insertions(+), 12 deletions(-) diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index ea4ff26..8bb369b 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -9,7 +9,7 @@ use super::{ topology::Topology, }; -type ScoreVec = Vec>>; +type ScoreVec = Vec>>; pub struct Maestro { inventory: Inventory, diff --git a/harmony/src/modules/dhcp.rs b/harmony/src/modules/dhcp.rs index bd332c1..e145e03 100644 --- a/harmony/src/modules/dhcp.rs +++ b/harmony/src/modules/dhcp.rs @@ -1,4 +1,3 @@ -use std::sync::Arc; use async_trait::async_trait; use derive_new::new; @@ -8,7 +7,7 @@ use crate::{ domain::{data::Version, interpret::InterpretStatus}, interpret::{Interpret, InterpretError, InterpretName, Outcome}, inventory::Inventory, - topology::{DHCPStaticEntry, DhcpServer, HAClusterTopology, HostBinding, IpAddress, Topology}, + topology::{DHCPStaticEntry, DhcpServer, HostBinding, IpAddress, Topology}, }; use crate::domain::score::Score; diff --git a/harmony/src/modules/dns.rs b/harmony/src/modules/dns.rs index 1002a35..ab4e3ec 100644 --- a/harmony/src/modules/dns.rs +++ b/harmony/src/modules/dns.rs @@ -7,7 +7,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{DnsRecord, DnsServer, HAClusterTopology, Topology}, + topology::{DnsRecord, DnsServer, Topology}, }; #[derive(Debug, new, Clone)] diff --git a/harmony/src/modules/dummy.rs b/harmony/src/modules/dummy.rs index 99e2f12..e9bd540 100644 --- a/harmony/src/modules/dummy.rs +++ b/harmony/src/modules/dummy.rs @@ -5,7 +5,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{HAClusterTopology, Topology}, + topology::Topology, }; /// Score that always errors. This is only useful for development/testing purposes. It does nothing diff --git a/harmony/src/modules/http.rs b/harmony/src/modules/http.rs index 1d6df51..419bd89 100644 --- a/harmony/src/modules/http.rs +++ b/harmony/src/modules/http.rs @@ -6,7 +6,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{HAClusterTopology, HttpServer, Topology, Url}, + topology::{HttpServer, Topology, Url}, }; #[derive(Debug, new, Clone)] diff --git a/harmony/src/modules/k8s/resource.rs b/harmony/src/modules/k8s/resource.rs index 878da22..e33cd7a 100644 --- a/harmony/src/modules/k8s/resource.rs +++ b/harmony/src/modules/k8s/resource.rs @@ -8,7 +8,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{HAClusterTopology, OcK8sclient, Topology}, + topology::{OcK8sclient, Topology}, }; #[derive(Debug, Clone)] diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index d7cd495..90f36da 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -8,7 +8,7 @@ use crate::{ inventory::Inventory, modules::k8s::deployment::K8sDeploymentScore, score::Score, - topology::{HAClusterTopology, OcK8sclient, Topology, Url}, + topology::{OcK8sclient, Topology, Url}, }; #[derive(Debug, Clone)] diff --git a/harmony/src/modules/load_balancer.rs b/harmony/src/modules/load_balancer.rs index 5358e84..f590fa1 100644 --- a/harmony/src/modules/load_balancer.rs +++ b/harmony/src/modules/load_balancer.rs @@ -6,7 +6,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{HAClusterTopology, LoadBalancer, LoadBalancerService, Topology}, + topology::{LoadBalancer, LoadBalancerService, Topology}, }; #[derive(Debug, Clone)] diff --git a/harmony/src/modules/okd/upgrade.rs b/harmony/src/modules/okd/upgrade.rs index a4fc6b1..a215c00 100644 --- a/harmony/src/modules/okd/upgrade.rs +++ b/harmony/src/modules/okd/upgrade.rs @@ -1,4 +1,4 @@ -use crate::{data::Version, score::Score}; +use crate::data::Version; #[derive(Debug, Clone)] pub struct OKDUpgradeScore { diff --git a/harmony/src/modules/opnsense/shell.rs b/harmony/src/modules/opnsense/shell.rs index f4cecad..225e2ad 100644 --- a/harmony/src/modules/opnsense/shell.rs +++ b/harmony/src/modules/opnsense/shell.rs @@ -8,7 +8,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{HAClusterTopology, Topology}, + topology::Topology, }; #[derive(Debug, Clone)] diff --git a/harmony/src/modules/tftp.rs b/harmony/src/modules/tftp.rs index 504459a..3e90fc0 100644 --- a/harmony/src/modules/tftp.rs +++ b/harmony/src/modules/tftp.rs @@ -6,7 +6,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{HAClusterTopology, Router, TftpServer, Topology, Url}, + topology::{Router, TftpServer, Topology, Url}, }; #[derive(Debug, new, Clone)] From fc718f11cfa60e1e220c2b655024406abcc79f62 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 2 Apr 2025 15:51:28 -0400 Subject: [PATCH 15/62] feat: Introduce Topology Trait for Compile-Time Safe Score Binding Introduce the `Topology` trait to ensure that `Maestro` can compile-time safely bind compatible `Scores` and `Topologies`. This refactoring includes updating `HarmonyTuiEvent`, `ScoreListWidget`, and related structures to work with generic `Topology` types, enhancing type safety and modularity. --- Cargo.lock | 12 ++++++- examples/lamp/Cargo.toml | 2 +- examples/lamp/src/main.rs | 9 ++++-- examples/tui/src/main.rs | 5 ++- harmony/src/domain/maestro/mod.rs | 5 +-- harmony/src/domain/score.rs | 17 +++++++++- harmony/src/modules/dns.rs | 5 ++- harmony_tui/src/lib.rs | 54 ++++++++++++------------------- harmony_tui/src/widget/score.rs | 42 +++++++++++++----------- 9 files changed, 87 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2cad7e9..d65658c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -823,7 +823,6 @@ dependencies = [ "env_logger", "harmony", "harmony_macros", - "harmony_tui", "harmony_types", "log", "tokio", @@ -860,6 +859,17 @@ dependencies = [ "url", ] +[[package]] +name = "example-topology" +version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "example-topology2" +version = "0.1.0" + [[package]] name = "example-tui" version = "0.1.0" diff --git a/examples/lamp/Cargo.toml b/examples/lamp/Cargo.toml index 902548e..1bdcf68 100644 --- a/examples/lamp/Cargo.toml +++ b/examples/lamp/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] harmony = { path = "../../harmony" } -harmony_tui = { path = "../../harmony_tui" } +#harmony_tui = { path = "../../harmony_tui" } harmony_types = { path = "../../harmony_types" } cidr = { workspace = true } tokio = { workspace = true } diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index 1643511..1251b8c 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -2,7 +2,8 @@ use harmony::{ data::Version, maestro::Maestro, modules::lamp::{LAMPConfig, LAMPScore}, - topology::Url, + score::Score, + topology::{HAClusterTopology, Topology, Url}, }; #[tokio::main] @@ -17,8 +18,12 @@ async fn main() { }, }; - Maestro::load_from_env() + Maestro::::load_from_env() .interpret(Box::new(lamp_stack)) .await .unwrap(); } + +fn clone_score + Clone + 'static>(score: S) -> Box { + Box::new(score.clone()) +} diff --git a/examples/tui/src/main.rs b/examples/tui/src/main.rs index 3a683d0..9623fa2 100644 --- a/examples/tui/src/main.rs +++ b/examples/tui/src/main.rs @@ -1,7 +1,10 @@ use harmony::{ inventory::Inventory, maestro::Maestro, - modules::{dummy::{ErrorScore, PanicScore, SuccessScore}, k8s::deployment::K8sDeploymentScore}, + modules::{ + dummy::{ErrorScore, PanicScore, SuccessScore}, + k8s::deployment::K8sDeploymentScore, + }, topology::HAClusterTopology, }; diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index 8bb369b..256c759 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -56,10 +56,7 @@ impl Maestro { score_mut.append(&mut scores); } - pub async fn interpret(&self, score: S) -> Result - where - S: Score, - { + pub async fn interpret(&self, score: Box>) -> Result { info!("Running score {score:?}"); let interpret = score.create_interpret(); info!("Launching interpret {interpret:?}"); diff --git a/harmony/src/domain/score.rs b/harmony/src/domain/score.rs index 672c1e5..6e4eed4 100644 --- a/harmony/src/domain/score.rs +++ b/harmony/src/domain/score.rs @@ -1,6 +1,21 @@ use super::{interpret::Interpret, topology::Topology}; -pub trait Score: std::fmt::Debug + Send + Sync { +pub trait Score: std::fmt::Debug + Send + Sync + CloneBoxScore { fn create_interpret(&self) -> Box>; fn name(&self) -> String; } + +pub trait CloneBoxScore { + fn clone_box(&self) -> Box>; +} + + +impl CloneBoxScore for S +where + T: Topology, + S: Score + Clone + 'static, +{ + fn clone_box(&self) -> Box> { + Box::new(self.clone()) + } +} diff --git a/harmony/src/modules/dns.rs b/harmony/src/modules/dns.rs index ab4e3ec..c96d08e 100644 --- a/harmony/src/modules/dns.rs +++ b/harmony/src/modules/dns.rs @@ -99,7 +99,10 @@ impl Interpret for DnsInterpret { inventory: &Inventory, topology: &T, ) -> Result { - info!("Executing {} on inventory {inventory:?}", >::get_name(self)); + info!( + "Executing {} on inventory {inventory:?}", + >::get_name(self) + ); self.serve_dhcp_entries(inventory, topology).await?; self.ensure_hosts_registered(topology).await?; diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index 7ee0301..10997ad 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -10,12 +10,12 @@ use widget::{help::HelpWidget, score::ScoreListWidget}; use std::{panic, sync::Arc, time::Duration}; use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind}; -use harmony::{maestro::Maestro, score::Score}; +use harmony::{maestro::Maestro, score::Score, topology::Topology}; use ratatui::{ self, Frame, layout::{Constraint, Layout, Position}, style::{Color, Style}, - widgets::{Block, Borders, ListItem}, + widgets::{Block, Borders}, }; pub mod tui { @@ -43,23 +43,25 @@ pub mod tui { /// init(maestro).await.unwrap(); /// } /// ``` -pub async fn init(maestro: Maestro) -> Result<(), Box> { +pub async fn init( + maestro: Maestro, +) -> Result<(), Box> { HarmonyTUI::new(maestro).init().await } -pub struct HarmonyTUI { - score: ScoreListWidget, +pub struct HarmonyTUI { + score: ScoreListWidget, should_quit: bool, tui_state: TuiWidgetState, } #[derive(Debug)] -enum HarmonyTuiEvent { - LaunchScore(ScoreItem), +enum HarmonyTuiEvent { + LaunchScore(Box>), } -impl HarmonyTUI { - pub fn new(maestro: Maestro) -> Self { +impl HarmonyTUI { + pub fn new(maestro: Maestro) -> Self { let maestro = Arc::new(maestro); let (_handle, sender) = Self::start_channel(maestro.clone()); let score = ScoreListWidget::new(Self::scores_list(&maestro), sender); @@ -72,9 +74,12 @@ impl HarmonyTUI { } fn start_channel( - maestro: Arc, - ) -> (tokio::task::JoinHandle<()>, mpsc::Sender) { - let (sender, mut receiver) = mpsc::channel::(32); + maestro: Arc>, + ) -> ( + tokio::task::JoinHandle<()>, + mpsc::Sender>, + ) { + let (sender, mut receiver) = mpsc::channel::>(32); let handle = tokio::spawn(async move { info!("Starting message channel receiver loop"); while let Some(event) = receiver.recv().await { @@ -84,8 +89,7 @@ impl HarmonyTUI { let maestro = maestro.clone(); let joinhandle_result = - tokio::spawn(async move { maestro.interpret(score_item.0).await }) - .await; + tokio::spawn(async move { maestro.interpret(score_item).await }).await; match joinhandle_result { Ok(interpretation_result) => match interpretation_result { @@ -163,13 +167,10 @@ impl HarmonyTUI { frame.render_widget(tui_logger, output_area) } - fn scores_list(maestro: &Maestro) -> Vec { + fn scores_list(maestro: &Maestro) -> Vec>> { let scores = maestro.scores(); let scores_read = scores.read().expect("Should be able to read scores"); - scores_read - .iter() - .map(|s| ScoreItem(s.clone_box())) - .collect() + scores_read.iter().map(|s| s.clone_box()).collect() } async fn handle_event(&mut self, event: &Event) { @@ -189,18 +190,3 @@ impl HarmonyTUI { } } } - -#[derive(Debug)] -struct ScoreItem(Box); - -impl ScoreItem { - pub fn clone(&self) -> Self { - Self(self.0.clone_box()) - } -} - -impl Into> for &ScoreItem { - fn into(self) -> ListItem<'static> { - ListItem::new(self.0.name()) - } -} diff --git a/harmony_tui/src/widget/score.rs b/harmony_tui/src/widget/score.rs index af992f7..514ab79 100644 --- a/harmony_tui/src/widget/score.rs +++ b/harmony_tui/src/widget/score.rs @@ -1,16 +1,14 @@ use std::sync::{Arc, RwLock}; use crossterm::event::{Event, KeyCode, KeyEventKind}; +use harmony::{score::Score, topology::Topology}; use log::{info, warn}; use ratatui::{ - Frame, - layout::Rect, - style::{Style, Stylize}, - widgets::{List, ListState, StatefulWidget, Widget}, + layout::Rect, style::{Style, Stylize}, widgets::{List, ListItem, ListState, StatefulWidget, Widget}, Frame }; use tokio::sync::mpsc; -use crate::{HarmonyTuiEvent, ScoreItem}; +use crate::HarmonyTuiEvent; #[derive(Debug)] enum ExecutionState { @@ -20,22 +18,22 @@ enum ExecutionState { } #[derive(Debug)] -struct Execution { +struct Execution { state: ExecutionState, - score: ScoreItem, + score: Box>, } #[derive(Debug)] -pub(crate) struct ScoreListWidget { +pub(crate) struct ScoreListWidget { list_state: Arc>, - scores: Vec, - execution: Option, - execution_history: Vec, - sender: mpsc::Sender, + scores: Vec>>, + execution: Option>, + execution_history: Vec>, + sender: mpsc::Sender>, } -impl ScoreListWidget { - pub(crate) fn new(scores: Vec, sender: mpsc::Sender) -> Self { +impl ScoreListWidget { + pub(crate) fn new(scores: Vec>>, sender: mpsc::Sender>) -> Self { let mut list_state = ListState::default(); list_state.select_first(); let list_state = Arc::new(RwLock::new(list_state)); @@ -58,9 +56,9 @@ impl ScoreListWidget { self.execution = Some(Execution { state: ExecutionState::INITIATED, - score: score.clone(), + score: score.clone_box(), }); - info!("{:#?}\n\nConfirm Execution (Press y/n)", score.0); + info!("{:#?}\n\nConfirm Execution (Press y/n)", score); } else { warn!("No Score selected, nothing to launch"); } @@ -94,7 +92,7 @@ impl ScoreListWidget { execution.state = ExecutionState::RUNNING; info!("Launch execution {:?}", execution); self.sender - .send(HarmonyTuiEvent::LaunchScore(execution.score.clone())) + .send(HarmonyTuiEvent::LaunchScore(execution.score.clone_box())) .await .expect("Should be able to send message"); } @@ -123,16 +121,22 @@ impl ScoreListWidget { } } -impl Widget for &ScoreListWidget { +impl Widget for &ScoreListWidget { fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) where Self: Sized, { let mut list_state = self.list_state.write().unwrap(); - let list = List::new(&self.scores) + let scores_items: Vec> = self.scores.iter().map(score_to_list_item).collect(); + let list = List::new(scores_items) .highlight_style(Style::new().bold().italic()) .highlight_symbol("🠊 "); StatefulWidget::render(list, area, buf, &mut list_state) } } + +fn score_to_list_item<'a, T: Topology>(score: &'a Box>) -> ListItem<'a> { + ListItem::new(score.name()) +} + From 8a1627e72860e67db5f5beb62f8be49cd5dae6d3 Mon Sep 17 00:00:00 2001 From: Willem Date: Wed, 2 Apr 2025 16:52:24 -0400 Subject: [PATCH 16/62] wip: refactoring --- Cargo.lock | 4 ---- harmony/src/domain/topology/ha_cluster.rs | 29 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d65658c..6ac2599 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -866,10 +866,6 @@ dependencies = [ "rand", ] -[[package]] -name = "example-topology2" -version = "0.1.0" - [[package]] name = "example-tui" version = "0.1.0" diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 0750b71..6a08ef3 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -78,6 +78,35 @@ impl HAClusterTopology { } } +#[async_trait] +impl DnsServer for HAClusterTopology{ + async fn register_dhcp_leases(&self, _register: bool) -> Result<(), ExecutorError> { + self.dns_server.register_dhcp_leases(_register) + } + async fn register_hosts(&self, _hosts: Vec) -> Result<(), ExecutorError> { + unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + } + fn remove_record( + &mut self, + _name: &str, + _record_type: DnsRecordType, + ) -> Result<(), ExecutorError> { + unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + } + async fn list_records(&self) -> Vec { + unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + } + fn get_ip(&self) -> IpAddress { + unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + } + fn get_host(&self) -> LogicalHost { + unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + } + async fn commit_config(&self) -> Result<(), ExecutorError> { + unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + } +} + #[derive(Debug)] struct DummyInfra; From 79213ba8d7500089aff3d3e38fa0e8d420d30a1e Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 3 Apr 2025 12:20:51 -0400 Subject: [PATCH 17/62] feat: implement passthrough for HAClusterTopology traits This commit completes the refactoring of the `HAClusterTopology` struct to implement all required traits via passthrough to the underlying infrastructure providers. - Implemented all traits (`DnsServer`, `LoadBalancer`, `HttpServer`, etc.) on `HAClusterTopology`. - Each trait method now simply calls the corresponding method on the underlying infrastructure provider. - This ensures that all functionality is delegated to the correct provider without duplicating logic. - Updated trait implementations to accept `&self` instead of `&mut self` where appropriate. - Fixed a compilation error in `remove_record` by changing the signature to accept `&self`. - Added unimplemented!() stubs for HttpServer traits. --- harmony/src/domain/topology/ha_cluster.rs | 139 +++++++++++++++++++--- harmony/src/domain/topology/network.rs | 4 +- harmony/src/infra/opnsense/dns.rs | 2 +- 3 files changed, 124 insertions(+), 21 deletions(-) diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 6a08ef3..0e9230b 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -79,32 +79,139 @@ impl HAClusterTopology { } #[async_trait] -impl DnsServer for HAClusterTopology{ - async fn register_dhcp_leases(&self, _register: bool) -> Result<(), ExecutorError> { - self.dns_server.register_dhcp_leases(_register) +impl DnsServer for HAClusterTopology { + async fn register_dhcp_leases(&self, register: bool) -> Result<(), ExecutorError> { + self.dns_server.register_dhcp_leases(register).await } - async fn register_hosts(&self, _hosts: Vec) -> Result<(), ExecutorError> { - unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + async fn register_hosts(&self, hosts: Vec) -> Result<(), ExecutorError> { + self.dns_server.register_hosts(hosts).await } - fn remove_record( - &mut self, - _name: &str, - _record_type: DnsRecordType, - ) -> Result<(), ExecutorError> { - unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + fn remove_record(&self, name: &str, record_type: DnsRecordType) -> Result<(), ExecutorError> { + self.dns_server.remove_record(name, record_type) } async fn list_records(&self) -> Vec { - unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + self.dns_server.list_records().await } + fn get_ip(&self) -> IpAddress { + self.dns_server.get_ip() + } + fn get_host(&self) -> LogicalHost { + self.dns_server.get_host() + } + async fn commit_config(&self) -> Result<(), ExecutorError> { + self.dns_server.commit_config().await + } +} + +#[async_trait] +impl LoadBalancer for HAClusterTopology { + fn get_ip(&self) -> IpAddress { + self.load_balancer.get_ip() + } + fn get_host(&self) -> LogicalHost { + self.load_balancer.get_host() + } + async fn add_service(&self, service: &LoadBalancerService) -> Result<(), ExecutorError> { + self.load_balancer.add_service(service).await + } + async fn remove_service(&self, service: &LoadBalancerService) -> Result<(), ExecutorError> { + self.load_balancer.remove_service(service).await + } + async fn list_services(&self) -> Vec { + self.load_balancer.list_services().await + } + async fn ensure_initialized(&self) -> Result<(), ExecutorError> { + self.load_balancer.ensure_initialized().await + } + async fn commit_config(&self) -> Result<(), ExecutorError> { + self.load_balancer.commit_config().await + } + async fn reload_restart(&self) -> Result<(), ExecutorError> { + self.load_balancer.reload_restart().await + } +} + +#[async_trait] +impl DhcpServer for HAClusterTopology { + async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError> { + self.dhcp_server.add_static_mapping(entry).await + } + async fn remove_static_mapping(&self, mac: &MacAddress) -> Result<(), ExecutorError> { + self.dhcp_server.remove_static_mapping(mac).await + } + async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)> { + self.dhcp_server.list_static_mappings().await + } + async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError> { + self.dhcp_server.set_next_server(ip).await + } + async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError> { + self.dhcp_server.set_boot_filename(boot_filename).await + } + fn get_ip(&self) -> IpAddress { + self.dhcp_server.get_ip() + } + fn get_host(&self) -> LogicalHost { + self.dhcp_server.get_host() + } + async fn commit_config(&self) -> Result<(), ExecutorError> { + self.dhcp_server.commit_config().await + } +} + +#[async_trait] +impl TftpServer for HAClusterTopology { + async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError> { + self.tftp_server.serve_files(url).await + } + fn get_ip(&self) -> IpAddress { + self.tftp_server.get_ip() + } + + async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError> { + self.tftp_server.set_ip(ip).await + } + async fn ensure_initialized(&self) -> Result<(), ExecutorError> { + self.tftp_server.ensure_initialized().await + } + async fn commit_config(&self) -> Result<(), ExecutorError> { + self.tftp_server.commit_config().await + } + async fn reload_restart(&self) -> Result<(), ExecutorError> { + self.tftp_server.reload_restart().await + } +} + +impl Router for HAClusterTopology { + fn get_gateway(&self) -> super::IpAddress { + self.router.get_gateway() + } + fn get_cidr(&self) -> cidr::Ipv4Cidr { + self.router.get_cidr() + } + fn get_host(&self) -> LogicalHost { + self.router.get_host() + } +} + +#[async_trait] +impl HttpServer for HAClusterTopology { + async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError> { + self.http_server.serve_files(url).await + } + fn get_ip(&self) -> IpAddress { unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } - fn get_host(&self) -> LogicalHost { + async fn ensure_initialized(&self) -> Result<(), ExecutorError> { unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } async fn commit_config(&self) -> Result<(), ExecutorError> { unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } + async fn reload_restart(&self) -> Result<(), ExecutorError> { + unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) + } } #[derive(Debug)] @@ -251,11 +358,7 @@ impl DnsServer for DummyInfra { async fn register_hosts(&self, _hosts: Vec) -> Result<(), ExecutorError> { unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } - fn remove_record( - &mut self, - _name: &str, - _record_type: DnsRecordType, - ) -> Result<(), ExecutorError> { + fn remove_record(&self, _name: &str, _record_type: DnsRecordType) -> Result<(), ExecutorError> { unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } async fn list_records(&self) -> Vec { diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index 523db2f..6aba5f3 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -63,7 +63,7 @@ pub trait DnsServer: Send + Sync { async fn register_dhcp_leases(&self, register: bool) -> Result<(), ExecutorError>; async fn register_hosts(&self, hosts: Vec) -> Result<(), ExecutorError>; fn remove_record( - &mut self, + &self, name: &str, record_type: DnsRecordType, ) -> Result<(), ExecutorError>; @@ -254,7 +254,7 @@ mod test { } fn remove_record( - &mut self, + &self, _name: &str, _record_type: DnsRecordType, ) -> Result<(), ExecutorError> { diff --git a/harmony/src/infra/opnsense/dns.rs b/harmony/src/infra/opnsense/dns.rs index 765cdb0..56f8136 100644 --- a/harmony/src/infra/opnsense/dns.rs +++ b/harmony/src/infra/opnsense/dns.rs @@ -30,7 +30,7 @@ impl DnsServer for OPNSenseFirewall { } fn remove_record( - &mut self, + &self, _name: &str, _record_type: crate::topology::DnsRecordType, ) -> Result<(), ExecutorError> { From e6384da57e88e9b59a5b451e8c4b85b61e280018 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 3 Apr 2025 13:40:46 -0400 Subject: [PATCH 18/62] Working on various ADR, cleaning up some stuff --- adr/003-infrastructure-abstractions.md | 16 +- adr/005-interactive-project.md | 48 +-- adr/core-abstractions/main_context_prompt.md | 360 ++++++++++++++++++ .../core-abstractions}/topology/Cargo.toml | 0 .../core-abstractions}/topology/src/main.rs | 0 .../topology/src/main_claude37_2.rs | 0 .../topology/src/main_claudev1.rs | 0 .../topology/src/main_gemini25pro.rs | 0 .../topology/src/main_geminifail.rs | 0 .../topology/src/main_right.rs | 0 .../topology/src/main_v1.rs | 0 adr/core-abstractions/topology2/Cargo.toml | 9 + adr/core-abstractions/topology2/src/main.rs | 183 +++++++++ .../topology2/src/main_capabilities.rs | 324 ++++++++++++++++ .../topology2/src/main_v1.rs | 34 ++ .../topology2/src/main_v2.rs | 76 ++++ .../topology2/src/main_v4.rs | 360 ++++++++++++++++++ 17 files changed, 1382 insertions(+), 28 deletions(-) create mode 100644 adr/core-abstractions/main_context_prompt.md rename {examples => adr/core-abstractions}/topology/Cargo.toml (100%) rename {examples => adr/core-abstractions}/topology/src/main.rs (100%) rename {examples => adr/core-abstractions}/topology/src/main_claude37_2.rs (100%) rename {examples => adr/core-abstractions}/topology/src/main_claudev1.rs (100%) rename {examples => adr/core-abstractions}/topology/src/main_gemini25pro.rs (100%) rename {examples => adr/core-abstractions}/topology/src/main_geminifail.rs (100%) rename {examples => adr/core-abstractions}/topology/src/main_right.rs (100%) rename {examples => adr/core-abstractions}/topology/src/main_v1.rs (100%) create mode 100644 adr/core-abstractions/topology2/Cargo.toml create mode 100644 adr/core-abstractions/topology2/src/main.rs create mode 100644 adr/core-abstractions/topology2/src/main_capabilities.rs create mode 100644 adr/core-abstractions/topology2/src/main_v1.rs create mode 100644 adr/core-abstractions/topology2/src/main_v2.rs create mode 100644 adr/core-abstractions/topology2/src/main_v4.rs diff --git a/adr/003-infrastructure-abstractions.md b/adr/003-infrastructure-abstractions.md index 3d01531..5785bd4 100644 --- a/adr/003-infrastructure-abstractions.md +++ b/adr/003-infrastructure-abstractions.md @@ -1,12 +1,18 @@ -**Architecture Decision Record: Harmony Infrastructure Abstractions** +## Architecture Decision Record: Core Harmony Infrastructure Abstractions -**Status**: Proposed +## Status -**Context**: Harmony is an infrastructure orchestrator written in pure Rust, aiming to provide real portability of automation across different cloud providers and infrastructure setups. To achieve this, we need to define infrastructure abstractions that are provider-agnostic and flexible enough to accommodate various use cases. +Proposed -**Decision**: We will define our infrastructure abstractions using a domain-driven approach, focusing on the core logic of Harmony. These abstractions will only include the absolutely required elements for a specific resource, without referencing specific providers or implementations. +## Context -**Example: Database Abstraction** +Harmony is an infrastructure orchestrator written in pure Rust, aiming to provide real portability of automation across different cloud providers and infrastructure setups. To achieve this, we need to define infrastructure abstractions that are provider-agnostic and flexible enough to accommodate various use cases. + +## Decision + +We will define our infrastructure abstractions using a domain-driven approach, focusing on the core logic of Harmony. These abstractions will only include the absolutely required elements for a specific resource, without referencing specific providers or implementations. + +### Example: Database Abstraction To deploy a database to any cloud provider, we define an abstraction that includes essential elements such as: ```rust diff --git a/adr/005-interactive-project.md b/adr/005-interactive-project.md index 9b5abc5..01e9794 100644 --- a/adr/005-interactive-project.md +++ b/adr/005-interactive-project.md @@ -2,7 +2,7 @@ ## Status -Draft +Proposal ## Context @@ -31,30 +31,10 @@ This ADR will outline the approach taken to go from a LAMP project to be standal ## Decision -# Option 1 : Score spec -To simplify onboarding of existing projects, we decided to integrate with Score Spec for the following reasons : +# Custom Rust DSL -- It is a CNCF project, which helps a lot with adoption and community building -- It already supports important targets for us including docker-compose and k8s -- It provides a way to define the application's infrastructure at the correct level of abstraction for us to deploy it anywhere -- that is the goal of the score-spec project -- Once we evolve, we can simply have a score compatible provider that allows any project with a score spec to be deployed on the harmony stack -- Score was built with enterprise use-cases in mind : Humanitec platform engineering customers - - -## Consequences - -### Positive - -- Score Community is growing, using harmony will be very easy for them - -### Negative - -- Score is not that big yet, and mostly used by Humanitec's clients (I guess), which is a hard to penetrate environment - -# Option 2 : Custom Rust DSL - -We decided to develop a rust based DSL. Even though this means people will be afraid of "Rust", we believe the numerous advantages are worth the risk. +We decided to develop a rust based DSL. Even though this means people might be "afraid of Rust", we believe the numerous advantages are worth the risk. The main selection criterias are : @@ -76,3 +56,25 @@ The main selection criterias are : - Lack of an existing community and ecosystem, which could slow down adoption. - Increased maintenance overhead as the DSL needs to be updated and supported internally. +## Alternatives considered + +### Score spec + +We considered integrating with the score-spec project : https://github.com/score-spec/spec + +The idea was to benefit from an existing community and ecosystem. The motivations to consider score were the following : + +- It is a CNCF project, which helps a lot with adoption and community building +- It already supports important targets for us including docker-compose and k8s +- It provides a way to define the application's infrastructure at the correct level of abstraction for us to deploy it anywhere -- that is the goal of the score-spec project +- Once we evolve, we can simply have a score compatible provider that allows any project with a score spec to be deployed on the harmony stack +- Score was built with enterprise use-cases in mind : Humanitec platform engineering customers + + +Positive Consequences + +- Score Community is growing, using harmony will be very easy for them + +Negative Consequences + +- Score is not that big yet, and mostly used by Humanitec's clients (I guess), which is a hard to penetrate environment diff --git a/adr/core-abstractions/main_context_prompt.md b/adr/core-abstractions/main_context_prompt.md new file mode 100644 index 0000000..4b1e54e --- /dev/null +++ b/adr/core-abstractions/main_context_prompt.md @@ -0,0 +1,360 @@ + +# Here is the current condenses architecture sample for Harmony's core abstractions + +```rust +use std::process::Command; + +pub trait Capability {} + +pub trait CommandCapability: Capability { + fn execute_command(&self, command: &str, args: &[&str]) -> Result; +} + +pub trait KubernetesCapability: Capability { + fn apply_manifest(&self, manifest: &str) -> Result<(), String>; + fn get_resource(&self, resource_type: &str, name: &str) -> Result; +} + +pub trait Topology { + fn name(&self) -> &str; +} + +pub trait Score { + fn compile(&self) -> Result>, String>; + fn name(&self) -> &str; +} + +pub struct LinuxHostTopology { + name: String, + host: String, +} + +impl Capability for LinuxHostTopology {} + +impl LinuxHostTopology { + pub fn new(name: String, host: String) -> Self { + Self { name, host } + } +} + +impl Topology for LinuxHostTopology { + fn name(&self) -> &str { + &self.name + } +} + +impl CommandCapability for LinuxHostTopology { + fn execute_command(&self, command: &str, args: &[&str]) -> Result { + println!("Executing on {}: {} {:?}", self.host, command, args); + // In a real implementation, this would SSH to the host and execute the command + let output = Command::new(command) + .args(args) + .output() + .map_err(|e| e.to_string())?; + + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + Err(String::from_utf8_lossy(&output.stderr).to_string()) + } + } +} + +pub struct K3DTopology { + name: String, + linux_host: LinuxHostTopology, + cluster_name: String, +} + +impl Capability for K3DTopology {} + +impl K3DTopology { + pub fn new(name: String, linux_host: LinuxHostTopology, cluster_name: String) -> Self { + Self { + name, + linux_host, + cluster_name, + } + } +} + +impl Topology for K3DTopology { + fn name(&self) -> &str { + &self.name + } +} + +impl CommandCapability for K3DTopology { + fn execute_command(&self, command: &str, args: &[&str]) -> Result { + self.linux_host.execute_command(command, args) + } +} + +impl KubernetesCapability for K3DTopology { + fn apply_manifest(&self, manifest: &str) -> Result<(), String> { + println!("Applying manifest to K3D cluster '{}'", self.cluster_name); + // Write manifest to a temporary file + let temp_file = format!("/tmp/manifest-harmony-temp.yaml"); + + // Use the linux_host directly to avoid capability trait bounds + self.linux_host + .execute_command("bash", &["-c", &format!("cat > {}", temp_file)])?; + + // Apply with kubectl + self.linux_host.execute_command("kubectl", &[ + "--context", + &format!("k3d-{}", self.cluster_name), + "apply", + "-f", + &temp_file, + ])?; + + Ok(()) + } + + fn get_resource(&self, resource_type: &str, name: &str) -> Result { + println!( + "Getting resource {}/{} from K3D cluster '{}'", + resource_type, name, self.cluster_name + ); + self.linux_host.execute_command("kubectl", &[ + "--context", + &format!("k3d-{}", self.cluster_name), + "get", + resource_type, + name, + "-o", + "yaml", + ]) + } +} + +pub struct CommandScore { + name: String, + command: String, + args: Vec, +} + +impl CommandScore { + pub fn new(name: String, command: String, args: Vec) -> Self { + Self { + name, + command, + args, + } + } +} + +pub trait Interpret { + fn execute(&self, topology: &T) -> Result; +} + +struct CommandInterpret; + +impl Interpret for CommandInterpret +where + T: Topology + CommandCapability, +{ + fn execute(&self, topology: &T) -> Result { + todo!() + } +} + +impl Score for CommandScore +where + T: Topology + CommandCapability, +{ + fn compile(&self) -> Result>, String> { + Ok(Box::new(CommandInterpret {})) + } + + fn name(&self) -> &str { + &self.name + } +} + + +#[derive(Clone)] +pub struct K8sResourceScore { + name: String, + manifest: String, +} + +impl K8sResourceScore { + pub fn new(name: String, manifest: String) -> Self { + Self { name, manifest } + } +} + +struct K8sResourceInterpret { + score: K8sResourceScore, +} + +impl Interpret for K8sResourceInterpret { + fn execute(&self, topology: &T) -> Result { + todo!() + } +} + +impl Score for K8sResourceScore +where + T: Topology + KubernetesCapability, +{ + fn compile(&self) -> Result + 'static)>, String> { + Ok(Box::new(K8sResourceInterpret { + score: self.clone(), + })) + } + + fn name(&self) -> &str { + &self.name + } +} + +pub struct Maestro { + topology: T, + scores: Vec>>, +} + + +impl Maestro { + pub fn new(topology: T) -> Self { + Self { + topology, + scores: Vec::new(), + } + } + + pub fn register_score(&mut self, score: S) + where + S: Score + 'static, + { + println!( + "Registering score '{}' for topology '{}'", + score.name(), + self.topology.name() + ); + self.scores.push(Box::new(score)); + } + + pub fn orchestrate(&self) -> Result<(), String> { + println!("Orchestrating topology '{}'", self.topology.name()); + for score in &self.scores { + let interpret = score.compile()?; + interpret.execute(&self.topology)?; + } + Ok(()) + } +} + +fn main() { + let linux_host = LinuxHostTopology::new("dev-machine".to_string(), "localhost".to_string()); + + let mut linux_maestro = Maestro::new(linux_host); + + linux_maestro.register_score(CommandScore::new( + "check-disk".to_string(), + "df".to_string(), + vec!["-h".to_string()], + )); + linux_maestro.orchestrate().unwrap(); + + // This would fail to compile if we tried to register a K8sResourceScore + // because LinuxHostTopology doesn't implement KubernetesCapability + //linux_maestro.register_score(K8sResourceScore::new( + // "...".to_string(), + // "...".to_string(), + //)); + + // Create a K3D topology which has both Command and Kubernetes capabilities + let k3d_host = LinuxHostTopology::new("k3d-host".to_string(), "localhost".to_string()); + + let k3d_topology = K3DTopology::new( + "dev-cluster".to_string(), + k3d_host, + "devcluster".to_string(), + ); + + // Create a maestro for the K3D topology + let mut k3d_maestro = Maestro::new(k3d_topology); + + // We can register both command scores and kubernetes scores + k3d_maestro.register_score(CommandScore::new( + "check-nodes".to_string(), + "kubectl".to_string(), + vec!["get".to_string(), "nodes".to_string()], + )); + + k3d_maestro.register_score(K8sResourceScore::new( + "deploy-nginx".to_string(), + r#" + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx + spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:latest + ports: + - containerPort: 80 + "# + .to_string(), + )); + + // Orchestrate both topologies + linux_maestro.orchestrate().unwrap(); + k3d_maestro.orchestrate().unwrap(); +} +``` + + +## Technical take + +The key insight is that we might not need a complex TypeMap or runtime capability checking. Instead, we should leverage Rust's trait system to express capability requirements directly in the type system. + +By clarifying the problem and focusing on type-level solutions rather than runtime checks, we can likely arrive at a simpler, more robust design that leverages the strengths of Rust's type system. + +## Philosophical Shifts + +1. **From Runtime to Compile-Time**: Move capability checking from runtime to compile-time. + +2. **From Objects to Functions**: Think of scores less as objects and more as functions that transform topologies. + +3. **From Homogeneous to Heterogeneous API**: Embrace different API paths for different capability combinations rather than trying to force everything through a single interface. + +4. **From Complex to Simple**: Focus on making common cases simple, even if it means less abstraction for uncommon cases. + +## High level concepts + +The high level concepts so far has evolved towards this definition. + +Topology -> Has -> Capabilities +Score -> Defines -> Work to be done / desired state +Interpret -> Requires -> Capabilities to execute a Score +Maestro -> Enforces -> Compatibility (through the type system at compile time) + +## Why Harmony + +The compile time safety is paramount here. Harmony's main goal is to make the entire software delivery pipeline robust. Current IaC tools are very hard to work with, require complex setups to test and debug real code. + +Leveraging Rust's compiler allows us to shift left a lot of the complexities and frustration that comes with using tools like Ansible that is Yaml based and quickly becomes brittle at scale. Or Terraform, when running a `terraform plan` makes you think everything is correct only to fail horribly when confidently launching `terraform apply` and leaving you with tens or hundreds of resources to clean manually. + +Of course, this requires a significant effort to get to the point where we have actually implemented all the logic. + +But using Rust and a Type Driven Design approach, we believe we are providing a much more robust foundation for our customer's and user's deployments anywhere. + +Also, having the full power of a mature programming language like Rust enables organizations and the community to customize their deployment any way they want, build upon it in a reliable way that has been evolved and proven over decades of enterprise dependency management, API definitions, etc. + +=== + +Given all this c diff --git a/examples/topology/Cargo.toml b/adr/core-abstractions/topology/Cargo.toml similarity index 100% rename from examples/topology/Cargo.toml rename to adr/core-abstractions/topology/Cargo.toml diff --git a/examples/topology/src/main.rs b/adr/core-abstractions/topology/src/main.rs similarity index 100% rename from examples/topology/src/main.rs rename to adr/core-abstractions/topology/src/main.rs diff --git a/examples/topology/src/main_claude37_2.rs b/adr/core-abstractions/topology/src/main_claude37_2.rs similarity index 100% rename from examples/topology/src/main_claude37_2.rs rename to adr/core-abstractions/topology/src/main_claude37_2.rs diff --git a/examples/topology/src/main_claudev1.rs b/adr/core-abstractions/topology/src/main_claudev1.rs similarity index 100% rename from examples/topology/src/main_claudev1.rs rename to adr/core-abstractions/topology/src/main_claudev1.rs diff --git a/examples/topology/src/main_gemini25pro.rs b/adr/core-abstractions/topology/src/main_gemini25pro.rs similarity index 100% rename from examples/topology/src/main_gemini25pro.rs rename to adr/core-abstractions/topology/src/main_gemini25pro.rs diff --git a/examples/topology/src/main_geminifail.rs b/adr/core-abstractions/topology/src/main_geminifail.rs similarity index 100% rename from examples/topology/src/main_geminifail.rs rename to adr/core-abstractions/topology/src/main_geminifail.rs diff --git a/examples/topology/src/main_right.rs b/adr/core-abstractions/topology/src/main_right.rs similarity index 100% rename from examples/topology/src/main_right.rs rename to adr/core-abstractions/topology/src/main_right.rs diff --git a/examples/topology/src/main_v1.rs b/adr/core-abstractions/topology/src/main_v1.rs similarity index 100% rename from examples/topology/src/main_v1.rs rename to adr/core-abstractions/topology/src/main_v1.rs diff --git a/adr/core-abstractions/topology2/Cargo.toml b/adr/core-abstractions/topology2/Cargo.toml new file mode 100644 index 0000000..ef57a6d --- /dev/null +++ b/adr/core-abstractions/topology2/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "example-topology2" +edition = "2024" +version.workspace = true +readme.workspace = true +license.workspace = true +publish = false + +[dependencies] diff --git a/adr/core-abstractions/topology2/src/main.rs b/adr/core-abstractions/topology2/src/main.rs new file mode 100644 index 0000000..313afa1 --- /dev/null +++ b/adr/core-abstractions/topology2/src/main.rs @@ -0,0 +1,183 @@ +// Clean capability-based design using type parameters + +trait Capability {} + +trait K8sCapability: Capability { + fn deploy_k8s_resource(&self, resource_yaml: &str); + fn execute_kubectl(&self, command: &str) -> String; +} + +trait LinuxCapability: Capability { + fn execute_command(&self, command: &str, args: &[&str]); + fn download_file(&self, url: &str, destination: &str) -> Result<(), String>; +} + +trait LoadBalancerCapability: Capability { + fn configure_load_balancer(&self, services: &[&str], port: u16); + fn get_load_balancer_status(&self) -> String; +} + +// Score trait with capability type parameter +trait Score { + fn execute(&self, capability: &C) -> String; +} + +// Topology implementations with marker trait +trait Topology {} + +struct K3DTopology {} +impl Topology for K3DTopology {} +impl Capability for K3DTopology {} +impl K8sCapability for K3DTopology { + fn deploy_k8s_resource(&self, resource_yaml: &str) { + todo!() + } + + fn execute_kubectl(&self, command: &str) -> String { + todo!() + } + // Implementation... +} + +struct LinuxTopology {} +impl Topology for LinuxTopology {} +impl Capability for LinuxTopology {} +impl LinuxCapability for LinuxTopology { + fn execute_command(&self, command: &str, args: &[&str]) { + todo!() + } + + fn download_file(&self, url: &str, destination: &str) -> Result<(), String> { + todo!() + } + // Implementation... +} + +struct OKDHaClusterTopology {} +impl Topology for OKDHaClusterTopology {} +impl Capability for OKDHaClusterTopology {} +impl K8sCapability for OKDHaClusterTopology { + fn deploy_k8s_resource(&self, resource_yaml: &str) { + todo!() + } + + fn execute_kubectl(&self, command: &str) -> String { + todo!() + } + // Implementation... +} +impl LinuxCapability for OKDHaClusterTopology { + fn execute_command(&self, command: &str, args: &[&str]) { + todo!() + } + + fn download_file(&self, url: &str, destination: &str) -> Result<(), String> { + todo!() + } + // Implementation... +} +impl LoadBalancerCapability for OKDHaClusterTopology { + fn configure_load_balancer(&self, services: &[&str], port: u16) { + todo!() + } + + fn get_load_balancer_status(&self) -> String { + todo!() + } + // Implementation... +} + +// Score implementations +struct LAMPScore {} +impl Score for LAMPScore { + fn execute(&self, capability: &dyn K8sCapability) -> String { + todo!() + // Implementation... + } +} + +struct BinaryScore {} +impl Score for BinaryScore { + fn execute(&self, capability: &dyn LinuxCapability) -> String { + todo!() + // Implementation... + } +} + +struct LoadBalancerScore {} +impl Score for LoadBalancerScore { + fn execute(&self, capability: &dyn LoadBalancerCapability) -> String { + todo!() + // Implementation... + } +} + +// Generic Maestro +struct Maestro { + topology: T, + scores: Vec String>>, +} + +impl Maestro { + fn new(topology: T) -> Self { + Self { + topology, + scores: Vec::new(), + } + } + + fn interpret_all(&mut self) -> Vec { + self.scores.iter_mut() + .map(|score| score(&self.topology)) + .collect() + } +} + +// Capability-specific extensions +impl Maestro { + fn register_k8s_score + 'static>(&mut self, score: S) { + let score_box = Box::new(move |topology: &T| { + score.execute(topology as &dyn K8sCapability) + }); + self.scores.push(score_box); + } +} + +impl Maestro { + fn register_linux_score + 'static>(&mut self, score: S) { + let score_box = Box::new(move |topology: &T| { + score.execute(topology as &dyn LinuxCapability) + }); + self.scores.push(score_box); + } +} + +impl Maestro { + fn register_lb_score + 'static>(&mut self, score: S) { + let score_box = Box::new(move |topology: &T| { + score.execute(topology as &dyn LoadBalancerCapability) + }); + self.scores.push(score_box); + } +} + +fn main() { + // Example usage + let k3d = K3DTopology {}; + let mut k3d_maestro = Maestro::new(k3d); + + // These will compile because K3D implements K8sCapability + k3d_maestro.register_k8s_score(LAMPScore {}); + + // This would not compile because K3D doesn't implement LoadBalancerCapability + // k3d_maestro.register_lb_score(LoadBalancerScore {}); + + let linux = LinuxTopology {}; + let mut linux_maestro = Maestro::new(linux); + + // This will compile because Linux implements LinuxCapability + linux_maestro.register_linux_score(BinaryScore {}); + + // This would not compile because Linux doesn't implement K8sCapability + // linux_maestro.register_k8s_score(LAMPScore {}); +} diff --git a/adr/core-abstractions/topology2/src/main_capabilities.rs b/adr/core-abstractions/topology2/src/main_capabilities.rs new file mode 100644 index 0000000..499069a --- /dev/null +++ b/adr/core-abstractions/topology2/src/main_capabilities.rs @@ -0,0 +1,324 @@ +fn main() { + // Create various topologies + let okd_topology = OKDHaClusterTopology::new(); + let k3d_topology = K3DTopology::new(); + let linux_topology = LinuxTopology::new(); + + // Create scores + let lamp_score = LAMPScore::new("MySQL 8.0", "PHP 8.1", "Apache 2.4"); + let binary_score = BinaryScore::new("https://example.com/binary", vec!["--arg1", "--arg2"]); + let load_balancer_score = LoadBalancerScore::new(vec!["service1", "service2"], 80); + + // Example 1: Running LAMP stack on OKD + println!("\n=== Deploying LAMP stack on OKD cluster ==="); + lamp_score.execute(&okd_topology); + + // Example 2: Running LAMP stack on K3D + println!("\n=== Deploying LAMP stack on K3D cluster ==="); + lamp_score.execute(&k3d_topology); + + // Example 3: Running binary on Linux host + println!("\n=== Running binary on Linux host ==="); + binary_score.execute(&linux_topology); + + // Example 4: Running binary on OKD (which can also run Linux commands) + println!("\n=== Running binary on OKD host ==="); + binary_score.execute(&okd_topology); + + // Example 5: Load balancer configuration on OKD + println!("\n=== Configuring load balancer on OKD ==="); + load_balancer_score.execute(&okd_topology); + + // The following would not compile: + // load_balancer_score.execute(&k3d_topology); // K3D doesn't implement LoadBalancerCapability + // lamp_score.execute(&linux_topology); // Linux doesn't implement K8sCapability +} + +// Base Topology trait +trait Topology { + fn name(&self) -> &str; +} + +// Define capabilities +trait K8sCapability { + fn deploy_k8s_resource(&self, resource_yaml: &str); + fn execute_kubectl(&self, command: &str) -> String; +} + +trait OKDCapability: K8sCapability { + fn execute_oc(&self, command: &str) -> String; +} + +trait LinuxCapability { + fn execute_command(&self, command: &str, args: &[&str]) -> String; + fn download_file(&self, url: &str, destination: &str) -> Result<(), String>; +} + +trait LoadBalancerCapability { + fn configure_load_balancer(&self, services: &[&str], port: u16); + fn get_load_balancer_status(&self) -> String; +} + +trait FirewallCapability { + fn open_port(&self, port: u16, protocol: &str); + fn close_port(&self, port: u16, protocol: &str); +} + +trait RouterCapability { + fn configure_route(&self, service: &str, hostname: &str); +} + +// Topology implementations +struct OKDHaClusterTopology { + cluster_name: String, +} + +impl OKDHaClusterTopology { + fn new() -> Self { + Self { + cluster_name: "okd-ha-cluster".to_string(), + } + } +} + +impl Topology for OKDHaClusterTopology { + fn name(&self) -> &str { + &self.cluster_name + } +} + +impl K8sCapability for OKDHaClusterTopology { + fn deploy_k8s_resource(&self, resource_yaml: &str) { + println!("Deploying K8s resource on OKD cluster: {}", resource_yaml); + } + + fn execute_kubectl(&self, command: &str) -> String { + println!("Executing kubectl command on OKD cluster: {}", command); + "kubectl command output".to_string() + } +} + +impl OKDCapability for OKDHaClusterTopology { + fn execute_oc(&self, command: &str) -> String { + println!("Executing oc command on OKD cluster: {}", command); + "oc command output".to_string() + } +} + +impl LinuxCapability for OKDHaClusterTopology { + fn execute_command(&self, command: &str, args: &[&str]) -> String { + println!( + "Executing command '{}' with args {:?} on OKD node", + command, args + ); + todo!() + } + + fn download_file(&self, url: &str, destination: &str) -> Result<(), String> { + println!( + "Downloading file from {} to {} on OKD node", + url, destination + ); + Ok(()) + } +} + +impl LoadBalancerCapability for OKDHaClusterTopology { + fn configure_load_balancer(&self, services: &[&str], port: u16) { + println!( + "Configuring load balancer for services {:?} on port {} in OKD", + services, port + ); + } + + fn get_load_balancer_status(&self) -> String { + "OKD Load Balancer: HEALTHY".to_string() + } +} + +impl FirewallCapability for OKDHaClusterTopology { + fn open_port(&self, port: u16, protocol: &str) { + println!( + "Opening port {} with protocol {} on OKD firewall", + port, protocol + ); + } + + fn close_port(&self, port: u16, protocol: &str) { + println!( + "Closing port {} with protocol {} on OKD firewall", + port, protocol + ); + } +} + +impl RouterCapability for OKDHaClusterTopology { + fn configure_route(&self, service: &str, hostname: &str) { + println!( + "Configuring route for service {} with hostname {} on OKD", + service, hostname + ); + } +} + +struct K3DTopology { + cluster_name: String, +} + +impl K3DTopology { + fn new() -> Self { + Self { + cluster_name: "k3d-local".to_string(), + } + } +} + +impl Topology for K3DTopology { + fn name(&self) -> &str { + &self.cluster_name + } +} + +impl K8sCapability for K3DTopology { + fn deploy_k8s_resource(&self, resource_yaml: &str) { + println!("Deploying K8s resource on K3D cluster: {}", resource_yaml); + } + + fn execute_kubectl(&self, command: &str) -> String { + println!("Executing kubectl command on K3D cluster: {}", command); + "kubectl command output from K3D".to_string() + } +} + +struct LinuxTopology { + hostname: String, +} + +impl LinuxTopology { + fn new() -> Self { + Self { + hostname: "linux-host".to_string(), + } + } +} + +impl Topology for LinuxTopology { + fn name(&self) -> &str { + &self.hostname + } +} + +impl LinuxCapability for LinuxTopology { + fn execute_command(&self, command: &str, args: &[&str]) -> String { + println!( + "Executing command '{}' with args {:?} on Linux host", + command, args + ); + todo!() + } + + fn download_file(&self, url: &str, destination: &str) -> Result<(), String> { + println!( + "Downloading file from {} to {} on Linux host", + url, destination + ); + Ok(()) + } +} + +// Score implementations +struct LAMPScore { + mysql_version: String, + php_version: String, + apache_version: String, +} + +impl LAMPScore { + fn new(mysql_version: &str, php_version: &str, apache_version: &str) -> Self { + Self { + mysql_version: mysql_version.to_string(), + php_version: php_version.to_string(), + apache_version: apache_version.to_string(), + } + } + + fn execute(&self, topology: &T) { + // Deploy MySQL + topology.deploy_k8s_resource("mysql-deployment.yaml"); + + // Deploy PHP + topology.deploy_k8s_resource("php-deployment.yaml"); + + // Deploy Apache + topology.deploy_k8s_resource("apache-deployment.yaml"); + + // Create service + topology.deploy_k8s_resource("lamp-service.yaml"); + + // Check deployment + let status = topology.execute_kubectl("get pods -l app=lamp"); + println!("LAMP deployment status: {}", status); + } +} + +struct BinaryScore { + url: String, + args: Vec, +} + +impl BinaryScore { + fn new(url: &str, args: Vec<&str>) -> Self { + Self { + url: url.to_string(), + args: args.iter().map(|s| s.to_string()).collect(), + } + } + + fn execute(&self, topology: &T) { + let destination = "/tmp/binary"; + + match topology.download_file(&self.url, destination) { + Ok(_) => { + println!("Binary downloaded successfully"); + + // Convert args to slice of &str + let args: Vec<&str> = self.args.iter().map(|s| s.as_str()).collect(); + + // Execute the binary + topology.execute_command(destination, &args); + println!("Binary execution completed"); + } + Err(e) => { + println!("Failed to download binary: {}", e); + } + } + } +} + +struct LoadBalancerScore { + services: Vec, + port: u16, +} + +impl LoadBalancerScore { + fn new(services: Vec<&str>, port: u16) -> Self { + Self { + services: services.iter().map(|s| s.to_string()).collect(), + port, + } + } + + fn execute(&self, topology: &T) { + println!("Configuring load balancer for services"); + + // Convert services to slice of &str + let services: Vec<&str> = self.services.iter().map(|s| s.as_str()).collect(); + + // Configure load balancer + topology.configure_load_balancer(&services, self.port); + + // Check status + let status = topology.get_load_balancer_status(); + println!("Load balancer status: {}", status); + } +} diff --git a/adr/core-abstractions/topology2/src/main_v1.rs b/adr/core-abstractions/topology2/src/main_v1.rs new file mode 100644 index 0000000..0771470 --- /dev/null +++ b/adr/core-abstractions/topology2/src/main_v1.rs @@ -0,0 +1,34 @@ +fn main() {} + +trait Topology {} + +struct DummyTopology {} + +impl Topology for DummyTopology {} + +impl Topology for LampTopology {} + +struct LampTopology {} + +struct Maestro { + topology: Box, +} + +trait Score { + type Topology: Topology; + fn execute(&self, topology: &Self::Topology); +} + +struct K8sScore {} +impl Score for K8sScore { + type Topology = LampTopology; + fn execute(&self, topology: &Box) { + todo!() + } +} + +impl Maestro { + pub fn execute(&self, score: Box>) { + score.execute(&self.topology); + } +} diff --git a/adr/core-abstractions/topology2/src/main_v2.rs b/adr/core-abstractions/topology2/src/main_v2.rs new file mode 100644 index 0000000..865d0dd --- /dev/null +++ b/adr/core-abstractions/topology2/src/main_v2.rs @@ -0,0 +1,76 @@ +fn main() { + // Example usage + let lamp_topology = LampTopology {}; + let k8s_score = K8sScore {}; + let docker_topology = DockerTopology{}; + + // Type-safe execution + let maestro = Maestro::new(Box::new(docker_topology)); + maestro.execute(&k8s_score); // This will work + + // This would fail at compile time if we tried: + // let dummy_topology = DummyTopology {}; + // let maestro = Maestro::new(Box::new(dummy_topology)); + // maestro.execute(&k8s_score); // Error: expected LampTopology, found DummyTopology +} + +// Base trait for all topologies +trait Topology { + // Common topology methods could go here + fn topology_type(&self) -> &str; +} + +struct DummyTopology {} +impl Topology for DummyTopology { + fn topology_type(&self) -> &str { "Dummy" } +} + +struct LampTopology {} +impl Topology for LampTopology { + fn topology_type(&self) -> &str { "LAMP" } +} + +struct DockerTopology {} + +impl Topology for DockerTopology { + fn topology_type(&self) -> &str { + todo!("DockerTopology") + } +} + +// The Score trait with an associated type for the required topology +trait Score { + type RequiredTopology: Topology + ?Sized; + fn execute(&self, topology: &Self::RequiredTopology); + fn score_type(&self) -> &str; +} + +// A score that requires LampTopology +struct K8sScore {} +impl Score for K8sScore { + type RequiredTopology = DockerTopology; + + fn execute(&self, topology: &Self::RequiredTopology) { + println!("Executing K8sScore on {} topology", topology.topology_type()); + // Implementation details... + } + + fn score_type(&self) -> &str { "K8s" } +} + +// A generic maestro that can work with any topology type +struct Maestro { + topology: Box, +} + +impl Maestro { + pub fn new(topology: Box) -> Self { + Maestro { topology } + } + + // Execute a score that requires this specific topology type + pub fn execute>(&self, score: &S) { + println!("Maestro executing {} score", score.score_type()); + score.execute(&*self.topology); + } +} diff --git a/adr/core-abstractions/topology2/src/main_v4.rs b/adr/core-abstractions/topology2/src/main_v4.rs new file mode 100644 index 0000000..8f8d004 --- /dev/null +++ b/adr/core-abstractions/topology2/src/main_v4.rs @@ -0,0 +1,360 @@ +fn main() { + // Create topologies + let okd_topology = OKDHaClusterTopology::new(); + let k3d_topology = K3DTopology::new(); + let linux_topology = LinuxTopology::new(); + + // Create scores - boxing them as trait objects for dynamic dispatch + let scores: Vec> = vec![ + Box::new(LAMPScore::new("MySQL 8.0", "PHP 8.1", "Apache 2.4")), + Box::new(BinaryScore::new("https://example.com/binary", vec!["--arg1", "--arg2"])), + Box::new(LoadBalancerScore::new(vec!["service1", "service2"], 80)), + ]; + + // Running scores on OKD topology (which has all capabilities) + println!("\n=== Running all scores on OKD HA Cluster ==="); + for score in &scores { + match score.execute(&okd_topology) { + Ok(result) => println!("Score executed successfully: {}", result), + Err(e) => println!("Failed to execute score: {}", e), + } + } + + // Running scores on K3D topology (only has K8s capability) + println!("\n=== Running scores on K3D Cluster ==="); + for score in &scores { + match score.execute(&k3d_topology) { + Ok(result) => println!("Score executed successfully: {}", result), + Err(e) => println!("Failed to execute score: {}", e), + } + } + + // Running scores on Linux topology (only has Linux capability) + println!("\n=== Running scores on Linux Host ==="); + for score in &scores { + match score.execute(&linux_topology) { + Ok(result) => println!("Score executed successfully: {}", result), + Err(e) => println!("Failed to execute score: {}", e), + } + } +} + +// Base Topology trait +trait Topology: Any { + fn name(&self) -> &str; + + // This method allows us to get type information at runtime + fn as_any(&self) -> &dyn Any; +} + +// Use Any trait for runtime type checking +use std::any::Any; + +// Define capabilities +trait K8sCapability { + fn deploy_k8s_resource(&self, resource_yaml: &str); + fn execute_kubectl(&self, command: &str) -> String; +} + +trait OKDCapability: K8sCapability { + fn execute_oc(&self, command: &str) -> String; +} + +trait LinuxCapability { + fn execute_command(&self, command: &str, args: &[&str]); + fn download_file(&self, url: &str, destination: &str) -> Result<(), String>; +} + +trait LoadBalancerCapability { + fn configure_load_balancer(&self, services: &[&str], port: u16); + fn get_load_balancer_status(&self) -> String; +} + +// Base Score trait with dynamic dispatch +trait Score { + // Generic execute method that takes any topology + fn execute(&self, topology: &dyn Topology) -> Result; + + // Optional method to get score type for better error messages + fn score_type(&self) -> &str; +} + +// Topology implementations +struct OKDHaClusterTopology { + cluster_name: String, +} + +impl OKDHaClusterTopology { + fn new() -> Self { + Self { cluster_name: "okd-ha-cluster".to_string() } + } +} + +impl Topology for OKDHaClusterTopology { + fn name(&self) -> &str { + &self.cluster_name + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl K8sCapability for OKDHaClusterTopology { + fn deploy_k8s_resource(&self, resource_yaml: &str) { + println!("Deploying K8s resource on OKD cluster: {}", resource_yaml); + } + + fn execute_kubectl(&self, command: &str) -> String { + println!("Executing kubectl command on OKD cluster: {}", command); + "kubectl command output".to_string() + } +} + +impl OKDCapability for OKDHaClusterTopology { + fn execute_oc(&self, command: &str) -> String { + println!("Executing oc command on OKD cluster: {}", command); + "oc command output".to_string() + } +} + +impl LinuxCapability for OKDHaClusterTopology { + fn execute_command(&self, command: &str, args: &[&str]) { + println!("Executing command '{}' with args {:?} on OKD node", command, args); + } + + fn download_file(&self, url: &str, destination: &str) -> Result<(), String> { + println!("Downloading file from {} to {} on OKD node", url, destination); + Ok(()) + } +} + +impl LoadBalancerCapability for OKDHaClusterTopology { + fn configure_load_balancer(&self, services: &[&str], port: u16) { + println!("Configuring load balancer for services {:?} on port {} in OKD", services, port); + } + + fn get_load_balancer_status(&self) -> String { + "OKD Load Balancer: HEALTHY".to_string() + } +} + +struct K3DTopology { + cluster_name: String, +} + +impl K3DTopology { + fn new() -> Self { + Self { cluster_name: "k3d-local".to_string() } + } +} + +impl Topology for K3DTopology { + fn name(&self) -> &str { + &self.cluster_name + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl K8sCapability for K3DTopology { + fn deploy_k8s_resource(&self, resource_yaml: &str) { + println!("Deploying K8s resource on K3D cluster: {}", resource_yaml); + } + + fn execute_kubectl(&self, command: &str) -> String { + println!("Executing kubectl command on K3D cluster: {}", command); + "kubectl command output from K3D".to_string() + } +} + +struct LinuxTopology { + hostname: String, +} + +impl LinuxTopology { + fn new() -> Self { + Self { hostname: "linux-host".to_string() } + } +} + +impl Topology for LinuxTopology { + fn name(&self) -> &str { + &self.hostname + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl LinuxCapability for LinuxTopology { + fn execute_command(&self, command: &str, args: &[&str]) { + println!("Executing command '{}' with args {:?} on Linux host", command, args); + } + + fn download_file(&self, url: &str, destination: &str) -> Result<(), String> { + println!("Downloading file from {} to {} on Linux host", url, destination); + Ok(()) + } +} + +// Score implementations using dynamic capability checks +struct LAMPScore { + mysql_version: String, + php_version: String, + apache_version: String, +} + +impl LAMPScore { + fn new(mysql_version: &str, php_version: &str, apache_version: &str) -> Self { + Self { + mysql_version: mysql_version.to_string(), + php_version: php_version.to_string(), + apache_version: apache_version.to_string(), + } + } + + // Helper method for typesafe execution + fn execute_with_k8s(&self, topology: &dyn K8sCapability) -> String { + println!("Deploying LAMP stack with MySQL {}, PHP {}, Apache {}", + self.mysql_version, self.php_version, self.apache_version); + + // Deploy MySQL + topology.deploy_k8s_resource("mysql-deployment.yaml"); + + // Deploy PHP + topology.deploy_k8s_resource("php-deployment.yaml"); + + // Deploy Apache + topology.deploy_k8s_resource("apache-deployment.yaml"); + + // Create service + topology.deploy_k8s_resource("lamp-service.yaml"); + + // Check deployment + let status = topology.execute_kubectl("get pods -l app=lamp"); + format!("LAMP deployment status: {}", status) + } +} + +impl Score for LAMPScore { + fn execute(&self, topology: &dyn Topology) -> Result { + // Try to downcast to K8sCapability + if let Some(k8s_topology) = topology.as_any().downcast_ref::() { + Ok(self.execute_with_k8s(k8s_topology)) + } else if let Some(k8s_topology) = topology.as_any().downcast_ref::() { + Ok(self.execute_with_k8s(k8s_topology)) + } else { + Err(format!("LAMPScore requires K8sCapability but topology {} doesn't provide it", + topology.name())) + } + } + + fn score_type(&self) -> &str { + "LAMP" + } +} + +struct BinaryScore { + url: String, + args: Vec, +} + +impl BinaryScore { + fn new(url: &str, args: Vec<&str>) -> Self { + Self { + url: url.to_string(), + args: args.iter().map(|s| s.to_string()).collect(), + } + } + + // Helper method for typesafe execution + fn execute_with_linux(&self, topology: &dyn LinuxCapability) -> Result { + let destination = "/tmp/binary"; + + // Download the binary + println!("Preparing to run binary from {}", self.url); + + match topology.download_file(&self.url, destination) { + Ok(_) => { + println!("Binary downloaded successfully"); + + // Convert args to slice of &str + let args: Vec<&str> = self.args.iter().map(|s| s.as_str()).collect(); + + // Execute the binary + topology.execute_command(destination, &args); + Ok("Binary execution completed successfully".to_string()) + }, + Err(e) => { + Err(format!("Failed to download binary: {}", e)) + } + } + } +} + +impl Score for BinaryScore { + fn execute(&self, topology: &dyn Topology) -> Result { + // Try to downcast to LinuxCapability + if let Some(linux_topology) = topology.as_any().downcast_ref::() { + self.execute_with_linux(linux_topology) + } else if let Some(linux_topology) = topology.as_any().downcast_ref::() { + self.execute_with_linux(linux_topology) + } else { + Err(format!("BinaryScore requires LinuxCapability but topology {} doesn't provide it", + topology.name())) + } + } + + fn score_type(&self) -> &str { + "Binary" + } +} + +struct LoadBalancerScore { + services: Vec, + port: u16, +} + +impl LoadBalancerScore { + fn new(services: Vec<&str>, port: u16) -> Self { + Self { + services: services.iter().map(|s| s.to_string()).collect(), + port, + } + } + + // Helper method for typesafe execution + fn execute_with_lb(&self, topology: &dyn LoadBalancerCapability) -> String { + println!("Configuring load balancer for services"); + + // Convert services to slice of &str + let services: Vec<&str> = self.services.iter().map(|s| s.as_str()).collect(); + + // Configure load balancer + topology.configure_load_balancer(&services, self.port); + + // Check status + let status = topology.get_load_balancer_status(); + format!("Load balancer configured successfully. Status: {}", status) + } +} + +impl Score for LoadBalancerScore { + fn execute(&self, topology: &dyn Topology) -> Result { + // Only OKDHaClusterTopology implements LoadBalancerCapability + if let Some(lb_topology) = topology.as_any().downcast_ref::() { + Ok(self.execute_with_lb(lb_topology)) + } else { + Err(format!("LoadBalancerScore requires LoadBalancerCapability but topology {} doesn't provide it", + topology.name())) + } + } + + fn score_type(&self) -> &str { + "LoadBalancer" + } +} From ab9b7476a49e735173dc1f4190485bdb8482b5b6 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 3 Apr 2025 13:41:29 -0400 Subject: [PATCH 19/62] feat: add load balancer score and frontend integration - Implemented `OKDLoadBalancerScore` and integrated it as a `FrontendScore`. - Added `FrontendScore` trait for TUI displayable scores. - Implemented `Display` for `OKDLoadBalancerScore`. - Updated `ScoreListWidget` to handle `FrontendScore` types. - Included load balancer score in the TUI. --- Cargo.lock | 7 ------- examples/nanodc/src/main.rs | 6 ++++++ examples/opnsense/src/main.rs | 5 ++--- harmony/src/domain/score.rs | 2 ++ harmony/src/domain/topology/ha_cluster.rs | 1 + harmony/src/modules/load_balancer.rs | 4 +++- harmony/src/modules/okd/load_balancer.rs | 10 +++++++++- harmony_tui/src/widget/score.rs | 6 ++++-- 8 files changed, 27 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ac2599..d825f9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -859,13 +859,6 @@ dependencies = [ "url", ] -[[package]] -name = "example-topology" -version = "0.1.0" -dependencies = [ - "rand", -] - [[package]] name = "example-tui" version = "0.1.0" diff --git a/examples/nanodc/src/main.rs b/examples/nanodc/src/main.rs index 3a683d0..66a5de5 100644 --- a/examples/nanodc/src/main.rs +++ b/examples/nanodc/src/main.rs @@ -12,6 +12,12 @@ async fn main() { let mut maestro = Maestro::new(inventory, topology); maestro.register_all(vec![ + // ADD scores : + // 1. OPNSense setup scores + // 2. Bootstrap node setup + // 3. Control plane setup + // 4. Workers setup + // 5. Various tools and apps setup Box::new(SuccessScore {}), Box::new(ErrorScore {}), Box::new(PanicScore {}), diff --git a/examples/opnsense/src/main.rs b/examples/opnsense/src/main.rs index 6315390..ddf781d 100644 --- a/examples/opnsense/src/main.rs +++ b/examples/opnsense/src/main.rs @@ -12,7 +12,7 @@ use harmony::{ modules::{ dummy::{ErrorScore, PanicScore, SuccessScore}, http::HttpScore, - okd::{dhcp::OKDDhcpScore, dns::OKDDnsScore}, + okd::{dhcp::OKDDhcpScore, dns::OKDDnsScore, load_balancer::OKDLoadBalancerScore}, opnsense::OPNsenseShellCommandScore, tftp::TftpScore, }, @@ -78,8 +78,7 @@ async fn main() { let dhcp_score = OKDDhcpScore::new(&topology, &inventory); let dns_score = OKDDnsScore::new(&topology); - let load_balancer_score = - harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology); + let load_balancer_score = OKDLoadBalancerScore::new(&topology); let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string())); let http_score = HttpScore::new(Url::LocalFolder( diff --git a/harmony/src/domain/score.rs b/harmony/src/domain/score.rs index 6e4eed4..f1b9378 100644 --- a/harmony/src/domain/score.rs +++ b/harmony/src/domain/score.rs @@ -19,3 +19,5 @@ where Box::new(self.clone()) } } + +pub trait FrontendScore: Score + std::fmt::Display {} diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 0e9230b..b6c1c9a 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -334,6 +334,7 @@ impl TftpServer for DummyInfra { #[async_trait] impl HttpServer for DummyInfra { async fn serve_files(&self, _url: &Url) -> Result<(), ExecutorError> { + unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } fn get_ip(&self) -> IpAddress { diff --git a/harmony/src/modules/load_balancer.rs b/harmony/src/modules/load_balancer.rs index f590fa1..91ff16b 100644 --- a/harmony/src/modules/load_balancer.rs +++ b/harmony/src/modules/load_balancer.rs @@ -5,7 +5,7 @@ use crate::{ data::{Id, Version}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, - score::Score, + score::{FrontendScore, Score}, topology::{LoadBalancer, LoadBalancerService, Topology}, }; @@ -19,6 +19,8 @@ pub struct LoadBalancerScore { // uuid? } +impl FrontendScore for LoadBalancerScore {} + impl Score for LoadBalancerScore { fn create_interpret(&self) -> Box> { Box::new(LoadBalancerInterpret::new(self.clone())) diff --git a/harmony/src/modules/okd/load_balancer.rs b/harmony/src/modules/okd/load_balancer.rs index c3b5264..47185e1 100644 --- a/harmony/src/modules/okd/load_balancer.rs +++ b/harmony/src/modules/okd/load_balancer.rs @@ -3,12 +3,20 @@ use std::net::SocketAddr; use crate::{ interpret::Interpret, modules::load_balancer::LoadBalancerScore, - score::Score, + score::{FrontendScore, Score}, topology::{ BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancer, LoadBalancerService, Topology }, }; +impl std::fmt::Display for OKDLoadBalancerScore { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + todo!() + } +} + +impl FrontendScore for OKDLoadBalancerScore {} + #[derive(Debug, Clone)] pub struct OKDLoadBalancerScore { load_balancer_score: LoadBalancerScore, diff --git a/harmony_tui/src/widget/score.rs b/harmony_tui/src/widget/score.rs index 514ab79..ea62249 100644 --- a/harmony_tui/src/widget/score.rs +++ b/harmony_tui/src/widget/score.rs @@ -1,7 +1,7 @@ use std::sync::{Arc, RwLock}; use crossterm::event::{Event, KeyCode, KeyEventKind}; -use harmony::{score::Score, topology::Topology}; +use harmony::{modules::okd::load_balancer::OKDLoadBalancerScore, score::Score, topology::{LoadBalancer, Topology}}; use log::{info, warn}; use ratatui::{ layout::Rect, style::{Style, Stylize}, widgets::{List, ListItem, ListState, StatefulWidget, Widget}, Frame @@ -23,10 +23,12 @@ struct Execution { score: Box>, } +impl FrontendScore for OKDLoadBalancerScore {} + #[derive(Debug)] pub(crate) struct ScoreListWidget { list_state: Arc>, - scores: Vec>>, + scores: Vec>>, execution: Option>, execution_history: Vec>, sender: mpsc::Sender>, From b4cc5cff4f3d40e3a18cb65dbf527148f4a109f4 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Sat, 5 Apr 2025 14:33:39 -0400 Subject: [PATCH 20/62] feat: add serde derive to Score types This commit adds `serde` dependency and derives `Serialize` trait for `Score` types. This is necessary for serialization and deserialization of these types, which is required to display Scores to various user interfaces - Added `serde` dependency to `harmony_types/Cargo.toml`. - Added `serde::Serialize` derive macro to `MacAddress` in `harmony_types/src/lib.rs`. - Added `serde::Serialize` derive macro to `Config` in `opnsense-config/src/config/config.rs`. - Added `serde::Serialize` derive macro to `Score` in `harmony_types/src/lib.rs`. - Added `serde::Serialize` derive macro to `Config` and `Score` in relevant modules. - Added placeholder `todo!()` implementations for `serialize` methods. These will be implemented in future commits. --- Cargo.lock | 4 + Cargo.toml | 1 + examples/lamp/src/main.rs | 7 +- examples/nanodc/src/main.rs | 5 +- harmony/Cargo.toml | 1 + harmony/src/domain/hardware/mod.rs | 269 ++++++++++++++++-- harmony/src/domain/interpret/mod.rs | 2 +- harmony/src/domain/score.rs | 26 +- harmony/src/domain/topology/ha_cluster.rs | 1 - harmony/src/domain/topology/host_binding.rs | 3 +- harmony/src/domain/topology/load_balancer.rs | 11 +- harmony/src/domain/topology/mod.rs | 37 ++- harmony/src/domain/topology/network.rs | 16 +- harmony/src/infra/hp_ilo/mod.rs | 3 +- harmony/src/infra/intel_amt/mod.rs | 3 +- harmony/src/infra/opnsense/management.rs | 3 +- harmony/src/modules/dhcp.rs | 6 +- harmony/src/modules/dns.rs | 3 +- harmony/src/modules/dummy.rs | 7 +- harmony/src/modules/http.rs | 3 +- harmony/src/modules/k8s/deployment.rs | 11 +- harmony/src/modules/k8s/resource.rs | 4 +- harmony/src/modules/lamp.rs | 9 +- harmony/src/modules/load_balancer.rs | 7 +- harmony/src/modules/mod.rs | 2 +- harmony/src/modules/okd/bootstrap_dhcp.rs | 4 +- .../modules/okd/bootstrap_load_balancer.rs | 7 +- harmony/src/modules/okd/dhcp.rs | 4 +- harmony/src/modules/okd/dns.rs | 4 +- harmony/src/modules/okd/load_balancer.rs | 11 +- harmony/src/modules/opnsense/mod.rs | 2 - harmony/src/modules/opnsense/shell.rs | 20 +- harmony/src/modules/opnsense/upgrade.rs | 10 + harmony/src/modules/tftp.rs | 7 +- harmony_tui/src/widget/score.rs | 17 +- harmony_types/Cargo.toml | 3 + harmony_types/src/lib.rs | 4 +- opnsense-config/src/config/config.rs | 10 + 38 files changed, 450 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d825f9c..f9c279f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1158,6 +1158,7 @@ dependencies = [ "rust-ipmi", "semver", "serde", + "serde-value", "serde_json", "serde_yaml", "tokio", @@ -1195,6 +1196,9 @@ dependencies = [ [[package]] name = "harmony_types" version = "0.1.0" +dependencies = [ + "serde", +] [[package]] name = "hashbrown" diff --git a/Cargo.toml b/Cargo.toml index 8c7afdd..c36cd27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ url = "2.5.4" kube = "0.98.0" k8s-openapi = { version = "0.24.0", features = [ "v1_30" ] } serde_yaml = "0.9.34" +serde-value = "0.7.0" http = "1.2.0" [workspace.dependencies.uuid] diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index 1251b8c..7277aa5 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -2,8 +2,7 @@ use harmony::{ data::Version, maestro::Maestro, modules::lamp::{LAMPConfig, LAMPScore}, - score::Score, - topology::{HAClusterTopology, Topology, Url}, + topology::{HAClusterTopology, Url}, }; #[tokio::main] @@ -23,7 +22,3 @@ async fn main() { .await .unwrap(); } - -fn clone_score + Clone + 'static>(score: S) -> Box { - Box::new(score.clone()) -} diff --git a/examples/nanodc/src/main.rs b/examples/nanodc/src/main.rs index 66a5de5..8aad09a 100644 --- a/examples/nanodc/src/main.rs +++ b/examples/nanodc/src/main.rs @@ -1,7 +1,10 @@ use harmony::{ inventory::Inventory, maestro::Maestro, - modules::{dummy::{ErrorScore, PanicScore, SuccessScore}, k8s::deployment::K8sDeploymentScore}, + modules::{ + dummy::{ErrorScore, PanicScore, SuccessScore}, + k8s::deployment::K8sDeploymentScore, + }, topology::HAClusterTopology, }; diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index f084af0..c5348d9 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -29,3 +29,4 @@ kube = { workspace = true } k8s-openapi = { workspace = true } serde_yaml = { workspace = true } http = { workspace = true } +serde-value = { workspace = true } diff --git a/harmony/src/domain/hardware/mod.rs b/harmony/src/domain/hardware/mod.rs index 47d7a33..8e24768 100644 --- a/harmony/src/domain/hardware/mod.rs +++ b/harmony/src/domain/hardware/mod.rs @@ -2,6 +2,8 @@ use std::sync::Arc; use derive_new::new; use harmony_types::net::MacAddress; +use serde::{Serialize, Serializer, ser::SerializeStruct}; +use serde_value::Value; pub type HostGroup = Vec; pub type SwitchGroup = Vec; @@ -75,10 +77,7 @@ impl PhysicalHost { } pub fn label(mut self, name: String, value: String) -> Self { - self.labels.push(Label { - _name: name, - _value: value, - }); + self.labels.push(Label { name, value }); self } @@ -88,7 +87,49 @@ impl PhysicalHost { } } -#[derive(new)] +// Custom Serialize implementation for PhysicalHost +impl Serialize for PhysicalHost { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Determine the number of fields + let mut num_fields = 5; // category, network, storage, labels, management + if self.memory_size.is_some() { + num_fields += 1; + } + if self.cpu_count.is_some() { + num_fields += 1; + } + + // Create a serialization structure + let mut state = serializer.serialize_struct("PhysicalHost", num_fields)?; + + // Serialize the standard fields + state.serialize_field("category", &self.category)?; + state.serialize_field("network", &self.network)?; + state.serialize_field("storage", &self.storage)?; + state.serialize_field("labels", &self.labels)?; + + // Serialize optional fields + if let Some(memory) = self.memory_size { + state.serialize_field("memory_size", &memory)?; + } + if let Some(cpu) = self.cpu_count { + state.serialize_field("cpu_count", &cpu)?; + } + + let mgmt_data = self.management.serialize_management(); + // pub management: Arc, + + // Handle management interface - either as a field or flattened + state.serialize_field("management", &mgmt_data)?; + + state.end() + } +} + +#[derive(new, Serialize)] pub struct ManualManagementInterface; impl ManagementInterface for ManualManagementInterface { @@ -101,7 +142,7 @@ impl ManagementInterface for ManualManagementInterface { } } -pub trait ManagementInterface: Send + Sync { +pub trait ManagementInterface: Send + Sync + SerializableManagement { fn boot_to_pxe(&self); fn get_supported_protocol_names(&self) -> String; } @@ -115,21 +156,49 @@ impl std::fmt::Debug for dyn ManagementInterface { } } -#[derive(Debug, Clone)] +// Define a trait for serializing management interfaces +pub trait SerializableManagement { + fn serialize_management(&self) -> Value; +} + +// Provide a blanket implementation for all types that implement both ManagementInterface and Serialize +impl SerializableManagement for T +where + T: ManagementInterface + Serialize, +{ + fn serialize_management(&self) -> Value { + serde_value::to_value(self).expect("ManagementInterface should serialize successfully") + } +} + +#[derive(Debug, Clone, Serialize)] pub enum HostCategory { Server, Firewall, Switch, } -#[derive(Debug, new, Clone)] +#[derive(Debug, new, Clone, Serialize)] pub struct NetworkInterface { pub name: Option, pub mac_address: MacAddress, pub speed: Option, } -#[derive(Debug, new, Clone)] +#[cfg(test)] +use harmony_macros::mac_address; +#[cfg(test)] +impl NetworkInterface { + pub fn dummy() -> Self { + Self { + name: Some(String::new()), + mac_address: mac_address!("00:00:00:00:00:00"), + speed: Some(0), + } + } +} + +#[derive(Debug, new, Clone, Serialize)] pub enum StorageConnectionType { Sata3g, Sata6g, @@ -137,13 +206,13 @@ pub enum StorageConnectionType { Sas12g, PCIE, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub enum StorageKind { SSD, NVME, HDD, } -#[derive(Debug, new, Clone)] +#[derive(Debug, new, Clone, Serialize)] pub struct Storage { pub connection: StorageConnectionType, pub kind: StorageKind, @@ -151,20 +220,33 @@ pub struct Storage { pub serial: String, } -#[derive(Debug, Clone)] +#[cfg(test)] +impl Storage { + pub fn dummy() -> Self { + Self { + connection: StorageConnectionType::Sata3g, + kind: StorageKind::SSD, + size: 0, + serial: String::new(), + } + } +} + +#[derive(Debug, Clone, Serialize)] pub struct Switch { _interface: Vec, _management_interface: NetworkInterface, } -#[derive(Debug, new, Clone)] +#[derive(Debug, new, Clone, Serialize)] pub struct Label { - _name: String, - _value: String, + pub name: String, + pub value: String, } + pub type Address = String; -#[derive(new, Debug)] +#[derive(new, Debug, Serialize)] pub struct Location { pub address: Address, pub name: String, @@ -178,3 +260,158 @@ impl Location { } } } + +#[cfg(test)] +mod tests { + use super::*; + use serde::{Deserialize, Serialize}; + use std::sync::Arc; + + // Mock implementation of ManagementInterface + #[derive(Debug, Clone, Serialize, Deserialize)] + struct MockHPIlo { + ip: String, + username: String, + password: String, + firmware_version: String, + } + + impl ManagementInterface for MockHPIlo { + fn boot_to_pxe(&self) {} + + fn get_supported_protocol_names(&self) -> String { + String::new() + } + } + + // Another mock implementation + #[derive(Debug, Clone, Serialize, Deserialize)] + struct MockDellIdrac { + hostname: String, + port: u16, + api_token: String, + } + + impl ManagementInterface for MockDellIdrac { + fn boot_to_pxe(&self) {} + + fn get_supported_protocol_names(&self) -> String { + String::new() + } + } + + #[test] + fn test_serialize_physical_host_with_hp_ilo() { + // Create a PhysicalHost with HP iLO management + let host = PhysicalHost { + category: HostCategory::Server, + network: vec![NetworkInterface::dummy()], + management: Arc::new(MockHPIlo { + ip: "192.168.1.100".to_string(), + username: "admin".to_string(), + password: "password123".to_string(), + firmware_version: "2.5.0".to_string(), + }), + storage: vec![Storage::dummy()], + labels: vec![Label::new("datacenter".to_string(), "us-east".to_string())], + memory_size: Some(64_000_000), + cpu_count: Some(16), + }; + + // Serialize to JSON + let json = serde_json::to_string(&host).expect("Failed to serialize host"); + + // Check that the serialized JSON contains the HP iLO details + assert!(json.contains("192.168.1.100")); + assert!(json.contains("admin")); + assert!(json.contains("password123")); + assert!(json.contains("firmware_version")); + assert!(json.contains("2.5.0")); + + // Parse back to verify structure (not the exact management interface) + let parsed: serde_json::Value = serde_json::from_str(&json).expect("Failed to parse JSON"); + + // Verify basic structure + assert_eq!(parsed["cpu_count"], 16); + assert_eq!(parsed["memory_size"], 64_000_000); + assert_eq!(parsed["network"][0]["name"], ""); + } + + #[test] + fn test_serialize_physical_host_with_dell_idrac() { + // Create a PhysicalHost with Dell iDRAC management + let host = PhysicalHost { + category: HostCategory::Server, + network: vec![NetworkInterface::dummy()], + management: Arc::new(MockDellIdrac { + hostname: "idrac-server01".to_string(), + port: 443, + api_token: "abcdef123456".to_string(), + }), + storage: vec![Storage::dummy()], + labels: vec![Label::new("env".to_string(), "production".to_string())], + memory_size: Some(128_000_000), + cpu_count: Some(32), + }; + + // Serialize to JSON + let json = serde_json::to_string(&host).expect("Failed to serialize host"); + + // Check that the serialized JSON contains the Dell iDRAC details + assert!(json.contains("idrac-server01")); + assert!(json.contains("443")); + assert!(json.contains("abcdef123456")); + + // Parse back to verify structure + let parsed: serde_json::Value = serde_json::from_str(&json).expect("Failed to parse JSON"); + + // Verify basic structure + assert_eq!(parsed["cpu_count"], 32); + assert_eq!(parsed["memory_size"], 128_000_000); + assert_eq!(parsed["storage"][0]["path"], serde_json::Value::Null); + } + + #[test] + fn test_different_management_implementations_produce_valid_json() { + // Create hosts with different management implementations + let host1 = PhysicalHost { + category: HostCategory::Server, + network: vec![], + management: Arc::new(MockHPIlo { + ip: "10.0.0.1".to_string(), + username: "root".to_string(), + password: "secret".to_string(), + firmware_version: "3.0.0".to_string(), + }), + storage: vec![], + labels: vec![], + memory_size: None, + cpu_count: None, + }; + + let host2 = PhysicalHost { + category: HostCategory::Server, + network: vec![], + management: Arc::new(MockDellIdrac { + hostname: "server02-idrac".to_string(), + port: 8443, + api_token: "token123".to_string(), + }), + storage: vec![], + labels: vec![], + memory_size: None, + cpu_count: None, + }; + + // Both should serialize successfully + let json1 = serde_json::to_string(&host1).expect("Failed to serialize host1"); + let json2 = serde_json::to_string(&host2).expect("Failed to serialize host2"); + + // Both JSONs should be valid and parseable + let _: serde_json::Value = serde_json::from_str(&json1).expect("Invalid JSON for host1"); + let _: serde_json::Value = serde_json::from_str(&json2).expect("Invalid JSON for host2"); + + // The JSONs should be different because they contain different management interfaces + assert_ne!(json1, json2); + } +} diff --git a/harmony/src/domain/interpret/mod.rs b/harmony/src/domain/interpret/mod.rs index 0268ffd..9cec988 100644 --- a/harmony/src/domain/interpret/mod.rs +++ b/harmony/src/domain/interpret/mod.rs @@ -37,7 +37,7 @@ impl std::fmt::Display for InterpretName { } #[async_trait] -pub trait Interpret: std::fmt::Debug + Send { +pub trait Interpret: std::fmt::Debug + Send { async fn execute(&self, inventory: &Inventory, topology: &T) -> Result; fn get_name(&self) -> InterpretName; diff --git a/harmony/src/domain/score.rs b/harmony/src/domain/score.rs index f1b9378..dbe7aa5 100644 --- a/harmony/src/domain/score.rs +++ b/harmony/src/domain/score.rs @@ -1,15 +1,35 @@ +use serde::Serialize; +use serde_value::Value; + use super::{interpret::Interpret, topology::Topology}; -pub trait Score: std::fmt::Debug + Send + Sync + CloneBoxScore { +pub trait Score: + std::fmt::Debug + Send + Sync + CloneBoxScore + SerializeScore +{ fn create_interpret(&self) -> Box>; fn name(&self) -> String; } +pub trait SerializeScore { + fn serialize(&self) -> Value; +} + +impl<'de, S, T> SerializeScore for S +where + T: Topology, + S: Score + Serialize, +{ + fn serialize(&self) -> Value { + // TODO not sure if this is the right place to handle the error or it should bubble + // up? + serde_value::to_value(&self).expect("Score should serialize successfully") + } +} + pub trait CloneBoxScore { fn clone_box(&self) -> Box>; } - impl CloneBoxScore for S where T: Topology, @@ -19,5 +39,3 @@ where Box::new(self.clone()) } } - -pub trait FrontendScore: Score + std::fmt::Display {} diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index b6c1c9a..0e9230b 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -334,7 +334,6 @@ impl TftpServer for DummyInfra { #[async_trait] impl HttpServer for DummyInfra { async fn serve_files(&self, _url: &Url) -> Result<(), ExecutorError> { - unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) } fn get_ip(&self) -> IpAddress { diff --git a/harmony/src/domain/topology/host_binding.rs b/harmony/src/domain/topology/host_binding.rs index c01869a..371f159 100644 --- a/harmony/src/domain/topology/host_binding.rs +++ b/harmony/src/domain/topology/host_binding.rs @@ -1,4 +1,5 @@ use derive_new::new; +use serde::Serialize; use crate::hardware::PhysicalHost; @@ -8,7 +9,7 @@ use super::LogicalHost; /// /// This is the only construct that directly maps a logical host to a physical host. /// It serves as a bridge between the logical cluster structure and the physical infrastructure. -#[derive(Debug, new, Clone)] +#[derive(Debug, new, Clone, Serialize)] pub struct HostBinding { /// Reference to the LogicalHost pub logical_host: LogicalHost, diff --git a/harmony/src/domain/topology/load_balancer.rs b/harmony/src/domain/topology/load_balancer.rs index 4ebcba8..afb9092 100644 --- a/harmony/src/domain/topology/load_balancer.rs +++ b/harmony/src/domain/topology/load_balancer.rs @@ -2,6 +2,7 @@ use std::{net::SocketAddr, str::FromStr}; use async_trait::async_trait; use log::debug; +use serde::Serialize; use super::{IpAddress, LogicalHost}; use crate::executors::ExecutorError; @@ -36,20 +37,20 @@ impl std::fmt::Debug for dyn LoadBalancer { f.write_fmt(format_args!("LoadBalancer {}", self.get_ip())) } } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Serialize)] pub struct LoadBalancerService { pub backend_servers: Vec, pub listening_port: SocketAddr, pub health_check: Option, } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Serialize)] pub struct BackendServer { pub address: String, pub port: u16, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize)] pub enum HttpMethod { GET, POST, @@ -91,14 +92,14 @@ impl std::fmt::Display for HttpMethod { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize)] pub enum HttpStatusCode { Success2xx, UserError4xx, ServerError5xx, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize)] pub enum HealthCheck { HTTP(String, HttpMethod, HttpStatusCode), TCP(Option), diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index e1c7f7c..fa0b7fe 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -12,6 +12,7 @@ mod network; pub use host_binding::*; pub use http::*; pub use network::*; +use serde::Serialize; pub use tftp::*; use std::net::IpAddr; @@ -20,8 +21,6 @@ pub trait Topology { fn name(&self) -> &str; } -pub trait Capability {} - pub type IpAddress = IpAddr; #[derive(Debug, Clone)] @@ -30,6 +29,18 @@ pub enum Url { Url(url::Url), } +impl Serialize for Url { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Url::LocalFolder(path) => serializer.serialize_str(path), + Url::Url(url) => serializer.serialize_str(&url.as_str()), + } + } +} + impl std::fmt::Display for Url { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -48,7 +59,7 @@ impl std::fmt::Display for Url { /// - A control plane node /// /// This abstraction focuses on the logical role and services, independent of the physical hardware. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct LogicalHost { /// The IP address of this logical host. pub ip: IpAddress, @@ -130,3 +141,23 @@ fn increment_ip(ip: IpAddress, increment: u32) -> Option { } } } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json; + + #[test] + fn test_serialize_local_folder() { + let url = Url::LocalFolder("path/to/folder".to_string()); + let serialized = serde_json::to_string(&url).unwrap(); + assert_eq!(serialized, "\"path/to/folder\""); + } + + #[test] + fn test_serialize_url() { + let url = Url::Url(url::Url::parse("https://example.com").unwrap()); + let serialized = serde_json::to_string(&url).unwrap(); + assert_eq!(serialized, "\"https://example.com/\""); + } +} diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index 6aba5f3..13d1902 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -2,10 +2,11 @@ use std::{net::Ipv4Addr, str::FromStr, sync::Arc}; use async_trait::async_trait; use harmony_types::net::MacAddress; +use serde::Serialize; use crate::executors::ExecutorError; -use super::{openshift::OpenshiftClient, IpAddress, LogicalHost}; +use super::{IpAddress, LogicalHost, openshift::OpenshiftClient}; #[derive(Debug)] pub struct DHCPStaticEntry { @@ -41,11 +42,10 @@ pub struct NetworkDomain { pub name: String, } #[async_trait] -pub trait OcK8sclient: Send + Sync + std::fmt::Debug { +pub trait OcK8sclient: Send + Sync + std::fmt::Debug { async fn oc_client(&self) -> Result, kube::Error>; } - #[async_trait] pub trait DhcpServer: Send + Sync + std::fmt::Debug { async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError>; @@ -62,11 +62,7 @@ pub trait DhcpServer: Send + Sync + std::fmt::Debug { pub trait DnsServer: Send + Sync { async fn register_dhcp_leases(&self, register: bool) -> Result<(), ExecutorError>; async fn register_hosts(&self, hosts: Vec) -> Result<(), ExecutorError>; - fn remove_record( - &self, - name: &str, - record_type: DnsRecordType, - ) -> Result<(), ExecutorError>; + fn remove_record(&self, name: &str, record_type: DnsRecordType) -> Result<(), ExecutorError>; async fn list_records(&self) -> Vec; fn get_ip(&self) -> IpAddress; fn get_host(&self) -> LogicalHost; @@ -117,7 +113,7 @@ pub enum Action { Deny, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub enum DnsRecordType { A, AAAA, @@ -138,7 +134,7 @@ impl std::fmt::Display for DnsRecordType { } } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct DnsRecord { pub host: String, pub domain: String, diff --git a/harmony/src/infra/hp_ilo/mod.rs b/harmony/src/infra/hp_ilo/mod.rs index 051f175..ff0e313 100644 --- a/harmony/src/infra/hp_ilo/mod.rs +++ b/harmony/src/infra/hp_ilo/mod.rs @@ -3,8 +3,9 @@ use crate::topology::IpAddress; use derive_new::new; use harmony_types::net::MacAddress; use log::info; +use serde::Serialize; -#[derive(new)] +#[derive(new, Serialize)] pub struct HPIlo { ip_address: Option, mac_address: Option, diff --git a/harmony/src/infra/intel_amt/mod.rs b/harmony/src/infra/intel_amt/mod.rs index 088afd5..7251729 100644 --- a/harmony/src/infra/intel_amt/mod.rs +++ b/harmony/src/infra/intel_amt/mod.rs @@ -2,8 +2,9 @@ use crate::hardware::ManagementInterface; use derive_new::new; use harmony_types::net::MacAddress; use log::info; +use serde::Serialize; -#[derive(new)] +#[derive(new, Serialize)] pub struct IntelAmtManagement { mac_address: MacAddress, } diff --git a/harmony/src/infra/opnsense/management.rs b/harmony/src/infra/opnsense/management.rs index db2bef4..a8dce18 100644 --- a/harmony/src/infra/opnsense/management.rs +++ b/harmony/src/infra/opnsense/management.rs @@ -1,7 +1,8 @@ use crate::hardware::ManagementInterface; use derive_new::new; +use serde::Serialize; -#[derive(new)] +#[derive(new, Serialize)] pub struct OPNSenseManagementInterface {} impl ManagementInterface for OPNSenseManagementInterface { diff --git a/harmony/src/modules/dhcp.rs b/harmony/src/modules/dhcp.rs index e145e03..6ef0c3d 100644 --- a/harmony/src/modules/dhcp.rs +++ b/harmony/src/modules/dhcp.rs @@ -1,7 +1,7 @@ - use async_trait::async_trait; use derive_new::new; use log::info; +use serde::Serialize; use crate::{ domain::{data::Version, interpret::InterpretStatus}, @@ -12,7 +12,7 @@ use crate::{ use crate::domain::score::Score; -#[derive(Debug, new, Clone)] +#[derive(Debug, new, Clone, Serialize)] pub struct DhcpScore { pub host_binding: Vec, pub next_server: Option, @@ -134,7 +134,7 @@ impl DhcpInterpret { } #[async_trait] -impl Interpret for DhcpInterpret { +impl Interpret for DhcpInterpret { fn get_name(&self) -> InterpretName { InterpretName::OPNSenseDHCP } diff --git a/harmony/src/modules/dns.rs b/harmony/src/modules/dns.rs index c96d08e..e52e2f9 100644 --- a/harmony/src/modules/dns.rs +++ b/harmony/src/modules/dns.rs @@ -1,6 +1,7 @@ use async_trait::async_trait; use derive_new::new; use log::info; +use serde::Serialize; use crate::{ data::Version, @@ -10,7 +11,7 @@ use crate::{ topology::{DnsRecord, DnsServer, Topology}, }; -#[derive(Debug, new, Clone)] +#[derive(Debug, new, Clone, Serialize)] pub struct DnsScore { dns_entries: Vec, register_dhcp_leases: Option, diff --git a/harmony/src/modules/dummy.rs b/harmony/src/modules/dummy.rs index e9bd540..2e63797 100644 --- a/harmony/src/modules/dummy.rs +++ b/harmony/src/modules/dummy.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use serde::Serialize; use crate::{ data::Version, @@ -10,7 +11,7 @@ use crate::{ /// Score that always errors. This is only useful for development/testing purposes. It does nothing /// except returning Err(InterpretError) when interpreted. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct ErrorScore; impl Score for ErrorScore { @@ -28,7 +29,7 @@ impl Score for ErrorScore { /// Score that always succeeds. This is only useful for development/testing purposes. It does nothing /// except returning Ok(Outcome::success) when interpreted. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct SuccessScore; impl Score for SuccessScore { @@ -81,7 +82,7 @@ impl Interpret for DummyInterpret { /// Score that always panics. This is only useful for development/testing purposes. It does nothing /// except panic! with an error message when interpreted -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct PanicScore; impl Score for PanicScore { diff --git a/harmony/src/modules/http.rs b/harmony/src/modules/http.rs index 419bd89..c98ff8f 100644 --- a/harmony/src/modules/http.rs +++ b/harmony/src/modules/http.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; use derive_new::new; +use serde::Serialize; use crate::{ data::{Id, Version}, @@ -9,7 +10,7 @@ use crate::{ topology::{HttpServer, Topology, Url}, }; -#[derive(Debug, new, Clone)] +#[derive(Debug, new, Clone, Serialize)] pub struct HttpScore { files_to_serve: Url, } diff --git a/harmony/src/modules/k8s/deployment.rs b/harmony/src/modules/k8s/deployment.rs index 4528ed6..cd2ad90 100644 --- a/harmony/src/modules/k8s/deployment.rs +++ b/harmony/src/modules/k8s/deployment.rs @@ -1,17 +1,22 @@ use k8s_openapi::api::apps::v1::Deployment; +use serde::Serialize; use serde_json::json; -use crate::{interpret::Interpret, score::Score, topology::{OcK8sclient, Topology}}; +use crate::{ + interpret::Interpret, + score::Score, + topology::{OcK8sclient, Topology}, +}; use super::resource::{K8sResourceInterpret, K8sResourceScore}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct K8sDeploymentScore { pub name: String, pub image: String, } -impl Score for K8sDeploymentScore { +impl Score for K8sDeploymentScore { fn create_interpret(&self) -> Box> { let deployment: Deployment = serde_json::from_value(json!( { diff --git a/harmony/src/modules/k8s/resource.rs b/harmony/src/modules/k8s/resource.rs index e33cd7a..505c4a4 100644 --- a/harmony/src/modules/k8s/resource.rs +++ b/harmony/src/modules/k8s/resource.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use k8s_openapi::NamespaceResourceScope; use kube::Resource; -use serde::de::DeserializeOwned; +use serde::{Serialize, de::DeserializeOwned}; use crate::{ data::{Id, Version}, @@ -11,7 +11,7 @@ use crate::{ topology::{OcK8sclient, Topology}, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct K8sResourceScore { pub resource: Vec, } diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index 90f36da..ef7227c 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -1,6 +1,7 @@ use std::path::{Path, PathBuf}; use async_trait::async_trait; +use serde::Serialize; use crate::{ data::{Id, Version}, @@ -11,7 +12,7 @@ use crate::{ topology::{OcK8sclient, Topology, Url}, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct LAMPScore { pub name: String, pub domain: Url, @@ -19,7 +20,7 @@ pub struct LAMPScore { pub php_version: Version, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct LAMPConfig { pub project_root: PathBuf, pub ssl_enabled: bool, @@ -34,7 +35,7 @@ impl Default for LAMPConfig { } } -impl Score for LAMPScore { +impl Score for LAMPScore { fn create_interpret(&self) -> Box> { todo!() } @@ -50,7 +51,7 @@ pub struct LAMPInterpret { } #[async_trait] -impl Interpret for LAMPInterpret { +impl Interpret for LAMPInterpret { async fn execute( &self, inventory: &Inventory, diff --git a/harmony/src/modules/load_balancer.rs b/harmony/src/modules/load_balancer.rs index 91ff16b..cd78f84 100644 --- a/harmony/src/modules/load_balancer.rs +++ b/harmony/src/modules/load_balancer.rs @@ -1,15 +1,16 @@ use async_trait::async_trait; use log::info; +use serde::Serialize; use crate::{ data::{Id, Version}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, - score::{FrontendScore, Score}, + score::Score, topology::{LoadBalancer, LoadBalancerService, Topology}, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct LoadBalancerScore { pub public_services: Vec, pub private_services: Vec, @@ -19,8 +20,6 @@ pub struct LoadBalancerScore { // uuid? } -impl FrontendScore for LoadBalancerScore {} - impl Score for LoadBalancerScore { fn create_interpret(&self) -> Box> { Box::new(LoadBalancerInterpret::new(self.clone())) diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index 51e164f..8456867 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -3,8 +3,8 @@ pub mod dns; pub mod dummy; pub mod http; pub mod k8s; +pub mod lamp; pub mod load_balancer; pub mod okd; pub mod opnsense; pub mod tftp; -pub mod lamp; diff --git a/harmony/src/modules/okd/bootstrap_dhcp.rs b/harmony/src/modules/okd/bootstrap_dhcp.rs index ebb0a9d..2e3dd6f 100644 --- a/harmony/src/modules/okd/bootstrap_dhcp.rs +++ b/harmony/src/modules/okd/bootstrap_dhcp.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + use crate::{ interpret::Interpret, inventory::Inventory, @@ -6,7 +8,7 @@ use crate::{ topology::{DhcpServer, HAClusterTopology, HostBinding, Topology}, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct OKDBootstrapDhcpScore { dhcp_score: DhcpScore, } diff --git a/harmony/src/modules/okd/bootstrap_load_balancer.rs b/harmony/src/modules/okd/bootstrap_load_balancer.rs index d983eed..d6cd2f3 100644 --- a/harmony/src/modules/okd/bootstrap_load_balancer.rs +++ b/harmony/src/modules/okd/bootstrap_load_balancer.rs @@ -1,15 +1,18 @@ use std::net::SocketAddr; +use serde::Serialize; + use crate::{ interpret::Interpret, modules::load_balancer::LoadBalancerScore, score::Score, topology::{ - BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancer, LoadBalancerService, Topology + BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancer, + LoadBalancerService, Topology, }, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct OKDBootstrapLoadBalancerScore { load_balancer_score: LoadBalancerScore, } diff --git a/harmony/src/modules/okd/dhcp.rs b/harmony/src/modules/okd/dhcp.rs index e3e0e09..a060b31 100644 --- a/harmony/src/modules/okd/dhcp.rs +++ b/harmony/src/modules/okd/dhcp.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + use crate::{ interpret::Interpret, inventory::Inventory, @@ -6,7 +8,7 @@ use crate::{ topology::{DhcpServer, HAClusterTopology, HostBinding, Topology}, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct OKDDhcpScore { dhcp_score: DhcpScore, } diff --git a/harmony/src/modules/okd/dns.rs b/harmony/src/modules/okd/dns.rs index 34c2dc2..6f99cb8 100644 --- a/harmony/src/modules/okd/dns.rs +++ b/harmony/src/modules/okd/dns.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + use crate::{ interpret::Interpret, modules::dns::DnsScore, @@ -5,7 +7,7 @@ use crate::{ topology::{DnsRecord, DnsRecordType, DnsServer, HAClusterTopology, Topology}, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct OKDDnsScore { dns_score: DnsScore, } diff --git a/harmony/src/modules/okd/load_balancer.rs b/harmony/src/modules/okd/load_balancer.rs index 47185e1..0345d46 100644 --- a/harmony/src/modules/okd/load_balancer.rs +++ b/harmony/src/modules/okd/load_balancer.rs @@ -1,11 +1,14 @@ use std::net::SocketAddr; +use serde::Serialize; + use crate::{ interpret::Interpret, modules::load_balancer::LoadBalancerScore, - score::{FrontendScore, Score}, + score::Score, topology::{ - BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancer, LoadBalancerService, Topology + BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancer, + LoadBalancerService, Topology, }, }; @@ -15,9 +18,7 @@ impl std::fmt::Display for OKDLoadBalancerScore { } } -impl FrontendScore for OKDLoadBalancerScore {} - -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct OKDLoadBalancerScore { load_balancer_score: LoadBalancerScore, } diff --git a/harmony/src/modules/opnsense/mod.rs b/harmony/src/modules/opnsense/mod.rs index 763195d..28b52cf 100644 --- a/harmony/src/modules/opnsense/mod.rs +++ b/harmony/src/modules/opnsense/mod.rs @@ -1,6 +1,4 @@ - mod shell; mod upgrade; pub use shell::*; pub use upgrade::*; - diff --git a/harmony/src/modules/opnsense/shell.rs b/harmony/src/modules/opnsense/shell.rs index 225e2ad..a35a43c 100644 --- a/harmony/src/modules/opnsense/shell.rs +++ b/harmony/src/modules/opnsense/shell.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use async_trait::async_trait; +use serde::Serialize; use tokio::sync::RwLock; use crate::{ @@ -13,10 +14,27 @@ use crate::{ #[derive(Debug, Clone)] pub struct OPNsenseShellCommandScore { + // TODO I am pretty sure we should not hold a direct reference to the + // opnsense_config::Config here. + // This causes a problem with serialization but also could cause many more problems as this + // is mixing concerns of configuration (which is the Responsibility of Scores to define) + // and state/execution which is the responsibility of interprets via topologies to manage + // + // I feel like a better solution would be for this Score/Interpret to require + // Topology + OPNSenseShell trait bindings pub opnsense: Arc>, pub command: String, } +impl Serialize for OPNsenseShellCommandScore { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + todo!("See comment about moving opnsense_config::Config outside the score") + } +} + impl Score for OPNsenseShellCommandScore { fn create_interpret(&self) -> Box> { Box::new(OPNsenseShellInterpret { @@ -37,7 +55,7 @@ pub struct OPNsenseShellInterpret { } #[async_trait] -impl Interpret for OPNsenseShellInterpret { +impl Interpret for OPNsenseShellInterpret { async fn execute( &self, _inventory: &Inventory, diff --git a/harmony/src/modules/opnsense/upgrade.rs b/harmony/src/modules/opnsense/upgrade.rs index 06d373f..45adf12 100644 --- a/harmony/src/modules/opnsense/upgrade.rs +++ b/harmony/src/modules/opnsense/upgrade.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use serde::Serialize; use tokio::sync::RwLock; use crate::{ @@ -15,6 +16,15 @@ pub struct OPNSenseLaunchUpgrade { pub opnsense: Arc>, } +impl Serialize for OPNSenseLaunchUpgrade { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + todo!("See comment in OPNSenseShellCommandScore and apply the same idea here") + } +} + impl Score for OPNSenseLaunchUpgrade { fn create_interpret(&self) -> Box> { let score = OPNsenseShellCommandScore { diff --git a/harmony/src/modules/tftp.rs b/harmony/src/modules/tftp.rs index 3e90fc0..357e480 100644 --- a/harmony/src/modules/tftp.rs +++ b/harmony/src/modules/tftp.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; use derive_new::new; +use serde::Serialize; use crate::{ data::{Id, Version}, @@ -9,12 +10,12 @@ use crate::{ topology::{Router, TftpServer, Topology, Url}, }; -#[derive(Debug, new, Clone)] +#[derive(Debug, new, Clone, Serialize)] pub struct TftpScore { files_to_serve: Url, } -impl Score for TftpScore { +impl Score for TftpScore { fn create_interpret(&self) -> Box> { Box::new(TftpInterpret::new(self.clone())) } @@ -30,7 +31,7 @@ pub struct TftpInterpret { } #[async_trait] -impl Interpret for TftpInterpret { +impl Interpret for TftpInterpret { async fn execute( &self, _inventory: &Inventory, diff --git a/harmony_tui/src/widget/score.rs b/harmony_tui/src/widget/score.rs index ea62249..b0d2c27 100644 --- a/harmony_tui/src/widget/score.rs +++ b/harmony_tui/src/widget/score.rs @@ -1,10 +1,13 @@ use std::sync::{Arc, RwLock}; use crossterm::event::{Event, KeyCode, KeyEventKind}; -use harmony::{modules::okd::load_balancer::OKDLoadBalancerScore, score::Score, topology::{LoadBalancer, Topology}}; +use harmony::{score::Score, topology::Topology}; use log::{info, warn}; use ratatui::{ - layout::Rect, style::{Style, Stylize}, widgets::{List, ListItem, ListState, StatefulWidget, Widget}, Frame + Frame, + layout::Rect, + style::{Style, Stylize}, + widgets::{List, ListItem, ListState, StatefulWidget, Widget}, }; use tokio::sync::mpsc; @@ -23,19 +26,20 @@ struct Execution { score: Box>, } -impl FrontendScore for OKDLoadBalancerScore {} - #[derive(Debug)] pub(crate) struct ScoreListWidget { list_state: Arc>, - scores: Vec>>, + scores: Vec>>, execution: Option>, execution_history: Vec>, sender: mpsc::Sender>, } impl ScoreListWidget { - pub(crate) fn new(scores: Vec>>, sender: mpsc::Sender>) -> Self { + pub(crate) fn new( + scores: Vec>>, + sender: mpsc::Sender>, + ) -> Self { let mut list_state = ListState::default(); list_state.select_first(); let list_state = Arc::new(RwLock::new(list_state)); @@ -141,4 +145,3 @@ impl Widget for &ScoreListWidget { fn score_to_list_item<'a, T: Topology>(score: &'a Box>) -> ListItem<'a> { ListItem::new(score.name()) } - diff --git a/harmony_types/Cargo.toml b/harmony_types/Cargo.toml index 9b2f97a..0b8c068 100644 --- a/harmony_types/Cargo.toml +++ b/harmony_types/Cargo.toml @@ -4,3 +4,6 @@ edition = "2024" version.workspace = true readme.workspace = true license.workspace = true + +[dependencies] +serde = { version = "1.0.209", features = ["derive"] } diff --git a/harmony_types/src/lib.rs b/harmony_types/src/lib.rs index 71dbbaf..9f4930d 100644 --- a/harmony_types/src/lib.rs +++ b/harmony_types/src/lib.rs @@ -1,5 +1,7 @@ pub mod net { - #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] + use serde::Serialize; + + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize)] pub struct MacAddress(pub [u8; 6]); impl MacAddress { diff --git a/opnsense-config/src/config/config.rs b/opnsense-config/src/config/config.rs index 3f71d55..10dab61 100644 --- a/opnsense-config/src/config/config.rs +++ b/opnsense-config/src/config/config.rs @@ -11,6 +11,7 @@ use crate::{ use log::{debug, info, trace, warn}; use opnsense_config_xml::OPNsense; use russh::client; +use serde::Serialize; use super::{ConfigManager, OPNsenseShell}; @@ -21,6 +22,15 @@ pub struct Config { shell: Arc, } +impl Serialize for Config { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + todo!() + } +} + impl Config { pub async fn new( repository: Arc, From 1cbf4de2a1487dc2615be6d15ca85f2c6f3458bc Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Sat, 5 Apr 2025 14:38:52 -0400 Subject: [PATCH 21/62] adr: proposal serde for score data representation and UI rendering Decouples score definitions from UI implementations by mandating `serde::Serialize` and `serde::Deserialize` for all `Score` structs. UIs will interact with scores via their serialized representation, enabling scalability and reducing complexity for score authors. This approach: - Scales better with new score types and UI targets. - Simplifies score authoring by removing the need for UI-specific display traits. - Leverages the `serde` ecosystem for robust data handling. Adding new field types requires updates to all UIs, a trade-off acknowledged in the ADR. --- adr/008-score-display-formatting.md | 62 +++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 adr/008-score-display-formatting.md diff --git a/adr/008-score-display-formatting.md b/adr/008-score-display-formatting.md new file mode 100644 index 0000000..7bc0620 --- /dev/null +++ b/adr/008-score-display-formatting.md @@ -0,0 +1,62 @@ +## Architecture Decision Record: Data Representation and UI Rendering for Score Types + +**Status:** Proposed + +**TL;DR:** `Score` types will be serialized (using `serde`) for presentation in UIs. This decouples data definition from presentation, improving scalability and reducing complexity for developers defining `Score` types. New UI types only need to handle existing field types, and new `Score` types don’t require UI changes as long as they use existing field types. Adding a new field type *does* require updates to all UIs. + +**Key benefits:** Scalability, reduced complexity for `Score` authors, decoupling of data and presentation. + +**Key trade-off:** Adding new field types requires updating all UIs. + +--- + +**Context:** + +Harmony is a pure Rust infrastructure orchestrator focused on compile-time safety and providing a developer-friendly, Ansible-module-like experience for defining infrastructure configurations via "Scores". These Scores (e.g., `LAMPScore`) are Rust structs composed of specific, strongly-typed fields (e.g., `VersionField`, `UrlField`, `PathField`) which are validated at compile-time using macros (`Version!`, `Url!`, etc.). + +A key requirement is displaying the configuration defined in these Scores across various user interfaces (Web UI, TUI, potentially Mobile UI, etc.) in a consistent and type-safe manner. As the number of Score types is expected to grow significantly (hundreds or thousands), we need a scalable approach for rendering their data that avoids tightly coupling Score definitions to specific UI implementations. + +The primary challenge is preventing the need for every `Score` struct author to implement multiple display traits (e.g., `Display`, `WebDisplay`, `TuiDisplay`) for every potential UI target. This would create an N x M complexity problem (N Scores * M UI types) and place an unreasonable burden on Score developers, hindering scalability and maintainability. + +**Decision:** + +1. **Mandatory Serialization:** All `Score` structs *must* implement `serde::Serialize` and `serde::Deserialize`. They *will not* be required to implement `std::fmt::Display` or any custom UI-specific display traits (e.g., `WebDisplay`, `TuiDisplay`). +2. **Field-Level Rendering:** Responsibility for rendering data will reside within the UI components. Each UI (Web, TUI, etc.) will implement logic to display *individual field types* (e.g., `UrlField`, `VersionField`, `IpAddressField`, `SecretField`). +3. **Data Access via Serialization:** UIs will primarily interact with `Score` data through its serialized representation (e.g., JSON obtained via `serde_json`). This provides a standardized interface for UIs to consume the data structure agnostic of the specific `Score` type. Alternatively, UIs *could* potentially use reflection or specific visitor patterns on the `Score` struct itself, but serialization is the preferred decoupling mechanism. + +**Rationale:** + +1. **Decoupling Data from Presentation:** This decision cleanly separates the data definition (`Score` structs and their fields) from the presentation logic (UI rendering). `Score` authors can focus solely on defining the data and its structure, while UI developers focus on how to best present known data *types*. +2. **Scalability:** This approach scales significantly better than requiring display trait implementations on Scores: + * Adding a *new Score type* requires *no changes* to existing UI code, provided it uses existing field types. + * Adding a *new UI type* requires implementing rendering logic only for the defined set of *field types*, not for every individual `Score` type. This reduces the N x M complexity to N + M complexity (approximately). +3. **Simplicity for Score Authors:** Requiring only `serde::Serialize + Deserialize` (which can often be derived automatically with `#[derive(Serialize, Deserialize)]`) is a much lower burden than implementing custom rendering logic for multiple, potentially unknown, UI targets. +4. **Leverages Rust Ecosystem Standards:** `serde` is the de facto standard for serialization and deserialization in Rust. Relying on it aligns with common Rust practices and benefits from its robustness, performance, and extensive tooling. +5. **Consistency for UIs:** Serialization provides a consistent, structured format (like JSON) for UIs to consume data, regardless of the underlying `Score` struct's complexity or composition. +6. **Flexibility for UI Implementation:** UIs can choose the best way to render each field type based on their capabilities (e.g., a `UrlField` might be a clickable link in a Web UI, plain text in a TUI; a `SecretField` might be masked). + +**Consequences:** + +**Positive:** + +* Greatly improved scalability for adding new Score types and UI targets. +* Strong separation of concerns between data definition and presentation. +* Reduced implementation burden and complexity for Score authors. +* Consistent mechanism for UIs to access and interpret Score data. +* Aligns well with the Hexagonal Architecture (ADR-002) by treating UIs as adapters interacting with the application core via a defined port (the serialized data contract). + +**Negative:** + +* Adding a *new field type* (e.g., `EmailField`) requires updates to *all* existing UI implementations to support rendering it. +* UI components become dependent on the set of defined field types and need comprehensive logic to handle each one appropriately. +* Potential minor overhead of serialization/deserialization compared to direct function calls (though likely negligible for UI purposes). +* Requires careful design and management of the standard library of field types. + +**Alternatives Considered:** + +1. **`Score` Implements `std::fmt::Display`:** + * _Rejected:_ Too simplistic. Only suitable for basic text rendering, doesn't cater to structured UIs (Web, etc.), and doesn't allow type-specific rendering logic (e.g., masking secrets). Doesn't scale to multiple UI formats. +2. **`Score` Implements Multiple Custom Display Traits (`WebDisplay`, `TuiDisplay`, etc.):** + * _Rejected:_ Leads directly to the N x M complexity problem. Tightly couples Score definitions to specific UI implementations. Places an excessive burden on Score authors, hindering adoption and scalability. +3. **Generic Display Trait with Context (`Score` implements `DisplayWithContext`):** + * _Rejected:_ More flexible than multiple traits, but still requires Score authors to implement potentially complex rendering logic within the `Score` definition itself. The `Score` would still need awareness of different UI contexts, leading to undesirable coupling. Managing context types adds complexity. From 31ae8365a6b2f399f5059b1fa595ef040a3a99e7 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 9 Apr 2025 16:09:54 -0400 Subject: [PATCH 22/62] docs: add quick demo and core architecture overview Adds a quick demo command using `cargo run -p example-tui` to launch a minimalist TUI with demo scores. Also includes a core architecture diagram and overview in the README for better understanding of the project structure. --- README.md | 16 ++++++++++------ .../Harmony_Core_Architecture.drawio.svg | 4 ++++ 2 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 docs/diagrams/Harmony_Core_Architecture.drawio.svg diff --git a/README.md b/README.md index 277356d..6fed6eb 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ -### Watch the whole repo on every change +# Harmony : Open Infrastructure Orchestration -Due to the current setup being a mix of separate repositories with gitignore and rust workspace, a few options are required for cargo-watch to have the desired behavior : +## Quick demo -```sh -RUST_LOG=info cargo watch --ignore-nothing -w harmony -w private_repos/ -x 'run --bin nationtech' -``` +`cargo run -p example-tui` -This will run the nationtech bin (likely `private_repos/nationtech/src/main.rs`) on any change in the harmony or private_repos folders. +This will launch Harmony's minimalist terminal ui which embeds a few demo scores. + +Usage instructions will be displayed at the bottom of the TUI. + +## Core architecture + +![Harmony Core Architecture](docs/diagrams/Harmony_Core_Architecture.drawio.svg) diff --git a/docs/diagrams/Harmony_Core_Architecture.drawio.svg b/docs/diagrams/Harmony_Core_Architecture.drawio.svg new file mode 100644 index 0000000..ba6e9fc --- /dev/null +++ b/docs/diagrams/Harmony_Core_Architecture.drawio.svg @@ -0,0 +1,4 @@ + + + +
create_interpret
Score<T>
A score defines a desirable state.

It can then be read by an Interpre
t that will apply the Score's
desired state to the Topology
Interpret<T>
An Interpret<T> knows how to apply the desired state from a score on a Topology T

The Interpret declares the Capabilities trait bounds required so it can execute its job.

Think of the musical metaphor : the Interpret reads a  trumpet score and requires a trumpet to play it.

In Harmony, the Interpret reads a DNS Score and requires a Topology that has the DNS Capability to apply it.
Maestro<T>
A Maestro owns a list of registered scores and a Topology.

The Maestro can executes Scores<T> on Topologies = T when the Topology T has all the required capabilities of the Score<T>

This compatibility will be verified at compile time thanks to the <T: Topology> bound. The program will not compile if a Score requires a Capability that is not provided by the Topology<T> compiled in the Maestro<T>
Topology
A topology is a group of ressource that provide specific Capabilities.

For example, an OPNSenseFirewall Topology will provide many core Capabilities including : DNS Server, Router, Firewall, HTTP Server, Unix Shell, OPNSenseShell, etc.

But it will not provide incompatible capabilities such as CephStorage, ContainerRuntime, Kubernetes, BrocadeSwitch, etc.
Inventory
The Inventory contains the hardware components. Everything from the physical address to a MacAddres may be defined in an Inventory.

This allows for bare metal topologies to operate Scores that will manage bare metal ressources such as physical hosts, switches, etc. Typically, in an hyperconverged cluster, Inventory components are treated like cattle. The Topology will make sure to attain the desired state by exploiting the Inventory components. If an inventory component fails (hard drive, server, GPU, etc), the Maestro can react and make sure that all the Scores are still applied properly, and refresh them if necessary, or raise alerts if the desired state cannot be restored automatically.
A Capability is a concept that is not implemented in code,
but is central to the way we define Scores,
Interprets and Topologies.

Capabilities are extremely diverse, but
 generally are an expression of a standard
 such as DNS Server, Packet Router, LoadBalancer, 
PXE Server, PXE Host, K8sCluster, etc.
\ No newline at end of file From 606ea43b51d6678f19deb11037716fef55b601a5 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Fri, 11 Apr 2025 11:01:05 -0400 Subject: [PATCH 23/62] fix: improve tests and remove unused code - Corrected XML test data to remove unnecessary `` tags, resolving failing tests. - Removed the unused `ratatui_utils` module and its associated code. - Simplified example in `harmony_tui/src/lib.rs` to use `tokio::main` and register scores directly with `Maestro`. This aligns with the project's evolving structure. --- examples/tui/src/main.rs | 5 +--- harmony_tui/src/lib.rs | 32 ++++++++++++++-------- harmony_tui/src/ratatui_utils.rs | 22 --------------- opnsense-config-xml/src/data/interfaces.rs | 4 --- 4 files changed, 21 insertions(+), 42 deletions(-) delete mode 100644 harmony_tui/src/ratatui_utils.rs diff --git a/examples/tui/src/main.rs b/examples/tui/src/main.rs index 9623fa2..05a768b 100644 --- a/examples/tui/src/main.rs +++ b/examples/tui/src/main.rs @@ -1,10 +1,7 @@ use harmony::{ inventory::Inventory, maestro::Maestro, - modules::{ - dummy::{ErrorScore, PanicScore, SuccessScore}, - k8s::deployment::K8sDeploymentScore, - }, + modules::dummy::{ErrorScore, PanicScore, SuccessScore}, topology::HAClusterTopology, }; diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index 10997ad..11208f0 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -1,4 +1,3 @@ -mod ratatui_utils; mod widget; use log::{debug, error, info}; @@ -30,17 +29,26 @@ pub mod tui { /// /// # Example /// -/// ```rust -/// use harmony; -/// use harmony_tui::init; -/// -/// #[harmony::main] -/// pub async fn main(maestro: harmony::Maestro) { -/// maestro.register(DeploymentScore::new("nginx-test", "nginx")); -/// maestro.register(OKDLoadBalancerScore::new(&maestro.inventory, &maestro.topology)); -/// // Register other scores as needed -/// -/// init(maestro).await.unwrap(); +/// ```rust,no_run +/// use harmony::{ +/// inventory::Inventory, +/// maestro::Maestro, +/// modules::dummy::{ErrorScore, PanicScore, SuccessScore}, +/// topology::HAClusterTopology, +/// }; +/// +/// #[tokio::main] +/// async fn main() { +/// let inventory = Inventory::autoload(); +/// let topology = HAClusterTopology::autoload(); +/// let mut maestro = Maestro::new(inventory, topology); +/// +/// maestro.register_all(vec![ +/// Box::new(SuccessScore {}), +/// Box::new(ErrorScore {}), +/// Box::new(PanicScore {}), +/// ]); +/// harmony_tui::init(maestro).await.unwrap(); /// } /// ``` pub async fn init( diff --git a/harmony_tui/src/ratatui_utils.rs b/harmony_tui/src/ratatui_utils.rs deleted file mode 100644 index 84b8659..0000000 --- a/harmony_tui/src/ratatui_utils.rs +++ /dev/null @@ -1,22 +0,0 @@ -use ratatui::layout::{Constraint, Flex, Layout, Rect}; - -/// Centers a [`Rect`] within another [`Rect`] using the provided [`Constraint`]s. -/// -/// # Examples -/// -/// ```rust -/// use ratatui::layout::{Constraint, Rect}; -/// -/// let area = Rect::new(0, 0, 100, 100); -/// let horizontal = Constraint::Percentage(20); -/// let vertical = Constraint::Percentage(30); -/// -/// let centered = center(area, horizontal, vertical); -/// ``` -pub(crate) fn center(area: Rect, horizontal: Constraint, vertical: Constraint) -> Rect { - let [area] = Layout::horizontal([horizontal]) - .flex(Flex::Center) - .areas(area); - let [area] = Layout::vertical([vertical]).flex(Flex::Center).areas(area); - area -} diff --git a/opnsense-config-xml/src/data/interfaces.rs b/opnsense-config-xml/src/data/interfaces.rs index 4e518c7..e0a84d3 100644 --- a/opnsense-config-xml/src/data/interfaces.rs +++ b/opnsense-config-xml/src/data/interfaces.rs @@ -132,22 +132,18 @@ mod test { - - - - From 0ba7f2536cbf8b36d315b4a53d90dd411e38a08d Mon Sep 17 00:00:00 2001 From: taha Date: Wed, 16 Apr 2025 17:39:17 +0000 Subject: [PATCH 24/62] docs: ADR for Helm Resource implementation style (#12) Co-authored-by: tahahawa Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/12 --- adr/000-ADR-Template.md | 33 ++++++++++++++ adr/009-helm-and-kustomize-handling.md | 61 ++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 adr/000-ADR-Template.md create mode 100644 adr/009-helm-and-kustomize-handling.md diff --git a/adr/000-ADR-Template.md b/adr/000-ADR-Template.md new file mode 100644 index 0000000..b9592cb --- /dev/null +++ b/adr/000-ADR-Template.md @@ -0,0 +1,33 @@ +# Architecture Decision Record: \ + +Name: \ + +Initial Date: \ + +Last Updated Date: \ + +## Status + +Proposed/Pending/Accepted/Implemented + +## Context + +The problem, background, the "why" behind this decision/discussion + +## Decision + +Proposed solution to the problem + +## Rationale + +Reasoning behind the decision + +## Consequences + +Pros/Cons of chosen solution + +## Alternatives considered + +Pros/Cons of various proposed solutions considered + +## Additional Notes diff --git a/adr/009-helm-and-kustomize-handling.md b/adr/009-helm-and-kustomize-handling.md new file mode 100644 index 0000000..dbebb8a --- /dev/null +++ b/adr/009-helm-and-kustomize-handling.md @@ -0,0 +1,61 @@ +# Architecture Decision Record: Helm and Kustomize Handling + +Name: Taha Hawa + +Initial Date: 2025-04-15 + +Last Updated Date: 2025-04-15 + +## Status + +Proposed + +## Context + +We need to find a way to handle Helm charts and deploy them to a Kubernetes cluster. Helm has a lot of extra functionality that we may or may not need. Kustomize handles Helm charts by inflating them and applying them as vanilla Kubernetes yaml. How should Harmony handle it? + +## Decision + +In order to move quickly and efficiently, Harmony should handle Helm charts similarly to how Kustomize does: invoke Helm to inflate/render the charts with the needed inputs, and deploy the rendered artifacts to Kubernetes as if it were vanilla manifests. + +## Rationale + +A lot of Helm's features aren't strictly necessary and would add unneeded overhead. This is likely the fastest way to go from zero to deployed. Other tools (e.g. Kustomize) already do this. Kustomize has tooling for patching and modifying k8s manifests before deploying, and Harmony should have that power too, even if it's not what Helm typically intends. + +Perhaps in future also have a Kustomize resource in Harmony? Which could handle Helm charts for Harmony as well/instead. + +## Consequences + +**Pros**: + +- Much easier (and faster) than implementing all of Helm's featureset +- Can potentially re-use code from K8sResource already present in Harmony +- Harmony retains more control over how the deployment goes after rendering (i.e. can act like Kustomize, or leverage Kustomize itself to modify deployments after rendering/inflation) +- Reduce (unstable) surface of dealing with Helm binary + +**Cons**: + +- Lose some Helm functionality +- Potentially lose some compatibility with Helm + +## Alternatives considered + +- ### Implement Helm resouce/client fully in Harmony + - **Pros**: + - Retain full compatibility with Helm as a tool + - Retain full functionality of Helm + - **Cons**: + - Longer dev time + - More complex integration + - Dealing with larger (unstable) surface of Helm as a binary +- ### Leverage Kustomize to deal with Helm charts + - **Pros**: + - Already has a good, minimal inflation solution built + - Powerful post-processing/patching + - Can integrate with `kubectl` + - **Cons**: + - Unstable binary tool/surface to deal with + - Still requires Helm to be installed as well as Kustomize + - Not all Helm features supported + +## Additional Notes From abd20b96a2a50cff4c119f89fe9595dfdb821af1 Mon Sep 17 00:00:00 2001 From: Taha Hawa Date: Sat, 19 Apr 2025 01:13:40 +0000 Subject: [PATCH 25/62] feat: harmony-cli v0.1 #8 (#9) Co-authored-by: tahahawa Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/9 Reviewed-by: johnride Co-authored-by: Taha Hawa Co-committed-by: Taha Hawa --- Cargo.lock | 1159 +++++++++++++++++++++++++------------- Cargo.toml | 9 +- README.md | 20 + examples/cli/Cargo.toml | 19 + examples/cli/src/main.rs | 38 ++ harmony_cli/Cargo.toml | 19 + harmony_cli/src/lib.rs | 318 +++++++++++ harmony_tui/src/lib.rs | 8 +- 8 files changed, 1205 insertions(+), 385 deletions(-) create mode 100644 examples/cli/Cargo.toml create mode 100644 examples/cli/src/main.rs create mode 100644 harmony_cli/Cargo.toml create mode 100644 harmony_cli/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f9c279f..9564fdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,7 +68,7 @@ dependencies = [ "const-random", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -103,9 +103,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -118,43 +118,60 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "assert_cmd" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", ] [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", @@ -163,9 +180,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" @@ -202,9 +219,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] name = "bcrypt-pbkdf" @@ -225,9 +242,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" dependencies = [ "serde", ] @@ -273,10 +290,21 @@ dependencies = [ ] [[package]] -name = "bumpalo" -version = "3.16.0" +name = "bstr" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byteorder" @@ -286,9 +314,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cassowary" @@ -316,9 +344,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.16" +version = "1.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b" +checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" dependencies = [ "shlex", ] @@ -342,9 +370,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", @@ -352,7 +380,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -371,6 +399,46 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "4.5.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "color-eyre" version = "0.6.3" @@ -400,9 +468,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "compact_str" @@ -439,7 +507,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "tiny-keccak", ] @@ -472,9 +540,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -488,18 +556,34 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "crossterm_winapi", "futures-core", - "mio", + "mio 1.0.3", "parking_lot", - "rustix", + "rustix 0.38.44", "signal-hook", "signal-hook-mio", "winapi", @@ -516,9 +600,9 @@ dependencies = [ [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "crypto-bigint" @@ -527,7 +611,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -539,7 +623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] @@ -581,9 +665,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ "darling_core", "darling_macro", @@ -591,9 +675,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", @@ -605,9 +689,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", @@ -616,9 +700,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "der" @@ -657,6 +741,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.10.7" @@ -680,6 +770,18 @@ dependencies = [ "syn", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "dyn-clone" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" + [[package]] name = "ecdsa" version = "0.16.9" @@ -712,7 +814,7 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", + "rand_core 0.6.4", "serde", "sha2", "subtle", @@ -721,9 +823,9 @@ dependencies = [ [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "elliptic-curve" @@ -740,7 +842,7 @@ dependencies = [ "hkdf", "pem-rfc7468", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -748,18 +850,18 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "env_filter" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", @@ -767,37 +869,53 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.5" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", - "humantime", + "jiff", "log", ] [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "example" version = "0.0.0" +[[package]] +name = "example-cli" +version = "0.1.0" +dependencies = [ + "assert_cmd", + "cidr", + "env_logger", + "harmony", + "harmony_cli", + "harmony_macros", + "harmony_types", + "log", + "tokio", + "url", +] + [[package]] name = "example-kube-rs" version = "0.1.0" @@ -806,7 +924,7 @@ dependencies = [ "env_logger", "harmony", "harmony_macros", - "http 1.2.0", + "http 1.3.1", "k8s-openapi", "kube", "log", @@ -886,17 +1004,17 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "ff" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -908,12 +1026,12 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "flate2" -version = "1.0.33" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", + "miniz_oxide 0.8.8", ] [[package]] @@ -936,9 +1054,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "foreign-types" @@ -972,9 +1090,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -987,9 +1105,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -997,15 +1115,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -1014,15 +1132,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -1031,21 +1149,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1059,6 +1177,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "fxhash" version = "0.2.1" @@ -1087,7 +1214,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -1113,7 +1252,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1146,7 +1285,7 @@ dependencies = [ "env_logger", "harmony_macros", "harmony_types", - "http 1.2.0", + "http 1.3.1", "k8s-openapi", "kube", "libredfish", @@ -1166,6 +1305,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "harmony_cli" +version = "0.1.0" +dependencies = [ + "assert_cmd", + "clap", + "harmony", + "harmony_tui", + "inquire", + "tokio", +] + [[package]] name = "harmony_macros" version = "0.1.0" @@ -1182,7 +1333,7 @@ name = "harmony_tui" version = "0.1.0" dependencies = [ "color-eyre", - "crossterm", + "crossterm 0.28.1", "env_logger", "harmony", "log", @@ -1200,12 +1351,6 @@ dependencies = [ "serde", ] -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - [[package]] name = "hashbrown" version = "0.15.2" @@ -1226,7 +1371,7 @@ dependencies = [ "base64 0.21.7", "bytes", "headers-core", - "http 1.2.0", + "http 1.3.1", "httpdate", "mime", "sha1", @@ -1238,7 +1383,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.2.0", + "http 1.3.1", ] [[package]] @@ -1279,11 +1424,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1299,9 +1444,9 @@ dependencies = [ [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -1326,27 +1471,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.2.0", + "http 1.3.1", ] [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", - "http 1.2.0", + "futures-core", + "http 1.3.1", "http-body 1.0.1", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.9.4" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1354,17 +1499,11 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -1386,14 +1525,14 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.2.0", + "http 1.3.1", "http-body 1.0.1", "httparse", "itoa", @@ -1405,15 +1544,15 @@ dependencies = [ [[package]] name = "hyper-http-proxy" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d06dbdfbacf34d996c6fb540a71a684a7aae9056c71951163af8a8a4c07b9a4" +checksum = "7ad4b0a1e37510028bc4ba81d0e38d239c39671b0f0ce9e02dfa93a8133f7c08" dependencies = [ "bytes", "futures-util", "headers", - "http 1.2.0", - "hyper 1.5.2", + "http 1.3.1", + "hyper 1.6.0", "hyper-rustls", "hyper-util", "pin-project-lite", @@ -1430,8 +1569,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", - "http 1.2.0", - "hyper 1.5.2", + "http 1.3.1", + "hyper 1.6.0", "hyper-util", "log", "rustls", @@ -1448,7 +1587,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.5.2", + "hyper 1.6.0", "hyper-util", "pin-project-lite", "tokio", @@ -1462,7 +1601,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.30", + "hyper 0.14.32", "native-tls", "tokio", "tokio-native-tls", @@ -1470,16 +1609,17 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.2.0", + "http 1.3.1", "http-body 1.0.1", - "hyper 1.5.2", + "hyper 1.6.0", + "libc", "pin-project-lite", "socket2", "tokio", @@ -1489,14 +1629,15 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core", ] @@ -1551,9 +1692,9 @@ dependencies = [ [[package]] name = "icu_locid_transform_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" [[package]] name = "icu_normalizer" @@ -1575,9 +1716,9 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" @@ -1596,9 +1737,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" @@ -1663,30 +1804,47 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "2.5.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown", ] [[package]] name = "indoc" -version = "2.0.5" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "inout" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", "generic-array", ] +[[package]] +name = "inquire" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" +dependencies = [ + "bitflags 2.9.0", + "crossterm 0.25.0", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width 0.1.14", +] + [[package]] name = "instability" version = "0.3.7" @@ -1702,9 +1860,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is_terminal_polyfill" @@ -1723,16 +1881,41 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ad87c89110f55e4cd4dc2893a9790820206729eaf221555f742d540b0724a0" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d076d5b64a7e2fe6f0743f02c43ca4a6725c0f904203bfe276a5b3e793103605" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -1746,7 +1929,7 @@ dependencies = [ "pest_derive", "regex", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -1785,10 +1968,10 @@ dependencies = [ "either", "futures", "home", - "http 1.2.0", + "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.5.2", + "hyper 1.6.0", "hyper-http-proxy", "hyper-rustls", "hyper-timeout", @@ -1803,7 +1986,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tokio-util", "tower", @@ -1819,12 +2002,12 @@ checksum = "97aa830b288a178a90e784d1b0f1539f2d200d2188c7b4a3146d9dc983d596f3" dependencies = [ "chrono", "form_urlencoded", - "http 1.2.0", + "http 1.3.1", "k8s-openapi", "serde", "serde-value", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -1838,15 +2021,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.158" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredfish" @@ -1863,15 +2046,21 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "lock_api" @@ -1885,9 +2074,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "log-panics" @@ -1904,7 +2093,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.2", + "hashbrown", ] [[package]] @@ -1936,31 +2125,42 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.2" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ - "hermit-abi", "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", @@ -1973,6 +2173,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -1981,7 +2190,7 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -1996,7 +2205,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -2052,9 +2261,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "opaque-debug" @@ -2064,11 +2273,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "cfg-if", "foreign-types", "libc", @@ -2090,15 +2299,15 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", @@ -2121,7 +2330,7 @@ dependencies = [ "russh-sftp", "serde", "serde_json", - "thiserror 1.0.63", + "thiserror 1.0.69", "tokio", "tokio-stream", "tokio-util", @@ -2135,9 +2344,9 @@ dependencies = [ "env_logger", "log", "pretty_assertions", - "rand", + "rand 0.8.5", "serde", - "thiserror 1.0.63", + "thiserror 1.0.69", "tokio", "uuid", "xml-rs", @@ -2174,9 +2383,9 @@ dependencies = [ [[package]] name = "p384" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" dependencies = [ "ecdsa", "elliptic-curve", @@ -2194,7 +2403,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "rand_core", + "rand_core 0.6.4", "sha2", ] @@ -2228,7 +2437,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -2262,9 +2471,9 @@ dependencies = [ [[package]] name = "pem" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" dependencies = [ "base64 0.22.1", "serde", @@ -2287,20 +2496,20 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" dependencies = [ "memchr", - "thiserror 2.0.11", + "thiserror 2.0.12", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" dependencies = [ "pest", "pest_generator", @@ -2308,9 +2517,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" dependencies = [ "pest", "pest_meta", @@ -2321,9 +2530,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.15" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" dependencies = [ "once_cell", "pest", @@ -2332,9 +2541,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -2376,15 +2585,15 @@ checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", "pkcs5", - "rand_core", + "rand_core 0.6.4", "spki", ] [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "poly1305" @@ -2410,12 +2619,54 @@ dependencies = [ ] [[package]] -name = "ppv-lite86" -version = "0.2.20" +name = "portable-atomic" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ - "zerocopy", + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.24", +] + +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "difflib", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", ] [[package]] @@ -2439,22 +2690,28 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "radium" version = "0.7.0" @@ -2468,8 +2725,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -2479,7 +2746,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -2488,7 +2765,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", ] [[package]] @@ -2497,10 +2783,10 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "cassowary", "compact_str", - "crossterm", + "crossterm 0.28.1", "indoc", "instability", "itertools", @@ -2514,18 +2800,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", ] [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -2535,9 +2821,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -2546,9 +2832,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" @@ -2564,7 +2850,7 @@ dependencies = [ "h2", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.30", + "hyper 0.14.32", "hyper-tls", "ipnet", "js-sys", @@ -2602,24 +2888,23 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rsa" -version = "0.9.6" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" dependencies = [ "const-oid", "digest", @@ -2628,7 +2913,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sha2", "signature", "spki", @@ -2645,7 +2930,7 @@ dependencies = [ "aes", "aes-gcm", "async-trait", - "bitflags 2.6.0", + "bitflags 2.9.0", "byteorder", "cbc", "chacha20", @@ -2666,8 +2951,8 @@ dependencies = [ "p384", "p521", "poly1305", - "rand", - "rand_core", + "rand 0.8.5", + "rand_core 0.6.4", "russh-cryptovec", "russh-keys", "sha1", @@ -2675,7 +2960,7 @@ dependencies = [ "ssh-encoding", "ssh-key", "subtle", - "thiserror 1.0.63", + "thiserror 1.0.69", "tokio", ] @@ -2722,8 +3007,8 @@ dependencies = [ "pkcs1", "pkcs5", "pkcs8", - "rand", - "rand_core", + "rand 0.8.5", + "rand_core 0.6.4", "rsa", "russh-cryptovec", "sec1", @@ -2733,7 +3018,7 @@ dependencies = [ "spki", "ssh-encoding", "ssh-key", - "thiserror 1.0.63", + "thiserror 1.0.69", "tokio", "tokio-stream", "typenum", @@ -2742,18 +3027,17 @@ dependencies = [ [[package]] name = "russh-sftp" -version = "2.0.6" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a72c8afe2041c17435eecd85d0b7291841486fd3d1c4082e0b212e5437ca42" +checksum = "f08ed364d54b74d988c964b464a53a1916379f9441cfd10ca8fb264be1349842" dependencies = [ - "async-trait", - "bitflags 2.6.0", + "bitflags 2.9.0", "bytes", "chrono", "flurry", "log", "serde", - "thiserror 1.0.63", + "thiserror 2.0.12", "tokio", "tokio-util", ] @@ -2768,9 +3052,9 @@ dependencies = [ "bitvec", "cbc", "hmac", - "rand", + "rand 0.8.5", "sha2", - "thiserror 1.0.63", + "thiserror 1.0.69", ] [[package]] @@ -2790,22 +3074,35 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.36" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f55e80d50763938498dd5ebb18647174e0c76dc38c5505294bb224624f30f36" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "errno", "libc", - "linux-raw-sys", - "windows-sys 0.52.0", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.21" +version = "0.23.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" dependencies = [ "log", "once_cell", @@ -2861,15 +3158,15 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" dependencies = [ "ring", "rustls-pki-types", @@ -2878,15 +3175,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "salsa20" @@ -2899,11 +3196,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2952,7 +3249,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -2965,7 +3262,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.0", "core-foundation 0.10.0", "core-foundation-sys", "libc", @@ -2990,15 +3287,15 @@ checksum = "689224d06523904ebcc9b482c6a3f4f7fb396096645c4cd10c0d2ff7371a34d3" [[package]] name = "semver" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -3015,9 +3312,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -3026,9 +3323,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -3127,7 +3424,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 0.8.11", + "mio 1.0.3", "signal-hook", ] @@ -3147,7 +3445,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -3161,15 +3459,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3221,9 +3519,9 @@ dependencies = [ [[package]] name = "ssh-key" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca9b366a80cf18bb6406f4cf4d10aebfb46140a8c0c33f666a144c5c76ecbafc" +checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3" dependencies = [ "bcrypt-pbkdf", "ed25519-dalek", @@ -3231,7 +3529,7 @@ dependencies = [ "p256", "p384", "p521", - "rand_core", + "rand_core 0.6.4", "rsa", "sec1", "sha2", @@ -3290,9 +3588,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.90" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -3351,40 +3649,46 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.12.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ - "cfg-if", "fastrand", + "getrandom 0.3.2", "once_cell", - "rustix", + "rustix 1.0.5", "windows-sys 0.59.0", ] [[package]] -name = "thiserror" -version = "1.0.63" +name = "termtree" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.63", + "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", @@ -3393,9 +3697,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -3433,14 +3737,14 @@ dependencies = [ [[package]] name = "tokio" -version = "1.40.0" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 1.0.3", "pin-project-lite", "signal-hook-registry", "socket2", @@ -3450,9 +3754,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -3471,9 +3775,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ "rustls", "tokio", @@ -3492,9 +3796,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" dependencies = [ "bytes", "futures-core", @@ -3527,9 +3831,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ "base64 0.22.1", - "bitflags 2.6.0", + "bitflags 2.9.0", "bytes", - "http 1.2.0", + "http 1.3.1", "http-body 1.0.1", "mime", "pin-project-lite", @@ -3552,9 +3856,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -3575,9 +3879,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -3612,9 +3916,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tui-logger" -version = "0.14.1" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a5249b5e5df37af5389721794f9839dad0b3f7b92445f24528199acf2f1805" +checksum = "2f3fb54e48fa37d5081603f7804c730734b381235ebd876cb1e66f853a7d2533" dependencies = [ "chrono", "fxhash", @@ -3622,13 +3926,14 @@ dependencies = [ "log", "parking_lot", "ratatui", + "unicode-segmentation", ] [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "ucd-trie" @@ -3638,9 +3943,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-segmentation" @@ -3724,20 +4029,20 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ - "getrandom", - "rand", + "getrandom 0.3.2", + "rand 0.9.1", "uuid-macro-internal", ] [[package]] name = "uuid-macro-internal" -version = "1.11.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08" +checksum = "72dcd78c4f979627a754f5522cea6e6a25e55139056535fe6e69c506cd64a862" dependencies = [ "proc-macro2", "quote", @@ -3762,6 +4067,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "want" version = "0.3.1" @@ -3778,25 +4092,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.93" +name = "wasi" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -3805,21 +4128,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3827,9 +4151,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -3840,15 +4164,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -3878,11 +4205,61 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.52.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", ] [[package]] @@ -4043,6 +4420,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -4066,9 +4452,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.22" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" +checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" [[package]] name = "yansi" @@ -4130,8 +4516,16 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive 0.8.24", ] [[package]] @@ -4146,19 +4540,30 @@ dependencies = [ ] [[package]] -name = "zerofrom" -version = "0.1.5" +name = "zerocopy-derive" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index c36cd27..4ba83eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "harmony_tui", "opnsense-config", "opnsense-config-xml", + "harmony_cli", ] [workspace.package] @@ -28,7 +29,7 @@ russh-keys = "0.45.0" rand = "0.8.5" url = "2.5.4" kube = "0.98.0" -k8s-openapi = { version = "0.24.0", features = [ "v1_30" ] } +k8s-openapi = { version = "0.24.0", features = ["v1_30"] } serde_yaml = "0.9.34" serde-value = "0.7.0" http = "1.2.0" @@ -36,7 +37,7 @@ http = "1.2.0" [workspace.dependencies.uuid] version = "1.11.0" features = [ - "v4", # Lets you generate random UUIDs - "fast-rng", # Use a faster (but still sufficiently random) RNG - "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs + "v4", # Lets you generate random UUIDs + "fast-rng", # Use a faster (but still sufficiently random) RNG + "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs ] diff --git a/README.md b/README.md index 6fed6eb..bda7b1a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,26 @@ This will launch Harmony's minimalist terminal ui which embeds a few demo scores Usage instructions will be displayed at the bottom of the TUI. +`cargo run --bin example-cli -- --help` + +This is the harmony CLI, a minimal implementation + +The current help text: + +```` +Usage: example-cli [OPTIONS] + +Options: + -y, --yes Run score(s) or not + -f, --filter Filter query + -i, --interactive Run interactive TUI or not + -a, --all Run all or nth, defaults to all + -n, --number Run nth matching, zero indexed [default: 0] + -l, --list list scores, will also be affected by run filter + -h, --help Print help + -V, --version Print version``` + ## Core architecture ![Harmony Core Architecture](docs/diagrams/Harmony_Core_Architecture.drawio.svg) +```` diff --git a/examples/cli/Cargo.toml b/examples/cli/Cargo.toml new file mode 100644 index 0000000..7e8495c --- /dev/null +++ b/examples/cli/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "example-cli" +edition = "2024" +version.workspace = true +readme.workspace = true +license.workspace = true +publish = false + +[dependencies] +harmony = { path = "../../harmony" } +harmony_cli = { path = "../../harmony_cli" } +harmony_types = { path = "../../harmony_types" } +cidr = { workspace = true } +tokio = { workspace = true } +harmony_macros = { path = "../../harmony_macros" } +log = { workspace = true } +env_logger = { workspace = true } +url = { workspace = true } +assert_cmd = "2.0.16" diff --git a/examples/cli/src/main.rs b/examples/cli/src/main.rs new file mode 100644 index 0000000..8689b02 --- /dev/null +++ b/examples/cli/src/main.rs @@ -0,0 +1,38 @@ +use harmony::{ + inventory::Inventory, + maestro::Maestro, + modules::dummy::{ErrorScore, PanicScore, SuccessScore}, + topology::HAClusterTopology, +}; + +#[tokio::main] +async fn main() { + let inventory = Inventory::autoload(); + let topology = HAClusterTopology::autoload(); + let mut maestro = Maestro::new(inventory, topology); + + maestro.register_all(vec![ + Box::new(SuccessScore {}), + Box::new(ErrorScore {}), + Box::new(PanicScore {}), + ]); + harmony_cli::init(maestro, None).await.unwrap(); +} + +use assert_cmd::Command; + +#[test] +fn test_example_success() { + let mut cmd = Command::cargo_bin("example-cli").unwrap(); + let assert = cmd.args(&["--yes", "--filter", "SuccessScore"]).assert(); + + assert.success(); +} + +#[test] +fn test_example_fail() { + let mut cmd_fail = Command::cargo_bin("example-cli").unwrap(); + let assert_fail = cmd_fail.args(&["--yes", "--filter", "ErrorScore"]).assert(); + + assert_fail.failure(); +} diff --git a/harmony_cli/Cargo.toml b/harmony_cli/Cargo.toml new file mode 100644 index 0000000..ad681bd --- /dev/null +++ b/harmony_cli/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "harmony_cli" +edition = "2024" +version.workspace = true +readme.workspace = true +license.workspace = true + +[dependencies] +assert_cmd = "2.0.17" +clap = { version = "4.5.35", features = ["derive"] } +harmony = { path = "../harmony" } +harmony_tui = { path = "../harmony_tui", optional = true } +inquire = "0.7.5" +tokio.workspace = true + + +[features] +default = ["tui"] +tui = ["dep:harmony_tui"] diff --git a/harmony_cli/src/lib.rs b/harmony_cli/src/lib.rs new file mode 100644 index 0000000..3d1bd72 --- /dev/null +++ b/harmony_cli/src/lib.rs @@ -0,0 +1,318 @@ +use clap::Parser; +use clap::builder::ArgPredicate; +use harmony; +use harmony::{score::Score, topology::Topology}; +use inquire::Confirm; + +#[cfg(feature = "tui")] +use harmony_tui; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct Args { + /// Run score(s) without prompt + #[arg(short, long, default_value_t = false, conflicts_with = "interactive")] + yes: bool, + + /// Filter query + #[arg(short, long, conflicts_with = "interactive")] + filter: Option, + + /// Run interactive TUI or not + #[arg(short, long, default_value_t = false)] + interactive: bool, + + /// Run all or nth, defaults to all + #[arg( + short, + long, + default_value_t = true, + default_value_if("number", ArgPredicate::IsPresent, "false"), + conflicts_with = "number", + conflicts_with = "interactive" + )] + all: bool, + + /// Run nth matching, zero indexed + #[arg(short, long, default_value_t = 0, conflicts_with = "interactive")] + number: usize, + + /// list scores, will also be affected by run filter + #[arg(short, long, default_value_t = false, conflicts_with = "interactive")] + list: bool, +} + +fn maestro_scores_filter( + maestro: &harmony::maestro::Maestro, + all: bool, + filter: Option, + number: usize, +) -> Vec>> { + let scores = maestro.scores(); + let scores_read = scores.read().expect("Should be able to read scores"); + let mut scores_vec: Vec>> = match filter { + Some(f) => scores_read + .iter() + .filter(|s| s.name().contains(&f)) + .map(|s| s.clone_box()) + .collect(), + None => scores_read.iter().map(|s| s.clone_box()).collect(), + }; + + if !all { + let score = scores_vec.get(number); + match score { + Some(s) => scores_vec = vec![s.clone_box()], + None => return vec![], + } + }; + + return scores_vec; +} + +// TODO: consider adding doctest for this function +fn list_scores_with_index( + scores_vec: &Vec>>, +) -> String { + let mut display_str = String::new(); + for (i, s) in scores_vec.iter().enumerate() { + let name = s.name(); + display_str.push_str(&format!("\n{i}: {name}")); + } + return display_str; +} + +pub async fn init( + maestro: harmony::maestro::Maestro, + args_struct: Option, +) -> Result<(), Box> { + let args = match args_struct { + Some(args) => args, + None => Args::parse(), + }; + + #[cfg(feature = "tui")] + if args.interactive { + return harmony_tui::init(maestro).await; + } + + #[cfg(not(feature = "tui"))] + if args.interactive { + return Err("Not compiled with interactive support".into()); + } + + let scores_vec = maestro_scores_filter(&maestro, args.all, args.filter, args.number); + + if scores_vec.len() == 0 { + return Err("No score found".into()); + } + + // if list option is specified, print filtered list and exit + if args.list { + println!("Available scores:"); + println!("{}", list_scores_with_index(&scores_vec)); + return Ok(()); + } + + // prompt user if --yes is not specified + if !args.yes { + let confirmation = Confirm::new( + format!( + "This will run the following scores: {}\n", + list_scores_with_index(&scores_vec) + ) + .as_str(), + ) + .with_default(true) + .prompt() + .expect("Unexpected prompt error"); + + if !confirmation { + return Ok(()); + } + } + + // Run filtered scores + for s in scores_vec { + println!("Running: {}", s.name()); + maestro.interpret(s).await?; + } + + Ok(()) +} + +#[cfg(test)] +mod test { + use harmony::{ + inventory::Inventory, + maestro::Maestro, + modules::dummy::{ErrorScore, PanicScore, SuccessScore}, + topology::HAClusterTopology, + }; + use harmony::{score::Score, topology::Topology}; + + fn init_test_maestro() -> Maestro { + let inventory = Inventory::autoload(); + let topology = HAClusterTopology::autoload(); + let mut maestro = Maestro::new(inventory, topology); + + maestro.register_all(vec![ + Box::new(SuccessScore {}), + Box::new(ErrorScore {}), + Box::new(PanicScore {}), + ]); + + maestro + } + + #[tokio::test] + async fn test_init_success_score() { + let maestro = init_test_maestro(); + let res = crate::init( + maestro, + Some(crate::Args { + yes: true, + filter: Some("SuccessScore".to_owned()), + interactive: false, + all: true, + number: 0, + list: false, + }), + ) + .await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn test_init_error_score() { + let maestro = init_test_maestro(); + + let res = crate::init( + maestro, + Some(crate::Args { + yes: true, + filter: Some("ErrorScore".to_owned()), + interactive: false, + all: true, + number: 0, + list: false, + }), + ) + .await; + + assert!(res.is_err()); + } + + #[tokio::test] + async fn test_init_number_score() { + let maestro = init_test_maestro(); + + let res = crate::init( + maestro, + Some(crate::Args { + yes: true, + filter: None, + interactive: false, + all: false, + number: 0, + list: false, + }), + ) + .await; + + assert!(res.is_ok()); + } + + #[tokio::test] + async fn test_filter_fn_all() { + let maestro = init_test_maestro(); + + let res = crate::maestro_scores_filter(&maestro, true, None, 0); + + assert!(res.len() == 3); + } + + #[tokio::test] + async fn test_filter_fn_all_success() { + let maestro = init_test_maestro(); + + let res = crate::maestro_scores_filter(&maestro, true, Some("Success".to_owned()), 0); + + assert!(res.len() == 1); + + assert!( + maestro + .interpret(res.get(0).unwrap().clone_box()) + .await + .is_ok() + ); + } + + #[tokio::test] + async fn test_filter_fn_all_error() { + let maestro = init_test_maestro(); + + let res = crate::maestro_scores_filter(&maestro, true, Some("Error".to_owned()), 0); + + assert!(res.len() == 1); + + assert!( + maestro + .interpret(res.get(0).unwrap().clone_box()) + .await + .is_err() + ); + } + + #[tokio::test] + async fn test_filter_fn_all_score() { + let maestro = init_test_maestro(); + + let res = crate::maestro_scores_filter(&maestro, true, Some("Score".to_owned()), 0); + + assert!(res.len() == 3); + + assert!( + maestro + .interpret(res.get(0).unwrap().clone_box()) + .await + .is_ok() + ); + assert!( + maestro + .interpret(res.get(1).unwrap().clone_box()) + .await + .is_err() + ); + } + + #[tokio::test] + async fn test_filter_fn_number() { + let maestro = init_test_maestro(); + + let res = crate::maestro_scores_filter(&maestro, false, None, 0); + + println!("{:#?}", res); + + assert!(res.len() == 1); + + assert!( + maestro + .interpret(res.get(0).unwrap().clone_box()) + .await + .is_ok() + ); + } + + #[tokio::test] + async fn test_filter_fn_number_invalid() { + let maestro = init_test_maestro(); + + let res = crate::maestro_scores_filter(&maestro, false, None, 11); + + println!("{:#?}", res); + + assert!(res.len() == 0); + } +} diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index 11208f0..58b4ab7 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -3,7 +3,7 @@ mod widget; use log::{debug, error, info}; use tokio::sync::mpsc; use tokio_stream::StreamExt; -use tui_logger::{TuiWidgetEvent, TuiWidgetState}; +use tui_logger::{TuiLoggerFile, TuiWidgetEvent, TuiWidgetState}; use widget::{help::HelpWidget, score::ScoreListWidget}; use std::{panic, sync::Arc, time::Duration}; @@ -36,13 +36,13 @@ pub mod tui { /// modules::dummy::{ErrorScore, PanicScore, SuccessScore}, /// topology::HAClusterTopology, /// }; -/// +/// /// #[tokio::main] /// async fn main() { /// let inventory = Inventory::autoload(); /// let topology = HAClusterTopology::autoload(); /// let mut maestro = Maestro::new(inventory, topology); -/// +/// /// maestro.register_all(vec![ /// Box::new(SuccessScore {}), /// Box::new(ErrorScore {}), @@ -123,7 +123,7 @@ impl HarmonyTUI { // Set default level for unknown targets to Trace tui_logger::set_default_level(log::LevelFilter::Info); std::fs::create_dir_all("log")?; - tui_logger::set_log_file("log/harmony.log").unwrap(); + tui_logger::set_log_file(TuiLoggerFile::new("log/harmony.log")); color_eyre::install()?; let mut terminal = ratatui::init(); From eeafa086f323ecd58f103d906a37245f26f7fa7f Mon Sep 17 00:00:00 2001 From: Willem Date: Wed, 23 Apr 2025 14:54:32 +0000 Subject: [PATCH 26/62] feat: Improve output of tui. From p-r tui-score-info (#11) WIP: formatted score debug print into a table with a name header and the score information below Co-authored-by: Jean-Gabriel Gill-Couture Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/11 Reviewed-by: johnride Co-authored-by: Willem Co-committed-by: Willem --- Cargo.lock | 3 + examples/lamp/Cargo.toml | 2 +- examples/lamp/src/main.rs | 10 +- examples/tui/src/main.rs | 57 +++++- harmony/src/domain/score.rs | 192 ++++++++++++++++++- harmony/src/domain/topology/load_balancer.rs | 1 + harmony_tui/Cargo.toml | 2 + harmony_tui/src/lib.rs | 7 +- harmony_tui/src/widget/score.rs | 23 ++- opnsense-config/src/lib.rs | 1 + 10 files changed, 277 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9564fdb..28e9b2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -941,6 +941,7 @@ dependencies = [ "env_logger", "harmony", "harmony_macros", + "harmony_tui", "harmony_types", "log", "tokio", @@ -1339,6 +1340,8 @@ dependencies = [ "log", "log-panics", "ratatui", + "serde-value", + "serde_json", "tokio", "tokio-stream", "tui-logger", diff --git a/examples/lamp/Cargo.toml b/examples/lamp/Cargo.toml index 1bdcf68..902548e 100644 --- a/examples/lamp/Cargo.toml +++ b/examples/lamp/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] harmony = { path = "../../harmony" } -#harmony_tui = { path = "../../harmony_tui" } +harmony_tui = { path = "../../harmony_tui" } harmony_types = { path = "../../harmony_types" } cidr = { workspace = true } tokio = { workspace = true } diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index 7277aa5..feb8b4f 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -1,5 +1,6 @@ use harmony::{ data::Version, + inventory::Inventory, maestro::Maestro, modules::lamp::{LAMPConfig, LAMPScore}, topology::{HAClusterTopology, Url}, @@ -17,8 +18,9 @@ async fn main() { }, }; - Maestro::::load_from_env() - .interpret(Box::new(lamp_stack)) - .await - .unwrap(); + let inventory = Inventory::autoload(); + let topology = HAClusterTopology::autoload(); + let mut maestro = Maestro::new(inventory, topology); + maestro.register_all(vec![Box::new(lamp_stack)]); + harmony_tui::init(maestro).await.unwrap(); } diff --git a/examples/tui/src/main.rs b/examples/tui/src/main.rs index 05a768b..5ed607a 100644 --- a/examples/tui/src/main.rs +++ b/examples/tui/src/main.rs @@ -1,9 +1,20 @@ +use std::net::{SocketAddr, SocketAddrV4}; + use harmony::{ inventory::Inventory, maestro::Maestro, - modules::dummy::{ErrorScore, PanicScore, SuccessScore}, - topology::HAClusterTopology, + modules::{ + dns::DnsScore, + dummy::{ErrorScore, PanicScore, SuccessScore}, + load_balancer::LoadBalancerScore, + okd::load_balancer::OKDLoadBalancerScore, + }, + topology::{ + BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, + LoadBalancerService, + }, }; +use harmony_macros::ipv4; #[tokio::main] async fn main() { @@ -11,10 +22,52 @@ async fn main() { let topology = HAClusterTopology::autoload(); let mut maestro = Maestro::new(inventory, topology); + maestro.register_all(vec![ Box::new(SuccessScore {}), Box::new(ErrorScore {}), Box::new(PanicScore {}), + Box::new(DnsScore::new(vec![], None)), + Box::new(build_large_score()), ]); harmony_tui::init(maestro).await.unwrap(); } + +fn build_large_score() -> LoadBalancerScore { + let backend_server = BackendServer { + address: "192.168.0.0".to_string(), + port: 342, + }; + let lb_service = LoadBalancerService { + backend_servers: vec![ + backend_server.clone(), + backend_server.clone(), + backend_server.clone(), + ], + listening_port: SocketAddr::V4(SocketAddrV4::new(ipv4!("192.168.0.0"), 49387)), + health_check: Some(HealthCheck::HTTP( + "/some_long_ass_path_to_see_how_it_is_displayed_but_it_has_to_be_even_longer" + .to_string(), + HttpMethod::GET, + HttpStatusCode::Success2xx, + )), + }; + LoadBalancerScore { + public_services: vec![ + lb_service.clone(), + lb_service.clone(), + lb_service.clone(), + lb_service.clone(), + lb_service.clone(), + lb_service.clone(), + ], + private_services: vec![ + lb_service.clone(), + lb_service.clone(), + lb_service.clone(), + lb_service.clone(), + lb_service.clone(), + lb_service.clone(), + ], + } +} diff --git a/harmony/src/domain/score.rs b/harmony/src/domain/score.rs index dbe7aa5..a48548c 100644 --- a/harmony/src/domain/score.rs +++ b/harmony/src/domain/score.rs @@ -1,10 +1,12 @@ +use std::collections::BTreeMap; + use serde::Serialize; use serde_value::Value; use super::{interpret::Interpret, topology::Topology}; pub trait Score: - std::fmt::Debug + Send + Sync + CloneBoxScore + SerializeScore + std::fmt::Debug + ScoreToString + Send + Sync + CloneBoxScore + SerializeScore { fn create_interpret(&self) -> Box>; fn name(&self) -> String; @@ -39,3 +41,191 @@ where Box::new(self.clone()) } } + +pub trait ScoreToString { + fn print_score_details(&self) -> String; + fn format_value_as_string(&self, val: &Value, indent: usize) -> String; + fn format_map(&self, map: &BTreeMap, indent: usize) -> String; + fn wrap_or_truncate(&self, s: &str, width: usize) -> Vec; +} + +impl ScoreToString for S +where + T: Topology, + S: Score + 'static, +{ + fn print_score_details(&self) -> String { + let mut output = String::new(); + output += "\n"; + output += &self.format_value_as_string(&self.serialize(), 0); + output += "\n"; + output + } + fn format_map(&self, map: &BTreeMap, indent: usize) -> String { + let pad = " ".repeat(indent * 2); + let mut output = String::new(); + + output += &format!( + "{}+--------------------------+--------------------------------------------------+\n", + pad + ); + output += &format!("{}| {:<24} | {:<48} |\n", pad, "score_name", self.name()); + output += &format!( + "{}+--------------------------+--------------------------------------------------+\n", + pad + ); + + for (k, v) in map { + let key_str = match k { + Value::String(s) => s.clone(), + other => format!("{:?}", other), + }; + + let formatted_val = self.format_value_as_string(v, indent + 1); + let mut lines = formatted_val.lines().map(|line| line.trim_start()); + + let wrapped_lines: Vec<_> = lines + .flat_map(|line| self.wrap_or_truncate(line.trim_start(), 48)) + .collect(); + + if let Some(first) = wrapped_lines.first() { + output += &format!("{}| {:<24} | {:<48} |\n", pad, key_str, first); + for line in &wrapped_lines[1..] { + output += &format!("{}| {:<24} | {:<48} |\n", pad, "", line); + } + } + + // let first_line = lines.next().unwrap_or(""); + // output += &format!("{}| {:<24} | {:<48} |\n", pad, key_str, first_line); + // + // for line in lines { + // output += &format!("{}| {:<24} | {:<48} |\n", pad, "", line); + // } + } + + output += &format!( + "{}+--------------------------+--------------------------------------------------+\n\n", + pad + ); + + output + } + + fn wrap_or_truncate(&self, s: &str, width: usize) -> Vec { + let mut lines = Vec::new(); + let mut current = s; + + while !current.is_empty() { + if current.len() <= width { + lines.push(current.to_string()); + break; + } + + // Try to wrap at whitespace if possible + let mut split_index = current[..width].rfind(' ').unwrap_or(width); + if split_index == 0 { + split_index = width; + } + + lines.push(current[..split_index].trim_end().to_string()); + current = current[split_index..].trim_start(); + } + + lines + } + + fn format_value_as_string(&self, val: &Value, indent: usize) -> String { + let pad = " ".repeat(indent * 2); + let mut output = String::new(); + + match val { + Value::Bool(b) => output += &format!("{}{}\n", pad, b), + Value::U8(u) => output += &format!("{}{}\n", pad, u), + Value::U16(u) => output += &format!("{}{}\n", pad, u), + Value::U32(u) => output += &format!("{}{}\n", pad, u), + Value::U64(u) => output += &format!("{}{}\n", pad, u), + Value::I8(i) => output += &format!("{}{}\n", pad, i), + Value::I16(i) => output += &format!("{}{}\n", pad, i), + Value::I32(i) => output += &format!("{}{}\n", pad, i), + Value::I64(i) => output += &format!("{}{}\n", pad, i), + Value::F32(f) => output += &format!("{}{}\n", pad, f), + Value::F64(f) => output += &format!("{}{}\n", pad, f), + Value::Char(c) => output += &format!("{}{}\n", pad, c), + Value::String(s) => output += &format!("{}{:<48}\n", pad, s), + Value::Unit => output += &format!("{}\n", pad), + Value::Bytes(bytes) => output += &format!("{}{:?}\n", pad, bytes), + + Value::Option(opt) => match opt { + Some(inner) => { + output += &format!("{}Option:\n", pad); + output += &self.format_value_as_string(inner, indent + 1); + } + None => output += &format!("{}None\n", pad), + }, + + Value::Newtype(inner) => { + output += &format!("{}Newtype:\n", pad); + output += &self.format_value_as_string(inner, indent + 1); + } + + Value::Seq(seq) => { + if seq.is_empty() { + output += &format!("{}[]\n", pad); + } else { + output += &format!("{}[\n", pad); + for item in seq { + output += &self.format_value_as_string(item, indent + 1); + } + output += &format!("{}]\n", pad); + } + } + + Value::Map(map) => { + if map.is_empty() { + output += &format!("{}\n", pad); + } else if indent == 0 { + output += &self.format_map(map, indent); + } else { + for (k, v) in map { + let key_str = match k { + Value::String(s) => s.clone(), + other => format!("{:?}", other), + }; + + let val_str = self + .format_value_as_string(v, indent + 1) + .trim() + .to_string(); + let val_lines: Vec<_> = val_str.lines().collect(); + + output += + &format!("{}{}: {}\n", pad, key_str, val_lines.first().unwrap_or(&"")); + for line in val_lines.iter().skip(1) { + output += &format!("{} {}\n", pad, line); + } + } + } + } + } + + output + } +} + +//TODO write test to check that the output is what it should be +// +#[cfg(test)] +mod tests { + use super::*; + use crate::modules::dns::DnsScore; + use crate::topology::{self, HAClusterTopology}; + + #[test] + fn test_format_values_as_string() { + let dns_score = Box::new(DnsScore::new(vec![], None)); + let print_score_output = + >::print_score_details(&dns_score); + let expected_empty_dns_score_table = "\n+--------------------------+--------------------------------------------------+\n| score_name | DnsScore |\n+--------------------------+--------------------------------------------------+\n| dns_entries | [] |\n| register_dhcp_leases | None |\n+--------------------------+--------------------------------------------------+\n\n\n"; + assert_eq!(print_score_output, expected_empty_dns_score_table); + } +} diff --git a/harmony/src/domain/topology/load_balancer.rs b/harmony/src/domain/topology/load_balancer.rs index afb9092..6127019 100644 --- a/harmony/src/domain/topology/load_balancer.rs +++ b/harmony/src/domain/topology/load_balancer.rs @@ -46,6 +46,7 @@ pub struct LoadBalancerService { #[derive(Debug, PartialEq, Clone, Serialize)] pub struct BackendServer { + // TODO should not be a string, probably IPAddress pub address: String, pub port: u16, } diff --git a/harmony_tui/Cargo.toml b/harmony_tui/Cargo.toml index a12e534..6aacedd 100644 --- a/harmony_tui/Cargo.toml +++ b/harmony_tui/Cargo.toml @@ -16,3 +16,5 @@ color-eyre = "0.6.3" tokio-stream = "0.1.17" tui-logger = "0.14.1" log-panics = "2.1.0" +serde-value.workspace = true +serde_json = "1.0.140" diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index 58b4ab7..c3a8a1a 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -159,12 +159,13 @@ impl HarmonyTUI { frame.render_widget(&help_block, help_area); frame.render_widget(HelpWidget::new(), help_block.inner(help_area)); - let [list_area, output_area] = + let [list_area, logger_area] = Layout::horizontal([Constraint::Min(30), Constraint::Percentage(100)]).areas(app_area); let block = Block::default().borders(Borders::RIGHT); frame.render_widget(&block, list_area); self.score.render(list_area, frame); + let tui_logger = tui_logger::TuiLoggerWidget::default() .style_error(Style::default().fg(Color::Red)) .style_warn(Style::default().fg(Color::LightRed)) @@ -172,9 +173,9 @@ impl HarmonyTUI { .style_debug(Style::default().fg(Color::Gray)) .style_trace(Style::default().fg(Color::Gray)) .state(&self.tui_state); - frame.render_widget(tui_logger, output_area) - } + frame.render_widget(tui_logger, logger_area); + } fn scores_list(maestro: &Maestro) -> Vec>> { let scores = maestro.scores(); let scores_read = scores.read().expect("Should be able to read scores"); diff --git a/harmony_tui/src/widget/score.rs b/harmony_tui/src/widget/score.rs index b0d2c27..3acb5c2 100644 --- a/harmony_tui/src/widget/score.rs +++ b/harmony_tui/src/widget/score.rs @@ -1,5 +1,6 @@ use std::sync::{Arc, RwLock}; +use crate::HarmonyTuiEvent; use crossterm::event::{Event, KeyCode, KeyEventKind}; use harmony::{score::Score, topology::Topology}; use log::{info, warn}; @@ -11,8 +12,6 @@ use ratatui::{ }; use tokio::sync::mpsc; -use crate::HarmonyTuiEvent; - #[derive(Debug)] enum ExecutionState { INITIATED, @@ -53,23 +52,27 @@ impl ScoreListWidget { } pub(crate) fn launch_execution(&mut self) { - let list_read = self.list_state.read().unwrap(); - if let Some(index) = list_read.selected() { - let score = self - .scores - .get(index) - .expect("List state should always match with internal Vec"); - + if let Some(score) = self.get_selected_score() { self.execution = Some(Execution { state: ExecutionState::INITIATED, score: score.clone_box(), }); - info!("{:#?}\n\nConfirm Execution (Press y/n)", score); + info!("{}\n\nConfirm Execution (Press y/n)", score.name()); + info!("{}", score.print_score_details()); } else { warn!("No Score selected, nothing to launch"); } } + pub(crate) fn get_selected_score(&self) -> Option>> { + let list_read = self.list_state.read().unwrap(); + if let Some(index) = list_read.selected() { + self.scores.get(index).map(|s| s.clone_box()) + } else { + None + } + } + pub(crate) fn scroll_down(&self) { self.list_state.write().unwrap().scroll_down_by(1); } diff --git a/opnsense-config/src/lib.rs b/opnsense-config/src/lib.rs index d11ec41..f83b3e0 100644 --- a/opnsense-config/src/lib.rs +++ b/opnsense-config/src/lib.rs @@ -12,6 +12,7 @@ mod test { use crate::Config; use pretty_assertions::assert_eq; + #[cfg(opnsenseendtoend)] #[tokio::test] async fn test_public_sdk() { let mac = "11:22:33:44:55:66"; From 027114c48ce91e1f6f5e5d6038c297b7bbe57827 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Fri, 11 Apr 2025 16:20:59 -0400 Subject: [PATCH 27/62] feat: introduce topology readiness and initialization Adds a `ensure_ready` method to the `Topology` trait to ensure the infrastructure is prepared before score execution. - Introduces a new `Outcome` status to indicate the result of the readiness check. - Implements a `topology_preparation_result` field in `Maestro` to track initialization status. - Adds a check in `interpret` to warn if the topology isn't fully initialized. - Provides detailed documentation for the `Topology` trait and `ensure_ready` method, including recommended patterns for complex setups. - Adds `async_trait` dependency. --- harmony/src/domain/maestro/mod.rs | 49 +++++++++++++++++++++++++----- harmony/src/domain/topology/mod.rs | 33 +++++++++++++++++++- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index 256c759..2bea72d 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -1,9 +1,9 @@ -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, Mutex, RwLock}; -use log::info; +use log::{info, warn}; use super::{ - interpret::{InterpretError, Outcome}, + interpret::{InterpretError, InterpretStatus, Outcome}, inventory::Inventory, score::Score, topology::Topology, @@ -15,6 +15,7 @@ pub struct Maestro { inventory: Inventory, topology: T, scores: Arc>>, + topology_preparation_result: Mutex>, } impl Maestro { @@ -23,9 +24,28 @@ impl Maestro { inventory, topology, scores: Arc::new(RwLock::new(Vec::new())), + topology_preparation_result: None.into(), } } + /// Ensures the associated Topology is ready for operations. + /// Delegates the readiness check and potential setup actions to the Topology. + pub async fn prepare_topology(&self) -> Result { + info!("Ensuring topology '{}' is ready...", self.topology.name()); + let outcome = self.topology.ensure_ready().await?; + info!( + "Topology '{}' readiness check complete: {}", + self.topology.name(), + outcome.status + ); + + self.topology_preparation_result + .lock() + .unwrap() + .replace(outcome.clone()); + Ok(outcome) + } + // Load the inventory and inventory from environment. // This function is able to discover the context that it is running in, such as k8s clusters, aws cloud, linux host, etc. // When the HARMONY_TOPOLOGY environment variable is not set, it will default to install k3s @@ -47,16 +67,31 @@ impl Maestro { } } - pub fn start(&mut self) { - info!("Starting Maestro"); - } - pub fn register_all(&mut self, mut scores: ScoreVec) { let mut score_mut = self.scores.write().expect("Should acquire lock"); score_mut.append(&mut scores); } + fn is_topology_initialized(&self) -> bool { + let result = self.topology_preparation_result.lock().unwrap(); + if let Some(outcome) = result.as_ref() { + match outcome.status { + InterpretStatus::SUCCESS => return true, + _ => return false, + } + } else { + false + } + } + pub async fn interpret(&self, score: Box>) -> Result { + if !self.is_topology_initialized() { + warn!( + "Launching interpret for score {} but Topology {} is not fully initialized!", + score.name(), + self.topology.name(), + ); + } info!("Running score {score:?}"); let interpret = score.create_interpret(); info!("Launching interpret {interpret:?}"); diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index fa0b7fe..16525f5 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -5,6 +5,7 @@ mod load_balancer; pub mod openshift; mod router; mod tftp; +use async_trait::async_trait; pub use ha_cluster::*; pub use load_balancer::*; pub use router::*; @@ -17,8 +18,38 @@ pub use tftp::*; use std::net::IpAddr; -pub trait Topology { +use super::interpret::{InterpretError, Outcome}; + +/// Represents a logical view of an infrastructure environment providing specific capabilities. +/// +/// A Topology acts as a self-contained "package" responsible for managing access +/// to its underlying resources and ensuring they are in a ready state before use. +/// It defines the contract for the capabilities it provides through implemented +/// capability traits (e.g., `HasK8sCapability`, `HasDnsServer`). +#[async_trait] +pub trait Topology: Send + Sync { + /// Returns a unique identifier or name for this specific topology instance. + /// This helps differentiate between multiple instances of potentially the same type. fn name(&self) -> &str; + + /// Ensures that the topology and its required underlying components or services + /// are ready to provide their declared capabilities. + /// + /// Implementations of this method MUST be idempotent. Subsequent calls after a + /// successful readiness check should ideally be cheap NO-OPs. + /// + /// This method encapsulates the logic for: + /// 1. **Checking Current State:** Assessing if the required resources/services are already running and configured. + /// 2. **Discovery:** Identifying the runtime environment (e.g., local Docker, AWS, existing cluster). + /// 3. **Initialization/Bootstrapping:** Performing necessary setup actions if not already ready. This might involve: + /// * Making API calls. + /// * Running external commands (e.g., `k3d`, `docker`). + /// * **Internal Orchestration:** For complex topologies, this method might manage dependencies on other sub-topologies, ensuring *their* `ensure_ready` is called first. Using nested `Maestros` to run setup `Scores` against these sub-topologies is the recommended pattern for non-trivial bootstrapping, allowing reuse of Harmony's core orchestration logic. + /// + /// # Returns + /// - `Ok(Outcome)`: Indicates the topology is now ready. The `Outcome` status might be `SUCCESS` if actions were taken, or `NOOP` if it was already ready. The message should provide context. + /// - `Err(TopologyError)`: Indicates the topology could not reach a ready state due to configuration issues, discovery failures, bootstrap errors, or unsupported environments. + async fn ensure_ready(&self) -> Result; } pub type IpAddress = IpAddr; From 6812d05849b0556524ef7ff2338b874bf25c0c17 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Mon, 14 Apr 2025 14:57:01 -0400 Subject: [PATCH 28/62] feat: Introduce K8sAnywhereTopology and refactor Kubernetes interactions This commit introduces a new topology, `K8sAnywhereTopology`, designed to handle Kubernetes deployments more flexibly. Key changes include: - Introduced `K8sAnywhereTopology` to encapsulate Kubernetes client management and configuration. - Refactored existing Kubernetes-related code to utilize the new topology. - Updated `OcK8sclient` to `K8sclient` across modules (k8s, lamp, deployment, resource) for consistency. - Ensured all relevant modules now interface with Kubernetes through the `K8sclient` trait. This change promotes a more modular and maintainable codebase for Kubernetes integrations within Harmony. --- harmony/src/domain/topology/ha_cluster.rs | 16 ++- .../domain/topology/{openshift.rs => k8s.rs} | 4 +- harmony/src/domain/topology/k8s_anywhere.rs | 119 ++++++++++++++++++ harmony/src/domain/topology/mod.rs | 4 +- harmony/src/domain/topology/network.rs | 6 +- harmony/src/modules/k8s/deployment.rs | 4 +- harmony/src/modules/k8s/resource.rs | 6 +- harmony/src/modules/lamp.rs | 4 +- 8 files changed, 145 insertions(+), 18 deletions(-) rename harmony/src/domain/topology/{openshift.rs => k8s.rs} (96%) create mode 100644 harmony/src/domain/topology/k8s_anywhere.rs diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 0e9230b..d2af961 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -3,6 +3,8 @@ use harmony_macros::ip; use harmony_types::net::MacAddress; use crate::executors::ExecutorError; +use crate::interpret::InterpretError; +use crate::interpret::Outcome; use super::DHCPStaticEntry; use super::DhcpServer; @@ -15,13 +17,13 @@ use super::IpAddress; use super::LoadBalancer; use super::LoadBalancerService; use super::LogicalHost; -use super::OcK8sclient; +use super::K8sclient; use super::Router; use super::TftpServer; use super::Topology; use super::Url; -use super::openshift::OpenshiftClient; +use super::k8s::K8sClient; use std::sync::Arc; #[derive(Debug, Clone)] @@ -40,16 +42,20 @@ pub struct HAClusterTopology { pub switch: Vec, } +#[async_trait] impl Topology for HAClusterTopology { fn name(&self) -> &str { todo!() } + async fn ensure_ready(&self) -> Result { + todo!("ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready.") + } } #[async_trait] -impl OcK8sclient for HAClusterTopology { - async fn oc_client(&self) -> Result, kube::Error> { - Ok(Arc::new(OpenshiftClient::try_default().await?)) +impl K8sclient for HAClusterTopology { + async fn k8s_client(&self) -> Result, kube::Error> { + Ok(Arc::new(K8sClient::try_default().await?)) } } diff --git a/harmony/src/domain/topology/openshift.rs b/harmony/src/domain/topology/k8s.rs similarity index 96% rename from harmony/src/domain/topology/openshift.rs rename to harmony/src/domain/topology/k8s.rs index 1790a06..ed345ee 100644 --- a/harmony/src/domain/topology/openshift.rs +++ b/harmony/src/domain/topology/k8s.rs @@ -2,11 +2,11 @@ use k8s_openapi::NamespaceResourceScope; use kube::{Api, Client, Error, Resource, api::PostParams}; use serde::de::DeserializeOwned; -pub struct OpenshiftClient { +pub struct K8sClient { client: Client, } -impl OpenshiftClient { +impl K8sClient { pub async fn try_default() -> Result { Ok(Self { client: Client::try_default().await?, diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs new file mode 100644 index 0000000..78e4a0e --- /dev/null +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -0,0 +1,119 @@ +use async_trait::async_trait; +use log::info; +use tokio::sync::OnceCell; + +use crate::interpret::{InterpretError, Outcome}; + +use super::{Topology, k8s::K8sClient}; + +struct K8sState { + client: K8sClient, + source: K8sSource, + message: String, +} + +enum K8sSource { + Existing, + K3d, + // TODO: Add variants for cloud providers like AwsEks, Gke, Aks +} + +pub struct K8sAnywhereTopology { + k8s_state: OnceCell>, +} + +impl K8sAnywhereTopology { + async fn try_load_default_kubeconfig(&self) -> Option { + todo!("Use kube-rs default behavior to load system kubeconfig"); + } + + async fn try_load_kubeconfig(&self, path: &str) -> Option { + todo!("Use kube-rs to load kubeconfig at path {path}"); + } + + async fn try_install_k3d(&self) -> Result { + todo!( + "Create Maestro with LocalDockerTopology or something along these lines and run a K3dInstallationScore on it" + ) + } + + async fn try_get_or_install_k8s_client(&self) -> Result, InterpretError> { + let k8s_anywhere_config = K8sAnywhereConfig { + kubeconfig: std::env::var("HARMONY_KUBECONFIG") + .ok() + .map(|v| v.to_string()), + use_system_kubeconfig: std::env::var("HARMONY_USE_SYSTEM_KUBECONFIG") + .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)), + autoinstall: std::env::var("HARMONY_AUTOINSTALL") + .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)), + }; + + if k8s_anywhere_config.use_system_kubeconfig { + match self.try_load_default_kubeconfig().await { + Some(client) => todo!(), + None => todo!(), + } + } + + if let Some(kubeconfig) = k8s_anywhere_config.kubeconfig { + match self.try_load_kubeconfig(&kubeconfig).await { + Some(client) => todo!(), + None => todo!(), + } + } + info!("No kubernetes configuration found"); + + if !k8s_anywhere_config.autoinstall { + info!( + "Harmony autoinstallation is not activated, do you wish to launch autoinstallation?" + ); + todo!("Prompt user"); + } + + match self.try_install_k3d().await { + Ok(client) => todo!(), + Err(_) => todo!(), + } + } +} + +struct K8sAnywhereConfig { + /// The path of the KUBECONFIG file that Harmony should use to interact with the Kubernetes + /// cluster + /// + /// Default : None + kubeconfig: Option, + + /// Whether to use the system KUBECONFIG, either the environment variable or the file in the + /// default or configured location + /// + /// Default : false + use_system_kubeconfig: bool, + + /// Whether to install automatically a kubernetes cluster + /// + /// When enabled, autoinstall will setup a K3D cluster on the localhost. https://k3d.io/stable/ + /// + /// Default: true + autoinstall: bool, +} + +#[async_trait] +impl Topology for K8sAnywhereTopology { + fn name(&self) -> &str { + todo!() + } + + async fn ensure_ready(&self) -> Result { + match self + .k8s_state + .get_or_try_init(|| self.try_get_or_install_k8s_client()) + .await? + { + Some(k8s_state) => Ok(Outcome::success(k8s_state.message.clone())), + None => Err(InterpretError::new( + "No K8s client could be found or installed".to_string(), + )), + } + } +} diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index 16525f5..29e12fc 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -1,8 +1,10 @@ mod ha_cluster; mod host_binding; mod http; +mod k8s_anywhere; +pub use k8s_anywhere::*; mod load_balancer; -pub mod openshift; +pub mod k8s; mod router; mod tftp; use async_trait::async_trait; diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index 13d1902..d4463ae 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -6,7 +6,7 @@ use serde::Serialize; use crate::executors::ExecutorError; -use super::{IpAddress, LogicalHost, openshift::OpenshiftClient}; +use super::{IpAddress, LogicalHost, k8s::K8sClient}; #[derive(Debug)] pub struct DHCPStaticEntry { @@ -42,8 +42,8 @@ pub struct NetworkDomain { pub name: String, } #[async_trait] -pub trait OcK8sclient: Send + Sync + std::fmt::Debug { - async fn oc_client(&self) -> Result, kube::Error>; +pub trait K8sclient: Send + Sync + std::fmt::Debug { + async fn k8s_client(&self) -> Result, kube::Error>; } #[async_trait] diff --git a/harmony/src/modules/k8s/deployment.rs b/harmony/src/modules/k8s/deployment.rs index cd2ad90..9e7178f 100644 --- a/harmony/src/modules/k8s/deployment.rs +++ b/harmony/src/modules/k8s/deployment.rs @@ -5,7 +5,7 @@ use serde_json::json; use crate::{ interpret::Interpret, score::Score, - topology::{OcK8sclient, Topology}, + topology::{K8sclient, Topology}, }; use super::resource::{K8sResourceInterpret, K8sResourceScore}; @@ -16,7 +16,7 @@ pub struct K8sDeploymentScore { pub image: String, } -impl Score for K8sDeploymentScore { +impl Score for K8sDeploymentScore { fn create_interpret(&self) -> Box> { let deployment: Deployment = serde_json::from_value(json!( { diff --git a/harmony/src/modules/k8s/resource.rs b/harmony/src/modules/k8s/resource.rs index 505c4a4..4e54be7 100644 --- a/harmony/src/modules/k8s/resource.rs +++ b/harmony/src/modules/k8s/resource.rs @@ -8,7 +8,7 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::{OcK8sclient, Topology}, + topology::{K8sclient, Topology}, }; #[derive(Debug, Clone, Serialize)] @@ -63,7 +63,7 @@ impl< + Default + Send + Sync, - T: Topology + OcK8sclient, + T: Topology + K8sclient, > Interpret for K8sResourceInterpret where ::DynamicType: Default, @@ -74,7 +74,7 @@ where topology: &T, ) -> Result { topology - .oc_client() + .k8s_client() .await .expect("Environment should provide enough information to instanciate a client") .apply_namespaced(&self.score.resource) diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index ef7227c..55eefdd 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -9,7 +9,7 @@ use crate::{ inventory::Inventory, modules::k8s::deployment::K8sDeploymentScore, score::Score, - topology::{OcK8sclient, Topology, Url}, + topology::{K8sclient, Topology, Url}, }; #[derive(Debug, Clone, Serialize)] @@ -51,7 +51,7 @@ pub struct LAMPInterpret { } #[async_trait] -impl Interpret for LAMPInterpret { +impl Interpret for LAMPInterpret { async fn execute( &self, inventory: &Inventory, From 3f6f1fa0d45ceb806b60d68ec7c2938c674320a7 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 17 Apr 2025 09:55:33 -0400 Subject: [PATCH 29/62] wip: Implement basic K8sAnywhere setup with K3d support - Added initial K8sAnywhere topology and related modules. - Implemented a basic K3d installation score for cluster bootstrapping. - Introduced LocalhostTopology for local development and testing. - Added necessary module structure and dependencies. - Implemented user prompt for K3d installation confirmation. - Added basic error handling and logging. - Refactored existing code to improve modularity and maintainability. - Included necessary tests to ensure functionality. --- harmony/src/domain/topology/k8s_anywhere.rs | 46 ++++++++++++++++----- harmony/src/domain/topology/localhost.rs | 20 +++++++++ harmony/src/domain/topology/mod.rs | 2 + harmony/src/modules/k3d/install.rs | 25 +++++++++++ harmony/src/modules/k3d/mod.rs | 3 ++ harmony/src/modules/mod.rs | 1 + 6 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 harmony/src/domain/topology/localhost.rs create mode 100644 harmony/src/modules/k3d/install.rs create mode 100644 harmony/src/modules/k3d/mod.rs diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 78e4a0e..1db1aca 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -1,8 +1,12 @@ +use std::io; + use async_trait::async_trait; -use log::info; +use log::{info, warn}; use tokio::sync::OnceCell; -use crate::interpret::{InterpretError, Outcome}; +use crate::{ + interpret::{InterpretError, Outcome}, inventory::Inventory, maestro::Maestro, topology::LocalhostTopology +}; use super::{Topology, k8s::K8sClient}; @@ -13,8 +17,8 @@ struct K8sState { } enum K8sSource { - Existing, - K3d, + RemoteCluster, + LocalK3d, // TODO: Add variants for cloud providers like AwsEks, Gke, Aks } @@ -23,7 +27,7 @@ pub struct K8sAnywhereTopology { } impl K8sAnywhereTopology { - async fn try_load_default_kubeconfig(&self) -> Option { + async fn try_load_system_kubeconfig(&self) -> Option { todo!("Use kube-rs default behavior to load system kubeconfig"); } @@ -32,9 +36,12 @@ impl K8sAnywhereTopology { } async fn try_install_k3d(&self) -> Result { + let maestro = Maestro::new(Inventory::autoload(), LocalhostTopology::new()); + let k3d_score = K3DInstallationScore::default(); + maestro.interpret(Box::new(k3d_score)).await; todo!( "Create Maestro with LocalDockerTopology or something along these lines and run a K3dInstallationScore on it" - ) + ); } async fn try_get_or_install_k8s_client(&self) -> Result, InterpretError> { @@ -49,7 +56,7 @@ impl K8sAnywhereTopology { }; if k8s_anywhere_config.use_system_kubeconfig { - match self.try_load_default_kubeconfig().await { + match self.try_load_system_kubeconfig().await { Some(client) => todo!(), None => todo!(), } @@ -61,17 +68,36 @@ impl K8sAnywhereTopology { None => todo!(), } } + info!("No kubernetes configuration found"); if !k8s_anywhere_config.autoinstall { info!( - "Harmony autoinstallation is not activated, do you wish to launch autoinstallation?" + "Harmony autoinstallation is not activated, do you wish to launch autoinstallation? (y/N) : " ); - todo!("Prompt user"); + let mut input = String::new(); + + io::stdin() + .read_line(&mut input) + .expect("Failed to read line"); + + let input = input.trim(); + + if !input.eq_ignore_ascii_case("y") { + warn!( + "Installation cancelled, K8sAnywhere could not initialize a valid Kubernetes client" + ); + return Ok(None); + } } + info!("Starting K8sAnywhere installation"); match self.try_install_k3d().await { - Ok(client) => todo!(), + Ok(client) => Ok(Some(K8sState { + client, + source: K8sSource::LocalK3d, + message: "Successfully installed K3D cluster and acquired client".to_string(), + })), Err(_) => todo!(), } } diff --git a/harmony/src/domain/topology/localhost.rs b/harmony/src/domain/topology/localhost.rs new file mode 100644 index 0000000..19804e8 --- /dev/null +++ b/harmony/src/domain/topology/localhost.rs @@ -0,0 +1,20 @@ +use async_trait::async_trait; +use derive_new::new; + +use crate::interpret::{InterpretError, Outcome}; + +use super::Topology; + +#[derive(new)] +pub struct LocalhostTopology; + +#[async_trait] +impl Topology for LocalhostTopology { + fn name(&self) -> &str { + "LocalHostTopology" + } + + async fn ensure_ready(&self) -> Result { + Ok(Outcome::success("Localhost is Chuck Norris, always ready.".to_string())) + } +} diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index 29e12fc..b3e3779 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -2,6 +2,8 @@ mod ha_cluster; mod host_binding; mod http; mod k8s_anywhere; +mod localhost; +pub use localhost::*; pub use k8s_anywhere::*; mod load_balancer; pub mod k8s; diff --git a/harmony/src/modules/k3d/install.rs b/harmony/src/modules/k3d/install.rs new file mode 100644 index 0000000..386120f --- /dev/null +++ b/harmony/src/modules/k3d/install.rs @@ -0,0 +1,25 @@ +use serde::Serialize; + +use crate::{score::Score, topology::Topology}; + +#[derive(Debug, Clone, Serialize)] +pub struct K3DInstallationScore {} + +impl Score for K3DInstallationScore { + fn create_interpret(&self) -> Box> { + todo!(" + 1. Decide if I create a new crate for k3d management, especially to avoid the ocrtograb dependency + 2. Implement k3d management + 3. Find latest tag + 4. Download k3d to some path managed by harmony (or not?) + 5. Bootstrap cluster + 6. Get kubeconfig + 7. Load kubeconfig in k8s anywhere + 8. Complete k8sanywhere setup + ") + } + + fn name(&self) -> String { + todo!() + } +} diff --git a/harmony/src/modules/k3d/mod.rs b/harmony/src/modules/k3d/mod.rs new file mode 100644 index 0000000..d7db5d4 --- /dev/null +++ b/harmony/src/modules/k3d/mod.rs @@ -0,0 +1,3 @@ + +mod install; + diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index 8456867..60ddfd2 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -8,3 +8,4 @@ pub mod load_balancer; pub mod okd; pub mod opnsense; pub mod tftp; +mod k3d; From 847d84b46ff854c24bf7e6cbc9cbb4891631adec Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 17 Apr 2025 13:04:06 -0400 Subject: [PATCH 30/62] wip: Started work on k3d crate --- Cargo.lock | 231 +++++++++++++++++++- Cargo.toml | 1 + harmony/src/domain/topology/k8s_anywhere.rs | 2 +- harmony/src/modules/k3d/mod.rs | 2 +- harmony/src/modules/mod.rs | 2 +- k3d/Cargo.toml | 23 ++ k3d/src/lib.rs | 12 + 7 files changed, 262 insertions(+), 11 deletions(-) create mode 100644 k3d/Cargo.toml create mode 100644 k3d/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 28e9b2b..554bcd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "assert_cmd" version = "2.0.17" @@ -401,9 +407,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.36" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", "clap_derive", @@ -411,9 +417,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.36" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", @@ -706,15 +712,24 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "pem-rfc7468", "zeroize", ] +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive-new" version = "0.7.0" @@ -1214,8 +1229,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1867,6 +1884,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1935,6 +1962,33 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "k3d-rs" +version = "0.1.0" +dependencies = [ + "async-trait", + "env_logger", + "log", + "octocrab", + "pretty_assertions", + "tokio", +] + [[package]] name = "k8s-openapi" version = "0.24.0" @@ -2213,6 +2267,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -2262,6 +2322,46 @@ dependencies = [ "memchr", ] +[[package]] +name = "octocrab" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf799a9982a4d0b4b3fa15b4c1ff7daf5bd0597f46456744dcbb6ddc2e4c827" +dependencies = [ + "arc-swap", + "async-trait", + "base64 0.22.1", + "bytes", + "cfg-if", + "chrono", + "either", + "futures", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "jsonwebtoken", + "once_cell", + "percent-encoding", + "pin-project", + "secrecy", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "snafu", + "tokio", + "tower", + "tower-http", + "tracing", + "url", + "web-time", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -2542,6 +2642,26 @@ dependencies = [ "sha2", ] +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2636,6 +2756,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -3030,9 +3156,9 @@ dependencies = [ [[package]] name = "russh-sftp" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f08ed364d54b74d988c964b464a53a1916379f9441cfd10ca8fb264be1349842" +checksum = "3bb94393cafad0530145b8f626d8687f1ee1dedb93d7ba7740d6ae81868b13b5" dependencies = [ "bitflags 2.9.0", "bytes", @@ -3336,6 +3462,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_tokenstream" version = "0.2.2" @@ -3451,6 +3587,18 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.12", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -3466,6 +3614,27 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +[[package]] +name = "snafu" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "socket2" version = "0.5.9" @@ -3719,6 +3888,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -3836,10 +4036,13 @@ dependencies = [ "base64 0.22.1", "bitflags 2.9.0", "bytes", + "futures-util", "http 1.3.1", "http-body 1.0.1", + "iri-string", "mime", "pin-project-lite", + "tower", "tower-layer", "tower-service", "tracing", @@ -4010,6 +4213,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -4184,6 +4388,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 4ba83eb..1791b36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "opnsense-config", "opnsense-config-xml", "harmony_cli", + "k3d", ] [workspace.package] diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 1db1aca..f91d88b 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -5,7 +5,7 @@ use log::{info, warn}; use tokio::sync::OnceCell; use crate::{ - interpret::{InterpretError, Outcome}, inventory::Inventory, maestro::Maestro, topology::LocalhostTopology + interpret::{InterpretError, Outcome}, inventory::Inventory, maestro::Maestro, modules::k3d::K3DInstallationScore, topology::LocalhostTopology }; use super::{Topology, k8s::K8sClient}; diff --git a/harmony/src/modules/k3d/mod.rs b/harmony/src/modules/k3d/mod.rs index d7db5d4..2d243c0 100644 --- a/harmony/src/modules/k3d/mod.rs +++ b/harmony/src/modules/k3d/mod.rs @@ -1,3 +1,3 @@ - mod install; +pub use install::*; diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index 60ddfd2..9baa98a 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -8,4 +8,4 @@ pub mod load_balancer; pub mod okd; pub mod opnsense; pub mod tftp; -mod k3d; +pub mod k3d; diff --git a/k3d/Cargo.toml b/k3d/Cargo.toml new file mode 100644 index 0000000..8e5c781 --- /dev/null +++ b/k3d/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "k3d-rs" +edition = "2021" +version.workspace = true +readme.workspace = true +license.workspace = true + +[dependencies] +#serde = { version = "1.0.123", features = [ "derive" ] } +log = { workspace = true } +env_logger = { workspace = true } +#russh = { workspace = true } +#russh-keys = { workspace = true } +#thiserror = "1.0" +async-trait = { workspace = true } +tokio = { workspace = true } +octocrab = "0.44.0" +#serde_json = "1.0.133" +#tokio-util = { version = "0.7.13", features = [ "codec" ] } +#tokio-stream = "0.1.17" + +[dev-dependencies] +pretty_assertions = "1.4.1" diff --git a/k3d/src/lib.rs b/k3d/src/lib.rs new file mode 100644 index 0000000..3227138 --- /dev/null +++ b/k3d/src/lib.rs @@ -0,0 +1,12 @@ +use std::path::PathBuf; + +pub struct K3d { +} + +impl K3d { + pub async fn download_latest_release(&self) -> Result { + + } +} + + From 15785dd2190ab419cb6c06f615ad62d58628d742 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Fri, 18 Apr 2025 23:22:37 -0400 Subject: [PATCH 31/62] feat: download and install k3d latest release - Implemented functionality to fetch the latest k3d release tag from GitHub. - Added logic to determine the appropriate binary URL based on the current platform. - Implemented downloading and saving the binary to a specified directory. - Included unit tests to verify the download and installation process. - Added a `K3D_BIN_FILE_NAME` constant for clarity. - Added logging for better debugging. --- Cargo.lock | 232 ++++++++++++++++++++++++++++++-- Cargo.toml | 2 +- k3d/Cargo.toml | 13 +- k3d/src/downloadable_asset.rs | 243 ++++++++++++++++++++++++++++++++++ k3d/src/lib.rs | 154 ++++++++++++++++++++- 5 files changed, 631 insertions(+), 13 deletions(-) create mode 100644 k3d/src/downloadable_asset.rs diff --git a/Cargo.lock b/Cargo.lock index 554bcd2..d09e201 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,6 +184,12 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.4.0" @@ -1293,6 +1299,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "harmony" version = "0.1.0" @@ -1310,7 +1335,7 @@ dependencies = [ "log", "opnsense-config", "opnsense-config-xml", - "reqwest", + "reqwest 0.11.27", "russh", "rust-ipmi", "semver", @@ -1529,7 +1554,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -1552,6 +1577,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.9", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -1627,6 +1653,22 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.11" @@ -1983,10 +2025,15 @@ version = "0.1.0" dependencies = [ "async-trait", "env_logger", + "futures-util", "log", "octocrab", "pretty_assertions", + "regex", + "reqwest 0.12.15", + "sha2", "tokio", + "url", ] [[package]] @@ -2095,7 +2142,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7f0a8985e53d18c60dc82e7b5fa512fd194ea4c0d8bf1409b65cf44f8b0a8d9" dependencies = [ "log", - "reqwest", + "reqwest 0.11.27", "serde", "serde_derive", "serde_json", @@ -2976,11 +3023,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", - "hyper-tls", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -2994,7 +3041,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", "tokio-native-tls", "tower-service", @@ -3005,6 +3052,52 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.9", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls", + "hyper-tls 0.6.0", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 2.2.0", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "system-configuration 0.6.1", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -3780,6 +3873,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -3800,7 +3896,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.0", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", ] [[package]] @@ -3813,6 +3920,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -4378,6 +4495,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -4431,7 +4561,7 @@ dependencies = [ "windows-interface", "windows-link", "windows-result", - "windows-strings", + "windows-strings 0.4.0", ] [[package]] @@ -4462,6 +4592,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings 0.3.1", + "windows-targets 0.53.0", +] + [[package]] name = "windows-result" version = "0.3.2" @@ -4471,6 +4612,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-strings" version = "0.4.0" @@ -4531,13 +4681,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4550,6 +4716,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4562,6 +4734,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4574,12 +4752,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4592,6 +4782,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4604,6 +4800,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4616,6 +4818,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4628,6 +4836,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index 1791b36..7219d71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ log = "0.4.22" env_logger = "0.11.5" derive-new = "0.7.0" async-trait = "0.1.82" -tokio = { version = "1.40.0", features = ["io-std", "fs"] } +tokio = { version = "1.40.0", features = ["io-std", "fs", "macros", "rt-multi-thread"] } cidr = "0.2.3" russh = "0.45.0" russh-keys = "0.45.0" diff --git a/k3d/Cargo.toml b/k3d/Cargo.toml index 8e5c781..633859d 100644 --- a/k3d/Cargo.toml +++ b/k3d/Cargo.toml @@ -15,9 +15,18 @@ env_logger = { workspace = true } async-trait = { workspace = true } tokio = { workspace = true } octocrab = "0.44.0" +regex = "1.11.1" +reqwest = { version = "0.12", features = ["stream"] } +#hyper-rustls = "0.27.5" +#hyper = { version = "1", features = [ "client" ] } +#hyper = { version = "1", features = ["full"] } +#http-body-util = "0.1" +#hyper-util = { version = "0.1", features = ["full"] } +url.workspace = true +sha2 = "0.10.8" +futures-util = "0.3.31" +#bytes = "1.10.1" #serde_json = "1.0.133" -#tokio-util = { version = "0.7.13", features = [ "codec" ] } -#tokio-stream = "0.1.17" [dev-dependencies] pretty_assertions = "1.4.1" diff --git a/k3d/src/downloadable_asset.rs b/k3d/src/downloadable_asset.rs new file mode 100644 index 0000000..cdf9bf2 --- /dev/null +++ b/k3d/src/downloadable_asset.rs @@ -0,0 +1,243 @@ +use futures_util::StreamExt; +use log::{debug, info, warn}; +use sha2::{Digest, Sha256}; +use std::io::Read; +use std::path::PathBuf; +use tokio::fs; +use tokio::fs::File; +use tokio::io::AsyncWriteExt; +use url::Url; + +#[derive(Debug)] +pub(crate) struct DownloadableAsset { + pub(crate) url: Url, + pub(crate) file_name: String, + pub(crate) checksum: String, +} + +impl DownloadableAsset { + fn verify_checksum(&self, file: PathBuf) -> bool { + if !file.exists() { + warn!("File does not exist: {:?}", file); + return false; + } + + let mut file = match std::fs::File::open(&file) { + Ok(file) => file, + Err(e) => { + warn!("Failed to open file for checksum verification: {:?}", e); + return false; + } + }; + + let mut hasher = Sha256::new(); + let mut buffer = [0; 1024 * 1024]; // 1MB buffer + + loop { + let bytes_read = match file.read(&mut buffer) { + Ok(0) => break, + Ok(n) => n, + Err(e) => { + warn!("Error reading file for checksum: {:?}", e); + return false; + } + }; + + hasher.update(&buffer[..bytes_read]); + } + + let result = hasher.finalize(); + let calculated_hash = format!("{:x}", result); + + debug!("Expected checksum: {}", self.checksum); + debug!("Calculated checksum: {}", calculated_hash); + + calculated_hash == self.checksum + } + + pub(crate) async fn download_to_path(&self, folder: PathBuf) -> Result { + if !folder.exists() { + fs::create_dir_all(&folder) + .await + .expect("Failed to create download directory"); + } + + let target_file_path = folder.join(&self.file_name); + debug!("Downloading to path: {:?}", target_file_path); + + if self.verify_checksum(target_file_path.clone()) { + debug!("File already exists with correct checksum, skipping download"); + return Ok(target_file_path); + } + + debug!("Downloading from URL: {}", self.url); + let client = reqwest::Client::new(); + let response = client + .get(self.url.clone()) + .send() + .await + .map_err(|e| format!("Failed to download file: {e}"))?; + + if !response.status().is_success() { + return Err(format!( + "Failed to download file, status: {}", + response.status() + )); + } + + let mut file = File::create(&target_file_path) + .await + .expect("Failed to create target file"); + + let mut stream = response.bytes_stream(); + while let Some(chunk_result) = stream.next().await { + let chunk = chunk_result.expect("Error while downloading file"); + file.write_all(&chunk) + .await + .expect("Failed to write data to file"); + } + + file.flush().await.expect("Failed to flush file"); + drop(file); + + if !self.verify_checksum(target_file_path.clone()) { + panic!("Downloaded file failed checksum verification"); + } + + info!( + "File downloaded and verified successfully: {}", + target_file_path.to_string_lossy() + ); + Ok(target_file_path) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use std::net::TcpListener; + use std::sync::OnceLock; + use std::thread; + + const BASE_TEST_PATH: &str = "/tmp/harmony-test-k3d-download"; + const TEST_SERVER_PORT: u16 = 18452; + const TEST_CONTENT: &str = "This is a test file."; + const TEST_CONTENT_HASH: &str = + "f29bc64a9d3732b4b9035125fdb3285f5b6455778edca72414671e0ca3b2e0de"; + + struct TestContext { + download_path: String, + domain: String, + } + + static TEST_SERVER: OnceLock<()> = OnceLock::new(); + + fn init_logs() { + let _ = env_logger::builder().try_init(); + } + + fn setup_test() -> TestContext { + init_logs(); + + TEST_SERVER.get_or_init(|| { + let listener = TcpListener::bind(format!("127.0.0.1:{}", TEST_SERVER_PORT)).unwrap(); + + thread::spawn(move || { + for stream in listener.incoming() { + thread::spawn(move || { + let mut stream = stream.expect("Stream opened correctly"); + let mut buffer = [0; 1024]; + let _ = stream.read(&mut buffer); + + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\n\r\n{}", + TEST_CONTENT.len(), + TEST_CONTENT + ); + + stream.write_all(response.as_bytes()).expect("Can write to stream"); + stream.flush().expect("Can flush stream"); + }); + } + }); + }); + + let test_id = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(); + let download_path = format!("{}/test_{}", BASE_TEST_PATH, test_id); + std::fs::create_dir_all(&download_path).unwrap(); + + assert!(wait_for_server_ready(1000), "Test server failed to start"); + + TestContext { + download_path, + domain: format!("127.0.0.1:{}", TEST_SERVER_PORT), + } + } + + fn wait_for_server_ready(timeout_ms: u64) -> bool { + let start = std::time::Instant::now(); + let timeout = std::time::Duration::from_millis(timeout_ms); + + while start.elapsed() < timeout { + if std::net::TcpStream::connect(format!("127.0.0.1:{}", TEST_SERVER_PORT)).is_ok() { + return true; + } + std::thread::sleep(std::time::Duration::from_millis(50)); + } + false + } + + #[tokio::test] + async fn test_download_to_path_success() { + let test = setup_test(); + + let asset = DownloadableAsset { + url: Url::parse(&format!("http://{}/test.txt", test.domain)).unwrap(), + file_name: "test.txt".to_string(), + checksum: TEST_CONTENT_HASH.to_string(), + }; + + let folder = PathBuf::from(&test.download_path); + let result = asset.download_to_path(folder).await.unwrap(); + + let downloaded_content = std::fs::read_to_string(result).unwrap(); + assert_eq!(downloaded_content, TEST_CONTENT); + } + + #[tokio::test] + async fn test_download_to_path_already_exists() { + let test = setup_test(); + let folder = PathBuf::from(&test.download_path); + + let asset = DownloadableAsset { + url: Url::parse(&format!("http://{}/test.txt", test.domain)).unwrap(), + file_name: "test.txt".to_string(), + checksum: TEST_CONTENT_HASH.to_string(), + }; + + let target_file_path = folder.join(&asset.file_name); + std::fs::write(&target_file_path, TEST_CONTENT).unwrap(); + + let result = asset.download_to_path(folder).await.unwrap(); + let content = std::fs::read_to_string(result).unwrap(); + assert_eq!(content, TEST_CONTENT); + } + + #[tokio::test] + async fn test_download_to_path_failure() { + let test = setup_test(); + + let asset = DownloadableAsset { + url: Url::parse("http://127.0.0.1:9999/test.txt").unwrap(), + file_name: "test.txt".to_string(), + checksum: "some_checksum".to_string(), + }; + + let result = asset.download_to_path(PathBuf::from(&test.download_path)).await; + assert!(result.is_err()); + } +} diff --git a/k3d/src/lib.rs b/k3d/src/lib.rs index 3227138..8e7fb72 100644 --- a/k3d/src/lib.rs +++ b/k3d/src/lib.rs @@ -1,12 +1,164 @@ +mod downloadable_asset; +use downloadable_asset::*; + +use log::{debug, info}; use std::path::PathBuf; +const K3D_BIN_FILE_NAME: &str = "k3d"; + pub struct K3d { + base_dir: PathBuf, } impl K3d { - pub async fn download_latest_release(&self) -> Result { + pub fn new(base_dir: PathBuf) -> Self { + Self { base_dir } + } + async fn get_binary_for_current_platform( + &self, + latest_release: octocrab::models::repos::Release, + ) -> DownloadableAsset { + let os = std::env::consts::OS; + let arch = std::env::consts::ARCH; + + debug!("Detecting platform: OS={}, ARCH={}", os, arch); + + // 2. Construct the binary name pattern based on platform + let binary_pattern = match (os, arch) { + ("linux", "x86") => "k3d-linux-386", + ("linux", "x86_64") => "k3d-linux-amd64", + ("linux", "arm") => "k3d-linux-arm", + ("linux", "aarch64") => "k3d-linux-arm64", + ("windows", "x86_64") => "k3d-windows-amd64.exe", + ("macos", "x86_64") => "k3d-darwin-amd64", + ("macos", "aarch64") => "k3d-darwin-arm64", + _ => panic!("Unsupported platform: {}-{}", os, arch), + }; + + debug!("Looking for binary matching pattern: {}", binary_pattern); + + // 3. Find the matching binary in release assets + let binary_asset = latest_release + .assets + .iter() + .find(|asset| asset.name == binary_pattern) + .unwrap_or_else(|| panic!("No matching binary found for {}", binary_pattern)); + + let binary_url = binary_asset.browser_download_url.clone(); + + // 4. Find and parse the checksums file + let checksums_asset = latest_release + .assets + .iter() + .find(|asset| asset.name == "checksums.txt") + .expect("Checksums file not found in release assets"); + + // 5. Download and parse checksums file + let checksums_url = checksums_asset.browser_download_url.clone(); + + let body = reqwest::get(checksums_url) + .await + .unwrap() + .text() + .await + .unwrap(); + println!("body: {body}"); + + // 6. Find the checksum for our binary + let checksum = body + .lines() + .find_map(|line| { + if line.ends_with(&binary_pattern) { + Some(line.split_whitespace().next().unwrap_or("").to_string()) + } else { + None + } + }) + .unwrap_or_else(|| panic!("Checksum not found for {}", binary_pattern)); + + debug!("Found binary at {} with checksum {}", binary_url, checksum); + + DownloadableAsset { + url: binary_url, + file_name: K3D_BIN_FILE_NAME.to_string(), + checksum, + } + } + + pub async fn download_latest_release(&self) -> Result { + let latest_release = self.get_latest_release_tag().await.unwrap(); + + let release_binary = self.get_binary_for_current_platform(latest_release).await; + info!("Foudn K3d binary to install : {release_binary:#?}"); + release_binary.download_to_path(self.base_dir.clone()).await + } + + // TODO : Make sure this will only find actual released versions, no prereleases or test + // builds + pub async fn get_latest_release_tag(&self) -> Result { + let octo = octocrab::instance(); + let latest_release = octo + .repos("k3d-io", "k3d") + .releases() + .get_latest() + .await + .map_err(|e| e.to_string())?; + // debug!("Got k3d releases {releases:#?}"); + println!("Got k3d first releases {latest_release:#?}"); + + Ok(latest_release) } } +#[cfg(test)] +mod test { + use regex::Regex; + use std::path::PathBuf; + use crate::{K3d, K3D_BIN_FILE_NAME}; + + #[tokio::test] + async fn k3d_latest_release_should_get_latest() { + let dir = get_clean_test_directory(); + + assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), false); + + let k3d = K3d::new(dir.clone()); + let latest_release = k3d.get_latest_release_tag().await.unwrap(); + + let tag_regex = Regex::new(r"^v\d+\.\d+\.\d+$").unwrap(); + assert!(tag_regex.is_match(&latest_release.tag_name)); + assert!(!latest_release.tag_name.is_empty()); + } + + #[tokio::test] + async fn k3d_download_latest_release_should_get_latest_bin() { + let dir = get_clean_test_directory(); + + assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), false); + + let k3d = K3d::new(dir.clone()); + let bin_file_path = k3d.download_latest_release().await.unwrap(); + assert_eq!(bin_file_path, dir.join(K3D_BIN_FILE_NAME)); + assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), true); + } + + fn get_clean_test_directory() -> PathBuf { + let dir = PathBuf::from("/tmp/harmony-k3d-test-dir"); + + if dir.exists() { + if let Err(e) = std::fs::remove_dir_all(&dir) { + // TODO sometimes this fails because of the race when running multiple tests at + // once + panic!("Failed to clean up test directory: {}", e); + } + } + + if let Err(e) = std::fs::create_dir_all(&dir) { + panic!("Failed to create test directory: {}", e); + } + + dir + } +} From 2229e9d7afb74f216452af884ff4b13c201c33d3 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Fri, 18 Apr 2025 23:36:39 -0400 Subject: [PATCH 32/62] chore: Cargo fmt --- harmony/src/domain/topology/ha_cluster.rs | 6 ++++-- harmony/src/domain/topology/k8s_anywhere.rs | 6 +++++- harmony/src/domain/topology/localhost.rs | 4 +++- harmony/src/domain/topology/mod.rs | 4 ++-- harmony/src/modules/k3d/mod.rs | 1 - harmony/src/modules/mod.rs | 2 +- k3d/src/downloadable_asset.rs | 16 +++++++++------- 7 files changed, 24 insertions(+), 15 deletions(-) diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index d2af961..a9bde4c 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -14,10 +14,10 @@ use super::DnsServer; use super::Firewall; use super::HttpServer; use super::IpAddress; +use super::K8sclient; use super::LoadBalancer; use super::LoadBalancerService; use super::LogicalHost; -use super::K8sclient; use super::Router; use super::TftpServer; @@ -48,7 +48,9 @@ impl Topology for HAClusterTopology { todo!() } async fn ensure_ready(&self) -> Result { - todo!("ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready.") + todo!( + "ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready." + ) } } diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index f91d88b..9eb4837 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -5,7 +5,11 @@ use log::{info, warn}; use tokio::sync::OnceCell; use crate::{ - interpret::{InterpretError, Outcome}, inventory::Inventory, maestro::Maestro, modules::k3d::K3DInstallationScore, topology::LocalhostTopology + interpret::{InterpretError, Outcome}, + inventory::Inventory, + maestro::Maestro, + modules::k3d::K3DInstallationScore, + topology::LocalhostTopology, }; use super::{Topology, k8s::K8sClient}; diff --git a/harmony/src/domain/topology/localhost.rs b/harmony/src/domain/topology/localhost.rs index 19804e8..dfa1c0b 100644 --- a/harmony/src/domain/topology/localhost.rs +++ b/harmony/src/domain/topology/localhost.rs @@ -15,6 +15,8 @@ impl Topology for LocalhostTopology { } async fn ensure_ready(&self) -> Result { - Ok(Outcome::success("Localhost is Chuck Norris, always ready.".to_string())) + Ok(Outcome::success( + "Localhost is Chuck Norris, always ready.".to_string(), + )) } } diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index b3e3779..e792227 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -3,10 +3,10 @@ mod host_binding; mod http; mod k8s_anywhere; mod localhost; -pub use localhost::*; pub use k8s_anywhere::*; -mod load_balancer; +pub use localhost::*; pub mod k8s; +mod load_balancer; mod router; mod tftp; use async_trait::async_trait; diff --git a/harmony/src/modules/k3d/mod.rs b/harmony/src/modules/k3d/mod.rs index 2d243c0..0dcdd5e 100644 --- a/harmony/src/modules/k3d/mod.rs +++ b/harmony/src/modules/k3d/mod.rs @@ -1,3 +1,2 @@ mod install; pub use install::*; - diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index 9baa98a..19d88d7 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -2,10 +2,10 @@ pub mod dhcp; pub mod dns; pub mod dummy; pub mod http; +pub mod k3d; pub mod k8s; pub mod lamp; pub mod load_balancer; pub mod okd; pub mod opnsense; pub mod tftp; -pub mod k3d; diff --git a/k3d/src/downloadable_asset.rs b/k3d/src/downloadable_asset.rs index cdf9bf2..53de329 100644 --- a/k3d/src/downloadable_asset.rs +++ b/k3d/src/downloadable_asset.rs @@ -142,20 +142,20 @@ mod tests { TEST_SERVER.get_or_init(|| { let listener = TcpListener::bind(format!("127.0.0.1:{}", TEST_SERVER_PORT)).unwrap(); - + thread::spawn(move || { for stream in listener.incoming() { thread::spawn(move || { let mut stream = stream.expect("Stream opened correctly"); let mut buffer = [0; 1024]; let _ = stream.read(&mut buffer); - + let response = format!( "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\n\r\n{}", TEST_CONTENT.len(), TEST_CONTENT ); - + stream.write_all(response.as_bytes()).expect("Can write to stream"); stream.flush().expect("Can flush stream"); }); @@ -203,7 +203,7 @@ mod tests { let folder = PathBuf::from(&test.download_path); let result = asset.download_to_path(folder).await.unwrap(); - + let downloaded_content = std::fs::read_to_string(result).unwrap(); assert_eq!(downloaded_content, TEST_CONTENT); } @@ -212,7 +212,7 @@ mod tests { async fn test_download_to_path_already_exists() { let test = setup_test(); let folder = PathBuf::from(&test.download_path); - + let asset = DownloadableAsset { url: Url::parse(&format!("http://{}/test.txt", test.domain)).unwrap(), file_name: "test.txt".to_string(), @@ -230,14 +230,16 @@ mod tests { #[tokio::test] async fn test_download_to_path_failure() { let test = setup_test(); - + let asset = DownloadableAsset { url: Url::parse("http://127.0.0.1:9999/test.txt").unwrap(), file_name: "test.txt".to_string(), checksum: "some_checksum".to_string(), }; - let result = asset.download_to_path(PathBuf::from(&test.download_path)).await; + let result = asset + .download_to_path(PathBuf::from(&test.download_path)) + .await; assert!(result.is_err()); } } From 83ba0e104498befc5ca63ea77575832d5c5fd76b Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Fri, 18 Apr 2025 23:45:40 -0400 Subject: [PATCH 33/62] fix: Initialize K3DInstallationScore correctly --- harmony/src/domain/topology/k8s_anywhere.rs | 4 ++-- harmony/src/modules/k3d/install.rs | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 9eb4837..2ba5a72 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -41,8 +41,8 @@ impl K8sAnywhereTopology { async fn try_install_k3d(&self) -> Result { let maestro = Maestro::new(Inventory::autoload(), LocalhostTopology::new()); - let k3d_score = K3DInstallationScore::default(); - maestro.interpret(Box::new(k3d_score)).await; + let k3d_score = K3DInstallationScore::new(); + maestro.interpret(Box::new(k3d_score)).await?; todo!( "Create Maestro with LocalDockerTopology or something along these lines and run a K3dInstallationScore on it" ); diff --git a/harmony/src/modules/k3d/install.rs b/harmony/src/modules/k3d/install.rs index 386120f..6cd2bf9 100644 --- a/harmony/src/modules/k3d/install.rs +++ b/harmony/src/modules/k3d/install.rs @@ -5,6 +5,12 @@ use crate::{score::Score, topology::Topology}; #[derive(Debug, Clone, Serialize)] pub struct K3DInstallationScore {} +impl K3DInstallationScore { + pub fn new() -> Self { + Self {} + } +} + impl Score for K3DInstallationScore { fn create_interpret(&self) -> Box> { todo!(" From 9e456bb4f592ff3880870e2522bbe153c009ea83 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Sat, 19 Apr 2025 15:06:05 -0400 Subject: [PATCH 34/62] chore: Refactor DownloadableAsset tests to use httptest instead of a local TcpListener --- Cargo.lock | 41 +++++++ k3d/Cargo.toml | 14 +-- k3d/src/downloadable_asset.rs | 208 ++++++++++++++++++++++------------ 3 files changed, 176 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d09e201..00a9b0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -568,6 +568,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crossterm" version = "0.25.0" @@ -1544,6 +1559,30 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "httptest" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde82de3ef9bd882493c6a5edbc3363ad928925b30ccecc0f2ddeb42601b3021" +dependencies = [ + "bstr", + "bytes", + "crossbeam-channel", + "form_urlencoded", + "futures", + "http 1.3.1", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", +] + [[package]] name = "hyper" version = "0.14.32" @@ -1581,6 +1620,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -2026,6 +2066,7 @@ dependencies = [ "async-trait", "env_logger", "futures-util", + "httptest", "log", "octocrab", "pretty_assertions", diff --git a/k3d/Cargo.toml b/k3d/Cargo.toml index 633859d..1124d75 100644 --- a/k3d/Cargo.toml +++ b/k3d/Cargo.toml @@ -6,27 +6,17 @@ readme.workspace = true license.workspace = true [dependencies] -#serde = { version = "1.0.123", features = [ "derive" ] } log = { workspace = true } -env_logger = { workspace = true } -#russh = { workspace = true } -#russh-keys = { workspace = true } -#thiserror = "1.0" async-trait = { workspace = true } tokio = { workspace = true } octocrab = "0.44.0" regex = "1.11.1" reqwest = { version = "0.12", features = ["stream"] } -#hyper-rustls = "0.27.5" -#hyper = { version = "1", features = [ "client" ] } -#hyper = { version = "1", features = ["full"] } -#http-body-util = "0.1" -#hyper-util = { version = "0.1", features = ["full"] } url.workspace = true sha2 = "0.10.8" futures-util = "0.3.31" -#bytes = "1.10.1" -#serde_json = "1.0.133" [dev-dependencies] +env_logger = { workspace = true } +httptest = "0.16.3" pretty_assertions = "1.4.1" diff --git a/k3d/src/downloadable_asset.rs b/k3d/src/downloadable_asset.rs index 53de329..ababc77 100644 --- a/k3d/src/downloadable_asset.rs +++ b/k3d/src/downloadable_asset.rs @@ -8,6 +8,33 @@ use tokio::fs::File; use tokio::io::AsyncWriteExt; use url::Url; +const CHECKSUM_FAILED_MSG: &str = "Downloaded file failed checksum verification"; + +/// Represents an asset that can be downloaded from a URL with checksum verification. +/// +/// This struct facilitates secure downloading of files from remote URLs by +/// verifying the integrity of the downloaded content using SHA-256 checksums. +/// It handles downloading the file, saving it to disk, and verifying the checksum matches +/// the expected value. +/// +/// # Examples +/// +/// ```compile_fail +/// # use url::Url; +/// # use std::path::PathBuf; +/// +/// # async fn example() -> Result<(), String> { +/// let asset = DownloadableAsset { +/// url: Url::parse("https://example.com/file.zip").unwrap(), +/// file_name: "file.zip".to_string(), +/// checksum: "a1b2c3d4e5f6...".to_string(), +/// }; +/// +/// let download_dir = PathBuf::from("/tmp/downloads"); +/// let file_path = asset.download_to_path(download_dir).await?; +/// # Ok(()) +/// # } +/// ``` #[derive(Debug)] pub(crate) struct DownloadableAsset { pub(crate) url: Url, @@ -55,6 +82,30 @@ impl DownloadableAsset { calculated_hash == self.checksum } + /// Downloads the asset to the specified directory, verifying its checksum. + /// + /// This function will: + /// 1. Create the target directory if it doesn't exist + /// 2. Check if the file already exists with the correct checksum + /// 3. If not, download the file from the URL + /// 4. Verify the downloaded file's checksum matches the expected value + /// + /// # Arguments + /// + /// * `folder` - The directory path where the file should be saved + /// + /// # Returns + /// + /// * `Ok(PathBuf)` - The path to the downloaded file on success + /// * `Err(String)` - A descriptive error message if the download or verification fails + /// + /// # Errors + /// + /// This function will return an error if: + /// - The network request fails + /// - The server responds with a non-success status code + /// - Writing to disk fails + /// - The checksum verification fails pub(crate) async fn download_to_path(&self, folder: PathBuf) -> Result { if !folder.exists() { fs::create_dir_all(&folder) @@ -101,7 +152,7 @@ impl DownloadableAsset { drop(file); if !self.verify_checksum(target_file_path.clone()) { - panic!("Downloaded file failed checksum verification"); + return Err(CHECKSUM_FAILED_MSG.to_string()); } info!( @@ -115,54 +166,20 @@ impl DownloadableAsset { #[cfg(test)] mod tests { use super::*; - use std::io::Write; - use std::net::TcpListener; - use std::sync::OnceLock; - use std::thread; + use httptest::{ + matchers::{self, request}, + responders, Expectation, Server, + }; const BASE_TEST_PATH: &str = "/tmp/harmony-test-k3d-download"; - const TEST_SERVER_PORT: u16 = 18452; const TEST_CONTENT: &str = "This is a test file."; const TEST_CONTENT_HASH: &str = "f29bc64a9d3732b4b9035125fdb3285f5b6455778edca72414671e0ca3b2e0de"; - struct TestContext { - download_path: String, - domain: String, - } - - static TEST_SERVER: OnceLock<()> = OnceLock::new(); - - fn init_logs() { + fn setup_test() -> (PathBuf, Server) { let _ = env_logger::builder().try_init(); - } - - fn setup_test() -> TestContext { - init_logs(); - - TEST_SERVER.get_or_init(|| { - let listener = TcpListener::bind(format!("127.0.0.1:{}", TEST_SERVER_PORT)).unwrap(); - - thread::spawn(move || { - for stream in listener.incoming() { - thread::spawn(move || { - let mut stream = stream.expect("Stream opened correctly"); - let mut buffer = [0; 1024]; - let _ = stream.read(&mut buffer); - - let response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\n\r\n{}", - TEST_CONTENT.len(), - TEST_CONTENT - ); - - stream.write_all(response.as_bytes()).expect("Can write to stream"); - stream.flush().expect("Can flush stream"); - }); - } - }); - }); + // Create unique test directory let test_id = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() @@ -170,51 +187,44 @@ mod tests { let download_path = format!("{}/test_{}", BASE_TEST_PATH, test_id); std::fs::create_dir_all(&download_path).unwrap(); - assert!(wait_for_server_ready(1000), "Test server failed to start"); - - TestContext { - download_path, - domain: format!("127.0.0.1:{}", TEST_SERVER_PORT), - } - } - - fn wait_for_server_ready(timeout_ms: u64) -> bool { - let start = std::time::Instant::now(); - let timeout = std::time::Duration::from_millis(timeout_ms); - - while start.elapsed() < timeout { - if std::net::TcpStream::connect(format!("127.0.0.1:{}", TEST_SERVER_PORT)).is_ok() { - return true; - } - std::thread::sleep(std::time::Duration::from_millis(50)); - } - false + (PathBuf::from(download_path), Server::run()) } #[tokio::test] async fn test_download_to_path_success() { - let test = setup_test(); + let (folder, server) = setup_test(); + + server.expect( + Expectation::matching(request::method_path("GET", "/test.txt")) + .respond_with(responders::status_code(200).body(TEST_CONTENT)), + ); let asset = DownloadableAsset { - url: Url::parse(&format!("http://{}/test.txt", test.domain)).unwrap(), + url: Url::parse(&server.url("/test.txt").to_string()).unwrap(), file_name: "test.txt".to_string(), checksum: TEST_CONTENT_HASH.to_string(), }; - let folder = PathBuf::from(&test.download_path); - let result = asset.download_to_path(folder).await.unwrap(); - + let result = asset + .download_to_path(folder.join("success")) + .await + .unwrap(); let downloaded_content = std::fs::read_to_string(result).unwrap(); assert_eq!(downloaded_content, TEST_CONTENT); } #[tokio::test] async fn test_download_to_path_already_exists() { - let test = setup_test(); - let folder = PathBuf::from(&test.download_path); + let (folder, server) = setup_test(); + + server.expect( + Expectation::matching(matchers::any()) + .times(0) + .respond_with(responders::status_code(200).body(TEST_CONTENT)), + ); let asset = DownloadableAsset { - url: Url::parse(&format!("http://{}/test.txt", test.domain)).unwrap(), + url: Url::parse(&server.url("/test.txt").to_string()).unwrap(), file_name: "test.txt".to_string(), checksum: TEST_CONTENT_HASH.to_string(), }; @@ -228,18 +238,66 @@ mod tests { } #[tokio::test] - async fn test_download_to_path_failure() { - let test = setup_test(); + async fn test_download_to_path_server_error() { + let (folder, server) = setup_test(); + + server.expect( + Expectation::matching(matchers::any()).respond_with(responders::status_code(404)), + ); let asset = DownloadableAsset { - url: Url::parse("http://127.0.0.1:9999/test.txt").unwrap(), + url: Url::parse(&server.url("/test.txt").to_string()).unwrap(), file_name: "test.txt".to_string(), - checksum: "some_checksum".to_string(), + checksum: TEST_CONTENT_HASH.to_string(), }; - let result = asset - .download_to_path(PathBuf::from(&test.download_path)) - .await; + let result = asset.download_to_path(folder.join("error")).await; assert!(result.is_err()); + assert!(result.unwrap_err().contains("status: 404")); + } + + #[tokio::test] + async fn test_download_to_path_checksum_failure() { + let (folder, server) = setup_test(); + + let invalid_content = "This is NOT the expected content"; + server.expect( + Expectation::matching(matchers::any()) + .respond_with(responders::status_code(200).body(invalid_content)), + ); + + let asset = DownloadableAsset { + url: Url::parse(&server.url("/test.txt").to_string()).unwrap(), + file_name: "test.txt".to_string(), + checksum: TEST_CONTENT_HASH.to_string(), + }; + + let join_handle = + tokio::spawn(async move { asset.download_to_path(folder.join("failure")).await }); + + assert_eq!( + join_handle.await.unwrap().err().unwrap(), + CHECKSUM_FAILED_MSG + ); + } + + #[tokio::test] + async fn test_download_with_specific_path_matcher() { + let (folder, server) = setup_test(); + + server.expect( + Expectation::matching(matchers::request::path("/specific/path.txt")) + .respond_with(responders::status_code(200).body(TEST_CONTENT)), + ); + + let asset = DownloadableAsset { + url: Url::parse(&server.url("/specific/path.txt").to_string()).unwrap(), + file_name: "path.txt".to_string(), + checksum: TEST_CONTENT_HASH.to_string(), + }; + + let result = asset.download_to_path(folder).await.unwrap(); + let downloaded_content = std::fs::read_to_string(result).unwrap(); + assert_eq!(downloaded_content, TEST_CONTENT); } } From 452ebc261481691771641e289f456397c3f6158f Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Sat, 19 Apr 2025 15:25:16 -0400 Subject: [PATCH 35/62] feat: add k3d installation interpret Adds a new interpret for k3d installation. This includes defining the `K3dInstallationInterpret` struct, implementing the `Interpret` trait for it, and adding the `K3dInstallation` variant to the `InterpretName` enum. The implementation currently contains `todo!()` placeholders for the actual logic. --- harmony/src/domain/interpret/mod.rs | 2 ++ harmony/src/modules/k3d/install.rs | 35 ++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/harmony/src/domain/interpret/mod.rs b/harmony/src/domain/interpret/mod.rs index 9cec988..0812ced 100644 --- a/harmony/src/domain/interpret/mod.rs +++ b/harmony/src/domain/interpret/mod.rs @@ -19,6 +19,7 @@ pub enum InterpretName { Dummy, Panic, OPNSense, + K3dInstallation, } impl std::fmt::Display for InterpretName { @@ -32,6 +33,7 @@ impl std::fmt::Display for InterpretName { InterpretName::Dummy => f.write_str("Dummy"), InterpretName::Panic => f.write_str("Panic"), InterpretName::OPNSense => f.write_str("OPNSense"), + InterpretName::K3dInstallation => f.write_str("K3dInstallation"), } } } diff --git a/harmony/src/modules/k3d/install.rs b/harmony/src/modules/k3d/install.rs index 6cd2bf9..0e0cec8 100644 --- a/harmony/src/modules/k3d/install.rs +++ b/harmony/src/modules/k3d/install.rs @@ -1,6 +1,13 @@ +use async_trait::async_trait; use serde::Serialize; -use crate::{score::Score, topology::Topology}; +use crate::{ + data::{Id, Version}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + score::Score, + topology::Topology, +}; #[derive(Debug, Clone, Serialize)] pub struct K3DInstallationScore {} @@ -29,3 +36,29 @@ impl Score for K3DInstallationScore { todo!() } } + +#[derive(Debug)] +struct K3dInstallationInterpret {} + +#[async_trait] +impl Interpret for K3dInstallationInterpret { + async fn execute( + &self, + inventory: &Inventory, + topology: &T, + ) -> Result { + todo!() + } + fn get_name(&self) -> InterpretName { + InterpretName::K3dInstallation + } + fn get_version(&self) -> Version { + todo!() + } + fn get_status(&self) -> InterpretStatus { + todo!() + } + fn get_children(&self) -> Vec { + todo!() + } +} From 0857aba039992437a687d14b82b03dd699d4bc1d Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 23 Apr 2025 10:15:51 -0400 Subject: [PATCH 36/62] Switch HAClusterTopology for K8sAnywhereTopology in lamp example --- examples/lamp/src/main.rs | 7 ++----- harmony_tui/src/lib.rs | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index feb8b4f..0887d04 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -1,9 +1,8 @@ use harmony::{ data::Version, - inventory::Inventory, maestro::Maestro, modules::lamp::{LAMPConfig, LAMPScore}, - topology::{HAClusterTopology, Url}, + topology::{K8sAnywhereTopology, Url}, }; #[tokio::main] @@ -18,9 +17,7 @@ async fn main() { }, }; - let inventory = Inventory::autoload(); - let topology = HAClusterTopology::autoload(); - let mut maestro = Maestro::new(inventory, topology); + let maestro = Maestro::::load_from_env(); maestro.register_all(vec![Box::new(lamp_stack)]); harmony_tui::init(maestro).await.unwrap(); } diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index c3a8a1a..908b057 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -51,7 +51,7 @@ pub mod tui { /// harmony_tui::init(maestro).await.unwrap(); /// } /// ``` -pub async fn init( +pub async fn init( maestro: Maestro, ) -> Result<(), Box> { HarmonyTUI::new(maestro).init().await @@ -63,12 +63,21 @@ pub struct HarmonyTUI { tui_state: TuiWidgetState, } -#[derive(Debug)] enum HarmonyTuiEvent { LaunchScore(Box>), } -impl HarmonyTUI { +impl std::fmt::Display for HarmonyTuiEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let output = match self { + HarmonyTuiEvent::LaunchScore(score) => format!("LaunchScore({})",score.name()), + }; + + f.write_str(&output) + } +} + +impl HarmonyTUI { pub fn new(maestro: Maestro) -> Self { let maestro = Arc::new(maestro); let (_handle, sender) = Self::start_channel(maestro.clone()); @@ -91,7 +100,7 @@ impl HarmonyTUI { let handle = tokio::spawn(async move { info!("Starting message channel receiver loop"); while let Some(event) = receiver.recv().await { - info!("Received event {event:#?}"); + info!("Received event {event}"); match event { HarmonyTuiEvent::LaunchScore(score_item) => { let maestro = maestro.clone(); From 45668638e1339ec2a9dd26ac9c5d15ec8f3cbf3c Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 23 Apr 2025 11:16:33 -0400 Subject: [PATCH 37/62] feat: TUI does not require Topology to implement Debug anymore --- examples/lamp/src/main.rs | 2 +- examples/tui/src/main.rs | 1 - harmony_tui/src/lib.rs | 4 ++-- harmony_tui/src/widget/score.rs | 16 ++++++++++++---- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index 0887d04..3075e31 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -17,7 +17,7 @@ async fn main() { }, }; - let maestro = Maestro::::load_from_env(); + let mut maestro = Maestro::::load_from_env(); maestro.register_all(vec![Box::new(lamp_stack)]); harmony_tui::init(maestro).await.unwrap(); } diff --git a/examples/tui/src/main.rs b/examples/tui/src/main.rs index 5ed607a..72f9aab 100644 --- a/examples/tui/src/main.rs +++ b/examples/tui/src/main.rs @@ -22,7 +22,6 @@ async fn main() { let topology = HAClusterTopology::autoload(); let mut maestro = Maestro::new(inventory, topology); - maestro.register_all(vec![ Box::new(SuccessScore {}), Box::new(ErrorScore {}), diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index 908b057..180d608 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -67,10 +67,10 @@ enum HarmonyTuiEvent { LaunchScore(Box>), } -impl std::fmt::Display for HarmonyTuiEvent { +impl std::fmt::Display for HarmonyTuiEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let output = match self { - HarmonyTuiEvent::LaunchScore(score) => format!("LaunchScore({})",score.name()), + HarmonyTuiEvent::LaunchScore(score) => format!("LaunchScore({})", score.name()), }; f.write_str(&output) diff --git a/harmony_tui/src/widget/score.rs b/harmony_tui/src/widget/score.rs index 3acb5c2..57a93fe 100644 --- a/harmony_tui/src/widget/score.rs +++ b/harmony_tui/src/widget/score.rs @@ -19,13 +19,21 @@ enum ExecutionState { CANCELED, } -#[derive(Debug)] struct Execution { state: ExecutionState, score: Box>, } -#[derive(Debug)] +impl std::fmt::Display for Execution { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "Execution of {} status {:?}", + self.score.name(), + self.state + )) + } +} + pub(crate) struct ScoreListWidget { list_state: Arc>, scores: Vec>>, @@ -34,7 +42,7 @@ pub(crate) struct ScoreListWidget { sender: mpsc::Sender>, } -impl ScoreListWidget { +impl ScoreListWidget { pub(crate) fn new( scores: Vec>>, sender: mpsc::Sender>, @@ -99,7 +107,7 @@ impl ScoreListWidget { match confirm { true => { execution.state = ExecutionState::RUNNING; - info!("Launch execution {:?}", execution); + info!("Launch execution {execution}"); self.sender .send(HarmonyTuiEvent::LaunchScore(execution.score.clone_box())) .await From 213fb25686ac0aeb8a4f29c24897285d33f78e46 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 23 Apr 2025 11:56:55 -0400 Subject: [PATCH 38/62] feat: Use inquire::Confirm instead of raw std::io::Read for K8sAnywhere installation confirmation prompt --- Cargo.lock | 1 + Cargo.toml | 1 + harmony/Cargo.toml | 1 + harmony/src/domain/topology/k8s_anywhere.rs | 17 ++++++----------- harmony_cli/Cargo.toml | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 00a9b0e..8020fb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1344,6 +1344,7 @@ dependencies = [ "harmony_macros", "harmony_types", "http 1.3.1", + "inquire", "k8s-openapi", "kube", "libredfish", diff --git a/Cargo.toml b/Cargo.toml index 7219d71..48fe426 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ k8s-openapi = { version = "0.24.0", features = ["v1_30"] } serde_yaml = "0.9.34" serde-value = "0.7.0" http = "1.2.0" +inquire = "0.7.5" [workspace.dependencies.uuid] version = "1.11.0" diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index c5348d9..a5d53d1 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -30,3 +30,4 @@ k8s-openapi = { workspace = true } serde_yaml = { workspace = true } http = { workspace = true } serde-value = { workspace = true } +inquire.workspace = true diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 2ba5a72..b1d5390 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -1,6 +1,7 @@ use std::io; use async_trait::async_trait; +use inquire::Confirm; use log::{info, warn}; use tokio::sync::OnceCell; @@ -76,18 +77,12 @@ impl K8sAnywhereTopology { info!("No kubernetes configuration found"); if !k8s_anywhere_config.autoinstall { - info!( - "Harmony autoinstallation is not activated, do you wish to launch autoinstallation? (y/N) : " - ); - let mut input = String::new(); + let confirmation = Confirm::new( "Harmony autoinstallation is not activated, do you wish to launch autoinstallation? : ") + .with_default(false) + .prompt() + .expect("Unexpected prompt error"); - io::stdin() - .read_line(&mut input) - .expect("Failed to read line"); - - let input = input.trim(); - - if !input.eq_ignore_ascii_case("y") { + if !confirmation { warn!( "Installation cancelled, K8sAnywhere could not initialize a valid Kubernetes client" ); diff --git a/harmony_cli/Cargo.toml b/harmony_cli/Cargo.toml index ad681bd..9d92103 100644 --- a/harmony_cli/Cargo.toml +++ b/harmony_cli/Cargo.toml @@ -10,7 +10,7 @@ assert_cmd = "2.0.17" clap = { version = "4.5.35", features = ["derive"] } harmony = { path = "../harmony" } harmony_tui = { path = "../harmony_tui", optional = true } -inquire = "0.7.5" +inquire.workspace = true tokio.workspace = true From 53aa47f91ed4de659fbd75a33143a946f33e95b1 Mon Sep 17 00:00:00 2001 From: Taha Hawa Date: Wed, 23 Apr 2025 18:22:27 +0000 Subject: [PATCH 39/62] feat: Initial helm score using helm-wrapper-rs (#14) Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/14 Co-authored-by: Taha Hawa Co-committed-by: Taha Hawa --- Cargo.lock | 24 ++++++ examples/cli/src/main.rs | 18 ---- harmony/Cargo.toml | 4 +- harmony/src/domain/topology/helm_command.rs | 1 + harmony/src/domain/topology/mod.rs | 3 + harmony/src/infra/opnsense/load_balancer.rs | 34 +++++--- harmony/src/modules/helm/chart.rs | 96 +++++++++++++++++++++ harmony/src/modules/helm/mod.rs | 1 + harmony/src/modules/mod.rs | 1 + 9 files changed, 149 insertions(+), 33 deletions(-) create mode 100644 harmony/src/domain/topology/helm_command.rs create mode 100644 harmony/src/modules/helm/chart.rs create mode 100644 harmony/src/modules/helm/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 8020fb8..e4688ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1343,12 +1343,14 @@ dependencies = [ "env_logger", "harmony_macros", "harmony_types", + "helm-wrapper-rs", "http 1.3.1", "inquire", "k8s-openapi", "kube", "libredfish", "log", + "non-blank-string-rs", "opnsense-config", "opnsense-config-xml", "reqwest 0.11.27", @@ -1453,6 +1455,19 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "helm-wrapper-rs" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc9253a7bbf4ba8ff6052d5ab7ddc6e2ca17cd8481d15636fb9f64611653880c" +dependencies = [ + "log", + "non-blank-string-rs", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "hermit-abi" version = "0.3.9" @@ -2328,6 +2343,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "non-blank-string-rs" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a05a02248b2e70f1943a59af287a28df78ef9adfc72ee5dc443381d3a1a1a5c" +dependencies = [ + "serde", +] + [[package]] name = "num-bigint" version = "0.4.6" diff --git a/examples/cli/src/main.rs b/examples/cli/src/main.rs index 8689b02..256d055 100644 --- a/examples/cli/src/main.rs +++ b/examples/cli/src/main.rs @@ -18,21 +18,3 @@ async fn main() { ]); harmony_cli::init(maestro, None).await.unwrap(); } - -use assert_cmd::Command; - -#[test] -fn test_example_success() { - let mut cmd = Command::cargo_bin("example-cli").unwrap(); - let assert = cmd.args(&["--yes", "--filter", "SuccessScore"]).assert(); - - assert.success(); -} - -#[test] -fn test_example_fail() { - let mut cmd_fail = Command::cargo_bin("example-cli").unwrap(); - let assert_fail = cmd_fail.args(&["--yes", "--filter", "ErrorScore"]).assert(); - - assert_fail.failure(); -} diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index a5d53d1..aae188d 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -7,7 +7,7 @@ license.workspace = true [dependencies] libredfish = "0.1.1" -reqwest = {version = "0.11", features = ["blocking", "json"] } +reqwest = { version = "0.11", features = ["blocking", "json"] } russh = "0.45.0" rust-ipmi = "0.1.1" semver = "1.0.23" @@ -31,3 +31,5 @@ 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" diff --git a/harmony/src/domain/topology/helm_command.rs b/harmony/src/domain/topology/helm_command.rs new file mode 100644 index 0000000..f3dd697 --- /dev/null +++ b/harmony/src/domain/topology/helm_command.rs @@ -0,0 +1 @@ +pub trait HelmCommand {} diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index e792227..3d773ff 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -20,6 +20,9 @@ pub use network::*; use serde::Serialize; pub use tftp::*; +mod helm_command; +pub use helm_command::*; + use std::net::IpAddr; use super::interpret::{InterpretError, Outcome}; diff --git a/harmony/src/infra/opnsense/load_balancer.rs b/harmony/src/infra/opnsense/load_balancer.rs index dd32a03..cae414a 100644 --- a/harmony/src/infra/opnsense/load_balancer.rs +++ b/harmony/src/infra/opnsense/load_balancer.rs @@ -370,10 +370,13 @@ mod tests { let result = get_servers_for_backend(&backend, &haproxy); // Check the result - assert_eq!(result, vec![BackendServer { - address: "192.168.1.1".to_string(), - port: 80, - },]); + assert_eq!( + result, + vec![BackendServer { + address: "192.168.1.1".to_string(), + port: 80, + },] + ); } #[test] fn test_get_servers_for_backend_no_linked_servers() { @@ -430,15 +433,18 @@ mod tests { // Call the function let result = get_servers_for_backend(&backend, &haproxy); // Check the result - assert_eq!(result, vec![ - BackendServer { - address: "some-hostname.test.mcd".to_string(), - port: 80, - }, - BackendServer { - address: "192.168.1.2".to_string(), - port: 8080, - }, - ]); + assert_eq!( + result, + vec![ + BackendServer { + address: "some-hostname.test.mcd".to_string(), + port: 80, + }, + BackendServer { + address: "192.168.1.2".to_string(), + port: 8080, + }, + ] + ); } } diff --git a/harmony/src/modules/helm/chart.rs b/harmony/src/modules/helm/chart.rs new file mode 100644 index 0000000..c4dad5e --- /dev/null +++ b/harmony/src/modules/helm/chart.rs @@ -0,0 +1,96 @@ +use crate::data::{Id, Version}; +use crate::interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}; +use crate::inventory::Inventory; +use crate::score::Score; +use crate::topology::{HelmCommand, Topology}; +use async_trait::async_trait; +use helm_wrapper_rs; +use helm_wrapper_rs::blocking::{DefaultHelmExecutor, HelmExecutor}; +use non_blank_string_rs::NonBlankString; +use serde::Serialize; +use serde::de::DeserializeOwned; +use std::collections::HashMap; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize)] +pub struct HelmChartScore { + pub namespace: Option, + pub release_name: NonBlankString, + pub chart_name: NonBlankString, + pub chart_version: NonBlankString, + pub values_overrides: Option>, +} + +impl Score for HelmChartScore { + fn create_interpret(&self) -> Box> { + todo!() + } + + fn name(&self) -> String { + "HelmChartScore".to_string() + } +} + +#[derive(Debug, Serialize)] +pub struct HelmChartInterpret { + pub score: HelmChartScore, +} + +#[async_trait] +impl Interpret for HelmChartInterpret { + async fn execute( + &self, + _inventory: &Inventory, + _topology: &T, + ) -> Result { + let ns = self + .score + .namespace + .as_ref() + .unwrap_or(todo!("Get namespace from active kubernetes cluster")); + let helm_executor = DefaultHelmExecutor::new(); + let res = helm_executor.install_or_upgrade( + ns, + &self.score.release_name, + &self.score.chart_name, + Some(&self.score.chart_version), + self.score.values_overrides.as_ref(), + None, + None, + ); + let status = match res { + Ok(status) => status, + Err(err) => return Err(InterpretError::new(err.to_string())), + }; + + match status { + helm_wrapper_rs::HelmDeployStatus::Deployed => Ok(Outcome::new( + InterpretStatus::SUCCESS, + "Helm Chart deployed".to_string(), + )), + helm_wrapper_rs::HelmDeployStatus::PendingInstall => Ok(Outcome::new( + InterpretStatus::RUNNING, + "Helm Chart Pending install".to_string(), + )), + helm_wrapper_rs::HelmDeployStatus::PendingUpgrade => Ok(Outcome::new( + InterpretStatus::RUNNING, + "Helm Chart pending upgrade".to_string(), + )), + helm_wrapper_rs::HelmDeployStatus::Failed => Err(InterpretError::new( + "Failed to install helm chart".to_string(), + )), + } + } + fn get_name(&self) -> InterpretName { + todo!() + } + fn get_version(&self) -> Version { + todo!() + } + fn get_status(&self) -> InterpretStatus { + todo!() + } + fn get_children(&self) -> Vec { + todo!() + } +} diff --git a/harmony/src/modules/helm/mod.rs b/harmony/src/modules/helm/mod.rs new file mode 100644 index 0000000..831fbe5 --- /dev/null +++ b/harmony/src/modules/helm/mod.rs @@ -0,0 +1 @@ +pub mod chart; diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index 19d88d7..a578ada 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -1,6 +1,7 @@ pub mod dhcp; pub mod dns; pub mod dummy; +pub mod helm; pub mod http; pub mod k3d; pub mod k8s; From da83019d85d6182c81c6e3d8b15c496628c0c228 Mon Sep 17 00:00:00 2001 From: Taha Hawa Date: Wed, 23 Apr 2025 14:53:36 -0400 Subject: [PATCH 40/62] remove need for debug in harmony-cli --- harmony_cli/src/lib.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/harmony_cli/src/lib.rs b/harmony_cli/src/lib.rs index 3d1bd72..9ecb8fc 100644 --- a/harmony_cli/src/lib.rs +++ b/harmony_cli/src/lib.rs @@ -42,7 +42,7 @@ pub struct Args { list: bool, } -fn maestro_scores_filter( +fn maestro_scores_filter( maestro: &harmony::maestro::Maestro, all: bool, filter: Option, @@ -71,9 +71,7 @@ fn maestro_scores_filter( } // TODO: consider adding doctest for this function -fn list_scores_with_index( - scores_vec: &Vec>>, -) -> String { +fn list_scores_with_index(scores_vec: &Vec>>) -> String { let mut display_str = String::new(); for (i, s) in scores_vec.iter().enumerate() { let name = s.name(); @@ -82,7 +80,7 @@ fn list_scores_with_index return display_str; } -pub async fn init( +pub async fn init( maestro: harmony::maestro::Maestro, args_struct: Option, ) -> Result<(), Box> { @@ -293,8 +291,6 @@ mod test { let res = crate::maestro_scores_filter(&maestro, false, None, 0); - println!("{:#?}", res); - assert!(res.len() == 1); assert!( @@ -311,8 +307,6 @@ mod test { let res = crate::maestro_scores_filter(&maestro, false, None, 11); - println!("{:#?}", res); - assert!(res.len() == 0); } } From 9345e63a322af3bdf2845d9e70bae4374aed4f3c Mon Sep 17 00:00:00 2001 From: Taha Hawa Date: Wed, 23 Apr 2025 15:31:02 -0400 Subject: [PATCH 41/62] fix: couple of changes to get a test working --- harmony/src/domain/topology/localhost.rs | 5 ++++- harmony/src/modules/helm/chart.rs | 14 ++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/harmony/src/domain/topology/localhost.rs b/harmony/src/domain/topology/localhost.rs index dfa1c0b..c5dcc75 100644 --- a/harmony/src/domain/topology/localhost.rs +++ b/harmony/src/domain/topology/localhost.rs @@ -3,7 +3,7 @@ use derive_new::new; use crate::interpret::{InterpretError, Outcome}; -use super::Topology; +use super::{HelmCommand, Topology}; #[derive(new)] pub struct LocalhostTopology; @@ -20,3 +20,6 @@ impl Topology for LocalhostTopology { )) } } + +// TODO: Delete this, temp for test +impl HelmCommand for LocalhostTopology {} diff --git a/harmony/src/modules/helm/chart.rs b/harmony/src/modules/helm/chart.rs index c4dad5e..08d2980 100644 --- a/harmony/src/modules/helm/chart.rs +++ b/harmony/src/modules/helm/chart.rs @@ -17,13 +17,15 @@ pub struct HelmChartScore { pub namespace: Option, pub release_name: NonBlankString, pub chart_name: NonBlankString, - pub chart_version: NonBlankString, + pub chart_version: Option, pub values_overrides: Option>, } -impl Score for HelmChartScore { +impl Score for HelmChartScore { fn create_interpret(&self) -> Box> { - todo!() + Box::new(HelmChartInterpret { + score: self.clone(), + }) } fn name(&self) -> String { @@ -47,13 +49,13 @@ impl Interpret for HelmChartInterpret { .score .namespace .as_ref() - .unwrap_or(todo!("Get namespace from active kubernetes cluster")); + .unwrap_or_else(|| todo!("Get namespace from active kubernetes cluster")); let helm_executor = DefaultHelmExecutor::new(); let res = helm_executor.install_or_upgrade( - ns, + &ns, &self.score.release_name, &self.score.chart_name, - Some(&self.score.chart_version), + self.score.chart_version.as_ref(), self.score.values_overrides.as_ref(), None, None, From dccc9c04f5322d9b06ef978971635236e296244e Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 24 Apr 2025 10:22:53 -0400 Subject: [PATCH 42/62] chore: Fix all warnings in the project, ignore unused variables mostly --- examples/kube-rs/src/main.rs | 10 +++++----- examples/nanodc/src/main.rs | 5 +---- examples/tui/src/main.rs | 1 - harmony/src/domain/interpret/mod.rs | 1 - harmony/src/domain/score.rs | 2 +- harmony/src/domain/topology/k8s_anywhere.rs | 16 ++++++---------- harmony/src/modules/helm/chart.rs | 2 -- harmony/src/modules/k3d/install.rs | 6 +++--- harmony/src/modules/okd/load_balancer.rs | 2 +- harmony/src/modules/okd/upgrade.rs | 8 ++++---- harmony/src/modules/opnsense/shell.rs | 2 +- harmony/src/modules/opnsense/upgrade.rs | 2 +- 12 files changed, 23 insertions(+), 34 deletions(-) diff --git a/examples/kube-rs/src/main.rs b/examples/kube-rs/src/main.rs index a8cc948..7f228e7 100644 --- a/examples/kube-rs/src/main.rs +++ b/examples/kube-rs/src/main.rs @@ -4,13 +4,13 @@ use harmony_macros::yaml; use k8s_openapi::{ api::{ apps::v1::{Deployment, DeploymentSpec}, - core::v1::{Container, Node, Pod, PodSpec, PodTemplateSpec}, + core::v1::{Container, PodSpec, PodTemplateSpec}, }, apimachinery::pkg::apis::meta::v1::LabelSelector, }; use kube::{ - Api, Client, Config, ResourceExt, - api::{ListParams, ObjectMeta, PostParams}, + Api, Client, ResourceExt, + api::{ObjectMeta, PostParams}, }; #[tokio::main] @@ -42,8 +42,7 @@ async fn main() { // println!("found node {} status {:?}", n.name_any(), n.status.unwrap()) // } - let nginxdeployment = nginx_deployment_2(); - let nginxdeployment = nginx_deployment_serde(); + assert_eq!(nginx_deployment(), nginx_macro()); assert_eq!(nginx_deployment_2(), nginx_macro()); assert_eq!(nginx_deployment_serde(), nginx_macro()); let nginxdeployment = nginx_macro(); @@ -149,6 +148,7 @@ fn nginx_deployment_2() -> Deployment { deployment } + fn nginx_deployment() -> Deployment { let deployment = Deployment { metadata: ObjectMeta { diff --git a/examples/nanodc/src/main.rs b/examples/nanodc/src/main.rs index 8aad09a..5c8c179 100644 --- a/examples/nanodc/src/main.rs +++ b/examples/nanodc/src/main.rs @@ -1,10 +1,7 @@ use harmony::{ inventory::Inventory, maestro::Maestro, - modules::{ - dummy::{ErrorScore, PanicScore, SuccessScore}, - k8s::deployment::K8sDeploymentScore, - }, + modules::dummy::{ErrorScore, PanicScore, SuccessScore}, topology::HAClusterTopology, }; diff --git a/examples/tui/src/main.rs b/examples/tui/src/main.rs index 72f9aab..e107d96 100644 --- a/examples/tui/src/main.rs +++ b/examples/tui/src/main.rs @@ -7,7 +7,6 @@ use harmony::{ dns::DnsScore, dummy::{ErrorScore, PanicScore, SuccessScore}, load_balancer::LoadBalancerScore, - okd::load_balancer::OKDLoadBalancerScore, }, topology::{ BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, diff --git a/harmony/src/domain/interpret/mod.rs b/harmony/src/domain/interpret/mod.rs index 0812ced..789edc6 100644 --- a/harmony/src/domain/interpret/mod.rs +++ b/harmony/src/domain/interpret/mod.rs @@ -7,7 +7,6 @@ use super::{ data::{Id, Version}, executors::ExecutorError, inventory::Inventory, - topology::Topology, }; pub enum InterpretName { diff --git a/harmony/src/domain/score.rs b/harmony/src/domain/score.rs index a48548c..216fb23 100644 --- a/harmony/src/domain/score.rs +++ b/harmony/src/domain/score.rs @@ -82,7 +82,7 @@ where }; let formatted_val = self.format_value_as_string(v, indent + 1); - let mut lines = formatted_val.lines().map(|line| line.trim_start()); + let lines = formatted_val.lines().map(|line| line.trim_start()); let wrapped_lines: Vec<_> = lines .flat_map(|line| self.wrap_or_truncate(line.trim_start(), 48)) diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index b1d5390..405e46d 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -1,5 +1,3 @@ -use std::io; - use async_trait::async_trait; use inquire::Confirm; use log::{info, warn}; @@ -16,15 +14,13 @@ use crate::{ use super::{Topology, k8s::K8sClient}; struct K8sState { - client: K8sClient, - source: K8sSource, + _client: K8sClient, + _source: K8sSource, message: String, } enum K8sSource { - RemoteCluster, LocalK3d, - // TODO: Add variants for cloud providers like AwsEks, Gke, Aks } pub struct K8sAnywhereTopology { @@ -62,14 +58,14 @@ impl K8sAnywhereTopology { if k8s_anywhere_config.use_system_kubeconfig { match self.try_load_system_kubeconfig().await { - Some(client) => todo!(), + Some(_client) => todo!(), None => todo!(), } } if let Some(kubeconfig) = k8s_anywhere_config.kubeconfig { match self.try_load_kubeconfig(&kubeconfig).await { - Some(client) => todo!(), + Some(_client) => todo!(), None => todo!(), } } @@ -93,8 +89,8 @@ impl K8sAnywhereTopology { info!("Starting K8sAnywhere installation"); match self.try_install_k3d().await { Ok(client) => Ok(Some(K8sState { - client, - source: K8sSource::LocalK3d, + _client: client, + _source: K8sSource::LocalK3d, message: "Successfully installed K3D cluster and acquired client".to_string(), })), Err(_) => todo!(), diff --git a/harmony/src/modules/helm/chart.rs b/harmony/src/modules/helm/chart.rs index 08d2980..ea03765 100644 --- a/harmony/src/modules/helm/chart.rs +++ b/harmony/src/modules/helm/chart.rs @@ -8,9 +8,7 @@ use helm_wrapper_rs; use helm_wrapper_rs::blocking::{DefaultHelmExecutor, HelmExecutor}; use non_blank_string_rs::NonBlankString; use serde::Serialize; -use serde::de::DeserializeOwned; use std::collections::HashMap; -use std::path::PathBuf; #[derive(Debug, Clone, Serialize)] pub struct HelmChartScore { diff --git a/harmony/src/modules/k3d/install.rs b/harmony/src/modules/k3d/install.rs index 0e0cec8..5317996 100644 --- a/harmony/src/modules/k3d/install.rs +++ b/harmony/src/modules/k3d/install.rs @@ -38,14 +38,14 @@ impl Score for K3DInstallationScore { } #[derive(Debug)] -struct K3dInstallationInterpret {} +pub struct K3dInstallationInterpret {} #[async_trait] impl Interpret for K3dInstallationInterpret { async fn execute( &self, - inventory: &Inventory, - topology: &T, + _inventory: &Inventory, + _topology: &T, ) -> Result { todo!() } diff --git a/harmony/src/modules/okd/load_balancer.rs b/harmony/src/modules/okd/load_balancer.rs index 0345d46..eb1ed44 100644 --- a/harmony/src/modules/okd/load_balancer.rs +++ b/harmony/src/modules/okd/load_balancer.rs @@ -13,7 +13,7 @@ use crate::{ }; impl std::fmt::Display for OKDLoadBalancerScore { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { todo!() } } diff --git a/harmony/src/modules/okd/upgrade.rs b/harmony/src/modules/okd/upgrade.rs index a215c00..5115cfb 100644 --- a/harmony/src/modules/okd/upgrade.rs +++ b/harmony/src/modules/okd/upgrade.rs @@ -2,15 +2,15 @@ use crate::data::Version; #[derive(Debug, Clone)] pub struct OKDUpgradeScore { - current_version: Version, - target_version: Version, + _current_version: Version, + _target_version: Version, } impl OKDUpgradeScore { pub fn new() -> Self { Self { - current_version: Version::from("4.17.0-okd-scos.0").unwrap(), - target_version: Version::from("").unwrap(), + _current_version: Version::from("4.17.0-okd-scos.0").unwrap(), + _target_version: Version::from("").unwrap(), } } } diff --git a/harmony/src/modules/opnsense/shell.rs b/harmony/src/modules/opnsense/shell.rs index a35a43c..90be4e6 100644 --- a/harmony/src/modules/opnsense/shell.rs +++ b/harmony/src/modules/opnsense/shell.rs @@ -27,7 +27,7 @@ pub struct OPNsenseShellCommandScore { } impl Serialize for OPNsenseShellCommandScore { - fn serialize(&self, serializer: S) -> Result + fn serialize(&self, _serializer: S) -> Result where S: serde::Serializer, { diff --git a/harmony/src/modules/opnsense/upgrade.rs b/harmony/src/modules/opnsense/upgrade.rs index 45adf12..c54fabe 100644 --- a/harmony/src/modules/opnsense/upgrade.rs +++ b/harmony/src/modules/opnsense/upgrade.rs @@ -17,7 +17,7 @@ pub struct OPNSenseLaunchUpgrade { } impl Serialize for OPNSenseLaunchUpgrade { - fn serialize(&self, serializer: S) -> Result + fn serialize(&self, _serializer: S) -> Result where S: serde::Serializer, { From 6c06a4ae07ff5607ceb0efe5f6dba0699be911fd Mon Sep 17 00:00:00 2001 From: Willem Date: Thu, 24 Apr 2025 15:51:28 +0000 Subject: [PATCH 43/62] feat: update ensure_ready to check helm is available (#17) I want to make sure the changes I'm working on in the ensure_ready don't break anything Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/17 Reviewed-by: taha Co-authored-by: Willem Co-committed-by: Willem --- harmony/src/domain/topology/k8s_anywhere.rs | 49 ++++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 405e46d..c8912d7 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -1,3 +1,5 @@ +use std::process::Command; + use async_trait::async_trait; use inquire::Confirm; use log::{info, warn}; @@ -11,7 +13,7 @@ use crate::{ topology::LocalhostTopology, }; -use super::{Topology, k8s::K8sClient}; +use super::{HelmCommand, Topology, k8s::K8sClient}; struct K8sState { _client: K8sClient, @@ -28,6 +30,29 @@ pub struct K8sAnywhereTopology { } impl K8sAnywhereTopology { + pub fn new() -> Self { + Self { + k8s_state: OnceCell::new(), + } + } + + fn is_helm_available(&self) -> Result<(), String> { + let version_result = Command::new("helm") + .arg("version") + .output() + .map_err(|e| format!("Failed to execute 'helm -version': {}", e))?; + + if !version_result.status.success() { + return Err("Failed to run 'helm -version'".to_string()); + } + + // Print the version output + let version_output = String::from_utf8_lossy(&version_result.stdout); + println!("Helm version: {}", version_output.trim()); + + Ok(()) + } + async fn try_load_system_kubeconfig(&self) -> Option { todo!("Use kube-rs default behavior to load system kubeconfig"); } @@ -126,15 +151,25 @@ impl Topology for K8sAnywhereTopology { } async fn ensure_ready(&self) -> Result { - match self + let k8s_state = self .k8s_state .get_or_try_init(|| self.try_get_or_install_k8s_client()) - .await? - { - Some(k8s_state) => Ok(Outcome::success(k8s_state.message.clone())), - None => Err(InterpretError::new( + .await?; + + let k8s_state: &K8sState = k8s_state + .as_ref() + .ok_or(InterpretError::new( "No K8s client could be found or installed".to_string(), - )), + ))?; + + match self.is_helm_available() { + Ok(()) => Ok(Outcome::success(format!( + "{} + helm available", + k8s_state.message.clone() + ))), + Err(_) => Err(InterpretError::new("helm unavailable".to_string())), } } } + +impl HelmCommand for K8sAnywhereTopology {} From 80bdd0ee8ac57f24496642b3f519d90e4e7f458f Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 24 Apr 2025 12:58:41 -0400 Subject: [PATCH 44/62] feat: introduce Maestro::initialize function that creates the maestro instance and ensure_ready the topology as well. Also refactor all relevant examples to use this new initialize function --- Cargo.lock | 1 + examples/cli/src/main.rs | 6 ++-- examples/kube-rs/Cargo.toml | 1 + examples/kube-rs/src/main.rs | 12 ++++++++ examples/nanodc/src/main.rs | 2 +- examples/opnsense/src/main.rs | 2 +- examples/tui/src/main.rs | 7 ++--- harmony/src/domain/maestro/mod.rs | 6 ++++ harmony/src/domain/topology/ha_cluster.rs | 16 +++++++++- harmony/src/domain/topology/k8s_anywhere.rs | 2 +- harmony/src/infra/opnsense/load_balancer.rs | 34 +++++++++------------ 11 files changed, 58 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4688ec..f58898d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -961,6 +961,7 @@ dependencies = [ "harmony", "harmony_macros", "http 1.3.1", + "inquire", "k8s-openapi", "kube", "log", diff --git a/examples/cli/src/main.rs b/examples/cli/src/main.rs index 256d055..58674ff 100644 --- a/examples/cli/src/main.rs +++ b/examples/cli/src/main.rs @@ -2,14 +2,14 @@ use harmony::{ inventory::Inventory, maestro::Maestro, modules::dummy::{ErrorScore, PanicScore, SuccessScore}, - topology::HAClusterTopology, + topology::LocalhostTopology, }; #[tokio::main] async fn main() { let inventory = Inventory::autoload(); - let topology = HAClusterTopology::autoload(); - let mut maestro = Maestro::new(inventory, topology); + let topology = LocalhostTopology::new(); + let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); maestro.register_all(vec![ Box::new(SuccessScore {}), diff --git a/examples/kube-rs/Cargo.toml b/examples/kube-rs/Cargo.toml index a4d7651..34fc64c 100644 --- a/examples/kube-rs/Cargo.toml +++ b/examples/kube-rs/Cargo.toml @@ -18,3 +18,4 @@ kube = "0.98.0" k8s-openapi = { version = "0.24.0", features = [ "v1_30" ] } http = "1.2.0" serde_yaml = "0.9.34" +inquire.workspace = true diff --git a/examples/kube-rs/src/main.rs b/examples/kube-rs/src/main.rs index 7f228e7..d7d941f 100644 --- a/examples/kube-rs/src/main.rs +++ b/examples/kube-rs/src/main.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; use harmony_macros::yaml; +use inquire::Confirm; use k8s_openapi::{ api::{ apps::v1::{Deployment, DeploymentSpec}, @@ -15,6 +16,17 @@ use kube::{ #[tokio::main] async fn main() { + let confirmation = Confirm::new( + "This will install various ressources to your default kubernetes cluster. Are you sure?", + ) + .with_default(false) + .prompt() + .expect("Unexpected prompt error"); + + if !confirmation { + return; + } + let client = Client::try_default() .await .expect("Should instanciate client from defaults"); diff --git a/examples/nanodc/src/main.rs b/examples/nanodc/src/main.rs index 5c8c179..27e6849 100644 --- a/examples/nanodc/src/main.rs +++ b/examples/nanodc/src/main.rs @@ -9,7 +9,7 @@ use harmony::{ async fn main() { let inventory = Inventory::autoload(); let topology = HAClusterTopology::autoload(); - let mut maestro = Maestro::new(inventory, topology); + let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); maestro.register_all(vec![ // ADD scores : diff --git a/examples/opnsense/src/main.rs b/examples/opnsense/src/main.rs index ddf781d..8e01dc0 100644 --- a/examples/opnsense/src/main.rs +++ b/examples/opnsense/src/main.rs @@ -84,7 +84,7 @@ async fn main() { let http_score = HttpScore::new(Url::LocalFolder( "./data/watchguard/pxe-http-files".to_string(), )); - let mut maestro = Maestro::new(inventory, topology); + let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); maestro.register_all(vec![ Box::new(dns_score), Box::new(dhcp_score), diff --git a/examples/tui/src/main.rs b/examples/tui/src/main.rs index e107d96..39d5039 100644 --- a/examples/tui/src/main.rs +++ b/examples/tui/src/main.rs @@ -9,8 +9,7 @@ use harmony::{ load_balancer::LoadBalancerScore, }, topology::{ - BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode, - LoadBalancerService, + BackendServer, DummyInfra, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancerService, }, }; use harmony_macros::ipv4; @@ -18,8 +17,8 @@ use harmony_macros::ipv4; #[tokio::main] async fn main() { let inventory = Inventory::autoload(); - let topology = HAClusterTopology::autoload(); - let mut maestro = Maestro::new(inventory, topology); + let topology = DummyInfra {}; + let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); maestro.register_all(vec![ Box::new(SuccessScore {}), diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index 2bea72d..27932d2 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -28,6 +28,12 @@ impl Maestro { } } + pub async fn initialize(inventory: Inventory, topology: T) -> Result { + let instance = Self::new(inventory, topology); + instance.prepare_topology().await?; + Ok(instance) + } + /// Ensures the associated Topology is ready for operations. /// Delegates the readiness check and potential setup actions to the Topology. pub async fn prepare_topology(&self) -> Result { diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index a9bde4c..766c93c 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -1,6 +1,7 @@ use async_trait::async_trait; use harmony_macros::ip; use harmony_types::net::MacAddress; +use log::info; use crate::executors::ExecutorError; use crate::interpret::InterpretError; @@ -223,7 +224,20 @@ impl HttpServer for HAClusterTopology { } #[derive(Debug)] -struct DummyInfra; +pub struct DummyInfra; + +#[async_trait] +impl Topology for DummyInfra { + fn name(&self) -> &str { + todo!() + } + + async fn ensure_ready(&self) -> Result { + let dummy_msg = "This is a dummy infrastructure that does nothing"; + info!("{dummy_msg}"); + Ok(Outcome::success(dummy_msg.to_string())) + } +} const UNIMPLEMENTED_DUMMY_INFRA: &str = "This is a dummy infrastructure, no operation is supported"; diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index c8912d7..79beca5 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -62,7 +62,7 @@ impl K8sAnywhereTopology { } async fn try_install_k3d(&self) -> Result { - let maestro = Maestro::new(Inventory::autoload(), LocalhostTopology::new()); + let maestro = Maestro::initialize(Inventory::autoload(), LocalhostTopology::new()).await?; let k3d_score = K3DInstallationScore::new(); maestro.interpret(Box::new(k3d_score)).await?; todo!( diff --git a/harmony/src/infra/opnsense/load_balancer.rs b/harmony/src/infra/opnsense/load_balancer.rs index cae414a..dd32a03 100644 --- a/harmony/src/infra/opnsense/load_balancer.rs +++ b/harmony/src/infra/opnsense/load_balancer.rs @@ -370,13 +370,10 @@ mod tests { let result = get_servers_for_backend(&backend, &haproxy); // Check the result - assert_eq!( - result, - vec![BackendServer { - address: "192.168.1.1".to_string(), - port: 80, - },] - ); + assert_eq!(result, vec![BackendServer { + address: "192.168.1.1".to_string(), + port: 80, + },]); } #[test] fn test_get_servers_for_backend_no_linked_servers() { @@ -433,18 +430,15 @@ mod tests { // Call the function let result = get_servers_for_backend(&backend, &haproxy); // Check the result - assert_eq!( - result, - vec![ - BackendServer { - address: "some-hostname.test.mcd".to_string(), - port: 80, - }, - BackendServer { - address: "192.168.1.2".to_string(), - port: 8080, - }, - ] - ); + assert_eq!(result, vec![ + BackendServer { + address: "some-hostname.test.mcd".to_string(), + port: 80, + }, + BackendServer { + address: "192.168.1.2".to_string(), + port: 8080, + }, + ]); } } From 508b97ca7c3909b00855d749c14e12ac1309151e Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 24 Apr 2025 13:14:35 -0400 Subject: [PATCH 45/62] chore: Fix more warnings --- harmony/src/domain/score.rs | 2 +- harmony/src/domain/topology/k8s_anywhere.rs | 18 ++++++++---------- harmony_cli/src/lib.rs | 1 - private_repos/example/Cargo.toml | 1 + 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/harmony/src/domain/score.rs b/harmony/src/domain/score.rs index 216fb23..0dac87b 100644 --- a/harmony/src/domain/score.rs +++ b/harmony/src/domain/score.rs @@ -218,7 +218,7 @@ where mod tests { use super::*; use crate::modules::dns::DnsScore; - use crate::topology::{self, HAClusterTopology}; + use crate::topology::HAClusterTopology; #[test] fn test_format_values_as_string() { diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 79beca5..18a2ccc 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -151,22 +151,20 @@ impl Topology for K8sAnywhereTopology { } async fn ensure_ready(&self) -> Result { - let k8s_state = self + let k8s_state = self .k8s_state .get_or_try_init(|| self.try_get_or_install_k8s_client()) .await?; - let k8s_state: &K8sState = k8s_state - .as_ref() - .ok_or(InterpretError::new( - "No K8s client could be found or installed".to_string(), - ))?; - + let k8s_state: &K8sState = k8s_state.as_ref().ok_or(InterpretError::new( + "No K8s client could be found or installed".to_string(), + ))?; + match self.is_helm_available() { Ok(()) => Ok(Outcome::success(format!( - "{} + helm available", - k8s_state.message.clone() - ))), + "{} + helm available", + k8s_state.message.clone() + ))), Err(_) => Err(InterpretError::new("helm unavailable".to_string())), } } diff --git a/harmony_cli/src/lib.rs b/harmony_cli/src/lib.rs index 9ecb8fc..47d3aff 100644 --- a/harmony_cli/src/lib.rs +++ b/harmony_cli/src/lib.rs @@ -147,7 +147,6 @@ mod test { modules::dummy::{ErrorScore, PanicScore, SuccessScore}, topology::HAClusterTopology, }; - use harmony::{score::Score, topology::Topology}; fn init_test_maestro() -> Maestro { let inventory = Inventory::autoload(); diff --git a/private_repos/example/Cargo.toml b/private_repos/example/Cargo.toml index f5aa56f..69b6c72 100644 --- a/private_repos/example/Cargo.toml +++ b/private_repos/example/Cargo.toml @@ -1,2 +1,3 @@ [package] name = "example" +edition = "2024" From f5e3f1aaea683eb7dbf2e479c6f6aebb4e806aaa Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 24 Apr 2025 13:16:20 -0400 Subject: [PATCH 46/62] feat: Add check.sh helper script to make sure code looks OK before pushing --- check.sh | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 check.sh diff --git a/check.sh b/check.sh new file mode 100644 index 0000000..a01f875 --- /dev/null +++ b/check.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -e +cargo check --all-targets --all-features --keep-going +cargo fmt --check +cargo test From d307893f1577c99dd46c230dae15e4e8bdf532f1 Mon Sep 17 00:00:00 2001 From: Taha Hawa Date: Thu, 24 Apr 2025 18:47:47 +0000 Subject: [PATCH 47/62] fix: small-fixes (#19) Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/19 Reviewed-by: johnride Co-authored-by: Taha Hawa Co-committed-by: Taha Hawa --- Cargo.lock | 1 + harmony/src/modules/helm/chart.rs | 4 ++-- harmony_cli/Cargo.toml | 1 + harmony_cli/src/lib.rs | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f58898d..197da40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1373,6 +1373,7 @@ version = "0.1.0" dependencies = [ "assert_cmd", "clap", + "env_logger", "harmony", "harmony_tui", "inquire", diff --git a/harmony/src/modules/helm/chart.rs b/harmony/src/modules/helm/chart.rs index ea03765..35d6863 100644 --- a/harmony/src/modules/helm/chart.rs +++ b/harmony/src/modules/helm/chart.rs @@ -6,7 +6,7 @@ use crate::topology::{HelmCommand, Topology}; use async_trait::async_trait; use helm_wrapper_rs; use helm_wrapper_rs::blocking::{DefaultHelmExecutor, HelmExecutor}; -use non_blank_string_rs::NonBlankString; +pub use non_blank_string_rs::NonBlankString; use serde::Serialize; use std::collections::HashMap; @@ -27,7 +27,7 @@ impl Score for HelmChartScore { } fn name(&self) -> String { - "HelmChartScore".to_string() + format!("{} {} HelmChartScore", self.release_name, self.chart_name) } } diff --git a/harmony_cli/Cargo.toml b/harmony_cli/Cargo.toml index 9d92103..e7d7398 100644 --- a/harmony_cli/Cargo.toml +++ b/harmony_cli/Cargo.toml @@ -12,6 +12,7 @@ harmony = { path = "../harmony" } harmony_tui = { path = "../harmony_tui", optional = true } inquire.workspace = true tokio.workspace = true +env_logger.workspace = true [features] diff --git a/harmony_cli/src/lib.rs b/harmony_cli/src/lib.rs index 47d3aff..a47df06 100644 --- a/harmony_cli/src/lib.rs +++ b/harmony_cli/src/lib.rs @@ -99,6 +99,8 @@ pub async fn init( return Err("Not compiled with interactive support".into()); } + env_logger::builder().init(); + let scores_vec = maestro_scores_filter(&maestro, args.all, args.filter, args.number); if scores_vec.len() == 0 { From fbcd3e4f7f2e52cbc763c26ac7fbf35017ed7c64 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 24 Apr 2025 17:36:01 -0400 Subject: [PATCH 48/62] feat: implement k3d cluster management - Adds functionality to download, install, and manage k3d clusters. - Includes methods for downloading the latest release, creating clusters, and verifying cluster existence. - Implements `ensure_k3d_installed`, `get_latest_release_tag`, `download_latest_release`, `is_k3d_installed`, `verify_cluster_exists`, `create_cluster` and `create_kubernetes_client`. - Provides a `get_client` method to access the Kubernetes client. - Includes unit tests for download and installation. - Adds handling for different operating systems. - Improves error handling and logging. - Introduces a `K3d` struct to encapsulate k3d cluster management logic. - Adds the ability to specify the cluster name during K3d initialization. --- Cargo.lock | 52 +++++ examples/lamp/src/main.rs | 8 +- harmony/Cargo.toml | 3 + harmony/src/domain/config.rs | 9 + harmony/src/domain/maestro/mod.rs | 21 -- harmony/src/domain/mod.rs | 1 + harmony/src/domain/topology/k8s.rs | 2 + harmony/src/domain/topology/k8s_anywhere.rs | 21 +- harmony/src/modules/k3d/install.rs | 50 +++-- harmony_cli/src/lib.rs | 2 +- k3d/Cargo.toml | 1 + k3d/src/lib.rs | 221 +++++++++++++++++++- 12 files changed, 335 insertions(+), 56 deletions(-) create mode 100644 harmony/src/domain/config.rs diff --git a/Cargo.lock b/Cargo.lock index 197da40..5d6c373 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -795,6 +795,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.59.0", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1341,14 +1362,17 @@ dependencies = [ "async-trait", "cidr", "derive-new", + "directories", "env_logger", "harmony_macros", "harmony_types", "helm-wrapper-rs", "http 1.3.1", "inquire", + "k3d-rs", "k8s-openapi", "kube", + "lazy_static", "libredfish", "log", "non-blank-string-rs", @@ -2085,6 +2109,7 @@ dependencies = [ "env_logger", "futures-util", "httptest", + "kube", "log", "octocrab", "pretty_assertions", @@ -2207,6 +2232,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.0", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -2572,6 +2607,12 @@ dependencies = [ "yaserde_derive", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-float" version = "2.10.1" @@ -3051,6 +3092,17 @@ dependencies = [ "bitflags 2.9.0", ] +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 2.0.12", +] + [[package]] name = "regex" version = "1.11.1" diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index 3075e31..d070871 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -1,5 +1,6 @@ use harmony::{ data::Version, + inventory::Inventory, maestro::Maestro, modules::lamp::{LAMPConfig, LAMPScore}, topology::{K8sAnywhereTopology, Url}, @@ -17,7 +18,12 @@ async fn main() { }, }; - let mut maestro = Maestro::::load_from_env(); + let mut maestro = Maestro::::initialize( + Inventory::autoload(), + K8sAnywhereTopology::new(), + ) + .await + .unwrap(); maestro.register_all(vec![Box::new(lamp_stack)]); harmony_tui::init(maestro).await.unwrap(); } diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index aae188d..5c36335 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -33,3 +33,6 @@ serde-value = { workspace = true } inquire.workspace = true helm-wrapper-rs = "0.4.0" non-blank-string-rs = "1.0.4" +k3d-rs = { path = "../k3d" } +directories = "6.0.0" +lazy_static = "1.5.0" diff --git a/harmony/src/domain/config.rs b/harmony/src/domain/config.rs new file mode 100644 index 0000000..320e9a0 --- /dev/null +++ b/harmony/src/domain/config.rs @@ -0,0 +1,9 @@ +use lazy_static::lazy_static; +use std::path::PathBuf; + +lazy_static! { + pub static ref HARMONY_CONFIG_DIR: PathBuf = directories::BaseDirs::new() + .unwrap() + .data_dir() + .join("harmony"); +} diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index 27932d2..f53fbee 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -52,27 +52,6 @@ impl Maestro { Ok(outcome) } - // Load the inventory and inventory from environment. - // This function is able to discover the context that it is running in, such as k8s clusters, aws cloud, linux host, etc. - // When the HARMONY_TOPOLOGY environment variable is not set, it will default to install k3s - // locally (lazily, if not installed yet, when the first execution occurs) and use that as a topology - // So, by default, the inventory is a single host that the binary is running on, and the - // topology is a single node k3s - // - // By default : - // - Linux => k3s - // - macos, windows => docker compose - // - // To run more complex cases like OKDHACluster, either provide the default target in the - // harmony infrastructure as code or as an environment variable - pub fn load_from_env() -> Self { - // Load env var HARMONY_TOPOLOGY - match std::env::var("HARMONY_TOPOLOGY") { - Ok(_) => todo!(), - Err(_) => todo!(), - } - } - pub fn register_all(&mut self, mut scores: ScoreVec) { let mut score_mut = self.scores.write().expect("Should acquire lock"); score_mut.append(&mut scores); diff --git a/harmony/src/domain/mod.rs b/harmony/src/domain/mod.rs index ece55ef..349191a 100644 --- a/harmony/src/domain/mod.rs +++ b/harmony/src/domain/mod.rs @@ -1,3 +1,4 @@ +pub mod config; pub mod data; pub mod executors; pub mod filter; diff --git a/harmony/src/domain/topology/k8s.rs b/harmony/src/domain/topology/k8s.rs index ed345ee..beecbf0 100644 --- a/harmony/src/domain/topology/k8s.rs +++ b/harmony/src/domain/topology/k8s.rs @@ -1,7 +1,9 @@ +use derive_new::new; use k8s_openapi::NamespaceResourceScope; use kube::{Api, Client, Error, Resource, api::PostParams}; use serde::de::DeserializeOwned; +#[derive(new)] pub struct K8sClient { client: Client, } diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 18a2ccc..780bc92 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -61,13 +61,15 @@ impl K8sAnywhereTopology { todo!("Use kube-rs to load kubeconfig at path {path}"); } - async fn try_install_k3d(&self) -> Result { + fn get_k3d_installation_score(&self) -> K3DInstallationScore { + K3DInstallationScore::default() + } + + async fn try_install_k3d(&self) -> Result<(), InterpretError> { let maestro = Maestro::initialize(Inventory::autoload(), LocalhostTopology::new()).await?; - let k3d_score = K3DInstallationScore::new(); + let k3d_score = self.get_k3d_installation_score(); maestro.interpret(Box::new(k3d_score)).await?; - todo!( - "Create Maestro with LocalDockerTopology or something along these lines and run a K3dInstallationScore on it" - ); + Ok(()) } async fn try_get_or_install_k8s_client(&self) -> Result, InterpretError> { @@ -112,9 +114,14 @@ impl K8sAnywhereTopology { } info!("Starting K8sAnywhere installation"); - match self.try_install_k3d().await { + self.try_install_k3d().await?; + let k3d_score = self.get_k3d_installation_score(); + match k3d_rs::K3d::new(k3d_score.installation_path, Some(k3d_score.cluster_name)) + .get_client() + .await + { Ok(client) => Ok(Some(K8sState { - _client: client, + _client: K8sClient::new(client), _source: K8sSource::LocalK3d, message: "Successfully installed K3D cluster and acquired client".to_string(), })), diff --git a/harmony/src/modules/k3d/install.rs b/harmony/src/modules/k3d/install.rs index 5317996..f825f2e 100644 --- a/harmony/src/modules/k3d/install.rs +++ b/harmony/src/modules/k3d/install.rs @@ -1,7 +1,11 @@ +use std::path::PathBuf; + use async_trait::async_trait; +use log::info; use serde::Serialize; use crate::{ + config::HARMONY_CONFIG_DIR, data::{Id, Version}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, @@ -10,26 +14,25 @@ use crate::{ }; #[derive(Debug, Clone, Serialize)] -pub struct K3DInstallationScore {} +pub struct K3DInstallationScore { + pub installation_path: PathBuf, + pub cluster_name: String, +} -impl K3DInstallationScore { - pub fn new() -> Self { - Self {} +impl Default for K3DInstallationScore { + fn default() -> Self { + Self { + installation_path: HARMONY_CONFIG_DIR.join("k3d"), + cluster_name: "harmony".to_string(), + } } } impl Score for K3DInstallationScore { fn create_interpret(&self) -> Box> { - todo!(" - 1. Decide if I create a new crate for k3d management, especially to avoid the ocrtograb dependency - 2. Implement k3d management - 3. Find latest tag - 4. Download k3d to some path managed by harmony (or not?) - 5. Bootstrap cluster - 6. Get kubeconfig - 7. Load kubeconfig in k8s anywhere - 8. Complete k8sanywhere setup - ") + Box::new(K3dInstallationInterpret { + score: self.clone(), + }) } fn name(&self) -> String { @@ -38,7 +41,9 @@ impl Score for K3DInstallationScore { } #[derive(Debug)] -pub struct K3dInstallationInterpret {} +pub struct K3dInstallationInterpret { + score: K3DInstallationScore, +} #[async_trait] impl Interpret for K3dInstallationInterpret { @@ -47,7 +52,20 @@ impl Interpret for K3dInstallationInterpret { _inventory: &Inventory, _topology: &T, ) -> Result { - todo!() + let k3d = k3d_rs::K3d::new( + self.score.installation_path.clone(), + Some(self.score.cluster_name.clone()), + ); + match k3d.ensure_installed().await { + Ok(_client) => { + let msg = format!("k3d cluster {} is installed ", self.score.cluster_name); + info!("{msg}"); + Ok(Outcome::success(msg)) + } + Err(msg) => Err(InterpretError::new(format!( + "K3dInstallationInterpret failed to ensure k3d is installed : {msg}" + ))), + } } fn get_name(&self) -> InterpretName { InterpretName::K3dInstallation diff --git a/harmony_cli/src/lib.rs b/harmony_cli/src/lib.rs index a47df06..33759fa 100644 --- a/harmony_cli/src/lib.rs +++ b/harmony_cli/src/lib.rs @@ -99,7 +99,7 @@ pub async fn init( return Err("Not compiled with interactive support".into()); } - env_logger::builder().init(); + let _ = env_logger::builder().try_init(); let scores_vec = maestro_scores_filter(&maestro, args.all, args.filter, args.number); diff --git a/k3d/Cargo.toml b/k3d/Cargo.toml index 1124d75..aaa15ce 100644 --- a/k3d/Cargo.toml +++ b/k3d/Cargo.toml @@ -15,6 +15,7 @@ reqwest = { version = "0.12", features = ["stream"] } url.workspace = true sha2 = "0.10.8" futures-util = "0.3.31" +kube.workspace = true [dev-dependencies] env_logger = { workspace = true } diff --git a/k3d/src/lib.rs b/k3d/src/lib.rs index 8e7fb72..88d5963 100644 --- a/k3d/src/lib.rs +++ b/k3d/src/lib.rs @@ -1,18 +1,23 @@ mod downloadable_asset; use downloadable_asset::*; -use log::{debug, info}; +use kube::Client; +use log::{debug, info, warn}; use std::path::PathBuf; const K3D_BIN_FILE_NAME: &str = "k3d"; pub struct K3d { base_dir: PathBuf, + cluster_name: Option, } impl K3d { - pub fn new(base_dir: PathBuf) -> Self { - Self { base_dir } + pub fn new(base_dir: PathBuf, cluster_name: Option) -> Self { + Self { + base_dir, + cluster_name, + } } async fn get_binary_for_current_platform( @@ -24,7 +29,6 @@ impl K3d { debug!("Detecting platform: OS={}, ARCH={}", os, arch); - // 2. Construct the binary name pattern based on platform let binary_pattern = match (os, arch) { ("linux", "x86") => "k3d-linux-386", ("linux", "x86_64") => "k3d-linux-amd64", @@ -38,7 +42,6 @@ impl K3d { debug!("Looking for binary matching pattern: {}", binary_pattern); - // 3. Find the matching binary in release assets let binary_asset = latest_release .assets .iter() @@ -47,14 +50,12 @@ impl K3d { let binary_url = binary_asset.browser_download_url.clone(); - // 4. Find and parse the checksums file let checksums_asset = latest_release .assets .iter() .find(|asset| asset.name == "checksums.txt") .expect("Checksums file not found in release assets"); - // 5. Download and parse checksums file let checksums_url = checksums_asset.browser_download_url.clone(); let body = reqwest::get(checksums_url) @@ -65,7 +66,6 @@ impl K3d { .unwrap(); println!("body: {body}"); - // 6. Find the checksum for our binary let checksum = body .lines() .find_map(|line| { @@ -109,6 +109,207 @@ impl K3d { Ok(latest_release) } + + /// Checks if k3d binary exists and is executable + /// + /// Verifies that: + /// 1. The k3d binary exists in the base directory + /// 2. It has proper executable permissions (on Unix systems) + /// 3. It responds correctly to a simple command (`k3d --version`) + pub fn is_installed(&self) -> bool { + let binary_path = self.base_dir.join(K3D_BIN_FILE_NAME); + + if !binary_path.exists() { + debug!("K3d binary not found at {:?}", binary_path); + return false; + } + + if !self.ensure_binary_executable(&binary_path) { + return false; + } + + self.can_execute_binary_check(&binary_path) + } + + /// Verifies if the specified cluster is already running + /// + /// Executes `k3d cluster list ` and checks for a successful response, + /// indicating that the cluster exists and is registered with k3d. + pub fn is_cluster_initialized(&self) -> bool { + let cluster_name = match &self.cluster_name { + Some(name) => name, + None => { + debug!("No cluster name specified, can't verify if cluster is initialized"); + return false; + } + }; + + let binary_path = self.base_dir.join(K3D_BIN_FILE_NAME); + if !binary_path.exists() { + return false; + } + + self.verify_cluster_exists(&binary_path, cluster_name) + } + + /// Creates a new k3d cluster with the specified name + /// + /// This method: + /// 1. Creates a new k3d cluster using `k3d cluster create ` + /// 2. Waits for the cluster to initialize + /// 3. Returns a configured Kubernetes client connected to the cluster + /// + /// # Returns + /// - `Ok(Client)` - Successfully created cluster and connected client + /// - `Err(String)` - Error message detailing what went wrong + pub async fn initialize_cluster(&self) -> Result { + let cluster_name = match &self.cluster_name { + Some(name) => name, + None => return Err("No cluster name specified for initialization".to_string()), + }; + + let binary_path = self.base_dir.join(K3D_BIN_FILE_NAME); + if !binary_path.exists() { + return Err(format!("K3d binary not found at {:?}", binary_path)); + } + + info!("Initializing k3d cluster '{}'", cluster_name); + + self.create_cluster(&binary_path, cluster_name)?; + self.create_kubernetes_client().await + } + + /// Ensures k3d is installed and the cluster is initialized + /// + /// This method provides a complete setup flow: + /// 1. Checks if k3d is installed, downloads and installs it if needed + /// 2. Verifies if the specified cluster exists, creates it if not + /// 3. Returns a Kubernetes client connected to the cluster + /// + /// # Returns + /// - `Ok(Client)` - Successfully ensured k3d and cluster are ready + /// - `Err(String)` - Error message if any step failed + pub async fn ensure_installed(&self) -> Result { + if !self.is_installed() { + info!("K3d is not installed, downloading latest release"); + self.download_latest_release() + .await + .map_err(|e| format!("Failed to download k3d: {}", e))?; + + if !self.is_installed() { + return Err("Failed to install k3d properly".to_string()); + } + } + + if !self.is_cluster_initialized() { + info!("Cluster is not initialized, initializing now"); + return self.initialize_cluster().await; + } + + info!("K3d and cluster are already properly set up"); + self.create_kubernetes_client().await + } + + // Private helper methods + + #[cfg(not(target_os = "windows"))] + fn ensure_binary_executable(&self, binary_path: &PathBuf) -> bool { + use std::os::unix::fs::PermissionsExt; + + let mut perms = match std::fs::metadata(binary_path) { + Ok(metadata) => metadata.permissions(), + Err(e) => { + debug!("Failed to get binary metadata: {}", e); + return false; + } + }; + + perms.set_mode(0o755); + + if let Err(e) = std::fs::set_permissions(binary_path, perms) { + debug!("Failed to set executable permissions on k3d binary: {}", e); + return false; + } + + true + } + + #[cfg(target_os = "windows")] + fn ensure_binary_executable(&self, _binary_path: &PathBuf) -> bool { + // Windows doesn't use executable file permissions + true + } + + fn can_execute_binary_check(&self, binary_path: &PathBuf) -> bool { + match std::process::Command::new(binary_path) + .arg("--version") + .output() + { + Ok(output) => { + if output.status.success() { + debug!("K3d binary is installed and working"); + true + } else { + debug!("K3d binary check failed: {:?}", output); + false + } + } + Err(e) => { + debug!("Failed to execute K3d binary: {}", e); + false + } + } + } + + fn verify_cluster_exists(&self, binary_path: &PathBuf, cluster_name: &str) -> bool { + match std::process::Command::new(binary_path) + .args(["cluster", "list", cluster_name, "--no-headers"]) + .output() + { + Ok(output) => { + if output.status.success() && !output.stdout.is_empty() { + debug!("Cluster '{}' is initialized", cluster_name); + true + } else { + debug!("Cluster '{}' is not initialized", cluster_name); + false + } + } + Err(e) => { + debug!("Failed to check cluster initialization: {}", e); + false + } + } + } + + fn create_cluster(&self, binary_path: &PathBuf, cluster_name: &str) -> Result<(), String> { + let output = std::process::Command::new(binary_path) + .args(["cluster", "create", cluster_name]) + .output() + .map_err(|e| format!("Failed to execute k3d command: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("Failed to create cluster: {}", stderr)); + } + + info!("Successfully created k3d cluster '{}'", cluster_name); + Ok(()) + } + + async fn create_kubernetes_client(&self) -> Result { + warn!("TODO this method is way too dumb, it should make sure that the client is connected to the k3d cluster actually represented by this instance, not just any default client"); + Client::try_default() + .await + .map_err(|e| format!("Failed to create Kubernetes client: {}", e)) + } + + pub async fn get_client(&self) -> Result { + match self.is_cluster_initialized() { + true => Ok(self.create_kubernetes_client().await?), + false => Err("Cannot get client! Cluster not initialized yet".to_string()), + } + } } #[cfg(test)] @@ -124,7 +325,7 @@ mod test { assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), false); - let k3d = K3d::new(dir.clone()); + let k3d = K3d::new(dir.clone(), None); let latest_release = k3d.get_latest_release_tag().await.unwrap(); let tag_regex = Regex::new(r"^v\d+\.\d+\.\d+$").unwrap(); @@ -138,7 +339,7 @@ mod test { assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), false); - let k3d = K3d::new(dir.clone()); + let k3d = K3d::new(dir.clone(), None); let bin_file_path = k3d.download_latest_release().await.unwrap(); assert_eq!(bin_file_path, dir.join(K3D_BIN_FILE_NAME)); assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), true); From 22752960f9d2dc7c1816c46a9558f30ce2f2bed6 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Fri, 25 Apr 2025 11:32:02 -0400 Subject: [PATCH 49/62] fix(k8s_anywhere): Ensure k3d cluster is started before use - Refactor k3d cluster management to explicitly start the cluster. - Introduce `start_cluster` function to ensure cluster is running before operations. - Improve error handling and logging during cluster startup. - Update `create_cluster` and other related functions to utilize the new startup mechanism. - Enhance reliability and prevent potential issues caused by an uninitialized cluster. - Add `run_k3d_command` to handle k3d commands with logging and error handling. --- examples/lamp/src/main.rs | 1 + harmony/src/domain/topology/k8s_anywhere.rs | 21 +++-- k3d/src/lib.rs | 85 ++++++++++++++++----- opnsense-config/src/config/config.rs | 2 +- opnsense-config/src/lib.rs | 2 +- 5 files changed, 81 insertions(+), 30 deletions(-) diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index d070871..27e5df6 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -8,6 +8,7 @@ use harmony::{ #[tokio::main] async fn main() { + // let _ = env_logger::Builder::from_default_env().filter_level(log::LevelFilter::Info).try_init(); let lamp_stack = LAMPScore { name: "harmony-lamp-demo".to_string(), domain: Url::Url(url::Url::parse("https://lampdemo.harmony.nationtech.io").unwrap()), diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 780bc92..5325915 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -116,17 +116,22 @@ impl K8sAnywhereTopology { info!("Starting K8sAnywhere installation"); self.try_install_k3d().await?; let k3d_score = self.get_k3d_installation_score(); - match k3d_rs::K3d::new(k3d_score.installation_path, Some(k3d_score.cluster_name)) - .get_client() - .await - { - Ok(client) => Ok(Some(K8sState { + // I feel like having to rely on the k3d_rs crate here is a smell + // I think we should have a way to interact more deeply with scores/interpret. Maybe the + // K3DInstallationScore should expose a method to get_client ? Not too sure what would be a + // good implementation due to the stateful nature of the k3d thing. Which is why I went + // with this solution for now + let k3d = k3d_rs::K3d::new(k3d_score.installation_path, Some(k3d_score.cluster_name)); + let state = match k3d.get_client().await { + Ok(client) => K8sState { _client: K8sClient::new(client), _source: K8sSource::LocalK3d, message: "Successfully installed K3D cluster and acquired client".to_string(), - })), + }, Err(_) => todo!(), - } + }; + + Ok(Some(state)) } } @@ -154,7 +159,7 @@ struct K8sAnywhereConfig { #[async_trait] impl Topology for K8sAnywhereTopology { fn name(&self) -> &str { - todo!() + "K8sAnywhereTopology" } async fn ensure_ready(&self) -> Result { diff --git a/k3d/src/lib.rs b/k3d/src/lib.rs index 88d5963..ff63704 100644 --- a/k3d/src/lib.rs +++ b/k3d/src/lib.rs @@ -117,7 +117,7 @@ impl K3d { /// 2. It has proper executable permissions (on Unix systems) /// 3. It responds correctly to a simple command (`k3d --version`) pub fn is_installed(&self) -> bool { - let binary_path = self.base_dir.join(K3D_BIN_FILE_NAME); + let binary_path = self.get_k3d_binary_path(); if !binary_path.exists() { debug!("K3d binary not found at {:?}", binary_path); @@ -131,15 +131,15 @@ impl K3d { self.can_execute_binary_check(&binary_path) } - /// Verifies if the specified cluster is already running + /// Verifies if the specified cluster is already created /// /// Executes `k3d cluster list ` and checks for a successful response, /// indicating that the cluster exists and is registered with k3d. pub fn is_cluster_initialized(&self) -> bool { - let cluster_name = match &self.cluster_name { - Some(name) => name, - None => { - debug!("No cluster name specified, can't verify if cluster is initialized"); + let cluster_name = match self.get_cluster_name() { + Ok(name) => name, + Err(_) => { + debug!("Could not get cluster name, can't verify if cluster is initialized"); return false; } }; @@ -152,6 +152,13 @@ impl K3d { self.verify_cluster_exists(&binary_path, cluster_name) } + fn get_cluster_name(&self) -> Result<&String, String> { + match &self.cluster_name { + Some(name) => Ok(name), + None => Err("No cluster name available".to_string()), + } + } + /// Creates a new k3d cluster with the specified name /// /// This method: @@ -163,22 +170,29 @@ impl K3d { /// - `Ok(Client)` - Successfully created cluster and connected client /// - `Err(String)` - Error message detailing what went wrong pub async fn initialize_cluster(&self) -> Result { - let cluster_name = match &self.cluster_name { - Some(name) => name, - None => return Err("No cluster name specified for initialization".to_string()), + let cluster_name = match self.get_cluster_name() { + Ok(name) => name, + Err(_) => return Err("Could not get cluster_name, cannot initialize".to_string()), }; - let binary_path = self.base_dir.join(K3D_BIN_FILE_NAME); - if !binary_path.exists() { - return Err(format!("K3d binary not found at {:?}", binary_path)); - } - info!("Initializing k3d cluster '{}'", cluster_name); - self.create_cluster(&binary_path, cluster_name)?; + self.create_cluster(cluster_name)?; self.create_kubernetes_client().await } + fn get_k3d_binary_path(&self) -> PathBuf { + self.base_dir.join(K3D_BIN_FILE_NAME) + } + + fn get_k3d_binary(&self) -> Result { + let path = self.get_k3d_binary_path(); + if !path.exists() { + return Err(format!("K3d binary not found at {:?}", path)); + } + Ok(path) + } + /// Ensures k3d is installed and the cluster is initialized /// /// This method provides a complete setup flow: @@ -206,6 +220,8 @@ impl K3d { return self.initialize_cluster().await; } + self.start_cluster().await?; + info!("K3d and cluster are already properly set up"); self.create_kubernetes_client().await } @@ -282,11 +298,27 @@ impl K3d { } } - fn create_cluster(&self, binary_path: &PathBuf, cluster_name: &str) -> Result<(), String> { - let output = std::process::Command::new(binary_path) - .args(["cluster", "create", cluster_name]) - .output() - .map_err(|e| format!("Failed to execute k3d command: {}", e))?; + pub fn run_k3d_command(&self, args: I) -> Result + where + I: IntoIterator, + S: AsRef, + { + let binary_path = self.get_k3d_binary()?; + let output = std::process::Command::new(binary_path).args(args).output(); + match output { + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + debug!("stderr : {}", stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + debug!("stdout : {}", stdout); + Ok(output) + } + Err(e) => Err(format!("Failed to execute k3d command: {}", e)), + } + } + + fn create_cluster(&self, cluster_name: &str) -> Result<(), String> { + let output = self.run_k3d_command(["cluster", "create", cluster_name])?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); @@ -310,6 +342,19 @@ impl K3d { false => Err("Cannot get client! Cluster not initialized yet".to_string()), } } + + async fn start_cluster(&self) -> Result<(), String> { + let cluster_name = self.get_cluster_name()?; + let output = self.run_k3d_command(["cluster", "start", cluster_name])?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("Failed to start cluster: {}", stderr)); + } + + info!("Successfully started k3d cluster '{}'", cluster_name); + Ok(()) + } } #[cfg(test)] diff --git a/opnsense-config/src/config/config.rs b/opnsense-config/src/config/config.rs index 10dab61..3f00c44 100644 --- a/opnsense-config/src/config/config.rs +++ b/opnsense-config/src/config/config.rs @@ -23,7 +23,7 @@ pub struct Config { } impl Serialize for Config { - fn serialize(&self, serializer: S) -> Result + fn serialize(&self, _serializer: S) -> Result where S: serde::Serializer, { diff --git a/opnsense-config/src/lib.rs b/opnsense-config/src/lib.rs index f83b3e0..a497133 100644 --- a/opnsense-config/src/lib.rs +++ b/opnsense-config/src/lib.rs @@ -10,11 +10,11 @@ mod test { use std::net::Ipv4Addr; use crate::Config; - use pretty_assertions::assert_eq; #[cfg(opnsenseendtoend)] #[tokio::test] async fn test_public_sdk() { + use pretty_assertions::assert_eq; let mac = "11:22:33:44:55:66"; let ip = Ipv4Addr::new(10, 100, 8, 200); let hostname = "test_hostname"; From 16a665241e6e98cd6a5046f42748c97a0b0f068e Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Fri, 25 Apr 2025 14:29:03 -0400 Subject: [PATCH 50/62] feat: LampScore implement dockerfile generation and image building - Added `build_dockerfile` function to generate a Dockerfile based on the LAMP stack for the given project. - Implemented `build_docker_image` to execute the docker build command and create the image. - Configured user and permissions for apache. - Included necessary apache configuration for security. - Added error handling for docker build failures. - Exposed port 80 for external access. - Added basic serialization to Config struct. --- Cargo.lock | 23 +++ harmony/Cargo.toml | 1 + harmony/src/domain/topology/ha_cluster.rs | 6 +- harmony/src/domain/topology/k8s_anywhere.rs | 25 ++- harmony/src/domain/topology/network.rs | 4 +- harmony/src/modules/lamp.rs | 178 +++++++++++++++++++- 6 files changed, 226 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d6c373..35c5d85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -833,6 +833,28 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "dockerfile_builder" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ac372e31c7dd054d0fc69ca96ca36ee8d1cf79881683ad6f783c47aba3dc6e2" +dependencies = [ + "dockerfile_builder_macros", + "eyre", +] + +[[package]] +name = "dockerfile_builder_macros" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b627d9019ce257916c7ada6f233cf22e1e5246b6d9426b20610218afb7fd3ec9" +dependencies = [ + "eyre", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dyn-clone" version = "1.0.19" @@ -1363,6 +1385,7 @@ dependencies = [ "cidr", "derive-new", "directories", + "dockerfile_builder", "env_logger", "harmony_macros", "harmony_types", diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index 5c36335..a140a8b 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -36,3 +36,4 @@ non-blank-string-rs = "1.0.4" k3d-rs = { path = "../k3d" } directories = "6.0.0" lazy_static = "1.5.0" +dockerfile_builder = "0.1.5" diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 766c93c..4e94f49 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -57,8 +57,10 @@ impl Topology for HAClusterTopology { #[async_trait] impl K8sclient for HAClusterTopology { - async fn k8s_client(&self) -> Result, kube::Error> { - Ok(Arc::new(K8sClient::try_default().await?)) + async fn k8s_client(&self) -> Result, String> { + Ok(Arc::new( + K8sClient::try_default().await.map_err(|e| e.to_string())?, + )) } } diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 5325915..f363524 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -1,4 +1,4 @@ -use std::process::Command; +use std::{process::Command, sync::Arc}; use async_trait::async_trait; use inquire::Confirm; @@ -13,10 +13,10 @@ use crate::{ topology::LocalhostTopology, }; -use super::{HelmCommand, Topology, k8s::K8sClient}; +use super::{HelmCommand, K8sclient, Topology, k8s::K8sClient}; struct K8sState { - _client: K8sClient, + client: Arc, _source: K8sSource, message: String, } @@ -29,6 +29,23 @@ pub struct K8sAnywhereTopology { k8s_state: OnceCell>, } +#[async_trait] +impl K8sclient for K8sAnywhereTopology { + async fn k8s_client(&self) -> Result, String> { + let state = match self.k8s_state.get() { + Some(state) => state, + None => return Err("K8s state not initialized yet".to_string()), + }; + + let state = match state { + Some(state) => state, + None => return Err("K8s client initialized but empty".to_string()), + }; + + Ok(state.client.clone()) + } +} + impl K8sAnywhereTopology { pub fn new() -> Self { Self { @@ -124,7 +141,7 @@ impl K8sAnywhereTopology { let k3d = k3d_rs::K3d::new(k3d_score.installation_path, Some(k3d_score.cluster_name)); let state = match k3d.get_client().await { Ok(client) => K8sState { - _client: K8sClient::new(client), + client: Arc::new(K8sClient::new(client)), _source: K8sSource::LocalK3d, message: "Successfully installed K3D cluster and acquired client".to_string(), }, diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index d4463ae..ce6ec1e 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -42,8 +42,8 @@ pub struct NetworkDomain { pub name: String, } #[async_trait] -pub trait K8sclient: Send + Sync + std::fmt::Debug { - async fn k8s_client(&self) -> Result, kube::Error>; +pub trait K8sclient: Send + Sync { + async fn k8s_client(&self) -> Result, String>; } #[async_trait] diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index 55eefdd..928e8b7 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -1,6 +1,7 @@ use std::path::{Path, PathBuf}; use async_trait::async_trait; +use log::info; use serde::Serialize; use crate::{ @@ -35,9 +36,11 @@ impl Default for LAMPConfig { } } -impl Score for LAMPScore { +impl Score for LAMPScore { fn create_interpret(&self) -> Box> { - todo!() + Box::new(LAMPInterpret { + score: self.clone(), + }) } fn name(&self) -> String { @@ -57,11 +60,23 @@ impl Interpret for LAMPInterpret { inventory: &Inventory, topology: &T, ) -> Result { + let image_name = match self.build_docker_image() { + Ok(name) => name, + Err(e) => { + return Err(InterpretError::new(format!( + "Could not build LAMP docker image {e}" + ))); + } + }; + info!("LAMP docker image built {image_name}"); + let deployment_score = K8sDeploymentScore { name: >::name(&self.score), - image: "local_image".to_string(), + image: image_name, }; + info!("LAMP deployment_score {deployment_score:?}"); + todo!(); deployment_score .create_interpret() .execute(inventory, topology) @@ -85,3 +100,160 @@ impl Interpret for LAMPInterpret { todo!() } } + +use dockerfile_builder::Dockerfile; +use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, WORKDIR}; +use std::fs; + +impl LAMPInterpret { + pub fn build_dockerfile( + &self, + score: &LAMPScore, + ) -> Result> { + let mut dockerfile = Dockerfile::new(); + + // Use the PHP version from the score to determine the base image + let php_version = score.php_version.to_string(); + let php_major_minor = php_version + .split('.') + .take(2) + .collect::>() + .join("."); + + // Base image selection - using official PHP image with Apache + dockerfile.push(FROM::from(format!("php:{}-apache", php_major_minor))); + + // Set environment variables for PHP configuration + dockerfile.push(ENV::from("PHP_MEMORY_LIMIT=256M")); + dockerfile.push(ENV::from("PHP_MAX_EXECUTION_TIME=30")); + dockerfile.push(ENV::from( + "PHP_ERROR_REPORTING=E_ERROR | E_WARNING | E_PARSE", + )); + + // Install necessary PHP extensions and dependencies + dockerfile.push(RUN::from( + "apt-get update && \ + apt-get install -y --no-install-recommends \ + libfreetype6-dev \ + libjpeg62-turbo-dev \ + libpng-dev \ + libzip-dev \ + unzip \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/*", + )); + + dockerfile.push(RUN::from( + "docker-php-ext-configure gd --with-freetype --with-jpeg && \ + docker-php-ext-install -j$(nproc) \ + gd \ + mysqli \ + pdo_mysql \ + zip \ + opcache", + )); + + // Copy PHP configuration + dockerfile.push(RUN::from("mkdir -p /usr/local/etc/php/conf.d/")); + + // Create and copy a custom PHP configuration + let php_config = r#" +memory_limit = ${PHP_MEMORY_LIMIT} +max_execution_time = ${PHP_MAX_EXECUTION_TIME} +error_reporting = ${PHP_ERROR_REPORTING} +display_errors = Off +log_errors = On +error_log = /dev/stderr +date.timezone = UTC + +; Opcache configuration for production +opcache.enable=1 +opcache.memory_consumption=128 +opcache.interned_strings_buffer=8 +opcache.max_accelerated_files=4000 +opcache.revalidate_freq=2 +opcache.fast_shutdown=1 +"#; + + // Save this configuration to a temporary file within the project root + let config_path = Path::new(&score.config.project_root).join("docker-php.ini"); + fs::write(&config_path, php_config)?; + + // Reference the file within the Docker context (where the build runs) + dockerfile.push(COPY::from( + "docker-php.ini /usr/local/etc/php/conf.d/docker-php.ini", + )); + + // Security hardening + dockerfile.push(RUN::from( + "a2enmod headers && \ + a2enmod rewrite && \ + sed -i 's/ServerTokens OS/ServerTokens Prod/' /etc/apache2/conf-enabled/security.conf && \ + sed -i 's/ServerSignature On/ServerSignature Off/' /etc/apache2/conf-enabled/security.conf" + )); + + // Create a dedicated user for running Apache + dockerfile.push(RUN::from( + "groupadd -g 1000 appuser && \ + useradd -u 1000 -g appuser -m -s /bin/bash appuser && \ + chown -R appuser:appuser /var/www/html", + )); + + // Set the working directory + dockerfile.push(WORKDIR::from("/var/www/html")); + + // Copy application code from the project root to the container + // Note: In Dockerfile, the COPY context is relative to the build context + // We'll handle the actual context in the build_docker_image method + dockerfile.push(COPY::from(". /var/www/html")); + + // Fix permissions + dockerfile.push(RUN::from("chown -R appuser:appuser /var/www/html")); + + // Expose Apache port + dockerfile.push(EXPOSE::from("80/tcp")); + + // Set the default command + dockerfile.push(CMD::from("apache2-foreground")); + + // Save the Dockerfile to disk in the project root + let dockerfile_path = Path::new(&score.config.project_root).join("Dockerfile"); + fs::write(&dockerfile_path, dockerfile.to_string())?; + + Ok(dockerfile_path) + } + + pub fn build_docker_image(&self) -> Result> { + info!("Generating Dockerfile"); + let dockerfile = self.build_dockerfile(&self.score)?; + + info!( + "Building Docker image with file {} from root {}", + dockerfile.to_string_lossy(), + self.score.config.project_root.to_string_lossy() + ); + let image_name = format!("{}-php-apache", self.score.name); + let project_root = &self.score.config.project_root; + + let output = std::process::Command::new("docker") + .args([ + "build", + "--file", + dockerfile.to_str().unwrap(), + "-t", + &image_name, + project_root.to_str().unwrap(), + ]) + .output()?; + + if !output.status.success() { + return Err(format!( + "Failed to build Docker image: {}", + String::from_utf8_lossy(&output.stderr) + ) + .into()); + } + + Ok(image_name) + } +} From f17948397fc285ddfca2abab21480ab94c493ae0 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Sun, 27 Apr 2025 15:55:12 -0400 Subject: [PATCH 51/62] feat: escape PHP_ERROR_REPORTING value in Dockerfile Escapes the value of the PHP_ERROR_REPORTING environment variable in the Dockerfile to prevent potential issues with shell interpretation. Uses EnvBuilder for a more structured approach. --- harmony/src/modules/lamp.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index 928e8b7..f83ded2 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -101,8 +101,8 @@ impl Interpret for LAMPInterpret { } } -use dockerfile_builder::Dockerfile; use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, WORKDIR}; +use dockerfile_builder::{Dockerfile, instruction_builder::EnvBuilder}; use std::fs; impl LAMPInterpret { @@ -126,9 +126,13 @@ impl LAMPInterpret { // Set environment variables for PHP configuration dockerfile.push(ENV::from("PHP_MEMORY_LIMIT=256M")); dockerfile.push(ENV::from("PHP_MAX_EXECUTION_TIME=30")); - dockerfile.push(ENV::from( - "PHP_ERROR_REPORTING=E_ERROR | E_WARNING | E_PARSE", - )); + dockerfile.push( + EnvBuilder::builder() + .key("PHP_ERROR_REPORTING") + .value("\"E_ERROR | E_WARNING | E_PARSE\"") + .build() + .unwrap(), + ); // Install necessary PHP extensions and dependencies dockerfile.push(RUN::from( From 5c026ae6dd914472a763121da5a2d6114e31ab87 Mon Sep 17 00:00:00 2001 From: Willem Date: Mon, 28 Apr 2025 10:11:57 -0400 Subject: [PATCH 52/62] chore: improved error message for helm unavailable --- harmony/src/domain/topology/k8s_anywhere.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index f363524..54b6012 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -194,7 +194,7 @@ impl Topology for K8sAnywhereTopology { "{} + helm available", k8s_state.message.clone() ))), - Err(_) => Err(InterpretError::new("helm unavailable".to_string())), + Err(e) => Err(InterpretError::new(format!("helm unavailable: {}", e))), } } } From 20551b4a80f55449b55e93e6d4a01a7fd3b70d94 Mon Sep 17 00:00:00 2001 From: Willem Date: Mon, 28 Apr 2025 14:11:44 -0400 Subject: [PATCH 53/62] adr for monitoring and alerting --- adr/010-monitoring-and-alerting.md | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 adr/010-monitoring-and-alerting.md diff --git a/adr/010-monitoring-and-alerting.md b/adr/010-monitoring-and-alerting.md new file mode 100644 index 0000000..d5ebb10 --- /dev/null +++ b/adr/010-monitoring-and-alerting.md @@ -0,0 +1,46 @@ +# Architecture Decision Record: Monitoring and Alerting + +Proposed by: Willem Rolleman +Date: April 28 2025 + +## Status + +Proposed + +## Context + +Currently our monitoring and alerting is done using grafana and prometheus alert manager, deployed via helm in k8s. We need to implement a monitoring and alerting solution that is managed by Harmony. A decision needs to be made as to how this should be implemented within Harmony. + +## Decision + +use existing HelmScore and pass the scores for grafana and prometheus for each individual project + +## Rationale + +This will allow the end user to choose to use the monitoring and alerting stack if they choose for both local as well as dev/prod projects. Grafana and Prometheus are installed via helm which is consitent with OKD, helm and other design choices. Allows the use of already defined Scores. + +## Alerternatives considered + +- ### Implement alerting and monitoring stack using existing HelmScore for each project + - **Pros**: + - Each project can choose to use the monitoring and alerting stack that they choose + - Less overhead in terms of care harmony code + - can add Box::new(grafana::grafanascore(namespace)) + - **Cons**: + - No default solution implemented + - Dev needs to chose what they use + - Increases complexity of score projects + +- ### Use OKD grafana and prometheus + - **Pros**: + - Minimal config to do in Harmony + - **Cons**: + - relies on OKD so will not working for local testing via k3d + +- ### Create a monitoring and alerting crate similar to harmony tui + - **Pros**: + - Creates a default solution that can be implemented or not depending on user choice + - **Cons**: + - more complex than using a helm score + + From db9c8d83e622eb70edb0b5eaac4a4c6d96405063 Mon Sep 17 00:00:00 2001 From: Willem Date: Mon, 28 Apr 2025 15:09:11 -0400 Subject: [PATCH 54/62] update adr --- adr/010-monitoring-and-alerting.md | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/adr/010-monitoring-and-alerting.md b/adr/010-monitoring-and-alerting.md index d5ebb10..a91968b 100644 --- a/adr/010-monitoring-and-alerting.md +++ b/adr/010-monitoring-and-alerting.md @@ -9,15 +9,16 @@ Proposed ## Context -Currently our monitoring and alerting is done using grafana and prometheus alert manager, deployed via helm in k8s. We need to implement a monitoring and alerting solution that is managed by Harmony. A decision needs to be made as to how this should be implemented within Harmony. +A harmony user should be able to initialize a monitoring stack easily, either at the first run of Harmony, or that integrates with existing proects and infra without creating multiple instances of the monitoring stack or overwriting existing alerts/configurations.The user also needs a simple way to configure the stack so that it watches the projects. There should be reasonable defaults configured that are easily customizable for each project ## Decision -use existing HelmScore and pass the scores for grafana and prometheus for each individual project +Create MonitoringStack score that creates a maestro to launch the monitoring stack or not if it is already present. +The MonitoringStack score can be passed to the maestro in the vec! scores list ## Rationale -This will allow the end user to choose to use the monitoring and alerting stack if they choose for both local as well as dev/prod projects. Grafana and Prometheus are installed via helm which is consitent with OKD, helm and other design choices. Allows the use of already defined Scores. +Having the score launch a maestro will allow the user to easily create a new monitoring stack and keeps composants grouped together. The MonitoringScore can handle all the logic for adding alerts, ensuring that the stack is running etc. ## Alerternatives considered @@ -30,6 +31,8 @@ This will allow the end user to choose to use the monitoring and alerting stack - No default solution implemented - Dev needs to chose what they use - Increases complexity of score projects + - Each project will create a new monitoring and alerting instance rather than joining the existing one + - ### Use OKD grafana and prometheus - **Pros**: @@ -39,8 +42,27 @@ This will allow the end user to choose to use the monitoring and alerting stack - ### Create a monitoring and alerting crate similar to harmony tui - **Pros**: - - Creates a default solution that can be implemented or not depending on user choice + - Creates a default solution that can be implemented once by harmony + - can create a join function that will allow a project to connect to the existing solution + - eliminates risk of creating multiple instances of grafana or prometheus - **Cons**: - more complex than using a helm score + - management of values files for individual functions becomes more complicated, ie how do you create alerts for one project via helm install that doesnt overwrite the other alerts +- ### Add monitoring to Maestro struct so whether the monitoring stack is used must be defined + - **Pros**: + - less for the user to define + - may be easier to set defaults + - **Cons**: + - feels counterintuitive + - would need to modify the structure of the maestro and how it operates which seems like a bad idea + - unclear how to allow user to pass custom values/configs to the monitoring stack for subsequent projects +- ### Create MonitoringStack score to add to scores vec! which loads a maestro to install stack if not ready or add custom endpoints/alerts to existing stack + - **Pros**: + - Maestro already accepts a list of scores to initialize + - leaving out the monitoring score simply means the user does not want monitoring + - if the monitoring stack is already created, the MonitoringStack score doesn't necessarily need to be added to each project + - composants of the monitoring stack are bundled together and can be expaned or modified from the same place + - **Cons**: + - maybe need to create From 254f392cb5e44fbf0e269e1e9b770090aa676014 Mon Sep 17 00:00:00 2001 From: taha Date: Tue, 29 Apr 2025 16:09:04 +0000 Subject: [PATCH 55/62] feat(HelmScore): Add values yaml option to helm chart score (#23) Co-authored-by: tahahawa Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/23 --- Cargo.lock | 63 +++++++++++++++++-------------- harmony/Cargo.toml | 1 + harmony/src/modules/helm/chart.rs | 16 +++++++- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35c5d85..76a6a96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -356,9 +356,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.19" +version = "1.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" +checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" dependencies = [ "shlex", ] @@ -382,9 +382,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", @@ -519,7 +519,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "once_cell", "tiny-keccak", ] @@ -1289,9 +1289,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", @@ -1409,6 +1409,7 @@ dependencies = [ "serde-value", "serde_json", "serde_yaml", + "temp-file", "tokio", "url", "uuid", @@ -2064,9 +2065,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ad87c89110f55e4cd4dc2893a9790820206729eaf221555f742d540b0724a0" +checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6" dependencies = [ "jiff-static", "log", @@ -2077,9 +2078,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d076d5b64a7e2fe6f0743f02c43ca4a6725c0f904203bfe276a5b3e793103605" +checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254" dependencies = [ "proc-macro2", "quote", @@ -2238,9 +2239,9 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libm" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72" [[package]] name = "libredfish" @@ -2947,7 +2948,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.24", + "zerocopy 0.8.25", ] [[package]] @@ -3073,7 +3074,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -3121,7 +3122,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "libredox", "thiserror 2.0.12", ] @@ -3259,7 +3260,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -3806,9 +3807,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -3996,9 +3997,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -4079,6 +4080,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "temp-file" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5ff282c3f91797f0acb021f3af7fffa8a78601f0f2fd0a9f79ee7dcf9a9af9e" + [[package]] name = "tempfile" version = "3.19.1" @@ -4259,9 +4266,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -5096,11 +5103,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ - "zerocopy-derive 0.8.24", + "zerocopy-derive 0.8.25", ] [[package]] @@ -5116,9 +5123,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index a140a8b..959f2da 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -37,3 +37,4 @@ k3d-rs = { path = "../k3d" } directories = "6.0.0" lazy_static = "1.5.0" dockerfile_builder = "0.1.5" +temp-file = "0.1.9" diff --git a/harmony/src/modules/helm/chart.rs b/harmony/src/modules/helm/chart.rs index 35d6863..00f7e5d 100644 --- a/harmony/src/modules/helm/chart.rs +++ b/harmony/src/modules/helm/chart.rs @@ -9,6 +9,8 @@ use helm_wrapper_rs::blocking::{DefaultHelmExecutor, HelmExecutor}; pub use non_blank_string_rs::NonBlankString; use serde::Serialize; use std::collections::HashMap; +use std::path::Path; +use temp_file::TempFile; #[derive(Debug, Clone, Serialize)] pub struct HelmChartScore { @@ -17,6 +19,7 @@ pub struct HelmChartScore { pub chart_name: NonBlankString, pub chart_version: Option, pub values_overrides: Option>, + pub values_yaml: Option, } impl Score for HelmChartScore { @@ -48,6 +51,16 @@ impl Interpret 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 res = helm_executor.install_or_upgrade( &ns, @@ -55,9 +68,10 @@ impl Interpret for HelmChartInterpret { &self.score.chart_name, self.score.chart_version.as_ref(), self.score.values_overrides.as_ref(), - None, + yaml_path, None, ); + let status = match res { Ok(status) => status, Err(err) => return Err(InterpretError::new(err.to_string())), From 87f6afc24983a7b95f0085b1362613194915b914 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 30 Apr 2025 15:27:10 -0400 Subject: [PATCH 56/62] feat: add mariadb helm deployment to lamp interpreter - Adds a `deploy_database` function to the `LAMPInterpret` struct to deploy a MariaDB database using Helm. - Integrates `HelmCommand` trait requirement to the `LAMPInterpret` struct. - Introduces `HelmChartScore` to manage MariaDB deployment. - Adds namespace configuration for helm deployments. - Updates trait bounds for `LAMPInterpret` to include `HelmCommand`. - Implements `get_namespace` function to retrieve the namespace. --- Cargo.lock | 2 +- examples/lamp/Cargo.toml | 2 +- examples/lamp/src/main.rs | 2 +- harmony/src/modules/helm/chart.rs | 49 ++++++++++++++++++++++- harmony/src/modules/k8s/mod.rs | 1 + harmony/src/modules/k8s/namespace.rs | 46 ++++++++++++++++++++++ harmony/src/modules/lamp.rs | 59 ++++++++++++++++++++++------ opnsense-config/src/lib.rs | 6 +-- 8 files changed, 148 insertions(+), 19 deletions(-) create mode 100644 harmony/src/modules/k8s/namespace.rs diff --git a/Cargo.lock b/Cargo.lock index 76a6a96..776066d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1020,8 +1020,8 @@ dependencies = [ "cidr", "env_logger", "harmony", + "harmony_cli", "harmony_macros", - "harmony_tui", "harmony_types", "log", "tokio", diff --git a/examples/lamp/Cargo.toml b/examples/lamp/Cargo.toml index 902548e..a433e79 100644 --- a/examples/lamp/Cargo.toml +++ b/examples/lamp/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] harmony = { path = "../../harmony" } -harmony_tui = { path = "../../harmony_tui" } +harmony_cli = { path = "../../harmony_cli" } harmony_types = { path = "../../harmony_types" } cidr = { workspace = true } tokio = { workspace = true } diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index 27e5df6..9d51d0b 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -26,5 +26,5 @@ async fn main() { .await .unwrap(); maestro.register_all(vec![Box::new(lamp_stack)]); - harmony_tui::init(maestro).await.unwrap(); + harmony_cli::init(maestro, None).await.unwrap(); } diff --git a/harmony/src/modules/helm/chart.rs b/harmony/src/modules/helm/chart.rs index 00f7e5d..1be66f6 100644 --- a/harmony/src/modules/helm/chart.rs +++ b/harmony/src/modules/helm/chart.rs @@ -6,10 +6,12 @@ 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)] @@ -20,6 +22,10 @@ pub struct HelmChartScore { pub chart_version: Option, pub values_overrides: Option>, pub values_yaml: Option, + pub create_namespace: bool, + + /// Wether to run `helm upgrade --install` under the hood or only install when not present + pub install_only: bool, } impl Score for HelmChartScore { @@ -62,6 +68,47 @@ impl Interpret for HelmChartInterpret { }; 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, @@ -69,7 +116,7 @@ impl Interpret for HelmChartInterpret { self.score.chart_version.as_ref(), self.score.values_overrides.as_ref(), yaml_path, - None, + Some(&helm_options), ); let status = match res { diff --git a/harmony/src/modules/k8s/mod.rs b/harmony/src/modules/k8s/mod.rs index df654fb..97e238f 100644 --- a/harmony/src/modules/k8s/mod.rs +++ b/harmony/src/modules/k8s/mod.rs @@ -1,2 +1,3 @@ pub mod deployment; +pub mod namespace; pub mod resource; diff --git a/harmony/src/modules/k8s/namespace.rs b/harmony/src/modules/k8s/namespace.rs new file mode 100644 index 0000000..aee87e3 --- /dev/null +++ b/harmony/src/modules/k8s/namespace.rs @@ -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, +} + +impl Score for K8sNamespaceScore { + fn create_interpret(&self) -> Box> { + 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() + } +} diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index f83ded2..5cb948f 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -1,9 +1,15 @@ +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 std::fs; use std::path::{Path, PathBuf}; +use std::str::FromStr; use async_trait::async_trait; use log::info; use serde::Serialize; +use crate::topology::HelmCommand; use crate::{ data::{Id, Version}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, @@ -13,6 +19,8 @@ use crate::{ topology::{K8sclient, Topology, Url}, }; +use super::helm::chart::HelmChartScore; + #[derive(Debug, Clone, Serialize)] pub struct LAMPScore { pub name: String, @@ -36,10 +44,11 @@ impl Default for LAMPConfig { } } -impl Score for LAMPScore { +impl Score for LAMPScore { fn create_interpret(&self) -> Box> { Box::new(LAMPInterpret { score: self.clone(), + namespace: "harmony-lamp".to_string(), }) } @@ -51,10 +60,11 @@ impl Score for LAMPScore { #[derive(Debug)] pub struct LAMPInterpret { score: LAMPScore, + namespace: String, } #[async_trait] -impl Interpret for LAMPInterpret { +impl Interpret for LAMPInterpret { async fn execute( &self, inventory: &Inventory, @@ -70,18 +80,23 @@ impl Interpret for LAMPInterpret { }; info!("LAMP docker image built {image_name}"); + info!("Deploying database"); + self.deploy_database(inventory, topology).await?; + let deployment_score = K8sDeploymentScore { name: >::name(&self.score), image: image_name, }; - info!("LAMP deployment_score {deployment_score:?}"); - todo!(); deployment_score .create_interpret() .execute(inventory, topology) .await?; - todo!() + + info!("LAMP deployment_score {deployment_score:?}"); + todo!("1. Use HelmChartScore to deploy mariadb + 2. 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 +116,31 @@ impl Interpret 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( &self, - score: &LAMPScore, - ) -> Result> { + inventory: &Inventory, + topology: &T, + ) -> Result { + 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: None, + create_namespace: true, + install_only: true, + values_yaml: None, + }; + + score.create_interpret().execute(inventory, topology).await + } + + fn build_dockerfile(&self, score: &LAMPScore) -> Result> { let mut dockerfile = Dockerfile::new(); // Use the PHP version from the score to determine the base image @@ -260,4 +291,8 @@ opcache.fast_shutdown=1 Ok(image_name) } + + fn get_namespace(&self) -> Option { + Some(NonBlankString::from_str(&self.namespace).unwrap()) + } } diff --git a/opnsense-config/src/lib.rs b/opnsense-config/src/lib.rs index a497133..5953875 100644 --- a/opnsense-config/src/lib.rs +++ b/opnsense-config/src/lib.rs @@ -4,14 +4,14 @@ pub mod modules; pub use config::Config; pub use error::Error; -#[cfg(test)] -mod test { + +#[cfg(e2e_test)] +mod e2e_test { use opnsense_config_xml::StaticMap; use std::net::Ipv4Addr; use crate::Config; - #[cfg(opnsenseendtoend)] #[tokio::test] async fn test_public_sdk() { use pretty_assertions::assert_eq; From bc2bd2f2f415dda5fd1e942339e360b090d17af1 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 30 Apr 2025 22:33:31 -0400 Subject: [PATCH 57/62] feat: push docker image to registry and deploy with full tag - Added functionality to tag and push the built Docker image to a specified registry. - Modified deployment score to use the full image tag (including registry and project). - Included error handling and logging for the `docker tag` and `docker push` commands. - Updated the `K8sDeploymentScore` struct to include a namespace field and environment variables for database credentials. - Added kebab-case conversion for deployment name and namespace. - Implemented a check_output function for better error reporting. --- Cargo.lock | 10 ++++ Cargo.toml | 1 + harmony/Cargo.toml | 27 +++++----- harmony/src/domain/config.rs | 4 ++ harmony/src/domain/topology/k8s.rs | 7 ++- harmony/src/modules/k8s/deployment.rs | 20 ++++--- harmony/src/modules/k8s/resource.rs | 6 ++- harmony/src/modules/lamp.rs | 77 +++++++++++++++++++++++++-- 8 files changed, 123 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 776066d..f84d847 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -524,6 +524,15 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1383,6 +1392,7 @@ version = "0.1.0" dependencies = [ "async-trait", "cidr", + "convert_case", "derive-new", "directories", "dockerfile_builder", diff --git a/Cargo.toml b/Cargo.toml index 48fe426..8dd08bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ serde_yaml = "0.9.34" serde-value = "0.7.0" http = "1.2.0" inquire = "0.7.5" +convert_case = "0.8.0" [workspace.dependencies.uuid] version = "1.11.0" diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index 959f2da..02a0ce7 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -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" @@ -38,3 +38,4 @@ directories = "6.0.0" lazy_static = "1.5.0" dockerfile_builder = "0.1.5" temp-file = "0.1.9" +convert_case.workspace = true diff --git a/harmony/src/domain/config.rs b/harmony/src/domain/config.rs index 320e9a0..0fa059f 100644 --- a/harmony/src/domain/config.rs +++ b/harmony/src/domain/config.rs @@ -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()); } diff --git a/harmony/src/domain/topology/k8s.rs b/harmony/src/domain/topology/k8s.rs index beecbf0..57b3668 100644 --- a/harmony/src/domain/topology/k8s.rs +++ b/harmony/src/domain/topology/k8s.rs @@ -38,7 +38,7 @@ impl K8sClient { Ok(result) } - pub async fn apply_namespaced(&self, resource: &Vec) -> Result + pub async fn apply_namespaced(&self, resource: &Vec, ns: Option<&str>) -> Result where K: Resource + Clone @@ -49,7 +49,10 @@ impl K8sClient { ::DynamicType: Default, { for r in resource.iter() { - let api: Api = Api::default_namespaced(self.client.clone()); + let api: Api = match ns { + Some(ns) => Api::namespaced(self.client.clone(), ns), + None => Api::default_namespaced(self.client.clone()), + }; api.create(&PostParams::default(), &r).await?; } todo!("") diff --git a/harmony/src/modules/k8s/deployment.rs b/harmony/src/modules/k8s/deployment.rs index 9e7178f..55f581f 100644 --- a/harmony/src/modules/k8s/deployment.rs +++ b/harmony/src/modules/k8s/deployment.rs @@ -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, + pub env_vars: serde_json::Value, } impl Score for K8sDeploymentScore { fn create_interpret(&self) -> Box> { - let deployment: Deployment = serde_json::from_value(json!( + let deployment = json!( { "metadata": { "name": self.name @@ -38,18 +41,21 @@ impl Score 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()), }) } diff --git a/harmony/src/modules/k8s/resource.rs b/harmony/src/modules/k8s/resource.rs index 4e54be7..6880292 100644 --- a/harmony/src/modules/k8s/resource.rs +++ b/harmony/src/modules/k8s/resource.rs @@ -14,12 +14,14 @@ use crate::{ #[derive(Debug, Clone, Serialize)] pub struct K8sResourceScore { pub resource: Vec, + pub namespace: Option, } impl K8sResourceScore { - pub fn single(resource: K) -> Self { + pub fn single(resource: K, namespace: Option) -> 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( diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index 5cb948f..2110904 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -1,14 +1,17 @@ +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::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}, @@ -80,22 +83,49 @@ impl Interpret 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: >::name(&self.score), - image: image_name, + name: >::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" + } + } + }, + ]), }; + info!("Deploying score {deployment_score:#?}"); + deployment_score .create_interpret() .execute(inventory, topology) .await?; info!("LAMP deployment_score {deployment_score:?}"); - todo!("1. Use HelmChartScore to deploy mariadb - 2. Use deploymentScore to deploy lamp docker container + 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?)"); } @@ -258,6 +288,43 @@ opcache.fast_shutdown=1 Ok(dockerfile_path) } + fn check_output( + &self, + output: &std::process::Output, + msg: &str, + ) -> Result<(), Box> { + 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> { + 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> { info!("Generating Dockerfile"); let dockerfile = self.build_dockerfile(&self.score)?; From c879ca143fbb94a93d9051e29ceeed15603f08d2 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 30 Apr 2025 23:36:12 -0400 Subject: [PATCH 58/62] feat: Add comments explaining a bit of what harmony does in the lamp demo --- examples/lamp/src/main.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index 9d51d0b..41adfb4 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -8,17 +8,30 @@ use harmony::{ #[tokio::main] async fn main() { - // let _ = env_logger::Builder::from_default_env().filter_level(log::LevelFilter::Info).try_init(); + // This here is the whole configuration to + // - setup a local K3D cluster + // - Build a docker image with the PHP project builtin and production grade settings + // - Deploy a mariadb database using a production grade helm chart + // - Deploy the new container using a kubernetes deployment + // - Configure networking between the PHP container and the database + // - Provision a public route and an SSL certificate automatically on production environments + // + // Enjoy :) let lamp_stack = LAMPScore { name: "harmony-lamp-demo".to_string(), domain: Url::Url(url::Url::parse("https://lampdemo.harmony.nationtech.io").unwrap()), php_version: Version::from("8.4.4").unwrap(), + // This config can be extended as needed for more complicated configurations config: LAMPConfig { project_root: "./php".into(), ..Default::default() }, }; + // You can choose the type of Topology you want, we suggest starting with the + // K8sAnywhereTopology as it is the most automatic one that enables you to easily deploy + // locally, to development environment from a CI, to staging, and to production with settings + // that automatically adapt to each environment grade. let mut maestro = Maestro::::initialize( Inventory::autoload(), K8sAnywhereTopology::new(), @@ -26,5 +39,7 @@ async fn main() { .await .unwrap(); maestro.register_all(vec![Box::new(lamp_stack)]); + // Here we bootstrap the CLI, this gives some nice features if you need them harmony_cli::init(maestro, None).await.unwrap(); } +// That's it, end of the infra as code. From 1c3669cb47e9a04edccef1d6500cc62f485afc8a Mon Sep 17 00:00:00 2001 From: Willem Date: Fri, 2 May 2025 11:56:27 -0400 Subject: [PATCH 59/62] chore: added default mariadb size and pass env variables to php app --- harmony/src/modules/lamp.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index 2110904..7d2b28d 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -3,6 +3,7 @@ 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; @@ -17,8 +18,8 @@ use crate::{ data::{Id, Version}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, - modules::k8s::deployment::K8sDeploymentScore, - score::Score, +modules::k8s::deployment::K8sDeploymentScore, +score::Score, topology::{K8sclient, Topology, Url}, }; @@ -36,6 +37,7 @@ pub struct LAMPScore { pub struct LAMPConfig { pub project_root: PathBuf, pub ssl_enabled: bool, + pub database_size: String, } impl Default for LAMPConfig { @@ -43,6 +45,7 @@ impl Default for LAMPConfig { LAMPConfig { project_root: Path::new("./src").to_path_buf(), ssl_enabled: true, + database_size: "2Gi".to_string(), } } } @@ -113,6 +116,10 @@ impl Interpret for LAMPInterpret { } } }, + { + "name": "MYSQL_HOST", + "value": secret_name + }, ]), }; @@ -152,6 +159,11 @@ impl LAMPInterpret { inventory: &Inventory, topology: &T, ) -> Result { + let mut mariadb_overrides = HashMap::new(); + mariadb_overrides.insert( + NonBlankString::from_str("primary.persistence.size").unwrap(), + self.score.config.database_size.clone(), + ); let score = HelmChartScore { namespace: self.get_namespace(), release_name: NonBlankString::from_str(&format!("{}-database", self.score.name)) @@ -161,7 +173,7 @@ impl LAMPInterpret { ) .unwrap(), chart_version: None, - values_overrides: None, + values_overrides: Some(mariadb_overrides), create_namespace: true, install_only: true, values_yaml: None, @@ -257,6 +269,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 && \ From a7ba9be486299485637f297c0b472c4a7c8746b3 Mon Sep 17 00:00:00 2001 From: Willem Date: Fri, 2 May 2025 12:03:18 -0400 Subject: [PATCH 60/62] feat:php program to fill pvc and report database usage --- examples/lamp/php/index.php | 84 ++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/examples/lamp/php/index.php b/examples/lamp/php/index.php index 6cf1a50..471f6a2 100644 --- a/examples/lamp/php/index.php +++ b/examples/lamp/php/index.php @@ -1,3 +1,85 @@ PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, +]; + +try { + $pdo = new PDO($dsn, $user, $pass, $options); + $pdo->exec("CREATE DATABASE IF NOT EXISTS `$db`"); + $pdo->exec("USE `$db`"); + $pdo->exec(" + CREATE TABLE IF NOT EXISTS filler ( + id INT AUTO_INCREMENT PRIMARY KEY, + data LONGBLOB + ) + "); +} catch (\PDOException $e) { + die("❌ DB connection failed: " . $e->getMessage()); +} + +function getDbStats($pdo, $db) { + $stmt = $pdo->query(" + SELECT + ROUND(SUM(data_length + index_length) / 1024 / 1024 / 1024, 2) AS total_size_gb, + SUM(table_rows) AS total_rows + FROM information_schema.tables + WHERE table_schema = '$db' + "); + $result = $stmt->fetch(); + $sizeGb = $result['total_size_gb'] ?? '0'; + $rows = $result['total_rows'] ?? '0'; + $avgMb = ($rows > 0) ? round(($sizeGb * 1024) / $rows, 2) : 0; + return [$sizeGb, $rows, $avgMb]; +} + +list($dbSize, $rowCount, $avgRowMb) = getDbStats($pdo, $db); + +$message = ''; + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['fill'])) { + $iterations = 1024; + $data = str_repeat(random_bytes(1024), 1024); // 1MB + $stmt = $pdo->prepare("INSERT INTO filler (data) VALUES (:data)"); + + for ($i = 0; $i < $iterations; $i++) { + $stmt->execute([':data' => $data]); + } + + list($dbSize, $rowCount, $avgRowMb) = getDbStats($pdo, $db); + + $message = "

✅ 1GB inserted into MariaDB successfully.

"; +} ?> + + + + + MariaDB Filler + + +

MariaDB Storage Filler

+ +
    +
  • 📦 MariaDB Used Size: GB
  • +
  • 📊 Total Rows:
  • +
  • 📐 Average Row Size: MB
  • +
+ +
+ +
+ + + From e1133ea114c5e8f1daf1d167f435d3917f47e49e Mon Sep 17 00:00:00 2001 From: Willem Date: Fri, 2 May 2025 15:02:50 -0400 Subject: [PATCH 61/62] use default database_size None in LampConfig to default to value from helm chart --- harmony/src/modules/lamp.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index 7d2b28d..47d6ca9 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -18,8 +18,8 @@ use crate::{ data::{Id, Version}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, -modules::k8s::deployment::K8sDeploymentScore, -score::Score, + modules::k8s::deployment::K8sDeploymentScore, + score::Score, topology::{K8sclient, Topology, Url}, }; @@ -37,7 +37,7 @@ pub struct LAMPScore { pub struct LAMPConfig { pub project_root: PathBuf, pub ssl_enabled: bool, - pub database_size: String, + pub database_size: Option, } impl Default for LAMPConfig { @@ -45,7 +45,7 @@ impl Default for LAMPConfig { LAMPConfig { project_root: Path::new("./src").to_path_buf(), ssl_enabled: true, - database_size: "2Gi".to_string(), + database_size: None, } } } @@ -159,11 +159,13 @@ impl LAMPInterpret { inventory: &Inventory, topology: &T, ) -> Result { - let mut mariadb_overrides = HashMap::new(); - mariadb_overrides.insert( - NonBlankString::from_str("primary.persistence.size").unwrap(), - self.score.config.database_size.clone(), - ); + 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)) @@ -173,7 +175,7 @@ impl LAMPInterpret { ) .unwrap(), chart_version: None, - values_overrides: Some(mariadb_overrides), + values_overrides: Some(values_overrides), create_namespace: true, install_only: true, values_yaml: None, @@ -181,7 +183,6 @@ impl LAMPInterpret { score.create_interpret().execute(inventory, topology).await } - fn build_dockerfile(&self, score: &LAMPScore) -> Result> { let mut dockerfile = Dockerfile::new(); From 78fffcd725d3c5f38feee4d68a1d99eed946b731 Mon Sep 17 00:00:00 2001 From: Willem Date: Fri, 2 May 2025 15:07:39 -0400 Subject: [PATCH 62/62] fix: specified 2Gi db size from LAMPconfig --- examples/lamp/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/lamp/src/main.rs b/examples/lamp/src/main.rs index 41adfb4..1aaca90 100644 --- a/examples/lamp/src/main.rs +++ b/examples/lamp/src/main.rs @@ -24,6 +24,7 @@ async fn main() { // This config can be extended as needed for more complicated configurations config: LAMPConfig { project_root: "./php".into(), + database_size: format!("2Gi").into(), ..Default::default() }, };