Merge pull request 'TenantManager_impl_k8s_anywhere' (#47) from TenantManager_impl_k8s_anywhere into master
All checks were successful
Run Check Script / check (push) Successful in 1m56s
All checks were successful
Run Check Script / check (push) Successful in 1m56s
Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/47
This commit is contained in:
commit
bf7a6d590c
24
Cargo.lock
generated
24
Cargo.lock
generated
@ -1070,6 +1070,21 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "example-tenant"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cidr",
|
||||
"env_logger",
|
||||
"harmony",
|
||||
"harmony_cli",
|
||||
"harmony_macros",
|
||||
"harmony_types",
|
||||
"log",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "example-tui"
|
||||
version = "0.1.0"
|
||||
@ -1409,12 +1424,14 @@ dependencies = [
|
||||
"derive-new",
|
||||
"directories",
|
||||
"dockerfile_builder",
|
||||
"dyn-clone",
|
||||
"email_address",
|
||||
"env_logger",
|
||||
"fqdn",
|
||||
"harmony_macros",
|
||||
"harmony_types",
|
||||
"helm-wrapper-rs",
|
||||
"hex",
|
||||
"http 1.3.1",
|
||||
"inquire",
|
||||
"k3d-rs",
|
||||
@ -1426,6 +1443,7 @@ dependencies = [
|
||||
"non-blank-string-rs",
|
||||
"opnsense-config",
|
||||
"opnsense-config-xml",
|
||||
"rand 0.9.1",
|
||||
"reqwest 0.11.27",
|
||||
"russh",
|
||||
"rust-ipmi",
|
||||
@ -1550,6 +1568,12 @@ version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hex-literal"
|
||||
version = "0.4.1"
|
||||
|
@ -137,8 +137,9 @@ Our approach addresses both customer and team multi-tenancy requirements:
|
||||
### Implementation Roadmap
|
||||
1. **Phase 1**: Implement VPN access and manual tenant provisioning
|
||||
2. **Phase 2**: Deploy TenantScore automation for namespace, RBAC, and NetworkPolicy management
|
||||
3. **Phase 3**: Integrate Keycloak for centralized identity management
|
||||
4. **Phase 4**: Add advanced monitoring and per-tenant observability
|
||||
4. **Phase 3**: Work on privilege escalation from pods, audit for weaknesses, enforce security policies on pod runtimes
|
||||
3. **Phase 4**: Integrate Keycloak for centralized identity management
|
||||
4. **Phase 5**: Add advanced monitoring and per-tenant observability
|
||||
|
||||
### TenantScore Structure Preview
|
||||
```rust
|
||||
|
41
adr/011-tenant/NetworkPolicy.yaml
Normal file
41
adr/011-tenant/NetworkPolicy.yaml
Normal file
@ -0,0 +1,41 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: tenant-isolation-policy
|
||||
namespace: testtenant
|
||||
spec:
|
||||
podSelector: {} # Selects all pods in the namespace
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector: {} # Allow from all pods in the same namespace
|
||||
egress:
|
||||
- to:
|
||||
- podSelector: {} # Allow to all pods in the same namespace
|
||||
- to:
|
||||
- podSelector: {}
|
||||
namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: openshift-dns # Target the openshift-dns namespace
|
||||
# Note, only opening port 53 is not enough, will have to dig deeper into this one eventually
|
||||
# ports:
|
||||
# - protocol: UDP
|
||||
# port: 53
|
||||
# - protocol: TCP
|
||||
# port: 53
|
||||
# Allow egress to public internet only
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
except:
|
||||
- 10.0.0.0/8 # RFC1918
|
||||
- 172.16.0.0/12 # RFC1918
|
||||
- 192.168.0.0/16 # RFC1918
|
||||
- 169.254.0.0/16 # Link-local
|
||||
- 127.0.0.0/8 # Loopback
|
||||
- 224.0.0.0/4 # Multicast
|
||||
- 240.0.0.0/4 # Reserved
|
||||
- 100.64.0.0/10 # Carrier-grade NAT
|
||||
- 0.0.0.0/8 # Reserved
|
95
adr/011-tenant/TestDeployment.yaml
Normal file
95
adr/011-tenant/TestDeployment.yaml
Normal file
@ -0,0 +1,95 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: testtenant
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: testtenant2
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: test-web
|
||||
namespace: testtenant
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: test-web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: test-web
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginxinc/nginx-unprivileged
|
||||
ports:
|
||||
- containerPort: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: test-web
|
||||
namespace: testtenant
|
||||
spec:
|
||||
selector:
|
||||
app: test-web
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: test-client
|
||||
namespace: testtenant
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: test-client
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: test-client
|
||||
spec:
|
||||
containers:
|
||||
- name: curl
|
||||
image: curlimages/curl:latest
|
||||
command: ["/bin/sh", "-c", "sleep 3600"]
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: test-web
|
||||
namespace: testtenant2
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: test-web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: test-web
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginxinc/nginx-unprivileged
|
||||
ports:
|
||||
- containerPort: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: test-web
|
||||
namespace: testtenant2
|
||||
spec:
|
||||
selector:
|
||||
app: test-web
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
18
examples/tenant/Cargo.toml
Normal file
18
examples/tenant/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "example-tenant"
|
||||
edition = "2024"
|
||||
version.workspace = true
|
||||
readme.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
harmony = { path = "../../harmony" }
|
||||
harmony_cli = { path = "../../harmony_cli" }
|
||||
harmony_types = { path = "../../harmony_types" }
|
||||
cidr = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
harmony_macros = { path = "../../harmony_macros" }
|
||||
log = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
url = { workspace = true }
|
41
examples/tenant/src/main.rs
Normal file
41
examples/tenant/src/main.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use harmony::{
|
||||
data::Id,
|
||||
inventory::Inventory,
|
||||
maestro::Maestro,
|
||||
modules::tenant::TenantScore,
|
||||
topology::{K8sAnywhereTopology, tenant::TenantConfig},
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let tenant = TenantScore {
|
||||
config: TenantConfig {
|
||||
id: Id::default(),
|
||||
name: "TestTenant".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
|
||||
Inventory::autoload(),
|
||||
K8sAnywhereTopology::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
maestro.register_all(vec![Box::new(tenant)]);
|
||||
harmony_cli::init(maestro, None).await.unwrap();
|
||||
}
|
||||
|
||||
// TODO write tests
|
||||
// - Create Tenant with default config mostly, make sure namespace is created
|
||||
// - deploy sample client/server app with nginx unprivileged and a service
|
||||
// - exec in the client pod and validate the following
|
||||
// - can reach internet
|
||||
// - can reach server pod
|
||||
// - can resolve dns queries to internet
|
||||
// - can resolve dns queries to services
|
||||
// - cannot reach services and pods in other namespaces
|
||||
// - Create Tenant with specific cpu/ram/storage requests / limits and make sure they are enforced by trying to
|
||||
// deploy a pod with lower requests/limits (accepted) and higher requests/limits (rejected)
|
||||
// - Create TenantCredentials and make sure they give only access to the correct tenant
|
@ -6,6 +6,8 @@ readme.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
rand = "0.9"
|
||||
hex = "0.4"
|
||||
libredfish = "0.1.1"
|
||||
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
||||
russh = "0.45.0"
|
||||
|
@ -1,5 +1,23 @@
|
||||
use rand::distr::Alphanumeric;
|
||||
use rand::distr::SampleString;
|
||||
use std::time::SystemTime;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A unique identifier designed for ease of use.
|
||||
///
|
||||
/// You can pass it any String to use and Id, or you can use the default format with `Id::default()`
|
||||
///
|
||||
/// The default format looks like this
|
||||
///
|
||||
/// `462d4c_g2COgai`
|
||||
///
|
||||
/// The first part is the unix timesamp in hexadecimal which makes Id easily sorted by creation time.
|
||||
/// Second part is a serie of 7 random characters.
|
||||
///
|
||||
/// **It is not meant to be very secure or unique**, it is suitable to generate up to 10 000 items per
|
||||
/// second with a reasonable collision rate of 0,000014 % as calculated by this calculator : https://kevingal.com/apps/collision.html
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Id {
|
||||
value: String,
|
||||
@ -16,3 +34,20 @@ impl std::fmt::Display for Id {
|
||||
f.write_str(&self.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Id {
|
||||
fn default() -> Self {
|
||||
let start = SystemTime::now();
|
||||
let since_the_epoch = start
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards");
|
||||
let timestamp = since_the_epoch.as_secs();
|
||||
|
||||
let hex_timestamp = format!("{:x}", timestamp & 0xffffff);
|
||||
|
||||
let random_part: String = Alphanumeric.sample_string(&mut rand::rng(), 7);
|
||||
|
||||
let value = format!("{}_{}", hex_timestamp, random_part);
|
||||
Self { value }
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
use derive_new::new;
|
||||
use k8s_openapi::NamespaceResourceScope;
|
||||
use k8s_openapi::{ClusterResourceScope, NamespaceResourceScope};
|
||||
use kube::{
|
||||
Api, Client, Config, Error, Resource,
|
||||
api::PostParams,
|
||||
config::{KubeConfigOptions, Kubeconfig},
|
||||
};
|
||||
use log::error;
|
||||
use log::{debug, error, trace};
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
#[derive(new)]
|
||||
@ -20,52 +20,35 @@ impl K8sClient {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn apply_all<
|
||||
K: Resource<Scope = NamespaceResourceScope>
|
||||
+ std::fmt::Debug
|
||||
+ Sync
|
||||
+ DeserializeOwned
|
||||
+ Default
|
||||
+ serde::Serialize
|
||||
+ Clone,
|
||||
>(
|
||||
&self,
|
||||
resource: &Vec<K>,
|
||||
) -> Result<Vec<K>, kube::Error>
|
||||
pub async fn apply<K>(&self, resource: &K, ns: Option<&str>) -> Result<K, Error>
|
||||
where
|
||||
K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize,
|
||||
<K as Resource>::Scope: ApplyStrategy<K>,
|
||||
<K as kube::Resource>::DynamicType: Default,
|
||||
{
|
||||
let mut result = vec![];
|
||||
for r in resource.iter() {
|
||||
let api: Api<K> = Api::all(self.client.clone());
|
||||
result.push(api.create(&PostParams::default(), &r).await?);
|
||||
}
|
||||
Ok(result)
|
||||
debug!(
|
||||
"Applying resource {:?} with ns {:?}",
|
||||
resource.meta().name,
|
||||
ns
|
||||
);
|
||||
trace!("{:#?}", serde_json::to_string(resource));
|
||||
|
||||
let api: Api<K> = <<K as Resource>::Scope as ApplyStrategy<K>>::get_api(&self.client, ns);
|
||||
api.create(&PostParams::default(), &resource).await
|
||||
}
|
||||
|
||||
pub async fn apply_namespaced<K>(
|
||||
&self,
|
||||
resource: &Vec<K>,
|
||||
ns: Option<&str>,
|
||||
) -> Result<Vec<K>, Error>
|
||||
pub async fn apply_many<K>(&self, resource: &Vec<K>, ns: Option<&str>) -> Result<Vec<K>, Error>
|
||||
where
|
||||
K: Resource<Scope = NamespaceResourceScope>
|
||||
+ Clone
|
||||
+ std::fmt::Debug
|
||||
+ DeserializeOwned
|
||||
+ serde::Serialize
|
||||
+ Default,
|
||||
K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize,
|
||||
<K as Resource>::Scope: ApplyStrategy<K>,
|
||||
<K as kube::Resource>::DynamicType: Default,
|
||||
{
|
||||
let mut resources = Vec::new();
|
||||
let mut result = Vec::new();
|
||||
for r in resource.iter() {
|
||||
let api: Api<K> = match ns {
|
||||
Some(ns) => Api::namespaced(self.client.clone(), ns),
|
||||
None => Api::default_namespaced(self.client.clone()),
|
||||
};
|
||||
resources.push(api.create(&PostParams::default(), &r).await?);
|
||||
result.push(self.apply(r, ns).await?);
|
||||
}
|
||||
Ok(resources)
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(crate) async fn from_kubeconfig(path: &str) -> Option<K8sClient> {
|
||||
@ -86,3 +69,35 @@ impl K8sClient {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ApplyStrategy<K: Resource> {
|
||||
fn get_api(client: &Client, ns: Option<&str>) -> Api<K>;
|
||||
}
|
||||
|
||||
/// Implementation for all resources that are cluster-scoped.
|
||||
/// It will always use `Api::all` and ignore the namespace parameter.
|
||||
impl<K> ApplyStrategy<K> for ClusterResourceScope
|
||||
where
|
||||
K: Resource<Scope = ClusterResourceScope>,
|
||||
<K as kube::Resource>::DynamicType: Default,
|
||||
{
|
||||
fn get_api(client: &Client, _ns: Option<&str>) -> Api<K> {
|
||||
Api::all(client.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation for all resources that are namespace-scoped.
|
||||
/// It will use `Api::namespaced` if a namespace is provided, otherwise
|
||||
/// it falls back to the default namespace configured in your kubeconfig.
|
||||
impl<K> ApplyStrategy<K> for NamespaceResourceScope
|
||||
where
|
||||
K: Resource<Scope = NamespaceResourceScope>,
|
||||
<K as kube::Resource>::DynamicType: Default,
|
||||
{
|
||||
fn get_api(client: &Client, ns: Option<&str>) -> Api<K> {
|
||||
match ns {
|
||||
Some(ns) => Api::namespaced(client.clone(), ns),
|
||||
None => Api::default_namespaced(client.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::{process::Command, sync::Arc};
|
||||
use std::{io::Error, process::Command, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use inquire::Confirm;
|
||||
@ -6,6 +6,7 @@ use log::{info, warn};
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use crate::{
|
||||
data::Id,
|
||||
executors::ExecutorError,
|
||||
interpret::{InterpretError, Outcome},
|
||||
inventory::Inventory,
|
||||
@ -170,6 +171,22 @@ impl K8sAnywhereTopology {
|
||||
Ok(Some(state))
|
||||
}
|
||||
|
||||
async fn ensure_k8s_tenant_manager(&self) -> Result<(), String> {
|
||||
if let Some(_) = self.tenant_manager.get() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.tenant_manager
|
||||
.get_or_try_init(async || -> Result<K8sTenantManager, String> {
|
||||
let k8s_client = self.k8s_client().await?;
|
||||
Ok(K8sTenantManager::new(k8s_client))
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_k8s_tenant_manager(&self) -> Result<&K8sTenantManager, ExecutorError> {
|
||||
match self.tenant_manager.get() {
|
||||
Some(t) => Ok(t),
|
||||
@ -217,6 +234,10 @@ impl Topology for K8sAnywhereTopology {
|
||||
"No K8s client could be found or installed".to_string(),
|
||||
))?;
|
||||
|
||||
self.ensure_k8s_tenant_manager()
|
||||
.await
|
||||
.map_err(|e| InterpretError::new(e))?;
|
||||
|
||||
match self.is_helm_available() {
|
||||
Ok(()) => Ok(Outcome::success(format!(
|
||||
"{} + helm available",
|
||||
@ -239,27 +260,27 @@ impl TenantManager for K8sAnywhereTopology {
|
||||
|
||||
async fn update_tenant_resource_limits(
|
||||
&self,
|
||||
tenant_name: &str,
|
||||
tenant_id: &Id,
|
||||
new_limits: &ResourceLimits,
|
||||
) -> Result<(), ExecutorError> {
|
||||
self.get_k8s_tenant_manager()?
|
||||
.update_tenant_resource_limits(tenant_name, new_limits)
|
||||
.update_tenant_resource_limits(tenant_id, new_limits)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_tenant_network_policy(
|
||||
&self,
|
||||
tenant_name: &str,
|
||||
tenant_id: &Id,
|
||||
new_policy: &TenantNetworkPolicy,
|
||||
) -> Result<(), ExecutorError> {
|
||||
self.get_k8s_tenant_manager()?
|
||||
.update_tenant_network_policy(tenant_name, new_policy)
|
||||
.update_tenant_network_policy(tenant_id, new_policy)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn deprovision_tenant(&self, tenant_name: &str) -> Result<(), ExecutorError> {
|
||||
async fn deprovision_tenant(&self, tenant_id: &Id) -> Result<(), ExecutorError> {
|
||||
self.get_k8s_tenant_manager()?
|
||||
.deprovision_tenant(tenant_name)
|
||||
.deprovision_tenant(tenant_id)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,17 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{executors::ExecutorError, topology::k8s::K8sClient};
|
||||
use crate::{data::Id, executors::ExecutorError, topology::k8s::K8sClient};
|
||||
use async_trait::async_trait;
|
||||
use derive_new::new;
|
||||
use k8s_openapi::api::core::v1::Namespace;
|
||||
use k8s_openapi::{
|
||||
NamespaceResourceScope,
|
||||
api::{
|
||||
core::v1::{Namespace, ResourceQuota},
|
||||
networking::v1::NetworkPolicy,
|
||||
},
|
||||
};
|
||||
use kube::Resource;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::json;
|
||||
|
||||
use super::{ResourceLimits, TenantConfig, TenantManager, TenantNetworkPolicy};
|
||||
@ -13,9 +21,29 @@ pub struct K8sTenantManager {
|
||||
k8s_client: Arc<K8sClient>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TenantManager for K8sTenantManager {
|
||||
async fn provision_tenant(&self, config: &TenantConfig) -> Result<(), ExecutorError> {
|
||||
impl K8sTenantManager {
|
||||
fn get_namespace_name(&self, config: &TenantConfig) -> String {
|
||||
config.name.clone()
|
||||
}
|
||||
|
||||
fn ensure_constraints(&self, namespace: &Namespace) -> Result<(), ExecutorError> {
|
||||
todo!("Validate that when tenant already exists (by id) that name has not changed");
|
||||
todo!("Make sure other Tenant constraints are respected by this k8s implementation");
|
||||
}
|
||||
|
||||
async fn apply_resource<
|
||||
K: Resource + std::fmt::Debug + Sync + DeserializeOwned + Default + serde::Serialize + Clone,
|
||||
>(
|
||||
&self,
|
||||
resource: K,
|
||||
) -> Result<K, ExecutorError>
|
||||
where
|
||||
<K as kube::Resource>::DynamicType: Default,
|
||||
{
|
||||
todo!("Apply tenant labels on resource and apply resource with k8s client properly")
|
||||
}
|
||||
|
||||
fn build_namespace(&self, config: &TenantConfig) -> Result<Namespace, ExecutorError> {
|
||||
let namespace = json!(
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
@ -25,14 +53,19 @@ impl TenantManager for K8sTenantManager {
|
||||
"harmony.nationtech.io/tenant.id": config.id,
|
||||
"harmony.nationtech.io/tenant.name": config.name,
|
||||
},
|
||||
"name": config.name,
|
||||
"name": self.get_namespace_name(config),
|
||||
},
|
||||
}
|
||||
);
|
||||
todo!("Validate that when tenant already exists (by id) that name has not changed");
|
||||
|
||||
let namespace: Namespace = serde_json::from_value(namespace).unwrap();
|
||||
serde_json::from_value(namespace).map_err(|e| {
|
||||
ExecutorError::ConfigurationError(format!(
|
||||
"Could not build TenantManager Namespace. {}",
|
||||
e
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn build_resource_quota(&self, config: &TenantConfig) -> Result<ResourceQuota, ExecutorError> {
|
||||
let resource_quota = json!(
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
@ -47,7 +80,7 @@ impl TenantManager for K8sTenantManager {
|
||||
"harmony.nationtech.io/tenant.id": config.id,
|
||||
"harmony.nationtech.io/tenant.name": config.name,
|
||||
},
|
||||
"namespace": config.name,
|
||||
"namespace": self.get_namespace_name(config),
|
||||
},
|
||||
"spec": {
|
||||
"hard": {
|
||||
@ -71,11 +104,104 @@ impl TenantManager for K8sTenantManager {
|
||||
}
|
||||
|
||||
);
|
||||
serde_json::from_value(resource_quota).map_err(|e| {
|
||||
ExecutorError::ConfigurationError(format!(
|
||||
"Could not build TenantManager ResourceQuota. {}",
|
||||
e
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn build_network_policy(&self, config: &TenantConfig) -> Result<NetworkPolicy, ExecutorError> {
|
||||
let network_policy = json!({
|
||||
"apiVersion": "networking.k8s.io/v1",
|
||||
"kind": "NetworkPolicy",
|
||||
"metadata": {
|
||||
"name": format!("{}-network-policy", config.name),
|
||||
},
|
||||
"spec": {
|
||||
"podSelector": {},
|
||||
"egress": [
|
||||
{ "to": [ {"podSelector": {}}]},
|
||||
{ "to":
|
||||
[
|
||||
{
|
||||
"podSelector": {},
|
||||
"namespaceSelector": {
|
||||
"matchLabels": {
|
||||
"kubernetes.io/metadata.name":"openshift-dns"
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{ "to": [
|
||||
{
|
||||
"ipBlock": {
|
||||
|
||||
"cidr": "0.0.0.0/0",
|
||||
// See https://en.wikipedia.org/wiki/Reserved_IP_addresses
|
||||
"except": [
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"192.0.0.0/24",
|
||||
"192.0.2.0/24",
|
||||
"192.88.99.0/24",
|
||||
"192.18.0.0/15",
|
||||
"198.51.100.0/24",
|
||||
"169.254.0.0/16",
|
||||
"203.0.113.0/24",
|
||||
"127.0.0.0/8",
|
||||
|
||||
// Not sure we should block this one as it is
|
||||
// used for multicast. But better block more than less.
|
||||
"224.0.0.0/4",
|
||||
"240.0.0.0/4",
|
||||
"100.64.0.0/10",
|
||||
"233.252.0.0/24",
|
||||
"0.0.0.0/8",
|
||||
],
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
"ingress": [
|
||||
{ "from": [ {"podSelector": {}}]}
|
||||
],
|
||||
"policyTypes": [
|
||||
"Ingress", "Egress",
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
serde_json::from_value(network_policy).map_err(|e| {
|
||||
ExecutorError::ConfigurationError(format!(
|
||||
"Could not build TenantManager NetworkPolicy. {}",
|
||||
e
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TenantManager for K8sTenantManager {
|
||||
async fn provision_tenant(&self, config: &TenantConfig) -> Result<(), ExecutorError> {
|
||||
let namespace = self.build_namespace(config)?;
|
||||
let resource_quota = self.build_resource_quota(config)?;
|
||||
let network_policy = self.build_network_policy(config)?;
|
||||
|
||||
self.ensure_constraints(&namespace)?;
|
||||
self.apply_resource(namespace).await?;
|
||||
self.apply_resource(resource_quota).await?;
|
||||
self.apply_resource(network_policy).await?;
|
||||
todo!();
|
||||
}
|
||||
|
||||
async fn update_tenant_resource_limits(
|
||||
&self,
|
||||
tenant_name: &str,
|
||||
tenant_id: &Id,
|
||||
new_limits: &ResourceLimits,
|
||||
) -> Result<(), ExecutorError> {
|
||||
todo!()
|
||||
@ -83,13 +209,13 @@ impl TenantManager for K8sTenantManager {
|
||||
|
||||
async fn update_tenant_network_policy(
|
||||
&self,
|
||||
tenant_name: &str,
|
||||
tenant_id: &Id,
|
||||
new_policy: &TenantNetworkPolicy,
|
||||
) -> Result<(), ExecutorError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn deprovision_tenant(&self, tenant_name: &str) -> Result<(), ExecutorError> {
|
||||
async fn deprovision_tenant(&self, tenant_id: &Id) -> Result<(), ExecutorError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
@ -16,31 +16,20 @@ pub trait TenantManager {
|
||||
async fn provision_tenant(&self, config: &TenantConfig) -> 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,
|
||||
tenant_id: &Id,
|
||||
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,
|
||||
tenant_id: &Id,
|
||||
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>;
|
||||
async fn deprovision_tenant(&self, tenant_id: &Id) -> Result<(), ExecutorError>;
|
||||
}
|
||||
|
@ -27,6 +27,28 @@ pub struct TenantConfig {
|
||||
pub labels_or_tags: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Default for TenantConfig {
|
||||
fn default() -> Self {
|
||||
let id = Id::default();
|
||||
Self {
|
||||
name: format!("tenant_{id}"),
|
||||
id,
|
||||
resource_limits: ResourceLimits {
|
||||
cpu_request_cores: 4.0,
|
||||
cpu_limit_cores: 4.0,
|
||||
memory_request_gb: 4.0,
|
||||
memory_limit_gb: 4.0,
|
||||
storage_total_gb: 20.0,
|
||||
},
|
||||
network_policy: TenantNetworkPolicy {
|
||||
default_inter_tenant_ingress: InterTenantIngressPolicy::DenyAll,
|
||||
default_internet_egress: InternetEgressPolicy::AllowAll,
|
||||
},
|
||||
labels_or_tags: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
pub struct ResourceLimits {
|
||||
/// Requested/guaranteed CPU cores (e.g., 2.0).
|
||||
|
@ -1,5 +1,6 @@
|
||||
use harmony_macros::ingress_path;
|
||||
use k8s_openapi::api::networking::v1::Ingress;
|
||||
use log::{debug, trace};
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
|
||||
@ -56,22 +57,24 @@ impl<T: Topology + K8sclient> Score<T> for K8sIngressScore {
|
||||
let ingress = json!(
|
||||
{
|
||||
"metadata": {
|
||||
"name": self.name
|
||||
"name": self.name.to_string(),
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{ "host": self.host,
|
||||
{ "host": self.host.to_string(),
|
||||
"http": {
|
||||
"paths": [
|
||||
{
|
||||
"path": path,
|
||||
"pathType": path_type.as_str(),
|
||||
"backend": [
|
||||
{
|
||||
"service": self.backend_service,
|
||||
"port": self.port
|
||||
"backend": {
|
||||
"service": {
|
||||
"name": self.backend_service.to_string(),
|
||||
"port": {
|
||||
"number": self.port,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -81,13 +84,16 @@ impl<T: Topology + K8sclient> Score<T> for K8sIngressScore {
|
||||
}
|
||||
);
|
||||
|
||||
trace!("Building ingresss object from Value {ingress:#}");
|
||||
let ingress: Ingress = serde_json::from_value(ingress).unwrap();
|
||||
debug!(
|
||||
"Successfully built Ingress for host {:?}",
|
||||
ingress.metadata.name
|
||||
);
|
||||
Box::new(K8sResourceInterpret {
|
||||
score: K8sResourceScore::single(
|
||||
ingress.clone(),
|
||||
self.namespace
|
||||
.clone()
|
||||
.map(|f| f.as_c_str().to_str().unwrap().to_string()),
|
||||
self.namespace.clone().map(|f| f.to_string()),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
use k8s_openapi::NamespaceResourceScope;
|
||||
use kube::Resource;
|
||||
use log::info;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
||||
use crate::{
|
||||
@ -75,11 +76,12 @@ where
|
||||
_inventory: &Inventory,
|
||||
topology: &T,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
info!("Applying {} resources", self.score.resource.len());
|
||||
topology
|
||||
.k8s_client()
|
||||
.await
|
||||
.expect("Environment should provide enough information to instanciate a client")
|
||||
.apply_namespaced(&self.score.resource, self.score.namespace.as_deref())
|
||||
.apply_many(&self.score.resource, self.score.namespace.as_deref())
|
||||
.await?;
|
||||
|
||||
Ok(Outcome::success(
|
||||
|
@ -14,7 +14,7 @@ use crate::{
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct TenantScore {
|
||||
config: TenantConfig,
|
||||
pub config: TenantConfig,
|
||||
}
|
||||
|
||||
impl<T: Topology + TenantManager> Score<T> for TenantScore {
|
||||
|
Loading…
Reference in New Issue
Block a user