diff --git a/harmony/src/domain/topology/k8s.rs b/harmony/src/domain/topology/k8s.rs index 9565a3d..fc91b0c 100644 --- a/harmony/src/domain/topology/k8s.rs +++ b/harmony/src/domain/topology/k8s.rs @@ -10,11 +10,22 @@ use log::{debug, error, trace}; use serde::de::DeserializeOwned; use similar::TextDiff; -#[derive(new)] +#[derive(new, Clone)] pub struct K8sClient { client: Client, } +impl std::fmt::Debug for K8sClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // This is a poor man's debug implementation for now as kube::Client does not provide much + // useful information + f.write_fmt(format_args!( + "K8sClient {{ kube client using default namespace {} }}", + self.client.default_namespace() + )) + } +} + impl K8sClient { pub async fn try_default() -> Result { Ok(Self { diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 6742b5a..65567f1 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -3,6 +3,7 @@ use std::{process::Command, sync::Arc}; use async_trait::async_trait; use inquire::Confirm; use log::{debug, info, warn}; +use serde::Serialize; use tokio::sync::OnceCell; use crate::{ @@ -20,22 +21,24 @@ use super::{ tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager}, }; +#[derive(Clone, Debug)] struct K8sState { client: Arc, _source: K8sSource, message: String, } -#[derive(Debug)] +#[derive(Debug, Clone)] enum K8sSource { LocalK3d, Kubeconfig, } +#[derive(Clone, Debug)] pub struct K8sAnywhereTopology { - k8s_state: OnceCell>, - tenant_manager: OnceCell, - config: K8sAnywhereConfig, + k8s_state: Arc>>, + tenant_manager: Arc>, + config: Arc, } #[async_trait] @@ -55,20 +58,29 @@ impl K8sclient for K8sAnywhereTopology { } } +impl Serialize for K8sAnywhereTopology { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + todo!() + } +} + impl K8sAnywhereTopology { pub fn from_env() -> Self { Self { - k8s_state: OnceCell::new(), - tenant_manager: OnceCell::new(), - config: K8sAnywhereConfig::from_env(), + k8s_state: Arc::new(OnceCell::new()), + tenant_manager: Arc::new(OnceCell::new()), + config: Arc::new(K8sAnywhereConfig::from_env()), } } pub fn with_config(config: K8sAnywhereConfig) -> Self { Self { - k8s_state: OnceCell::new(), - tenant_manager: OnceCell::new(), - config, + k8s_state: Arc::new(OnceCell::new()), + tenant_manager: Arc::new(OnceCell::new()), + config: Arc::new(config), } } @@ -200,6 +212,7 @@ impl K8sAnywhereTopology { } } +#[derive(Clone, Debug)] pub struct K8sAnywhereConfig { /// The path of the KUBECONFIG file that Harmony should use to interact with the Kubernetes /// cluster diff --git a/harmony/src/domain/topology/tenant/k8s.rs b/harmony/src/domain/topology/tenant/k8s.rs index 6ad5ae1..705b0c4 100644 --- a/harmony/src/domain/topology/tenant/k8s.rs +++ b/harmony/src/domain/topology/tenant/k8s.rs @@ -22,7 +22,7 @@ use serde_json::json; use super::{TenantConfig, TenantManager}; -#[derive(new)] +#[derive(new, Clone, Debug)] pub struct K8sTenantManager { k8s_client: Arc, } diff --git a/harmony/src/modules/application/features/continuous_delivery.rs b/harmony/src/modules/application/features/continuous_delivery.rs new file mode 100644 index 0000000..3530142 --- /dev/null +++ b/harmony/src/modules/application/features/continuous_delivery.rs @@ -0,0 +1,42 @@ +use async_trait::async_trait; +use log::info; + +use crate::{modules::application::ApplicationFeature, topology::Topology}; + +/// ContinuousDelivery in Harmony provides this functionality : +/// +/// - **Package** the application +/// - **Push** to an artifact registry +/// - **Deploy** to a testing environment +/// - **Deploy** to a production environment +/// +/// It is intended to be used as an application feature passed down to an ApplicationInterpret. For +/// example : +/// +/// ```rust,ignore +/// let app = RustApplicationScore { +/// name: "My Rust App".to_string(), +/// features: vec![ContinuousDelivery::default()], +/// }; +/// ``` +/// +/// *Note :* +/// +/// By default, the Harmony Opinionated Pipeline is built using these technologies : +/// +/// - Gitea Action (executes pipeline steps) +/// - Docker to build an OCI container image +/// - Helm chart to package Kubernetes resources +/// - Harbor as artifact registru +/// - ArgoCD to install/upgrade/rollback/inspect k8s resources +/// - Kubernetes for runtime orchestration +#[derive(Debug, Default)] +pub struct ContinuousDelivery {} + +#[async_trait] +impl ApplicationFeature for ContinuousDelivery { + async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { + info!("Installing ContinuousDelivery feature"); + todo!() + } +} diff --git a/harmony/src/modules/application/features/endpoint.rs b/harmony/src/modules/application/features/endpoint.rs new file mode 100644 index 0000000..f4940ed --- /dev/null +++ b/harmony/src/modules/application/features/endpoint.rs @@ -0,0 +1,39 @@ +use async_trait::async_trait; +use log::info; + +use crate::{ + modules::application::ApplicationFeature, + topology::{K8sclient, Topology}, +}; + +#[derive(Debug)] +pub struct PublicEndpoint { + application_port: u16, +} + +/// Use port 3000 as default port. Harmony wants to provide "sane defaults" in general, and in this +/// particular context, using port 80 goes against our philosophy to provide production grade +/// defaults out of the box. Using an unprivileged port is a good security practice and will allow +/// for unprivileged containers to work with this out of the box. +/// +/// Now, why 3000 specifically? Many popular web/network frameworks use it by default, there is no +/// perfect answer for this but many Rust and Python libraries tend to use 3000. +impl Default for PublicEndpoint { + fn default() -> Self { + Self { + application_port: 3000, + } + } +} + +/// For now we only suport K8s ingress, but we will support more stuff at some point +#[async_trait] +impl ApplicationFeature for PublicEndpoint { + async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { + info!( + "Making sure public endpoint is installed for port {}", + self.application_port + ); + todo!() + } +} diff --git a/harmony/src/modules/application/features/mod.rs b/harmony/src/modules/application/features/mod.rs new file mode 100644 index 0000000..0e034fc --- /dev/null +++ b/harmony/src/modules/application/features/mod.rs @@ -0,0 +1,8 @@ +mod endpoint; +pub use endpoint::*; + +mod monitoring; +pub use monitoring::*; + +mod continuous_delivery; +pub use continuous_delivery::*; diff --git a/harmony/src/modules/application/features/monitoring.rs b/harmony/src/modules/application/features/monitoring.rs new file mode 100644 index 0000000..91ad72d --- /dev/null +++ b/harmony/src/modules/application/features/monitoring.rs @@ -0,0 +1,18 @@ +use async_trait::async_trait; +use log::info; + +use crate::{ + modules::application::ApplicationFeature, + topology::{HelmCommand, Topology}, +}; + +#[derive(Debug, Default)] +pub struct Monitoring {} + +#[async_trait] +impl ApplicationFeature for Monitoring { + async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { + info!("Ensuring monitoring is available for application"); + todo!("create and execute k8s prometheus score, depends on Will's work") + } +} diff --git a/harmony/src/modules/application/mod.rs b/harmony/src/modules/application/mod.rs new file mode 100644 index 0000000..2a3ee24 --- /dev/null +++ b/harmony/src/modules/application/mod.rs @@ -0,0 +1,67 @@ +pub mod features; +mod rust; +pub use rust::*; + +use async_trait::async_trait; +use serde::Serialize; + +use crate::{ + data::{Id, Version}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + topology::Topology, +}; + +#[derive(Debug)] +pub struct ApplicationInterpret { + features: Vec>>, +} + +#[async_trait] +impl Interpret for ApplicationInterpret { + async fn execute( + &self, + _inventory: &Inventory, + _topology: &T, + ) -> Result { + 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!() + } +} + +/// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability, +/// ContinuousIntegration, ContinuousDelivery +#[async_trait] +pub trait ApplicationFeature: std::fmt::Debug + Send + Sync { + async fn ensure_installed(&self, topology: &T) -> Result<(), String>; +} + +impl Serialize for Box> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + todo!() + } +} + +impl Clone for Box> { + fn clone(&self) -> Self { + todo!() + } +} diff --git a/harmony/src/modules/application/rust.rs b/harmony/src/modules/application/rust.rs new file mode 100644 index 0000000..0ef41b3 --- /dev/null +++ b/harmony/src/modules/application/rust.rs @@ -0,0 +1,25 @@ +use serde::Serialize; + +use crate::{ + score::Score, + topology::{Topology, Url}, +}; + +use super::{ApplicationFeature, ApplicationInterpret}; + +#[derive(Debug, Serialize, Clone)] +pub struct RustWebappScore { + pub name: String, + pub domain: Url, + pub features: Vec>>, +} + +impl Score for RustWebappScore { + fn create_interpret(&self) -> Box> { + Box::new(ApplicationInterpret { features: todo!() }) + } + + fn name(&self) -> String { + format!("{}-RustWebapp", self.name) + } +} diff --git a/harmony/src/modules/mod.rs b/harmony/src/modules/mod.rs index ec5f050..e9b6c52 100644 --- a/harmony/src/modules/mod.rs +++ b/harmony/src/modules/mod.rs @@ -1,3 +1,4 @@ +pub mod application; pub mod cert_manager; pub mod dhcp; pub mod dns; diff --git a/harmony_cli/src/lib.rs b/harmony_cli/src/lib.rs index 33759fa..50beb6e 100644 --- a/harmony_cli/src/lib.rs +++ b/harmony_cli/src/lib.rs @@ -12,15 +12,15 @@ use harmony_tui; pub struct Args { /// Run score(s) without prompt #[arg(short, long, default_value_t = false, conflicts_with = "interactive")] - yes: bool, + pub yes: bool, /// Filter query #[arg(short, long, conflicts_with = "interactive")] - filter: Option, + pub filter: Option, /// Run interactive TUI or not #[arg(short, long, default_value_t = false)] - interactive: bool, + pub interactive: bool, /// Run all or nth, defaults to all #[arg( @@ -31,15 +31,15 @@ pub struct Args { conflicts_with = "number", conflicts_with = "interactive" )] - all: bool, + pub all: bool, /// Run nth matching, zero indexed #[arg(short, long, default_value_t = 0, conflicts_with = "interactive")] - number: usize, + pub number: usize, /// list scores, will also be affected by run filter #[arg(short, long, default_value_t = false, conflicts_with = "interactive")] - list: bool, + pub list: bool, } fn maestro_scores_filter(