feat: Application module architecture and placeholder features #70

Merged
letian merged 11 commits from feat/applicationModule into master 2025-07-01 19:40:43 +00:00
11 changed files with 242 additions and 18 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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>,
}

View File

@ -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!()
}
}

View 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!()
}
}

View File

@ -0,0 +1,8 @@
mod endpoint;
pub use endpoint::*;
mod monitoring;
pub use monitoring::*;
mod continuous_delivery;
pub use continuous_delivery::*;

View 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")
}
}

View 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!()
}
}

View 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)
}
}

View File

@ -1,3 +1,4 @@
pub mod application;
pub mod cert_manager;
pub mod dhcp;
pub mod dns;

View File

@ -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>(