feat: Application module architecture and placeholder features #70
| @ -8,11 +8,19 @@ use kube::{ | |||||||
| use log::{debug, error, trace}; | use log::{debug, error, trace}; | ||||||
| use serde::de::DeserializeOwned; | use serde::de::DeserializeOwned; | ||||||
| 
 | 
 | ||||||
| #[derive(new)] | #[derive(new, Clone)] | ||||||
| pub struct K8sClient { | pub struct K8sClient { | ||||||
|     client: Client, |     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 { | impl K8sClient { | ||||||
|     pub async fn try_default() -> Result<Self, Error> { |     pub async fn try_default() -> Result<Self, Error> { | ||||||
|         Ok(Self { |         Ok(Self { | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ use std::{process::Command, sync::Arc}; | |||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| use inquire::Confirm; | use inquire::Confirm; | ||||||
| use log::{debug, info, warn}; | use log::{debug, info, warn}; | ||||||
|  | use serde::Serialize; | ||||||
| use tokio::sync::OnceCell; | use tokio::sync::OnceCell; | ||||||
| 
 | 
 | ||||||
| use crate::{ | use crate::{ | ||||||
| @ -20,22 +21,24 @@ use super::{ | |||||||
|     tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager}, |     tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | #[derive(Clone, Debug)] | ||||||
| struct K8sState { | struct K8sState { | ||||||
|     client: Arc<K8sClient>, |     client: Arc<K8sClient>, | ||||||
|     _source: K8sSource, |     _source: K8sSource, | ||||||
|     message: String, |     message: String, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Debug)] | #[derive(Debug, Clone)] | ||||||
| enum K8sSource { | enum K8sSource { | ||||||
|     LocalK3d, |     LocalK3d, | ||||||
|     Kubeconfig, |     Kubeconfig, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[derive(Clone, Debug)] | ||||||
| pub struct K8sAnywhereTopology { | pub struct K8sAnywhereTopology { | ||||||
|     k8s_state: OnceCell<Option<K8sState>>, |     k8s_state: Arc<OnceCell<Option<K8sState>>>, | ||||||
|     tenant_manager: OnceCell<K8sTenantManager>, |     tenant_manager: Arc<OnceCell<K8sTenantManager>>, | ||||||
|     config: K8sAnywhereConfig, |     config: Arc<K8sAnywhereConfig>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| @ -55,20 +58,28 @@ impl K8sclient for K8sAnywhereTopology { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | impl Serialize for K8sAnywhereTopology { | ||||||
|  |     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | ||||||
|  |     where | ||||||
|  |         S: serde::Serializer { | ||||||
|  |         todo!() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| impl K8sAnywhereTopology { | impl K8sAnywhereTopology { | ||||||
|     pub fn from_env() -> Self { |     pub fn from_env() -> Self { | ||||||
|         Self { |         Self { | ||||||
|             k8s_state: OnceCell::new(), |             k8s_state: Arc::new(OnceCell::new()), | ||||||
|             tenant_manager: OnceCell::new(), |             tenant_manager: Arc::new(OnceCell::new()), | ||||||
|             config: K8sAnywhereConfig::from_env(), |             config: Arc::new(K8sAnywhereConfig::from_env()), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn with_config(config: K8sAnywhereConfig) -> Self { |     pub fn with_config(config: K8sAnywhereConfig) -> Self { | ||||||
|         Self { |         Self { | ||||||
|             k8s_state: OnceCell::new(), |             k8s_state: Arc::new(OnceCell::new()), | ||||||
|             tenant_manager: OnceCell::new(), |             tenant_manager: Arc::new(OnceCell::new()), | ||||||
|             config, |             config: Arc::new(config), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -200,6 +211,7 @@ impl K8sAnywhereTopology { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[derive(Clone, Debug)] | ||||||
| pub struct K8sAnywhereConfig { | pub struct K8sAnywhereConfig { | ||||||
|     /// The path of the KUBECONFIG file that Harmony should use to interact with the Kubernetes
 |     /// The path of the KUBECONFIG file that Harmony should use to interact with the Kubernetes
 | ||||||
|     /// cluster
 |     /// cluster
 | ||||||
|  | |||||||
| @ -22,7 +22,7 @@ use serde_json::json; | |||||||
| 
 | 
 | ||||||
| use super::{TenantConfig, TenantManager}; | use super::{TenantConfig, TenantManager}; | ||||||
| 
 | 
 | ||||||
| #[derive(new)] | #[derive(new, Clone, Debug)] | ||||||
| pub struct K8sTenantManager { | pub struct K8sTenantManager { | ||||||
|     k8s_client: Arc<K8sClient>, |     k8s_client: Arc<K8sClient>, | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
|  | use log::info; | ||||||
| 
 | 
 | ||||||
| use crate::modules::application::{Application, ApplicationFeature}; | use crate::{modules::application::{Application, ApplicationFeature}, topology::{K8sclient, Topology}}; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| pub struct PublicEndpoint { | pub struct PublicEndpoint { | ||||||
| @ -22,9 +23,11 @@ impl Default for PublicEndpoint { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// For now we only suport K8s ingress, but we will support more stuff at some point
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl ApplicationFeature for PublicEndpoint { | impl <T: Topology + K8sclient + 'static> ApplicationFeature<T> for PublicEndpoint { | ||||||
|     async fn ensure_installed(&self) -> Result<(), String> { |     async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { | ||||||
|  |         info!("Making sure public endpoint is installed for port {}", self.application_port); | ||||||
|         todo!() |         todo!() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -36,40 +39,3 @@ impl ApplicationFeature for PublicEndpoint { | |||||||
|         todo!() |         todo!() | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| // Design options here :
 |  | ||||||
| //
 |  | ||||||
| // 1. Forget about ApplicationFeature trait. The Features are just other scores that are passed on to
 |  | ||||||
| // the ApplicationInterpret as children (and we can rename children dependencies maybe?)
 |  | ||||||
| //
 |  | ||||||
| // 2. Go forward with the ApplicationFeature trait. There are important question marks here :
 |  | ||||||
| //  - What about the installation lifecycle management? This was defined as being handled by a
 |  | ||||||
| //  Topology. The thing here is that I am not sure wether application features belong at the
 |  | ||||||
| //  Topology level or not. Functionnaly they are pretty similar. Topology provides software
 |  | ||||||
| //  infrastructure features that Scores will then install themselves on. Most of the time those very
 |  | ||||||
| //  features are installed using Scores with lower level dependencies. For example, :
 |  | ||||||
| //
 |  | ||||||
| //  AlertingFeature depends on T: Topology + AlertSender
 |  | ||||||
| //  AlertSender is implemented as KubePrometheus which depends on T: Topology + HelmCommand
 |  | ||||||
| //  HelmCommand relies on T: Topology + K8sClient
 |  | ||||||
| //
 |  | ||||||
| //  With that said, would it work with `features: Vec<box dyn Score<T>>` instead of `features:
 |  | ||||||
| //  Vec<box dyn ApplicationFeature>>` ?
 |  | ||||||
| //
 |  | ||||||
| //  Let's unpack this :
 |  | ||||||
| //
 |  | ||||||
| //  RustWebappScore<T: Topology> {
 |  | ||||||
| //    features: Vec<box dyn Score<T>>,
 |  | ||||||
| //  }
 |  | ||||||
| //
 |  | ||||||
| //  This brings in a significant problem : RustWebappScore becomes generic, which is a problem for
 |  | ||||||
| //  Clone and Serialize bounds.
 |  | ||||||
| //
 |  | ||||||
| //  But that can be fixed easily I think ?
 |  | ||||||
| //
 |  | ||||||
| //  RustWebappScore<T: Topology + Clone + Serialize> {
 |  | ||||||
| //    features: Vec<box dyn Score<T>>,
 |  | ||||||
| //  }
 |  | ||||||
| //
 |  | ||||||
| //  Oh right not quite because it is `dyn`.
 |  | ||||||
| //
 |  | ||||||
|  | |||||||
| @ -1,6 +1,9 @@ | |||||||
| mod endpoint; | mod endpoint; | ||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| pub use endpoint::*; | pub use endpoint::*; | ||||||
|  | use log::info; | ||||||
|  | 
 | ||||||
|  | use crate::topology::{HelmCommand, Topology}; | ||||||
| 
 | 
 | ||||||
| use super::ApplicationFeature; | use super::ApplicationFeature; | ||||||
| 
 | 
 | ||||||
| @ -8,29 +11,72 @@ use super::ApplicationFeature; | |||||||
| pub struct SoftwareQualityChecks {} | pub struct SoftwareQualityChecks {} | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl ApplicationFeature for SoftwareQualityChecks { | impl<T: Topology + 'static>  ApplicationFeature<T> for SoftwareQualityChecks { | ||||||
|     async fn ensure_installed(&self) -> Result<(), String> { |     // Either allow ApplicationFeature to self-install, which means passing Topology and Inventory
 | ||||||
|  |     // here. This would be a very easy thing to be done reliably by the ApplicationInterpret.
 | ||||||
|  |     // However, I feel like this would probably better be a list of Scores (or some sort of
 | ||||||
|  |     // executable) to be orchestrated by the maestro. We will soon have to manage more complex
 | ||||||
|  |     // lifecycles, dependencies, parallelism, etc.
 | ||||||
|  |     //
 | ||||||
|  |     //
 | ||||||
|  |     // Or change the ApplicationFeature trait to a Score trait.
 | ||||||
|  |     //
 | ||||||
|  |     // For now I'll go with the first option
 | ||||||
|  |     async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { | ||||||
|  |         info!("Ensuring SoftwareQualityChecks are installed for application"); | ||||||
|         todo!() |         todo!() | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     async fn is_installed(&self) -> Result<bool, String> { |     async fn is_installed(&self) -> Result<bool, String> { | ||||||
|         todo!() |         todo!() | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     async fn uninstall(&self) -> Result<(), String> { |     async fn uninstall(&self) -> Result<(), String> { | ||||||
|         todo!() |         todo!() | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// 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
 | ||||||
|  | /// 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)] | #[derive(Debug, Default)] | ||||||
| pub struct ContinuousDelivery {} | pub struct ContinuousDelivery {} | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl ApplicationFeature for ContinuousDelivery { | impl <T: Topology + 'static> ApplicationFeature<T> for ContinuousDelivery { | ||||||
|     async fn ensure_installed(&self) -> Result<(), String> { |     async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { | ||||||
|  |         info!("Installing ContinuousDelivery feature"); | ||||||
|         todo!() |         todo!() | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     async fn is_installed(&self) -> Result<bool, String> { |     async fn is_installed(&self) -> Result<bool, String> { | ||||||
|         todo!() |         todo!() | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     async fn uninstall(&self) -> Result<(), String> { |     async fn uninstall(&self) -> Result<(), String> { | ||||||
|         todo!() |         todo!() | ||||||
|     } |     } | ||||||
| @ -40,10 +86,12 @@ impl ApplicationFeature for ContinuousDelivery { | |||||||
| pub struct Monitoring {} | pub struct Monitoring {} | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl ApplicationFeature for Monitoring { | impl <T: Topology + HelmCommand + 'static> ApplicationFeature<T> for Monitoring { | ||||||
|     async fn ensure_installed(&self) -> Result<(), String> { |     async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { | ||||||
|         todo!() |         info!("Ensuring monitoring is available for application"); | ||||||
|  |         todo!("create and execute k8s prometheus score, depends on Will's work") | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     async fn is_installed(&self) -> Result<bool, String> { |     async fn is_installed(&self) -> Result<bool, String> { | ||||||
|         todo!() |         todo!() | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| mod rust; |  | ||||||
| pub mod features; | pub mod features; | ||||||
|  | mod rust; | ||||||
| pub use rust::*; | pub use rust::*; | ||||||
| 
 | 
 | ||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| @ -32,13 +32,17 @@ impl<T: Topology> Score<T> for GoApplicationScore { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| pub struct ApplicationInterpret { | pub struct ApplicationInterpret<T: Topology + std::fmt::Debug> { | ||||||
|     features: Vec<Box<dyn ApplicationFeature>>, |     features: Vec<Box<dyn ApplicationFeature<T>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl<T: Topology> Interpret<T> for ApplicationInterpret { | impl<T: Topology + std::fmt::Debug> Interpret<T> for ApplicationInterpret<T> { | ||||||
|     async fn execute(&self, _inventory: &Inventory, _topology: &T) -> Result<Outcome, InterpretError> { |     async fn execute( | ||||||
|  |         &self, | ||||||
|  |         _inventory: &Inventory, | ||||||
|  |         _topology: &T, | ||||||
|  |     ) -> Result<Outcome, InterpretError> { | ||||||
|         todo!() |         todo!() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -64,13 +68,13 @@ trait Application {} | |||||||
| /// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability,
 | /// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability,
 | ||||||
| /// ContinuousIntegration, ContinuousDelivery
 | /// ContinuousIntegration, ContinuousDelivery
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| pub trait ApplicationFeature: std::fmt::Debug + Send + Sync { | pub trait ApplicationFeature<T: Topology>: std::fmt::Debug + Send + Sync { | ||||||
|     async fn ensure_installed(&self) -> Result<(), String>; |     async fn ensure_installed(&self, topology: &T) -> Result<(), String>; | ||||||
| 
					
					letian marked this conversation as resolved
					
						
						
							Outdated
						
					
				 | |||||||
|     async fn is_installed(&self) -> Result<bool, String>; |     async fn is_installed(&self) -> Result<bool, String>; | ||||||
|     async fn uninstall(&self) -> Result<(), String>; |     async fn uninstall(&self) -> Result<(), String>; | ||||||
| 
					
					letian marked this conversation as resolved
					
						
						
							Outdated
						
					
				 
				
					
						letian
						commented  
 `is_installed` and `uninstall` seem unnecessary for now | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Serialize for Box<dyn ApplicationFeature> { | impl<T: Topology> Serialize for Box<dyn ApplicationFeature<T>> { | ||||||
|     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> |     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | ||||||
|     where |     where | ||||||
|         S: serde::Serializer, |         S: serde::Serializer, | ||||||
| @ -79,7 +83,7 @@ impl Serialize for Box<dyn ApplicationFeature> { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Clone for Box<dyn ApplicationFeature> { | impl<T: Topology> Clone for Box<dyn ApplicationFeature<T>> { | ||||||
|     fn clone(&self) -> Self { |     fn clone(&self) -> Self { | ||||||
|         todo!() |         todo!() | ||||||
|     } |     } | ||||||
| @ -89,8 +93,8 @@ impl Clone for Box<dyn ApplicationFeature> { | |||||||
| pub struct BackupFeature; | pub struct BackupFeature; | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl ApplicationFeature for BackupFeature { | impl <T: Topology > ApplicationFeature<T> for BackupFeature { | ||||||
|     async fn ensure_installed(&self) -> Result<(), String> { |     async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { | ||||||
|         todo!() |         todo!() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -8,13 +8,13 @@ use crate::{ | |||||||
| use super::{ApplicationFeature, ApplicationInterpret}; | use super::{ApplicationFeature, ApplicationInterpret}; | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, Serialize, Clone)] | #[derive(Debug, Serialize, Clone)] | ||||||
| pub struct RustWebappScore { | pub struct RustWebappScore<T: Topology + Clone + Serialize> { | ||||||
|     pub name: String, |     pub name: String, | ||||||
|     pub domain: Url, |     pub domain: Url, | ||||||
|     pub features: Vec<Box<dyn ApplicationFeature>>, |     pub features: Vec<Box<dyn ApplicationFeature<T>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl<T: Topology> Score<T> for RustWebappScore { | impl<T: Topology + std::fmt::Debug + Clone + Serialize + 'static> Score<T> for RustWebappScore<T> { | ||||||
|     fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { |     fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { | ||||||
|         Box::new(ApplicationInterpret { features: todo!() }) |         Box::new(ApplicationInterpret { features: todo!() }) | ||||||
|     } |     } | ||||||
| @ -23,4 +23,3 @@ impl<T: Topology> Score<T> for RustWebappScore { | |||||||
|         format!("{}-RustWebapp", self.name) |         format!("{}-RustWebapp", self.name) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 |  | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	
naming could be revisited, but good enough for now