From 895fb02f4e58f676a2935ccf8bda747a77677209 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 28 May 2025 22:33:46 -0400 Subject: [PATCH 1/3] feat: Add initial Tenant traits and data structures --- harmony/src/domain/topology/tenant/mod.rs | 186 ++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 harmony/src/domain/topology/tenant/mod.rs diff --git a/harmony/src/domain/topology/tenant/mod.rs b/harmony/src/domain/topology/tenant/mod.rs new file mode 100644 index 0000000..f290ea8 --- /dev/null +++ b/harmony/src/domain/topology/tenant/mod.rs @@ -0,0 +1,186 @@ +use std::collections::HashMap; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] // Assuming serde for Scores +pub struct TenantConfig { + /// A unique, human-readable name for the tenant (e.g., "client-alpha", "project-phoenix"). + /// This will be used as the primary identifier for management operations. + pub name: String, + + /// An optional description for the tenant. + pub description: Option, + + /// Desired resource allocations and limits for the tenant. + pub resource_limits: ResourceLimits, + + /// High-level network isolation policies for the tenant. + pub network_policy: TenantNetworkPolicy, + + /// Key-value pairs for provider-specific tagging, labeling, or metadata. + /// Useful for billing, organization, or filtering within the provider's console. + pub labels_or_tags: HashMap, + // Note: User/group management for the tenant is deferred to a future ADR. + // For now, the TenantManager sets up the space; how it's accessed internally + // by the tenant's own users is a separate concern. +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TenantContext { + /// The provider-specific internal identifier for the tenant's primary isolation unit + /// (e.g., Kubernetes namespace name, OpenStack project ID, AWS Account ID). + pub provider_internal_id: String, + + /// The logical name of the tenant, matching `TenantConfig.name`. + pub name: String, + + /// Current operational status of the tenant. + pub status: TenantStatus, + + /// Effective resource limits currently applied to the tenant. + /// This might differ slightly from requested if the provider adjusted them. + pub effective_resource_limits: ResourceLimits, + + /// Effective network policy currently applied. + pub effective_network_policy: TenantNetworkPolicy, + + /// Additional provider-specific data or endpoints relevant to the tenant. + /// (e.g., K8s API server endpoint scoped to the namespace, if applicable). + pub provider_specific_data: HashMap, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum TenantStatus { + Provisioning, + Active, + Updating, + Deleting, + Error(String), + Unknown, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct ResourceLimits { + /// Requested/guaranteed CPU cores (e.g., 2.0). + pub cpu_request_cores: Option, + /// Maximum CPU cores the tenant can burst to (e.g., 4.0). + pub cpu_limit_cores: Option, + + /// Requested/guaranteed memory in Gigabytes (e.g., 8.0). + pub memory_request_gb: Option, + /// Maximum memory in Gigabytes tenant can burst to (e.g., 16.0). + pub memory_limit_gb: Option, + + /// Total persistent storage allocation in Gigabytes across all volumes. + pub storage_total_gb: Option, + /// Maximum number of distinct persistent volumes/claims. + pub persistent_volume_claim_count: Option, + // /// Optional: Storage limits per class, if needed for more granular control. + // pub storage_gb_per_class: Option>, + + /// Maximum number of load balancers. + pub load_balancer_count: Option, + /// Maximum number of public IP addresses. + pub public_ip_count: Option, + + /// Provider-specific or custom quotas (e.g., "gpu_count:2", "snapshot_count:10"). + /// Values are strings to accommodate various provider formats. + pub custom_quotas: HashMap, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TenantNetworkPolicy { + /// Policy for ingress traffic originating from other tenants within the same Harmony-managed environment. + pub default_inter_tenant_ingress: InterTenantIngressPolicy, + + /// Policy for egress traffic destined for the public internet. + pub default_internet_egress: InternetEgressPolicy, + + /// List of common cluster-internal services this tenant should be ableto access. + pub allowed_cluster_services: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum InterTenantIngressPolicy { + /// Deny all traffic from other tenants by default. + DenyAll, + // Future: AllowFromSameGroup (if tenants can be logically grouped) + // Future: AllowLabeled (if tenants can specify labels for selective inter-tenant comms) +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum InternetEgressPolicy { + /// Allow all outbound traffic to the internet. + AllowAll, + /// Deny all outbound traffic to the internet by default. + DenyAll, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ClusterServiceType { + /// e.g., kube-dns, CoreDNS. + Dns, + /// Access to an internal image registry. + InternalImageRegistry, + /// Access to centralized monitoring endpoints (e.g., Prometheus federation). + MonitoringService, +} + +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; + +use crate::executors::ExecutorError; + +#[async_trait] +pub trait TenantManager: Send + Sync + std::fmt::Debug { + /// Provisions a new tenant based on the provided configuration. + /// This operation should be idempotent; if a tenant with the same `config.name` + /// already exists and matches the config, it may succeed without changes. + /// If it exists but differs, it might return an error or attempt to update (TBD by implementer). + /// + /// # Arguments + /// * `config`: The desired configuration for the new tenant. + /// + /// # Returns + /// A `TenantContext` representing the provisioned tenant. + async fn provision_tenant(&self, config: &TenantConfig) -> Result; + + /// Retrieves the current details and context of an existing tenant. + /// + /// # Arguments + /// * `tenant_name`: The logical name of the tenant to retrieve. + /// + /// # Returns + /// An `Option`, which is `None` if the tenant does not exist. + async fn get_tenant_details(&self, tenant_name: &str) -> Result, ExecutorError>; + + /// Updates the resource limits for an existing tenant. + /// + /// # Arguments + /// * `tenant_name`: The logical name of the tenant to update. + /// * `new_limits`: The new set of resource limits to apply. + async fn update_tenant_resource_limits( + &self, + tenant_name: &str, + new_limits: &ResourceLimits, + ) -> Result<(), ExecutorError>; + + /// Updates the high-level network isolation policy for an existing tenant. + /// + /// # Arguments + /// * `tenant_name`: The logical name of the tenant to update. + /// * `new_policy`: The new network policy to apply. + async fn update_tenant_network_policy( + &self, + tenant_name: &str, + new_policy: &TenantNetworkPolicy, + ) -> Result<(), ExecutorError>; + + /// Decommissions an existing tenant, removing its isolated context and associated resources. + /// This operation should be idempotent. + /// + /// # Arguments + /// * `tenant_name`: The logical name of the tenant to deprovision. + async fn deprovision_tenant(&self, tenant_name: &str) -> Result<(), ExecutorError>; + + /// Lists the logical names of all tenants currently managed by this `TenantManager` instance. + async fn list_tenant_names(&self) -> Result, ExecutorError>; +} From bf16566b4e0bd62d0ece20050680cabdd7519aeb Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 29 May 2025 07:22:30 -0400 Subject: [PATCH 2/3] wip: Clean up some unnecessary bits in the Tenant module and move manager to its own file --- harmony/src/domain/topology/load_balancer.rs | 11 +- harmony/src/domain/topology/mod.rs | 1 + harmony/src/domain/topology/tenant/manager.rs | 65 +++++++++++ harmony/src/domain/topology/tenant/mod.rs | 102 +----------------- harmony/src/modules/helm/command.rs | 5 +- 5 files changed, 77 insertions(+), 107 deletions(-) create mode 100644 harmony/src/domain/topology/tenant/manager.rs diff --git a/harmony/src/domain/topology/load_balancer.rs b/harmony/src/domain/topology/load_balancer.rs index 6127019..7cc326e 100644 --- a/harmony/src/domain/topology/load_balancer.rs +++ b/harmony/src/domain/topology/load_balancer.rs @@ -7,6 +7,12 @@ use serde::Serialize; use super::{IpAddress, LogicalHost}; use crate::executors::ExecutorError; +impl std::fmt::Debug for dyn LoadBalancer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("LoadBalancer {}", self.get_ip())) + } +} + #[async_trait] pub trait LoadBalancer: Send + Sync { fn get_ip(&self) -> IpAddress; @@ -32,11 +38,6 @@ pub trait LoadBalancer: Send + Sync { } } -impl std::fmt::Debug for dyn LoadBalancer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("LoadBalancer {}", self.get_ip())) - } -} #[derive(Debug, PartialEq, Clone, Serialize)] pub struct LoadBalancerService { pub backend_servers: Vec, diff --git a/harmony/src/domain/topology/mod.rs b/harmony/src/domain/topology/mod.rs index 3d773ff..abf317d 100644 --- a/harmony/src/domain/topology/mod.rs +++ b/harmony/src/domain/topology/mod.rs @@ -3,6 +3,7 @@ mod host_binding; mod http; mod k8s_anywhere; mod localhost; +pub mod tenant; pub use k8s_anywhere::*; pub use localhost::*; pub mod k8s; diff --git a/harmony/src/domain/topology/tenant/manager.rs b/harmony/src/domain/topology/tenant/manager.rs new file mode 100644 index 0000000..2aeb772 --- /dev/null +++ b/harmony/src/domain/topology/tenant/manager.rs @@ -0,0 +1,65 @@ +use super::*; +use async_trait::async_trait; + +use crate::executors::ExecutorError; + +#[async_trait] +pub trait TenantManager: Send + Sync + std::fmt::Debug { + /// Provisions a new tenant based on the provided configuration. + /// This operation should be idempotent; if a tenant with the same `config.name` + /// already exists and matches the config, it will succeed without changes. + /// If it exists but differs, it will be updated, or return an error if the update + /// action is not supported + /// + /// # Arguments + /// * `config`: The desired configuration for the new tenant. + /// + /// # Returns + /// A `TenantContext` representing the provisioned tenant. + async fn provision_tenant(&self, config: &TenantConfig) + -> Result; + + /// Retrieves the current details and context of an existing tenant. + /// + /// # Arguments + /// * `tenant_name`: The logical name of the tenant to retrieve. + /// + /// # Returns + /// An `Option`, which is `None` if the tenant does not exist. + async fn get_tenant_details( + &self, + tenant_name: &str, + ) -> Result, ExecutorError>; + + /// Updates the resource limits for an existing tenant. + /// + /// # Arguments + /// * `tenant_name`: The logical name of the tenant to update. + /// * `new_limits`: The new set of resource limits to apply. + async fn update_tenant_resource_limits( + &self, + tenant_name: &str, + new_limits: &ResourceLimits, + ) -> Result<(), ExecutorError>; + + /// Updates the high-level network isolation policy for an existing tenant. + /// + /// # Arguments + /// * `tenant_name`: The logical name of the tenant to update. + /// * `new_policy`: The new network policy to apply. + async fn update_tenant_network_policy( + &self, + tenant_name: &str, + new_policy: &TenantNetworkPolicy, + ) -> Result<(), ExecutorError>; + + /// Decommissions an existing tenant, removing its isolated context and associated resources. + /// This operation should be idempotent. + /// + /// # Arguments + /// * `tenant_name`: The logical name of the tenant to deprovision. + async fn deprovision_tenant(&self, tenant_name: &str) -> Result<(), ExecutorError>; + + /// Lists the logical names of all tenants currently managed by this `TenantManager` instance. + async fn list_tenant_names(&self) -> Result, ExecutorError>; +} diff --git a/harmony/src/domain/topology/tenant/mod.rs b/harmony/src/domain/topology/tenant/mod.rs index f290ea8..383f8f5 100644 --- a/harmony/src/domain/topology/tenant/mod.rs +++ b/harmony/src/domain/topology/tenant/mod.rs @@ -1,3 +1,7 @@ +mod manager; +pub use manager::*; +use serde::{Deserialize, Serialize}; + use std::collections::HashMap; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] // Assuming serde for Scores @@ -32,29 +36,12 @@ pub struct TenantContext { /// The logical name of the tenant, matching `TenantConfig.name`. pub name: String, - /// Current operational status of the tenant. - pub status: TenantStatus, - /// Effective resource limits currently applied to the tenant. /// This might differ slightly from requested if the provider adjusted them. pub effective_resource_limits: ResourceLimits, /// Effective network policy currently applied. pub effective_network_policy: TenantNetworkPolicy, - - /// Additional provider-specific data or endpoints relevant to the tenant. - /// (e.g., K8s API server endpoint scoped to the namespace, if applicable). - pub provider_specific_data: HashMap, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum TenantStatus { - Provisioning, - Active, - Updating, - Deleting, - Error(String), - Unknown, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] @@ -75,15 +62,10 @@ pub struct ResourceLimits { pub persistent_volume_claim_count: Option, // /// Optional: Storage limits per class, if needed for more granular control. // pub storage_gb_per_class: Option>, - /// Maximum number of load balancers. pub load_balancer_count: Option, /// Maximum number of public IP addresses. pub public_ip_count: Option, - - /// Provider-specific or custom quotas (e.g., "gpu_count:2", "snapshot_count:10"). - /// Values are strings to accommodate various provider formats. - pub custom_quotas: HashMap, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -93,17 +75,12 @@ pub struct TenantNetworkPolicy { /// Policy for egress traffic destined for the public internet. pub default_internet_egress: InternetEgressPolicy, - - /// List of common cluster-internal services this tenant should be ableto access. - pub allowed_cluster_services: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum InterTenantIngressPolicy { /// Deny all traffic from other tenants by default. DenyAll, - // Future: AllowFromSameGroup (if tenants can be logically grouped) - // Future: AllowLabeled (if tenants can specify labels for selective inter-tenant comms) } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -113,74 +90,3 @@ pub enum InternetEgressPolicy { /// Deny all outbound traffic to the internet by default. DenyAll, } - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum ClusterServiceType { - /// e.g., kube-dns, CoreDNS. - Dns, - /// Access to an internal image registry. - InternalImageRegistry, - /// Access to centralized monitoring endpoints (e.g., Prometheus federation). - MonitoringService, -} - -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; - -use crate::executors::ExecutorError; - -#[async_trait] -pub trait TenantManager: Send + Sync + std::fmt::Debug { - /// Provisions a new tenant based on the provided configuration. - /// This operation should be idempotent; if a tenant with the same `config.name` - /// already exists and matches the config, it may succeed without changes. - /// If it exists but differs, it might return an error or attempt to update (TBD by implementer). - /// - /// # Arguments - /// * `config`: The desired configuration for the new tenant. - /// - /// # Returns - /// A `TenantContext` representing the provisioned tenant. - async fn provision_tenant(&self, config: &TenantConfig) -> Result; - - /// Retrieves the current details and context of an existing tenant. - /// - /// # Arguments - /// * `tenant_name`: The logical name of the tenant to retrieve. - /// - /// # Returns - /// An `Option`, which is `None` if the tenant does not exist. - async fn get_tenant_details(&self, tenant_name: &str) -> Result, ExecutorError>; - - /// Updates the resource limits for an existing tenant. - /// - /// # Arguments - /// * `tenant_name`: The logical name of the tenant to update. - /// * `new_limits`: The new set of resource limits to apply. - async fn update_tenant_resource_limits( - &self, - tenant_name: &str, - new_limits: &ResourceLimits, - ) -> Result<(), ExecutorError>; - - /// Updates the high-level network isolation policy for an existing tenant. - /// - /// # Arguments - /// * `tenant_name`: The logical name of the tenant to update. - /// * `new_policy`: The new network policy to apply. - async fn update_tenant_network_policy( - &self, - tenant_name: &str, - new_policy: &TenantNetworkPolicy, - ) -> Result<(), ExecutorError>; - - /// Decommissions an existing tenant, removing its isolated context and associated resources. - /// This operation should be idempotent. - /// - /// # Arguments - /// * `tenant_name`: The logical name of the tenant to deprovision. - async fn deprovision_tenant(&self, tenant_name: &str) -> Result<(), ExecutorError>; - - /// Lists the logical names of all tenants currently managed by this `TenantManager` instance. - async fn list_tenant_names(&self) -> Result, ExecutorError>; -} diff --git a/harmony/src/modules/helm/command.rs b/harmony/src/modules/helm/command.rs index 2b28766..41db685 100644 --- a/harmony/src/modules/helm/command.rs +++ b/harmony/src/modules/helm/command.rs @@ -1,12 +1,9 @@ use async_trait::async_trait; use log::debug; -use non_blank_string_rs::NonBlankString; use serde::Serialize; use std::collections::HashMap; -use std::env::temp_dir; -use std::ffi::OsStr; use std::io::ErrorKind; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::process::{Command, Output}; use temp_dir::{self, TempDir}; use temp_file::TempFile; From 7cd541bdd87e155553a621e2cf2bf684d0fc980e Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 29 May 2025 11:47:25 -0400 Subject: [PATCH 3/3] chore: Fix pr comments, remove many YAGNI things --- harmony/src/domain/data/id.rs | 2 +- harmony/src/domain/topology/tenant/manager.rs | 21 +--------- harmony/src/domain/topology/tenant/mod.rs | 42 ++++--------------- 3 files changed, 10 insertions(+), 55 deletions(-) diff --git a/harmony/src/domain/data/id.rs b/harmony/src/domain/data/id.rs index 05717b4..a710721 100644 --- a/harmony/src/domain/data/id.rs +++ b/harmony/src/domain/data/id.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Id { value: String, } diff --git a/harmony/src/domain/topology/tenant/manager.rs b/harmony/src/domain/topology/tenant/manager.rs index 2aeb772..b1b7eb3 100644 --- a/harmony/src/domain/topology/tenant/manager.rs +++ b/harmony/src/domain/topology/tenant/manager.rs @@ -13,23 +13,7 @@ pub trait TenantManager: Send + Sync + std::fmt::Debug { /// /// # Arguments /// * `config`: The desired configuration for the new tenant. - /// - /// # Returns - /// A `TenantContext` representing the provisioned tenant. - async fn provision_tenant(&self, config: &TenantConfig) - -> Result; - - /// Retrieves the current details and context of an existing tenant. - /// - /// # Arguments - /// * `tenant_name`: The logical name of the tenant to retrieve. - /// - /// # Returns - /// An `Option`, which is `None` if the tenant does not exist. - async fn get_tenant_details( - &self, - tenant_name: &str, - ) -> Result, ExecutorError>; + async fn provision_tenant(&self, config: &TenantConfig) -> Result<(), ExecutorError>; /// Updates the resource limits for an existing tenant. /// @@ -59,7 +43,4 @@ pub trait TenantManager: Send + Sync + std::fmt::Debug { /// # Arguments /// * `tenant_name`: The logical name of the tenant to deprovision. async fn deprovision_tenant(&self, tenant_name: &str) -> Result<(), ExecutorError>; - - /// Lists the logical names of all tenants currently managed by this `TenantManager` instance. - async fn list_tenant_names(&self) -> Result, ExecutorError>; } diff --git a/harmony/src/domain/topology/tenant/mod.rs b/harmony/src/domain/topology/tenant/mod.rs index 383f8f5..0704a34 100644 --- a/harmony/src/domain/topology/tenant/mod.rs +++ b/harmony/src/domain/topology/tenant/mod.rs @@ -4,14 +4,16 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use crate::data::Id; + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] // Assuming serde for Scores pub struct TenantConfig { - /// A unique, human-readable name for the tenant (e.g., "client-alpha", "project-phoenix"). - /// This will be used as the primary identifier for management operations. - pub name: String, + /// This will be used as the primary unique identifier for management operations and will never + /// change for the entire lifetime of the tenant + pub id: Id, - /// An optional description for the tenant. - pub description: Option, + /// A human-readable name for the tenant (e.g., "client-alpha", "project-phoenix"). + pub name: String, /// Desired resource allocations and limits for the tenant. pub resource_limits: ResourceLimits, @@ -22,26 +24,6 @@ pub struct TenantConfig { /// Key-value pairs for provider-specific tagging, labeling, or metadata. /// Useful for billing, organization, or filtering within the provider's console. pub labels_or_tags: HashMap, - // Note: User/group management for the tenant is deferred to a future ADR. - // For now, the TenantManager sets up the space; how it's accessed internally - // by the tenant's own users is a separate concern. -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct TenantContext { - /// The provider-specific internal identifier for the tenant's primary isolation unit - /// (e.g., Kubernetes namespace name, OpenStack project ID, AWS Account ID). - pub provider_internal_id: String, - - /// The logical name of the tenant, matching `TenantConfig.name`. - pub name: String, - - /// Effective resource limits currently applied to the tenant. - /// This might differ slightly from requested if the provider adjusted them. - pub effective_resource_limits: ResourceLimits, - - /// Effective network policy currently applied. - pub effective_network_policy: TenantNetworkPolicy, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] @@ -57,15 +39,7 @@ pub struct ResourceLimits { pub memory_limit_gb: Option, /// Total persistent storage allocation in Gigabytes across all volumes. - pub storage_total_gb: Option, - /// Maximum number of distinct persistent volumes/claims. - pub persistent_volume_claim_count: Option, - // /// Optional: Storage limits per class, if needed for more granular control. - // pub storage_gb_per_class: Option>, - /// Maximum number of load balancers. - pub load_balancer_count: Option, - /// Maximum number of public IP addresses. - pub public_ip_count: Option, + pub storage_total_gb: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]