feat: Application module architecture and placeholder features (#70)
With this architecture, we have an extensible application module for which we can easily define new features and add them to application scores. All this is driven by the ApplicationInterpret, who understands features and make sure they are "installed". The drawback of this design is that we now have three different places to launch scores within Harmony : Maestro, Topology and Interpret. This is an architectural smell and I am not sure how to deal with it at the moment. However, all these places where execution is performed make sense semantically : an ApplicationInterpret must understand ApplicationFeatures and can very well be responsible of them. Same goes for a Topology which provides features itself by composition (ex. K8sAnywhereTopology implements TenantManager) so it is natural for this very imp lementation to know how to install itself. Co-authored-by: Ian Letourneau <ian@noma.to> Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/70 Co-authored-by: Jean-Gabriel Gill-Couture <jg@nationtech.io> Co-committed-by: Jean-Gabriel Gill-Couture <jg@nationtech.io>
This commit is contained in:
parent
9bf6aac82e
commit
284cc6afd7
@ -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<Self, Error> {
|
||||
Ok(Self {
|
||||
|
@ -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<K8sClient>,
|
||||
_source: K8sSource,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
enum K8sSource {
|
||||
LocalK3d,
|
||||
Kubeconfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct K8sAnywhereTopology {
|
||||
k8s_state: OnceCell<Option<K8sState>>,
|
||||
tenant_manager: OnceCell<K8sTenantManager>,
|
||||
config: K8sAnywhereConfig,
|
||||
k8s_state: Arc<OnceCell<Option<K8sState>>>,
|
||||
tenant_manager: Arc<OnceCell<K8sTenantManager>>,
|
||||
config: Arc<K8sAnywhereConfig>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -55,20 +58,29 @@ 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 {
|
||||
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
|
||||
|
@ -22,7 +22,7 @@ use serde_json::json;
|
||||
|
||||
use super::{TenantConfig, TenantManager};
|
||||
|
||||
#[derive(new)]
|
||||
#[derive(new, Clone, Debug)]
|
||||
pub struct K8sTenantManager {
|
||||
k8s_client: Arc<K8sClient>,
|
||||
}
|
||||
|
@ -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<T: Topology + 'static> ApplicationFeature<T> for ContinuousDelivery {
|
||||
async fn ensure_installed(&self, _topology: &T) -> Result<(), String> {
|
||||
info!("Installing ContinuousDelivery feature");
|
||||
todo!()
|
||||
}
|
||||
}
|
39
harmony/src/modules/application/features/endpoint.rs
Normal file
39
harmony/src/modules/application/features/endpoint.rs
Normal file
@ -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<T: Topology + K8sclient + 'static> ApplicationFeature<T> for PublicEndpoint {
|
||||
async fn ensure_installed(&self, _topology: &T) -> Result<(), String> {
|
||||
info!(
|
||||
"Making sure public endpoint is installed for port {}",
|
||||
self.application_port
|
||||
);
|
||||
todo!()
|
||||
}
|
||||
}
|
8
harmony/src/modules/application/features/mod.rs
Normal file
8
harmony/src/modules/application/features/mod.rs
Normal file
@ -0,0 +1,8 @@
|
||||
mod endpoint;
|
||||
pub use endpoint::*;
|
||||
|
||||
mod monitoring;
|
||||
pub use monitoring::*;
|
||||
|
||||
mod continuous_delivery;
|
||||
pub use continuous_delivery::*;
|
18
harmony/src/modules/application/features/monitoring.rs
Normal file
18
harmony/src/modules/application/features/monitoring.rs
Normal file
@ -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<T: Topology + HelmCommand + 'static> ApplicationFeature<T> 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")
|
||||
}
|
||||
}
|
67
harmony/src/modules/application/mod.rs
Normal file
67
harmony/src/modules/application/mod.rs
Normal file
@ -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<T: Topology + std::fmt::Debug> {
|
||||
features: Vec<Box<dyn ApplicationFeature<T>>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Topology + std::fmt::Debug> Interpret<T> for ApplicationInterpret<T> {
|
||||
async fn execute(
|
||||
&self,
|
||||
_inventory: &Inventory,
|
||||
_topology: &T,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_name(&self) -> InterpretName {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_version(&self) -> Version {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_status(&self) -> InterpretStatus {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_children(&self) -> Vec<Id> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability,
|
||||
/// ContinuousIntegration, ContinuousDelivery
|
||||
#[async_trait]
|
||||
pub trait ApplicationFeature<T: Topology>: std::fmt::Debug + Send + Sync {
|
||||
async fn ensure_installed(&self, topology: &T) -> Result<(), String>;
|
||||
}
|
||||
|
||||
impl<T: Topology> Serialize for Box<dyn ApplicationFeature<T>> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Topology> Clone for Box<dyn ApplicationFeature<T>> {
|
||||
fn clone(&self) -> Self {
|
||||
todo!()
|
||||
}
|
||||
}
|
25
harmony/src/modules/application/rust.rs
Normal file
25
harmony/src/modules/application/rust.rs
Normal file
@ -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<T: Topology + Clone + Serialize> {
|
||||
pub name: String,
|
||||
pub domain: Url,
|
||||
pub features: Vec<Box<dyn ApplicationFeature<T>>>,
|
||||
}
|
||||
|
||||
impl<T: Topology + std::fmt::Debug + Clone + Serialize + 'static> Score<T> for RustWebappScore<T> {
|
||||
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
|
||||
Box::new(ApplicationInterpret { features: todo!() })
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
format!("{}-RustWebapp", self.name)
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
pub mod application;
|
||||
pub mod cert_manager;
|
||||
pub mod dhcp;
|
||||
pub mod dns;
|
||||
|
@ -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<String>,
|
||||
pub filter: Option<String>,
|
||||
|
||||
/// 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<T: Topology>(
|
||||
|
Loading…
Reference in New Issue
Block a user