Compare commits
5 Commits
runtime-pr
...
08dbfd827b
| Author | SHA1 | Date | |
|---|---|---|---|
| 08dbfd827b | |||
| ffe175df1b | |||
| f9d1935c71 | |||
| 4f8523ab69 | |||
| 6ddf48591b |
1747
Cargo.lock
generated
1747
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
13
Cargo.toml
@@ -9,8 +9,6 @@ members = [
|
|||||||
"harmony_tui",
|
"harmony_tui",
|
||||||
"opnsense-config",
|
"opnsense-config",
|
||||||
"opnsense-config-xml",
|
"opnsense-config-xml",
|
||||||
"harmony_cli",
|
|
||||||
"k3d",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
@@ -23,23 +21,22 @@ log = "0.4.22"
|
|||||||
env_logger = "0.11.5"
|
env_logger = "0.11.5"
|
||||||
derive-new = "0.7.0"
|
derive-new = "0.7.0"
|
||||||
async-trait = "0.1.82"
|
async-trait = "0.1.82"
|
||||||
tokio = { version = "1.40.0", features = ["io-std", "fs", "macros", "rt-multi-thread"] }
|
tokio = { version = "1.40.0", features = ["io-std", "fs"] }
|
||||||
cidr = "0.2.3"
|
cidr = "0.2.3"
|
||||||
russh = "0.45.0"
|
russh = "0.45.0"
|
||||||
russh-keys = "0.45.0"
|
russh-keys = "0.45.0"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
kube = "0.98.0"
|
kube = "0.98.0"
|
||||||
k8s-openapi = { version = "0.24.0", features = ["v1_30"] }
|
k8s-openapi = { version = "0.24.0", features = [ "v1_30" ] }
|
||||||
serde_yaml = "0.9.34"
|
serde_yaml = "0.9.34"
|
||||||
serde-value = "0.7.0"
|
serde-value = "0.7.0"
|
||||||
http = "1.2.0"
|
http = "1.2.0"
|
||||||
inquire = "0.7.5"
|
|
||||||
|
|
||||||
[workspace.dependencies.uuid]
|
[workspace.dependencies.uuid]
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
features = [
|
features = [
|
||||||
"v4", # Lets you generate random UUIDs
|
"v4", # Lets you generate random UUIDs
|
||||||
"fast-rng", # Use a faster (but still sufficiently random) RNG
|
"fast-rng", # Use a faster (but still sufficiently random) RNG
|
||||||
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
|
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
|
||||||
]
|
]
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -8,26 +8,6 @@ This will launch Harmony's minimalist terminal ui which embeds a few demo scores
|
|||||||
|
|
||||||
Usage instructions will be displayed at the bottom of the TUI.
|
Usage instructions will be displayed at the bottom of the TUI.
|
||||||
|
|
||||||
`cargo run --bin example-cli -- --help`
|
|
||||||
|
|
||||||
This is the harmony CLI, a minimal implementation
|
|
||||||
|
|
||||||
The current help text:
|
|
||||||
|
|
||||||
````
|
|
||||||
Usage: example-cli [OPTIONS]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-y, --yes Run score(s) or not
|
|
||||||
-f, --filter <FILTER> Filter query
|
|
||||||
-i, --interactive Run interactive TUI or not
|
|
||||||
-a, --all Run all or nth, defaults to all
|
|
||||||
-n, --number <NUMBER> Run nth matching, zero indexed [default: 0]
|
|
||||||
-l, --list list scores, will also be affected by run filter
|
|
||||||
-h, --help Print help
|
|
||||||
-V, --version Print version```
|
|
||||||
|
|
||||||
## Core architecture
|
## Core architecture
|
||||||
|
|
||||||

|

|
||||||
````
|
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
# Architecture Decision Record: Monitoring and Alerting
|
|
||||||
|
|
||||||
Proposed by: Willem Rolleman
|
|
||||||
Date: April 28 2025
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Proposed
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
A harmony user should be able to initialize a monitoring stack easily, either at the first run of Harmony, or that integrates with existing proects and infra without creating multiple instances of the monitoring stack or overwriting existing alerts/configurations.The user also needs a simple way to configure the stack so that it watches the projects. There should be reasonable defaults configured that are easily customizable for each project
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
Create MonitoringStack score that creates a maestro to launch the monitoring stack or not if it is already present.
|
|
||||||
The MonitoringStack score can be passed to the maestro in the vec! scores list
|
|
||||||
|
|
||||||
## Rationale
|
|
||||||
|
|
||||||
Having the score launch a maestro will allow the user to easily create a new monitoring stack and keeps composants grouped together. The MonitoringScore can handle all the logic for adding alerts, ensuring that the stack is running etc.
|
|
||||||
|
|
||||||
## Alerternatives considered
|
|
||||||
|
|
||||||
- ### Implement alerting and monitoring stack using existing HelmScore for each project
|
|
||||||
- **Pros**:
|
|
||||||
- Each project can choose to use the monitoring and alerting stack that they choose
|
|
||||||
- Less overhead in terms of care harmony code
|
|
||||||
- can add Box::new(grafana::grafanascore(namespace))
|
|
||||||
- **Cons**:
|
|
||||||
- No default solution implemented
|
|
||||||
- Dev needs to chose what they use
|
|
||||||
- Increases complexity of score projects
|
|
||||||
- Each project will create a new monitoring and alerting instance rather than joining the existing one
|
|
||||||
|
|
||||||
|
|
||||||
- ### Use OKD grafana and prometheus
|
|
||||||
- **Pros**:
|
|
||||||
- Minimal config to do in Harmony
|
|
||||||
- **Cons**:
|
|
||||||
- relies on OKD so will not working for local testing via k3d
|
|
||||||
|
|
||||||
- ### Create a monitoring and alerting crate similar to harmony tui
|
|
||||||
- **Pros**:
|
|
||||||
- Creates a default solution that can be implemented once by harmony
|
|
||||||
- can create a join function that will allow a project to connect to the existing solution
|
|
||||||
- eliminates risk of creating multiple instances of grafana or prometheus
|
|
||||||
- **Cons**:
|
|
||||||
- more complex than using a helm score
|
|
||||||
- management of values files for individual functions becomes more complicated, ie how do you create alerts for one project via helm install that doesnt overwrite the other alerts
|
|
||||||
|
|
||||||
- ### Add monitoring to Maestro struct so whether the monitoring stack is used must be defined
|
|
||||||
- **Pros**:
|
|
||||||
- less for the user to define
|
|
||||||
- may be easier to set defaults
|
|
||||||
- **Cons**:
|
|
||||||
- feels counterintuitive
|
|
||||||
- would need to modify the structure of the maestro and how it operates which seems like a bad idea
|
|
||||||
- unclear how to allow user to pass custom values/configs to the monitoring stack for subsequent projects
|
|
||||||
|
|
||||||
- ### Create MonitoringStack score to add to scores vec! which loads a maestro to install stack if not ready or add custom endpoints/alerts to existing stack
|
|
||||||
- **Pros**:
|
|
||||||
- Maestro already accepts a list of scores to initialize
|
|
||||||
- leaving out the monitoring score simply means the user does not want monitoring
|
|
||||||
- if the monitoring stack is already created, the MonitoringStack score doesn't necessarily need to be added to each project
|
|
||||||
- composants of the monitoring stack are bundled together and can be expaned or modified from the same place
|
|
||||||
- **Cons**:
|
|
||||||
- maybe need to create
|
|
||||||
5
check.sh
5
check.sh
@@ -1,5 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
cargo check --all-targets --all-features --keep-going
|
|
||||||
cargo fmt --check
|
|
||||||
cargo test
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "example-cli"
|
|
||||||
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 }
|
|
||||||
assert_cmd = "2.0.16"
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
use harmony::{
|
|
||||||
inventory::Inventory,
|
|
||||||
maestro::Maestro,
|
|
||||||
modules::dummy::{ErrorScore, PanicScore, SuccessScore},
|
|
||||||
topology::LocalhostTopology,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let inventory = Inventory::autoload();
|
|
||||||
let topology = LocalhostTopology::new();
|
|
||||||
let mut maestro = Maestro::initialize(inventory, topology).await.unwrap();
|
|
||||||
|
|
||||||
maestro.register_all(vec![
|
|
||||||
Box::new(SuccessScore {}),
|
|
||||||
Box::new(ErrorScore {}),
|
|
||||||
Box::new(PanicScore {}),
|
|
||||||
]);
|
|
||||||
harmony_cli::init(maestro, None).await.unwrap();
|
|
||||||
}
|
|
||||||
@@ -18,4 +18,3 @@ kube = "0.98.0"
|
|||||||
k8s-openapi = { version = "0.24.0", features = [ "v1_30" ] }
|
k8s-openapi = { version = "0.24.0", features = [ "v1_30" ] }
|
||||||
http = "1.2.0"
|
http = "1.2.0"
|
||||||
serde_yaml = "0.9.34"
|
serde_yaml = "0.9.34"
|
||||||
inquire.workspace = true
|
|
||||||
|
|||||||
@@ -1,32 +1,20 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use harmony_macros::yaml;
|
use harmony_macros::yaml;
|
||||||
use inquire::Confirm;
|
|
||||||
use k8s_openapi::{
|
use k8s_openapi::{
|
||||||
api::{
|
api::{
|
||||||
apps::v1::{Deployment, DeploymentSpec},
|
apps::v1::{Deployment, DeploymentSpec},
|
||||||
core::v1::{Container, PodSpec, PodTemplateSpec},
|
core::v1::{Container, Node, Pod, PodSpec, PodTemplateSpec},
|
||||||
},
|
},
|
||||||
apimachinery::pkg::apis::meta::v1::LabelSelector,
|
apimachinery::pkg::apis::meta::v1::LabelSelector,
|
||||||
};
|
};
|
||||||
use kube::{
|
use kube::{
|
||||||
Api, Client, ResourceExt,
|
Api, Client, Config, ResourceExt,
|
||||||
api::{ObjectMeta, PostParams},
|
api::{ListParams, ObjectMeta, PostParams},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let confirmation = Confirm::new(
|
|
||||||
"This will install various ressources to your default kubernetes cluster. Are you sure?",
|
|
||||||
)
|
|
||||||
.with_default(false)
|
|
||||||
.prompt()
|
|
||||||
.expect("Unexpected prompt error");
|
|
||||||
|
|
||||||
if !confirmation {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let client = Client::try_default()
|
let client = Client::try_default()
|
||||||
.await
|
.await
|
||||||
.expect("Should instanciate client from defaults");
|
.expect("Should instanciate client from defaults");
|
||||||
@@ -54,7 +42,8 @@ async fn main() {
|
|||||||
// println!("found node {} status {:?}", n.name_any(), n.status.unwrap())
|
// println!("found node {} status {:?}", n.name_any(), n.status.unwrap())
|
||||||
// }
|
// }
|
||||||
|
|
||||||
assert_eq!(nginx_deployment(), nginx_macro());
|
let nginxdeployment = nginx_deployment_2();
|
||||||
|
let nginxdeployment = nginx_deployment_serde();
|
||||||
assert_eq!(nginx_deployment_2(), nginx_macro());
|
assert_eq!(nginx_deployment_2(), nginx_macro());
|
||||||
assert_eq!(nginx_deployment_serde(), nginx_macro());
|
assert_eq!(nginx_deployment_serde(), nginx_macro());
|
||||||
let nginxdeployment = nginx_macro();
|
let nginxdeployment = nginx_macro();
|
||||||
@@ -160,7 +149,6 @@ fn nginx_deployment_2() -> Deployment {
|
|||||||
|
|
||||||
deployment
|
deployment
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nginx_deployment() -> Deployment {
|
fn nginx_deployment() -> Deployment {
|
||||||
let deployment = Deployment {
|
let deployment = Deployment {
|
||||||
metadata: ObjectMeta {
|
metadata: ObjectMeta {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
harmony = { path = "../../harmony" }
|
harmony = { path = "../../harmony" }
|
||||||
harmony_tui = { path = "../../harmony_tui" }
|
#harmony_tui = { path = "../../harmony_tui" }
|
||||||
harmony_types = { path = "../../harmony_types" }
|
harmony_types = { path = "../../harmony_types" }
|
||||||
cidr = { workspace = true }
|
cidr = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
use harmony::{
|
use harmony::{
|
||||||
data::Version,
|
data::Version,
|
||||||
inventory::Inventory,
|
|
||||||
maestro::Maestro,
|
maestro::Maestro,
|
||||||
modules::lamp::{LAMPConfig, LAMPProfile, LAMPScore},
|
modules::lamp::{LAMPConfig, LAMPScore},
|
||||||
topology::{K8sAnywhereTopology, Url},
|
topology::{HAClusterTopology, Url},
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// let _ = env_logger::Builder::from_default_env().filter_level(log::LevelFilter::Info).try_init();
|
|
||||||
let lamp_stack = LAMPScore {
|
let lamp_stack = LAMPScore {
|
||||||
name: "harmony-lamp-demo".to_string(),
|
name: "harmony-lamp-demo".to_string(),
|
||||||
domain: Url::Url(url::Url::parse("https://lampdemo.harmony.nationtech.io").unwrap()),
|
domain: Url::Url(url::Url::parse("https://lampdemo.harmony.nationtech.io").unwrap()),
|
||||||
@@ -18,18 +15,10 @@ async fn main() {
|
|||||||
project_root: "./php".into(),
|
project_root: "./php".into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
profiles: HashMap::from([
|
|
||||||
("dev", LAMPProfile { ssl_enabled: false }),
|
|
||||||
("prod", LAMPProfile { ssl_enabled: true }),
|
|
||||||
]),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
|
Maestro::<HAClusterTopology>::load_from_env()
|
||||||
Inventory::autoload(),
|
.interpret(Box::new(lamp_stack))
|
||||||
K8sAnywhereTopology::new(),
|
.await
|
||||||
)
|
.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
maestro.register_all(vec![Box::new(lamp_stack)]);
|
|
||||||
harmony_tui::init(maestro).await.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
use harmony::{
|
use harmony::{
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
maestro::Maestro,
|
maestro::Maestro,
|
||||||
modules::dummy::{ErrorScore, PanicScore, SuccessScore},
|
modules::{
|
||||||
|
dummy::{ErrorScore, PanicScore, SuccessScore},
|
||||||
|
k8s::deployment::K8sDeploymentScore,
|
||||||
|
},
|
||||||
topology::HAClusterTopology,
|
topology::HAClusterTopology,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -9,7 +12,7 @@ use harmony::{
|
|||||||
async fn main() {
|
async fn main() {
|
||||||
let inventory = Inventory::autoload();
|
let inventory = Inventory::autoload();
|
||||||
let topology = HAClusterTopology::autoload();
|
let topology = HAClusterTopology::autoload();
|
||||||
let mut maestro = Maestro::initialize(inventory, topology).await.unwrap();
|
let mut maestro = Maestro::new(inventory, topology);
|
||||||
|
|
||||||
maestro.register_all(vec![
|
maestro.register_all(vec![
|
||||||
// ADD scores :
|
// ADD scores :
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ async fn main() {
|
|||||||
let http_score = HttpScore::new(Url::LocalFolder(
|
let http_score = HttpScore::new(Url::LocalFolder(
|
||||||
"./data/watchguard/pxe-http-files".to_string(),
|
"./data/watchguard/pxe-http-files".to_string(),
|
||||||
));
|
));
|
||||||
let mut maestro = Maestro::initialize(inventory, topology).await.unwrap();
|
let mut maestro = Maestro::new(inventory, topology);
|
||||||
maestro.register_all(vec![
|
maestro.register_all(vec![
|
||||||
Box::new(dns_score),
|
Box::new(dns_score),
|
||||||
Box::new(dhcp_score),
|
Box::new(dhcp_score),
|
||||||
|
|||||||
@@ -1,70 +1,24 @@
|
|||||||
use std::net::{SocketAddr, SocketAddrV4};
|
|
||||||
|
|
||||||
use harmony::{
|
use harmony::{
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
maestro::Maestro,
|
maestro::Maestro,
|
||||||
modules::{
|
modules::{
|
||||||
dns::DnsScore,
|
dns::DnsScore,
|
||||||
dummy::{ErrorScore, PanicScore, SuccessScore},
|
dummy::{ErrorScore, PanicScore, SuccessScore},
|
||||||
load_balancer::LoadBalancerScore,
|
|
||||||
},
|
|
||||||
topology::{
|
|
||||||
BackendServer, DummyInfra, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancerService,
|
|
||||||
},
|
},
|
||||||
|
topology::HAClusterTopology,
|
||||||
};
|
};
|
||||||
use harmony_macros::ipv4;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let inventory = Inventory::autoload();
|
let inventory = Inventory::autoload();
|
||||||
let topology = DummyInfra {};
|
let topology = HAClusterTopology::autoload();
|
||||||
let mut maestro = Maestro::initialize(inventory, topology).await.unwrap();
|
let mut maestro = Maestro::new(inventory, topology);
|
||||||
|
|
||||||
maestro.register_all(vec![
|
maestro.register_all(vec![
|
||||||
Box::new(SuccessScore {}),
|
Box::new(SuccessScore {}),
|
||||||
Box::new(ErrorScore {}),
|
Box::new(ErrorScore {}),
|
||||||
Box::new(PanicScore {}),
|
Box::new(PanicScore {}),
|
||||||
Box::new(DnsScore::new(vec![], None)),
|
Box::new(DnsScore::new(vec![], None)),
|
||||||
Box::new(build_large_score()),
|
|
||||||
]);
|
]);
|
||||||
harmony_tui::init(maestro).await.unwrap();
|
harmony_tui::init(maestro).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_large_score() -> LoadBalancerScore {
|
|
||||||
let backend_server = BackendServer {
|
|
||||||
address: "192.168.0.0".to_string(),
|
|
||||||
port: 342,
|
|
||||||
};
|
|
||||||
let lb_service = LoadBalancerService {
|
|
||||||
backend_servers: vec![
|
|
||||||
backend_server.clone(),
|
|
||||||
backend_server.clone(),
|
|
||||||
backend_server.clone(),
|
|
||||||
],
|
|
||||||
listening_port: SocketAddr::V4(SocketAddrV4::new(ipv4!("192.168.0.0"), 49387)),
|
|
||||||
health_check: Some(HealthCheck::HTTP(
|
|
||||||
"/some_long_ass_path_to_see_how_it_is_displayed_but_it_has_to_be_even_longer"
|
|
||||||
.to_string(),
|
|
||||||
HttpMethod::GET,
|
|
||||||
HttpStatusCode::Success2xx,
|
|
||||||
)),
|
|
||||||
};
|
|
||||||
LoadBalancerScore {
|
|
||||||
public_services: vec![
|
|
||||||
lb_service.clone(),
|
|
||||||
lb_service.clone(),
|
|
||||||
lb_service.clone(),
|
|
||||||
lb_service.clone(),
|
|
||||||
lb_service.clone(),
|
|
||||||
lb_service.clone(),
|
|
||||||
],
|
|
||||||
private_services: vec![
|
|
||||||
lb_service.clone(),
|
|
||||||
lb_service.clone(),
|
|
||||||
lb_service.clone(),
|
|
||||||
lb_service.clone(),
|
|
||||||
lb_service.clone(),
|
|
||||||
lb_service.clone(),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ license.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libredfish = "0.1.1"
|
libredfish = "0.1.1"
|
||||||
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
reqwest = {version = "0.11", features = ["blocking", "json"] }
|
||||||
russh = "0.45.0"
|
russh = "0.45.0"
|
||||||
rust-ipmi = "0.1.1"
|
rust-ipmi = "0.1.1"
|
||||||
semver = "1.0.23"
|
semver = "1.0.23"
|
||||||
@@ -30,11 +30,3 @@ k8s-openapi = { workspace = true }
|
|||||||
serde_yaml = { workspace = true }
|
serde_yaml = { workspace = true }
|
||||||
http = { workspace = true }
|
http = { workspace = true }
|
||||||
serde-value = { workspace = true }
|
serde-value = { workspace = true }
|
||||||
inquire.workspace = true
|
|
||||||
helm-wrapper-rs = "0.4.0"
|
|
||||||
non-blank-string-rs = "1.0.4"
|
|
||||||
k3d-rs = { path = "../k3d" }
|
|
||||||
directories = "6.0.0"
|
|
||||||
lazy_static = "1.5.0"
|
|
||||||
dockerfile_builder = "0.1.5"
|
|
||||||
temp-file = "0.1.9"
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
use lazy_static::lazy_static;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
pub static ref HARMONY_CONFIG_DIR: PathBuf = directories::BaseDirs::new()
|
|
||||||
.unwrap()
|
|
||||||
.data_dir()
|
|
||||||
.join("harmony");
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,7 @@ use super::{
|
|||||||
data::{Id, Version},
|
data::{Id, Version},
|
||||||
executors::ExecutorError,
|
executors::ExecutorError,
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
|
topology::Topology,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum InterpretName {
|
pub enum InterpretName {
|
||||||
@@ -18,7 +19,6 @@ pub enum InterpretName {
|
|||||||
Dummy,
|
Dummy,
|
||||||
Panic,
|
Panic,
|
||||||
OPNSense,
|
OPNSense,
|
||||||
K3dInstallation,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for InterpretName {
|
impl std::fmt::Display for InterpretName {
|
||||||
@@ -32,19 +32,14 @@ impl std::fmt::Display for InterpretName {
|
|||||||
InterpretName::Dummy => f.write_str("Dummy"),
|
InterpretName::Dummy => f.write_str("Dummy"),
|
||||||
InterpretName::Panic => f.write_str("Panic"),
|
InterpretName::Panic => f.write_str("Panic"),
|
||||||
InterpretName::OPNSense => f.write_str("OPNSense"),
|
InterpretName::OPNSense => f.write_str("OPNSense"),
|
||||||
InterpretName::K3dInstallation => f.write_str("K3dInstallation"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Interpret<T>: std::fmt::Debug + Send {
|
pub trait Interpret<T>: std::fmt::Debug + Send {
|
||||||
async fn execute(
|
async fn execute(&self, inventory: &Inventory, topology: &T)
|
||||||
&self,
|
-> Result<Outcome, InterpretError>;
|
||||||
inventory: &Inventory,
|
|
||||||
topology: &T,
|
|
||||||
profile: &String,
|
|
||||||
) -> Result<Outcome, InterpretError>;
|
|
||||||
fn get_name(&self) -> InterpretName;
|
fn get_name(&self) -> InterpretName;
|
||||||
fn get_version(&self) -> Version;
|
fn get_version(&self) -> Version;
|
||||||
fn get_status(&self) -> InterpretStatus;
|
fn get_status(&self) -> InterpretStatus;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use log::{info, warn};
|
use log::info;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
interpret::{InterpretError, InterpretStatus, Outcome},
|
interpret::{InterpretError, Outcome},
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::Topology,
|
topology::Topology,
|
||||||
@@ -15,44 +15,40 @@ pub struct Maestro<T: Topology> {
|
|||||||
inventory: Inventory,
|
inventory: Inventory,
|
||||||
topology: T,
|
topology: T,
|
||||||
scores: Arc<RwLock<ScoreVec<T>>>,
|
scores: Arc<RwLock<ScoreVec<T>>>,
|
||||||
topology_preparation_result: Mutex<Option<Outcome>>,
|
|
||||||
profile: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Topology> Maestro<T> {
|
impl<T: Topology> Maestro<T> {
|
||||||
pub fn new(inventory: Inventory, topology: T, profile: String) -> Self {
|
pub fn new(inventory: Inventory, topology: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inventory,
|
inventory,
|
||||||
topology,
|
topology,
|
||||||
scores: Arc::new(RwLock::new(Vec::new())),
|
scores: Arc::new(RwLock::new(Vec::new())),
|
||||||
topology_preparation_result: None.into(),
|
|
||||||
profile,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn initialize(inventory: Inventory, topology: T) -> Result<Self, InterpretError> {
|
// Load the inventory and inventory from environment.
|
||||||
let profile = "dev".to_string(); // TODO: retrieve from env?
|
// This function is able to discover the context that it is running in, such as k8s clusters, aws cloud, linux host, etc.
|
||||||
let instance = Self::new(inventory, topology, profile);
|
// When the HARMONY_TOPOLOGY environment variable is not set, it will default to install k3s
|
||||||
instance.prepare_topology().await?;
|
// locally (lazily, if not installed yet, when the first execution occurs) and use that as a topology
|
||||||
Ok(instance)
|
// So, by default, the inventory is a single host that the binary is running on, and the
|
||||||
|
// topology is a single node k3s
|
||||||
|
//
|
||||||
|
// By default :
|
||||||
|
// - Linux => k3s
|
||||||
|
// - macos, windows => docker compose
|
||||||
|
//
|
||||||
|
// To run more complex cases like OKDHACluster, either provide the default target in the
|
||||||
|
// harmony infrastructure as code or as an environment variable
|
||||||
|
pub fn load_from_env() -> Self {
|
||||||
|
// Load env var HARMONY_TOPOLOGY
|
||||||
|
match std::env::var("HARMONY_TOPOLOGY") {
|
||||||
|
Ok(_) => todo!(),
|
||||||
|
Err(_) => todo!(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensures the associated Topology is ready for operations.
|
pub fn start(&mut self) {
|
||||||
/// Delegates the readiness check and potential setup actions to the Topology.
|
info!("Starting Maestro");
|
||||||
pub async fn prepare_topology(&self) -> Result<Outcome, InterpretError> {
|
|
||||||
info!("Ensuring topology '{}' is ready...", self.topology.name());
|
|
||||||
let outcome = self.topology.ensure_ready().await?;
|
|
||||||
info!(
|
|
||||||
"Topology '{}' readiness check complete: {}",
|
|
||||||
self.topology.name(),
|
|
||||||
outcome.status
|
|
||||||
);
|
|
||||||
|
|
||||||
self.topology_preparation_result
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.replace(outcome.clone());
|
|
||||||
Ok(outcome)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_all(&mut self, mut scores: ScoreVec<T>) {
|
pub fn register_all(&mut self, mut scores: ScoreVec<T>) {
|
||||||
@@ -60,32 +56,11 @@ impl<T: Topology> Maestro<T> {
|
|||||||
score_mut.append(&mut scores);
|
score_mut.append(&mut scores);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_topology_initialized(&self) -> bool {
|
|
||||||
let result = self.topology_preparation_result.lock().unwrap();
|
|
||||||
if let Some(outcome) = result.as_ref() {
|
|
||||||
match outcome.status {
|
|
||||||
InterpretStatus::SUCCESS => return true,
|
|
||||||
_ => return false,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn interpret(&self, score: Box<dyn Score<T>>) -> Result<Outcome, InterpretError> {
|
pub async fn interpret(&self, score: Box<dyn Score<T>>) -> Result<Outcome, InterpretError> {
|
||||||
if !self.is_topology_initialized() {
|
|
||||||
warn!(
|
|
||||||
"Launching interpret for score {} but Topology {} is not fully initialized!",
|
|
||||||
score.name(),
|
|
||||||
self.topology.name(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
info!("Running score {score:?}");
|
info!("Running score {score:?}");
|
||||||
let interpret = score.apply_profile(&self.profile).create_interpret();
|
let interpret = score.create_interpret();
|
||||||
info!("Launching interpret {interpret:?}");
|
info!("Launching interpret {interpret:?}");
|
||||||
let result = interpret
|
let result = interpret.execute(&self.inventory, &self.topology).await;
|
||||||
.execute(&self.inventory, &self.topology, &self.profile)
|
|
||||||
.await;
|
|
||||||
info!("Got result {result:?}");
|
info!("Got result {result:?}");
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
pub mod config;
|
|
||||||
pub mod data;
|
pub mod data;
|
||||||
pub mod executors;
|
pub mod executors;
|
||||||
pub mod filter;
|
pub mod filter;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_value::Value;
|
use serde_value::Value;
|
||||||
|
|
||||||
@@ -8,9 +6,6 @@ use super::{interpret::Interpret, topology::Topology};
|
|||||||
pub trait Score<T: Topology>:
|
pub trait Score<T: Topology>:
|
||||||
std::fmt::Debug + ScoreToString<T> + Send + Sync + CloneBoxScore<T> + SerializeScore<T>
|
std::fmt::Debug + ScoreToString<T> + Send + Sync + CloneBoxScore<T> + SerializeScore<T>
|
||||||
{
|
{
|
||||||
fn apply_profile(&self, profile: &String) -> Box<dyn Score<T>> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<T>>;
|
fn create_interpret(&self) -> Box<dyn Interpret<T>>;
|
||||||
fn name(&self) -> String;
|
fn name(&self) -> String;
|
||||||
}
|
}
|
||||||
@@ -48,8 +43,6 @@ where
|
|||||||
pub trait ScoreToString<T: Topology> {
|
pub trait ScoreToString<T: Topology> {
|
||||||
fn print_score_details(&self) -> String;
|
fn print_score_details(&self) -> String;
|
||||||
fn format_value_as_string(&self, val: &Value, indent: usize) -> String;
|
fn format_value_as_string(&self, val: &Value, indent: usize) -> String;
|
||||||
fn format_map(&self, map: &BTreeMap<Value, Value>, indent: usize) -> String;
|
|
||||||
fn wrap_or_truncate(&self, s: &str, width: usize) -> Vec<String>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, T> ScoreToString<T> for S
|
impl<S, T> ScoreToString<T> for S
|
||||||
@@ -64,78 +57,6 @@ where
|
|||||||
output += "\n";
|
output += "\n";
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
fn format_map(&self, map: &BTreeMap<Value, Value>, indent: usize) -> String {
|
|
||||||
let pad = " ".repeat(indent * 2);
|
|
||||||
let mut output = String::new();
|
|
||||||
|
|
||||||
output += &format!(
|
|
||||||
"{}+--------------------------+--------------------------------------------------+\n",
|
|
||||||
pad
|
|
||||||
);
|
|
||||||
output += &format!("{}| {:<24} | {:<48} |\n", pad, "score_name", self.name());
|
|
||||||
output += &format!(
|
|
||||||
"{}+--------------------------+--------------------------------------------------+\n",
|
|
||||||
pad
|
|
||||||
);
|
|
||||||
|
|
||||||
for (k, v) in map {
|
|
||||||
let key_str = match k {
|
|
||||||
Value::String(s) => s.clone(),
|
|
||||||
other => format!("{:?}", other),
|
|
||||||
};
|
|
||||||
|
|
||||||
let formatted_val = self.format_value_as_string(v, indent + 1);
|
|
||||||
let lines = formatted_val.lines().map(|line| line.trim_start());
|
|
||||||
|
|
||||||
let wrapped_lines: Vec<_> = lines
|
|
||||||
.flat_map(|line| self.wrap_or_truncate(line.trim_start(), 48))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if let Some(first) = wrapped_lines.first() {
|
|
||||||
output += &format!("{}| {:<24} | {:<48} |\n", pad, key_str, first);
|
|
||||||
for line in &wrapped_lines[1..] {
|
|
||||||
output += &format!("{}| {:<24} | {:<48} |\n", pad, "", line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// let first_line = lines.next().unwrap_or("");
|
|
||||||
// output += &format!("{}| {:<24} | {:<48} |\n", pad, key_str, first_line);
|
|
||||||
//
|
|
||||||
// for line in lines {
|
|
||||||
// output += &format!("{}| {:<24} | {:<48} |\n", pad, "", line);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
output += &format!(
|
|
||||||
"{}+--------------------------+--------------------------------------------------+\n\n",
|
|
||||||
pad
|
|
||||||
);
|
|
||||||
|
|
||||||
output
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wrap_or_truncate(&self, s: &str, width: usize) -> Vec<String> {
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
let mut current = s;
|
|
||||||
|
|
||||||
while !current.is_empty() {
|
|
||||||
if current.len() <= width {
|
|
||||||
lines.push(current.to_string());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to wrap at whitespace if possible
|
|
||||||
let mut split_index = current[..width].rfind(' ').unwrap_or(width);
|
|
||||||
if split_index == 0 {
|
|
||||||
split_index = width;
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push(current[..split_index].trim_end().to_string());
|
|
||||||
current = current[split_index..].trim_start();
|
|
||||||
}
|
|
||||||
|
|
||||||
lines
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_value_as_string(&self, val: &Value, indent: usize) -> String {
|
fn format_value_as_string(&self, val: &Value, indent: usize) -> String {
|
||||||
let pad = " ".repeat(indent * 2);
|
let pad = " ".repeat(indent * 2);
|
||||||
@@ -154,7 +75,7 @@ where
|
|||||||
Value::F32(f) => output += &format!("{}{}\n", pad, f),
|
Value::F32(f) => output += &format!("{}{}\n", pad, f),
|
||||||
Value::F64(f) => output += &format!("{}{}\n", pad, f),
|
Value::F64(f) => output += &format!("{}{}\n", pad, f),
|
||||||
Value::Char(c) => output += &format!("{}{}\n", pad, c),
|
Value::Char(c) => output += &format!("{}{}\n", pad, c),
|
||||||
Value::String(s) => output += &format!("{}{:<48}\n", pad, s),
|
Value::String(s) => output += &format!("{}{:<26}\n", pad, s),
|
||||||
Value::Unit => output += &format!("{}<unit>\n", pad),
|
Value::Unit => output += &format!("{}<unit>\n", pad),
|
||||||
Value::Bytes(bytes) => output += &format!("{}{:?}\n", pad, bytes),
|
Value::Bytes(bytes) => output += &format!("{}{:?}\n", pad, bytes),
|
||||||
|
|
||||||
@@ -186,27 +107,38 @@ where
|
|||||||
Value::Map(map) => {
|
Value::Map(map) => {
|
||||||
if map.is_empty() {
|
if map.is_empty() {
|
||||||
output += &format!("{}<empty map>\n", pad);
|
output += &format!("{}<empty map>\n", pad);
|
||||||
} else if indent == 0 {
|
|
||||||
output += &self.format_map(map, indent);
|
|
||||||
} else {
|
} else {
|
||||||
|
output += &format!(
|
||||||
|
"{}+--------------------------+----------------------------+\n",
|
||||||
|
pad
|
||||||
|
);
|
||||||
|
output += &format!("{}| {:<24} | {:<26} |\n", pad, "score_name", self.name());
|
||||||
|
output += &format!(
|
||||||
|
"{}+--------------------------+----------------------------+\n",
|
||||||
|
pad
|
||||||
|
);
|
||||||
|
|
||||||
for (k, v) in map {
|
for (k, v) in map {
|
||||||
let key_str = match k {
|
let key_str = match k {
|
||||||
Value::String(s) => s.clone(),
|
Value::String(s) => s.clone(),
|
||||||
other => format!("{:?}", other),
|
other => format!("{:?}", other),
|
||||||
};
|
};
|
||||||
|
let val_str = match v {
|
||||||
|
Value::String(s) => s.clone(),
|
||||||
|
Value::Bool(b) => b.to_string(),
|
||||||
|
Value::Option(Some(inner)) => format!("{:?}", inner),
|
||||||
|
Value::Option(None) => "None".to_string(),
|
||||||
|
Value::Seq(seq) => format!("{:?}", seq),
|
||||||
|
_ => format!("{:?}", v),
|
||||||
|
};
|
||||||
|
|
||||||
let val_str = self
|
output += &format!("{}| {:<24} | {:<26} |\n", pad, key_str, val_str);
|
||||||
.format_value_as_string(v, indent + 1)
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
let val_lines: Vec<_> = val_str.lines().collect();
|
|
||||||
|
|
||||||
output +=
|
|
||||||
&format!("{}{}: {}\n", pad, key_str, val_lines.first().unwrap_or(&""));
|
|
||||||
for line in val_lines.iter().skip(1) {
|
|
||||||
output += &format!("{} {}\n", pad, line);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output += &format!(
|
||||||
|
"{}+--------------------------+----------------------------+\n\n",
|
||||||
|
pad
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,16 +151,16 @@ where
|
|||||||
//
|
//
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use crate::modules::dns::DnsScore;
|
use crate::modules::dns::DnsScore;
|
||||||
use crate::topology::HAClusterTopology;
|
use crate::topology::{self, HAClusterTopology};
|
||||||
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_values_as_string() {
|
fn test_format_values_as_string() {
|
||||||
|
|
||||||
let dns_score = Box::new(DnsScore::new(vec![], None));
|
let dns_score = Box::new(DnsScore::new(vec![], None));
|
||||||
let print_score_output =
|
let print_score_output = <DnsScore as ScoreToString<HAClusterTopology>>::print_score_details(&dns_score);
|
||||||
<DnsScore as ScoreToString<HAClusterTopology>>::print_score_details(&dns_score);
|
let expected_empty_dns_score_table = "\n+--------------------------+----------------------------+\n| score_name | DnsScore |\n+--------------------------+----------------------------+\n| dns_entries | [] |\n| register_dhcp_leases | None |\n+--------------------------+----------------------------+\n\n\n";
|
||||||
let expected_empty_dns_score_table = "\n+--------------------------+--------------------------------------------------+\n| score_name | DnsScore |\n+--------------------------+--------------------------------------------------+\n| dns_entries | [] |\n| register_dhcp_leases | None |\n+--------------------------+--------------------------------------------------+\n\n\n";
|
|
||||||
assert_eq!(print_score_output, expected_empty_dns_score_table);
|
assert_eq!(print_score_output, expected_empty_dns_score_table);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use harmony_macros::ip;
|
use harmony_macros::ip;
|
||||||
use harmony_types::net::MacAddress;
|
use harmony_types::net::MacAddress;
|
||||||
use log::info;
|
|
||||||
|
|
||||||
use crate::executors::ExecutorError;
|
use crate::executors::ExecutorError;
|
||||||
use crate::interpret::InterpretError;
|
|
||||||
use crate::interpret::Outcome;
|
|
||||||
|
|
||||||
use super::DHCPStaticEntry;
|
use super::DHCPStaticEntry;
|
||||||
use super::DhcpServer;
|
use super::DhcpServer;
|
||||||
@@ -15,16 +12,16 @@ use super::DnsServer;
|
|||||||
use super::Firewall;
|
use super::Firewall;
|
||||||
use super::HttpServer;
|
use super::HttpServer;
|
||||||
use super::IpAddress;
|
use super::IpAddress;
|
||||||
use super::K8sclient;
|
|
||||||
use super::LoadBalancer;
|
use super::LoadBalancer;
|
||||||
use super::LoadBalancerService;
|
use super::LoadBalancerService;
|
||||||
use super::LogicalHost;
|
use super::LogicalHost;
|
||||||
|
use super::OcK8sclient;
|
||||||
use super::Router;
|
use super::Router;
|
||||||
use super::TftpServer;
|
use super::TftpServer;
|
||||||
|
|
||||||
use super::Topology;
|
use super::Topology;
|
||||||
use super::Url;
|
use super::Url;
|
||||||
use super::k8s::K8sClient;
|
use super::openshift::OpenshiftClient;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -43,24 +40,16 @@ pub struct HAClusterTopology {
|
|||||||
pub switch: Vec<LogicalHost>,
|
pub switch: Vec<LogicalHost>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Topology for HAClusterTopology {
|
impl Topology for HAClusterTopology {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {
|
|
||||||
todo!(
|
|
||||||
"ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl K8sclient for HAClusterTopology {
|
impl OcK8sclient for HAClusterTopology {
|
||||||
async fn k8s_client(&self) -> Result<Arc<K8sClient>, String> {
|
async fn oc_client(&self) -> Result<Arc<OpenshiftClient>, kube::Error> {
|
||||||
Ok(Arc::new(
|
Ok(Arc::new(OpenshiftClient::try_default().await?))
|
||||||
K8sClient::try_default().await.map_err(|e| e.to_string())?,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,20 +215,7 @@ impl HttpServer for HAClusterTopology {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DummyInfra;
|
struct DummyInfra;
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Topology for DummyInfra {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {
|
|
||||||
let dummy_msg = "This is a dummy infrastructure that does nothing";
|
|
||||||
info!("{dummy_msg}");
|
|
||||||
Ok(Outcome::success(dummy_msg.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const UNIMPLEMENTED_DUMMY_INFRA: &str = "This is a dummy infrastructure, no operation is supported";
|
const UNIMPLEMENTED_DUMMY_INFRA: &str = "This is a dummy infrastructure, no operation is supported";
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
pub trait HelmCommand {}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
use std::{process::Command, sync::Arc};
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use inquire::Confirm;
|
|
||||||
use log::{info, warn};
|
|
||||||
use tokio::sync::OnceCell;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
interpret::{InterpretError, Outcome},
|
|
||||||
inventory::Inventory,
|
|
||||||
maestro::Maestro,
|
|
||||||
modules::k3d::K3DInstallationScore,
|
|
||||||
topology::LocalhostTopology,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{HelmCommand, K8sclient, Topology, k8s::K8sClient};
|
|
||||||
|
|
||||||
struct K8sState {
|
|
||||||
client: Arc<K8sClient>,
|
|
||||||
_source: K8sSource,
|
|
||||||
message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum K8sSource {
|
|
||||||
LocalK3d,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct K8sAnywhereTopology {
|
|
||||||
k8s_state: OnceCell<Option<K8sState>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl K8sclient for K8sAnywhereTopology {
|
|
||||||
async fn k8s_client(&self) -> Result<Arc<K8sClient>, String> {
|
|
||||||
let state = match self.k8s_state.get() {
|
|
||||||
Some(state) => state,
|
|
||||||
None => return Err("K8s state not initialized yet".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let state = match state {
|
|
||||||
Some(state) => state,
|
|
||||||
None => return Err("K8s client initialized but empty".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(state.client.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl K8sAnywhereTopology {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
k8s_state: OnceCell::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_helm_available(&self) -> Result<(), String> {
|
|
||||||
let version_result = Command::new("helm")
|
|
||||||
.arg("version")
|
|
||||||
.output()
|
|
||||||
.map_err(|e| format!("Failed to execute 'helm -version': {}", e))?;
|
|
||||||
|
|
||||||
if !version_result.status.success() {
|
|
||||||
return Err("Failed to run 'helm -version'".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print the version output
|
|
||||||
let version_output = String::from_utf8_lossy(&version_result.stdout);
|
|
||||||
println!("Helm version: {}", version_output.trim());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn try_load_system_kubeconfig(&self) -> Option<K8sClient> {
|
|
||||||
todo!("Use kube-rs default behavior to load system kubeconfig");
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn try_load_kubeconfig(&self, path: &str) -> Option<K8sClient> {
|
|
||||||
todo!("Use kube-rs to load kubeconfig at path {path}");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_k3d_installation_score(&self) -> K3DInstallationScore {
|
|
||||||
K3DInstallationScore::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn try_install_k3d(&self) -> Result<(), InterpretError> {
|
|
||||||
let maestro = Maestro::initialize(Inventory::autoload(), LocalhostTopology::new()).await?;
|
|
||||||
let k3d_score = self.get_k3d_installation_score();
|
|
||||||
maestro.interpret(Box::new(k3d_score)).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn try_get_or_install_k8s_client(&self) -> Result<Option<K8sState>, InterpretError> {
|
|
||||||
let k8s_anywhere_config = K8sAnywhereConfig {
|
|
||||||
kubeconfig: std::env::var("HARMONY_KUBECONFIG")
|
|
||||||
.ok()
|
|
||||||
.map(|v| v.to_string()),
|
|
||||||
use_system_kubeconfig: std::env::var("HARMONY_USE_SYSTEM_KUBECONFIG")
|
|
||||||
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
|
|
||||||
autoinstall: std::env::var("HARMONY_AUTOINSTALL")
|
|
||||||
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
|
|
||||||
};
|
|
||||||
|
|
||||||
if k8s_anywhere_config.use_system_kubeconfig {
|
|
||||||
match self.try_load_system_kubeconfig().await {
|
|
||||||
Some(_client) => todo!(),
|
|
||||||
None => todo!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(kubeconfig) = k8s_anywhere_config.kubeconfig {
|
|
||||||
match self.try_load_kubeconfig(&kubeconfig).await {
|
|
||||||
Some(_client) => todo!(),
|
|
||||||
None => todo!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("No kubernetes configuration found");
|
|
||||||
|
|
||||||
if !k8s_anywhere_config.autoinstall {
|
|
||||||
let confirmation = Confirm::new( "Harmony autoinstallation is not activated, do you wish to launch autoinstallation? : ")
|
|
||||||
.with_default(false)
|
|
||||||
.prompt()
|
|
||||||
.expect("Unexpected prompt error");
|
|
||||||
|
|
||||||
if !confirmation {
|
|
||||||
warn!(
|
|
||||||
"Installation cancelled, K8sAnywhere could not initialize a valid Kubernetes client"
|
|
||||||
);
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Starting K8sAnywhere installation");
|
|
||||||
self.try_install_k3d().await?;
|
|
||||||
let k3d_score = self.get_k3d_installation_score();
|
|
||||||
// I feel like having to rely on the k3d_rs crate here is a smell
|
|
||||||
// I think we should have a way to interact more deeply with scores/interpret. Maybe the
|
|
||||||
// K3DInstallationScore should expose a method to get_client ? Not too sure what would be a
|
|
||||||
// good implementation due to the stateful nature of the k3d thing. Which is why I went
|
|
||||||
// with this solution for now
|
|
||||||
let k3d = k3d_rs::K3d::new(k3d_score.installation_path, Some(k3d_score.cluster_name));
|
|
||||||
let state = match k3d.get_client().await {
|
|
||||||
Ok(client) => K8sState {
|
|
||||||
client: Arc::new(K8sClient::new(client)),
|
|
||||||
_source: K8sSource::LocalK3d,
|
|
||||||
message: "Successfully installed K3D cluster and acquired client".to_string(),
|
|
||||||
},
|
|
||||||
Err(_) => todo!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(state))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct K8sAnywhereConfig {
|
|
||||||
/// The path of the KUBECONFIG file that Harmony should use to interact with the Kubernetes
|
|
||||||
/// cluster
|
|
||||||
///
|
|
||||||
/// Default : None
|
|
||||||
kubeconfig: Option<String>,
|
|
||||||
|
|
||||||
/// Whether to use the system KUBECONFIG, either the environment variable or the file in the
|
|
||||||
/// default or configured location
|
|
||||||
///
|
|
||||||
/// Default : false
|
|
||||||
use_system_kubeconfig: bool,
|
|
||||||
|
|
||||||
/// Whether to install automatically a kubernetes cluster
|
|
||||||
///
|
|
||||||
/// When enabled, autoinstall will setup a K3D cluster on the localhost. https://k3d.io/stable/
|
|
||||||
///
|
|
||||||
/// Default: true
|
|
||||||
autoinstall: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Topology for K8sAnywhereTopology {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
"K8sAnywhereTopology"
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {
|
|
||||||
let k8s_state = self
|
|
||||||
.k8s_state
|
|
||||||
.get_or_try_init(|| self.try_get_or_install_k8s_client())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let k8s_state: &K8sState = k8s_state.as_ref().ok_or(InterpretError::new(
|
|
||||||
"No K8s client could be found or installed".to_string(),
|
|
||||||
))?;
|
|
||||||
|
|
||||||
match self.is_helm_available() {
|
|
||||||
Ok(()) => Ok(Outcome::success(format!(
|
|
||||||
"{} + helm available",
|
|
||||||
k8s_state.message.clone()
|
|
||||||
))),
|
|
||||||
Err(e) => Err(InterpretError::new(format!("helm unavailable: {}", e))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HelmCommand for K8sAnywhereTopology {}
|
|
||||||
@@ -46,7 +46,6 @@ pub struct LoadBalancerService {
|
|||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Serialize)]
|
#[derive(Debug, PartialEq, Clone, Serialize)]
|
||||||
pub struct BackendServer {
|
pub struct BackendServer {
|
||||||
// TODO should not be a string, probably IPAddress
|
|
||||||
pub address: String,
|
pub address: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
use derive_new::new;
|
|
||||||
|
|
||||||
use crate::interpret::{InterpretError, Outcome};
|
|
||||||
|
|
||||||
use super::{HelmCommand, Topology};
|
|
||||||
|
|
||||||
#[derive(new)]
|
|
||||||
pub struct LocalhostTopology;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Topology for LocalhostTopology {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
"LocalHostTopology"
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {
|
|
||||||
Ok(Outcome::success(
|
|
||||||
"Localhost is Chuck Norris, always ready.".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Delete this, temp for test
|
|
||||||
impl HelmCommand for LocalhostTopology {}
|
|
||||||
@@ -1,15 +1,10 @@
|
|||||||
mod ha_cluster;
|
mod ha_cluster;
|
||||||
mod host_binding;
|
mod host_binding;
|
||||||
mod http;
|
mod http;
|
||||||
mod k8s_anywhere;
|
|
||||||
mod localhost;
|
|
||||||
pub use k8s_anywhere::*;
|
|
||||||
pub use localhost::*;
|
|
||||||
pub mod k8s;
|
|
||||||
mod load_balancer;
|
mod load_balancer;
|
||||||
|
pub mod openshift;
|
||||||
mod router;
|
mod router;
|
||||||
mod tftp;
|
mod tftp;
|
||||||
use async_trait::async_trait;
|
|
||||||
pub use ha_cluster::*;
|
pub use ha_cluster::*;
|
||||||
pub use load_balancer::*;
|
pub use load_balancer::*;
|
||||||
pub use router::*;
|
pub use router::*;
|
||||||
@@ -20,43 +15,10 @@ pub use network::*;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
pub use tftp::*;
|
pub use tftp::*;
|
||||||
|
|
||||||
mod helm_command;
|
|
||||||
pub use helm_command::*;
|
|
||||||
|
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
use super::interpret::{InterpretError, Outcome};
|
pub trait Topology {
|
||||||
|
|
||||||
/// Represents a logical view of an infrastructure environment providing specific capabilities.
|
|
||||||
///
|
|
||||||
/// A Topology acts as a self-contained "package" responsible for managing access
|
|
||||||
/// to its underlying resources and ensuring they are in a ready state before use.
|
|
||||||
/// It defines the contract for the capabilities it provides through implemented
|
|
||||||
/// capability traits (e.g., `HasK8sCapability`, `HasDnsServer`).
|
|
||||||
#[async_trait]
|
|
||||||
pub trait Topology: Send + Sync {
|
|
||||||
/// Returns a unique identifier or name for this specific topology instance.
|
|
||||||
/// This helps differentiate between multiple instances of potentially the same type.
|
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
|
|
||||||
/// Ensures that the topology and its required underlying components or services
|
|
||||||
/// are ready to provide their declared capabilities.
|
|
||||||
///
|
|
||||||
/// Implementations of this method MUST be idempotent. Subsequent calls after a
|
|
||||||
/// successful readiness check should ideally be cheap NO-OPs.
|
|
||||||
///
|
|
||||||
/// This method encapsulates the logic for:
|
|
||||||
/// 1. **Checking Current State:** Assessing if the required resources/services are already running and configured.
|
|
||||||
/// 2. **Discovery:** Identifying the runtime environment (e.g., local Docker, AWS, existing cluster).
|
|
||||||
/// 3. **Initialization/Bootstrapping:** Performing necessary setup actions if not already ready. This might involve:
|
|
||||||
/// * Making API calls.
|
|
||||||
/// * Running external commands (e.g., `k3d`, `docker`).
|
|
||||||
/// * **Internal Orchestration:** For complex topologies, this method might manage dependencies on other sub-topologies, ensuring *their* `ensure_ready` is called first. Using nested `Maestros` to run setup `Scores` against these sub-topologies is the recommended pattern for non-trivial bootstrapping, allowing reuse of Harmony's core orchestration logic.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// - `Ok(Outcome)`: Indicates the topology is now ready. The `Outcome` status might be `SUCCESS` if actions were taken, or `NOOP` if it was already ready. The message should provide context.
|
|
||||||
/// - `Err(TopologyError)`: Indicates the topology could not reach a ready state due to configuration issues, discovery failures, bootstrap errors, or unsupported environments.
|
|
||||||
async fn ensure_ready(&self) -> Result<Outcome, InterpretError>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type IpAddress = IpAddr;
|
pub type IpAddress = IpAddr;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use serde::Serialize;
|
|||||||
|
|
||||||
use crate::executors::ExecutorError;
|
use crate::executors::ExecutorError;
|
||||||
|
|
||||||
use super::{IpAddress, LogicalHost, k8s::K8sClient};
|
use super::{IpAddress, LogicalHost, openshift::OpenshiftClient};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DHCPStaticEntry {
|
pub struct DHCPStaticEntry {
|
||||||
@@ -42,8 +42,8 @@ pub struct NetworkDomain {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait K8sclient: Send + Sync {
|
pub trait OcK8sclient: Send + Sync + std::fmt::Debug {
|
||||||
async fn k8s_client(&self) -> Result<Arc<K8sClient>, String>;
|
async fn oc_client(&self) -> Result<Arc<OpenshiftClient>, kube::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
use derive_new::new;
|
|
||||||
use k8s_openapi::NamespaceResourceScope;
|
use k8s_openapi::NamespaceResourceScope;
|
||||||
use kube::{Api, Client, Error, Resource, api::PostParams};
|
use kube::{Api, Client, Error, Resource, api::PostParams};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
#[derive(new)]
|
pub struct OpenshiftClient {
|
||||||
pub struct K8sClient {
|
|
||||||
client: Client,
|
client: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl K8sClient {
|
impl OpenshiftClient {
|
||||||
pub async fn try_default() -> Result<Self, Error> {
|
pub async fn try_default() -> Result<Self, Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
client: Client::try_default().await?,
|
client: Client::try_default().await?,
|
||||||
@@ -75,7 +75,6 @@ impl<T: Topology> Interpret<T> for DummyInterpret {
|
|||||||
&self,
|
&self,
|
||||||
_inventory: &Inventory,
|
_inventory: &Inventory,
|
||||||
_topology: &T,
|
_topology: &T,
|
||||||
_profile: &String,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
) -> Result<Outcome, InterpretError> {
|
||||||
self.result.clone()
|
self.result.clone()
|
||||||
}
|
}
|
||||||
@@ -122,7 +121,6 @@ impl<T: Topology> Interpret<T> for PanicInterpret {
|
|||||||
&self,
|
&self,
|
||||||
_inventory: &Inventory,
|
_inventory: &Inventory,
|
||||||
_topology: &T,
|
_topology: &T,
|
||||||
_profile: &String,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
) -> Result<Outcome, InterpretError> {
|
||||||
panic!("Panic interpret always panics when executed")
|
panic!("Panic interpret always panics when executed")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
use crate::data::{Id, Version};
|
|
||||||
use crate::interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome};
|
|
||||||
use crate::inventory::Inventory;
|
|
||||||
use crate::score::Score;
|
|
||||||
use crate::topology::{HelmCommand, Topology};
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use helm_wrapper_rs;
|
|
||||||
use helm_wrapper_rs::blocking::{DefaultHelmExecutor, HelmExecutor};
|
|
||||||
pub use non_blank_string_rs::NonBlankString;
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::Path;
|
|
||||||
use temp_file::TempFile;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct HelmChartScore {
|
|
||||||
pub namespace: Option<NonBlankString>,
|
|
||||||
pub release_name: NonBlankString,
|
|
||||||
pub chart_name: NonBlankString,
|
|
||||||
pub chart_version: Option<NonBlankString>,
|
|
||||||
pub values_overrides: Option<HashMap<NonBlankString, String>>,
|
|
||||||
pub values_yaml: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Topology + HelmCommand> Score<T> for HelmChartScore {
|
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
|
||||||
Box::new(HelmChartInterpret {
|
|
||||||
score: self.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> String {
|
|
||||||
format!("{} {} HelmChartScore", self.release_name, self.chart_name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct HelmChartInterpret {
|
|
||||||
pub score: HelmChartScore,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<T: Topology + HelmCommand> Interpret<T> for HelmChartInterpret {
|
|
||||||
async fn execute(
|
|
||||||
&self,
|
|
||||||
_inventory: &Inventory,
|
|
||||||
_topology: &T,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
let ns = self
|
|
||||||
.score
|
|
||||||
.namespace
|
|
||||||
.as_ref()
|
|
||||||
.unwrap_or_else(|| todo!("Get namespace from active kubernetes cluster"));
|
|
||||||
|
|
||||||
let tf: TempFile;
|
|
||||||
let yaml_path: Option<&Path> = match self.score.values_yaml.as_ref() {
|
|
||||||
Some(yaml_str) => {
|
|
||||||
tf = temp_file::with_contents(yaml_str.as_bytes());
|
|
||||||
Some(tf.path())
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let helm_executor = DefaultHelmExecutor::new();
|
|
||||||
let res = helm_executor.install_or_upgrade(
|
|
||||||
&ns,
|
|
||||||
&self.score.release_name,
|
|
||||||
&self.score.chart_name,
|
|
||||||
self.score.chart_version.as_ref(),
|
|
||||||
self.score.values_overrides.as_ref(),
|
|
||||||
yaml_path,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
let status = match res {
|
|
||||||
Ok(status) => status,
|
|
||||||
Err(err) => return Err(InterpretError::new(err.to_string())),
|
|
||||||
};
|
|
||||||
|
|
||||||
match status {
|
|
||||||
helm_wrapper_rs::HelmDeployStatus::Deployed => Ok(Outcome::new(
|
|
||||||
InterpretStatus::SUCCESS,
|
|
||||||
"Helm Chart deployed".to_string(),
|
|
||||||
)),
|
|
||||||
helm_wrapper_rs::HelmDeployStatus::PendingInstall => Ok(Outcome::new(
|
|
||||||
InterpretStatus::RUNNING,
|
|
||||||
"Helm Chart Pending install".to_string(),
|
|
||||||
)),
|
|
||||||
helm_wrapper_rs::HelmDeployStatus::PendingUpgrade => Ok(Outcome::new(
|
|
||||||
InterpretStatus::RUNNING,
|
|
||||||
"Helm Chart pending upgrade".to_string(),
|
|
||||||
)),
|
|
||||||
helm_wrapper_rs::HelmDeployStatus::Failed => Err(InterpretError::new(
|
|
||||||
"Failed to install helm chart".to_string(),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub mod chart;
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use log::info;
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
config::HARMONY_CONFIG_DIR,
|
|
||||||
data::{Id, Version},
|
|
||||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
|
||||||
inventory::Inventory,
|
|
||||||
score::Score,
|
|
||||||
topology::Topology,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct K3DInstallationScore {
|
|
||||||
pub installation_path: PathBuf,
|
|
||||||
pub cluster_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for K3DInstallationScore {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
installation_path: HARMONY_CONFIG_DIR.join("k3d"),
|
|
||||||
cluster_name: "harmony".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Topology> Score<T> for K3DInstallationScore {
|
|
||||||
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
|
|
||||||
Box::new(K3dInstallationInterpret {
|
|
||||||
score: self.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> String {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct K3dInstallationInterpret {
|
|
||||||
score: K3DInstallationScore,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<T: Topology> Interpret<T> for K3dInstallationInterpret {
|
|
||||||
async fn execute(
|
|
||||||
&self,
|
|
||||||
_inventory: &Inventory,
|
|
||||||
_topology: &T,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
let k3d = k3d_rs::K3d::new(
|
|
||||||
self.score.installation_path.clone(),
|
|
||||||
Some(self.score.cluster_name.clone()),
|
|
||||||
);
|
|
||||||
match k3d.ensure_installed().await {
|
|
||||||
Ok(_client) => {
|
|
||||||
let msg = format!("k3d cluster {} is installed ", self.score.cluster_name);
|
|
||||||
info!("{msg}");
|
|
||||||
Ok(Outcome::success(msg))
|
|
||||||
}
|
|
||||||
Err(msg) => Err(InterpretError::new(format!(
|
|
||||||
"K3dInstallationInterpret failed to ensure k3d is installed : {msg}"
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn get_name(&self) -> InterpretName {
|
|
||||||
InterpretName::K3dInstallation
|
|
||||||
}
|
|
||||||
fn get_version(&self) -> Version {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn get_status(&self) -> InterpretStatus {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
fn get_children(&self) -> Vec<Id> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
mod install;
|
|
||||||
pub use install::*;
|
|
||||||
@@ -5,7 +5,7 @@ use serde_json::json;
|
|||||||
use crate::{
|
use crate::{
|
||||||
interpret::Interpret,
|
interpret::Interpret,
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::{K8sclient, Topology},
|
topology::{OcK8sclient, Topology},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::resource::{K8sResourceInterpret, K8sResourceScore};
|
use super::resource::{K8sResourceInterpret, K8sResourceScore};
|
||||||
@@ -16,7 +16,7 @@ pub struct K8sDeploymentScore {
|
|||||||
pub image: String,
|
pub image: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Topology + K8sclient> Score<T> for K8sDeploymentScore {
|
impl<T: Topology + OcK8sclient> Score<T> for K8sDeploymentScore {
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
let deployment: Deployment = serde_json::from_value(json!(
|
let deployment: Deployment = serde_json::from_value(json!(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::{
|
|||||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::{K8sclient, Topology},
|
topology::{OcK8sclient, Topology},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -63,7 +63,7 @@ impl<
|
|||||||
+ Default
|
+ Default
|
||||||
+ Send
|
+ Send
|
||||||
+ Sync,
|
+ Sync,
|
||||||
T: Topology + K8sclient,
|
T: Topology + OcK8sclient,
|
||||||
> Interpret<T> for K8sResourceInterpret<K>
|
> Interpret<T> for K8sResourceInterpret<K>
|
||||||
where
|
where
|
||||||
<K as kube::Resource>::DynamicType: Default,
|
<K as kube::Resource>::DynamicType: Default,
|
||||||
@@ -74,7 +74,7 @@ where
|
|||||||
topology: &T,
|
topology: &T,
|
||||||
) -> Result<Outcome, InterpretError> {
|
) -> Result<Outcome, InterpretError> {
|
||||||
topology
|
topology
|
||||||
.k8s_client()
|
.oc_client()
|
||||||
.await
|
.await
|
||||||
.expect("Environment should provide enough information to instanciate a client")
|
.expect("Environment should provide enough information to instanciate a client")
|
||||||
.apply_namespaced(&self.score.resource)
|
.apply_namespaced(&self.score.resource)
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::info;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -12,7 +9,7 @@ use crate::{
|
|||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
modules::k8s::deployment::K8sDeploymentScore,
|
modules::k8s::deployment::K8sDeploymentScore,
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::{K8sclient, Topology, Url},
|
topology::{OcK8sclient, Topology, Url},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -21,7 +18,6 @@ pub struct LAMPScore {
|
|||||||
pub domain: Url,
|
pub domain: Url,
|
||||||
pub config: LAMPConfig,
|
pub config: LAMPConfig,
|
||||||
pub php_version: Version,
|
pub php_version: Version,
|
||||||
pub profiles: HashMap<&'static str, LAMPProfile>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -30,11 +26,6 @@ pub struct LAMPConfig {
|
|||||||
pub ssl_enabled: bool,
|
pub ssl_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct LAMPProfile {
|
|
||||||
pub ssl_enabled: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LAMPConfig {
|
impl Default for LAMPConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
LAMPConfig {
|
LAMPConfig {
|
||||||
@@ -44,28 +35,9 @@ impl Default for LAMPConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Topology + K8sclient> Score<T> for LAMPScore {
|
impl<T: Topology> Score<T> for LAMPScore {
|
||||||
fn apply_profile(&self, profile: &String) -> Box<dyn Score<T>> {
|
|
||||||
let profile = match self.profiles.get(profile.as_str()) {
|
|
||||||
Some(profile) => profile,
|
|
||||||
None => panic!("Not good"), // TODO: better handling
|
|
||||||
};
|
|
||||||
|
|
||||||
let config = LAMPConfig {
|
|
||||||
ssl_enabled: profile.ssl_enabled,
|
|
||||||
..self.config.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
Box::new(LAMPScore {
|
|
||||||
config,
|
|
||||||
..self.clone()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
Box::new(LAMPInterpret {
|
todo!()
|
||||||
score: self.clone(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
@@ -79,34 +51,20 @@ pub struct LAMPInterpret {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T: Topology + K8sclient> Interpret<T> for LAMPInterpret {
|
impl<T: Topology + OcK8sclient> Interpret<T> for LAMPInterpret {
|
||||||
async fn execute(
|
async fn execute(
|
||||||
&self,
|
&self,
|
||||||
inventory: &Inventory,
|
inventory: &Inventory,
|
||||||
topology: &T,
|
topology: &T,
|
||||||
profile: &String,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
) -> Result<Outcome, InterpretError> {
|
||||||
let image_name = match self.build_docker_image() {
|
|
||||||
Ok(name) => name,
|
|
||||||
Err(e) => {
|
|
||||||
return Err(InterpretError::new(format!(
|
|
||||||
"Could not build LAMP docker image {e}"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
info!("LAMP docker image built {image_name}");
|
|
||||||
|
|
||||||
let deployment_score = K8sDeploymentScore {
|
let deployment_score = K8sDeploymentScore {
|
||||||
name: <LAMPScore as Score<T>>::name(&self.score),
|
name: <LAMPScore as Score<T>>::name(&self.score),
|
||||||
image: image_name,
|
image: "local_image".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("LAMP deployment_score {deployment_score:?}");
|
|
||||||
todo!();
|
|
||||||
deployment_score
|
deployment_score
|
||||||
.apply_profile(profile)
|
|
||||||
.create_interpret()
|
.create_interpret()
|
||||||
.execute(inventory, topology, profile)
|
.execute(inventory, topology)
|
||||||
.await?;
|
.await?;
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
@@ -127,164 +85,3 @@ impl<T: Topology + K8sclient> Interpret<T> for LAMPInterpret {
|
|||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, WORKDIR};
|
|
||||||
use dockerfile_builder::{Dockerfile, instruction_builder::EnvBuilder};
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
impl LAMPInterpret {
|
|
||||||
pub fn build_dockerfile(
|
|
||||||
&self,
|
|
||||||
score: &LAMPScore,
|
|
||||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
|
||||||
let mut dockerfile = Dockerfile::new();
|
|
||||||
|
|
||||||
// Use the PHP version from the score to determine the base image
|
|
||||||
let php_version = score.php_version.to_string();
|
|
||||||
let php_major_minor = php_version
|
|
||||||
.split('.')
|
|
||||||
.take(2)
|
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.join(".");
|
|
||||||
|
|
||||||
// Base image selection - using official PHP image with Apache
|
|
||||||
dockerfile.push(FROM::from(format!("php:{}-apache", php_major_minor)));
|
|
||||||
|
|
||||||
// Set environment variables for PHP configuration
|
|
||||||
dockerfile.push(ENV::from("PHP_MEMORY_LIMIT=256M"));
|
|
||||||
dockerfile.push(ENV::from("PHP_MAX_EXECUTION_TIME=30"));
|
|
||||||
dockerfile.push(
|
|
||||||
EnvBuilder::builder()
|
|
||||||
.key("PHP_ERROR_REPORTING")
|
|
||||||
.value("\"E_ERROR | E_WARNING | E_PARSE\"")
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Install necessary PHP extensions and dependencies
|
|
||||||
dockerfile.push(RUN::from(
|
|
||||||
"apt-get update && \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
libfreetype6-dev \
|
|
||||||
libjpeg62-turbo-dev \
|
|
||||||
libpng-dev \
|
|
||||||
libzip-dev \
|
|
||||||
unzip \
|
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*",
|
|
||||||
));
|
|
||||||
|
|
||||||
dockerfile.push(RUN::from(
|
|
||||||
"docker-php-ext-configure gd --with-freetype --with-jpeg && \
|
|
||||||
docker-php-ext-install -j$(nproc) \
|
|
||||||
gd \
|
|
||||||
mysqli \
|
|
||||||
pdo_mysql \
|
|
||||||
zip \
|
|
||||||
opcache",
|
|
||||||
));
|
|
||||||
|
|
||||||
// Copy PHP configuration
|
|
||||||
dockerfile.push(RUN::from("mkdir -p /usr/local/etc/php/conf.d/"));
|
|
||||||
|
|
||||||
// Create and copy a custom PHP configuration
|
|
||||||
let php_config = r#"
|
|
||||||
memory_limit = ${PHP_MEMORY_LIMIT}
|
|
||||||
max_execution_time = ${PHP_MAX_EXECUTION_TIME}
|
|
||||||
error_reporting = ${PHP_ERROR_REPORTING}
|
|
||||||
display_errors = Off
|
|
||||||
log_errors = On
|
|
||||||
error_log = /dev/stderr
|
|
||||||
date.timezone = UTC
|
|
||||||
|
|
||||||
; Opcache configuration for production
|
|
||||||
opcache.enable=1
|
|
||||||
opcache.memory_consumption=128
|
|
||||||
opcache.interned_strings_buffer=8
|
|
||||||
opcache.max_accelerated_files=4000
|
|
||||||
opcache.revalidate_freq=2
|
|
||||||
opcache.fast_shutdown=1
|
|
||||||
"#;
|
|
||||||
|
|
||||||
// Save this configuration to a temporary file within the project root
|
|
||||||
let config_path = Path::new(&score.config.project_root).join("docker-php.ini");
|
|
||||||
fs::write(&config_path, php_config)?;
|
|
||||||
|
|
||||||
// Reference the file within the Docker context (where the build runs)
|
|
||||||
dockerfile.push(COPY::from(
|
|
||||||
"docker-php.ini /usr/local/etc/php/conf.d/docker-php.ini",
|
|
||||||
));
|
|
||||||
|
|
||||||
// Security hardening
|
|
||||||
dockerfile.push(RUN::from(
|
|
||||||
"a2enmod headers && \
|
|
||||||
a2enmod rewrite && \
|
|
||||||
sed -i 's/ServerTokens OS/ServerTokens Prod/' /etc/apache2/conf-enabled/security.conf && \
|
|
||||||
sed -i 's/ServerSignature On/ServerSignature Off/' /etc/apache2/conf-enabled/security.conf"
|
|
||||||
));
|
|
||||||
|
|
||||||
// Create a dedicated user for running Apache
|
|
||||||
dockerfile.push(RUN::from(
|
|
||||||
"groupadd -g 1000 appuser && \
|
|
||||||
useradd -u 1000 -g appuser -m -s /bin/bash appuser && \
|
|
||||||
chown -R appuser:appuser /var/www/html",
|
|
||||||
));
|
|
||||||
|
|
||||||
// Set the working directory
|
|
||||||
dockerfile.push(WORKDIR::from("/var/www/html"));
|
|
||||||
|
|
||||||
// Copy application code from the project root to the container
|
|
||||||
// Note: In Dockerfile, the COPY context is relative to the build context
|
|
||||||
// We'll handle the actual context in the build_docker_image method
|
|
||||||
dockerfile.push(COPY::from(". /var/www/html"));
|
|
||||||
|
|
||||||
// Fix permissions
|
|
||||||
dockerfile.push(RUN::from("chown -R appuser:appuser /var/www/html"));
|
|
||||||
|
|
||||||
// Expose Apache port
|
|
||||||
dockerfile.push(EXPOSE::from("80/tcp"));
|
|
||||||
|
|
||||||
// Set the default command
|
|
||||||
dockerfile.push(CMD::from("apache2-foreground"));
|
|
||||||
|
|
||||||
// Save the Dockerfile to disk in the project root
|
|
||||||
let dockerfile_path = Path::new(&score.config.project_root).join("Dockerfile");
|
|
||||||
fs::write(&dockerfile_path, dockerfile.to_string())?;
|
|
||||||
|
|
||||||
Ok(dockerfile_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_docker_image(&self) -> Result<String, Box<dyn std::error::Error>> {
|
|
||||||
info!("Generating Dockerfile");
|
|
||||||
let dockerfile = self.build_dockerfile(&self.score)?;
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Building Docker image with file {} from root {}",
|
|
||||||
dockerfile.to_string_lossy(),
|
|
||||||
self.score.config.project_root.to_string_lossy()
|
|
||||||
);
|
|
||||||
let image_name = format!("{}-php-apache", self.score.name);
|
|
||||||
let project_root = &self.score.config.project_root;
|
|
||||||
|
|
||||||
let output = std::process::Command::new("docker")
|
|
||||||
.args([
|
|
||||||
"build",
|
|
||||||
"--file",
|
|
||||||
dockerfile.to_str().unwrap(),
|
|
||||||
"-t",
|
|
||||||
&image_name,
|
|
||||||
project_root.to_str().unwrap(),
|
|
||||||
])
|
|
||||||
.output()?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
return Err(format!(
|
|
||||||
"Failed to build Docker image: {}",
|
|
||||||
String::from_utf8_lossy(&output.stderr)
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(image_name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
pub mod dhcp;
|
pub mod dhcp;
|
||||||
pub mod dns;
|
pub mod dns;
|
||||||
pub mod dummy;
|
pub mod dummy;
|
||||||
pub mod helm;
|
|
||||||
pub mod http;
|
pub mod http;
|
||||||
pub mod k3d;
|
|
||||||
pub mod k8s;
|
pub mod k8s;
|
||||||
pub mod lamp;
|
pub mod lamp;
|
||||||
pub mod load_balancer;
|
pub mod load_balancer;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
impl std::fmt::Display for OKDLoadBalancerScore {
|
impl std::fmt::Display for OKDLoadBalancerScore {
|
||||||
fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ use crate::data::Version;
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct OKDUpgradeScore {
|
pub struct OKDUpgradeScore {
|
||||||
_current_version: Version,
|
current_version: Version,
|
||||||
_target_version: Version,
|
target_version: Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OKDUpgradeScore {
|
impl OKDUpgradeScore {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
_current_version: Version::from("4.17.0-okd-scos.0").unwrap(),
|
current_version: Version::from("4.17.0-okd-scos.0").unwrap(),
|
||||||
_target_version: Version::from("").unwrap(),
|
target_version: Version::from("").unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ pub struct OPNsenseShellCommandScore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for OPNsenseShellCommandScore {
|
impl Serialize for OPNsenseShellCommandScore {
|
||||||
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,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pub struct OPNSenseLaunchUpgrade {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for OPNSenseLaunchUpgrade {
|
impl Serialize for OPNSenseLaunchUpgrade {
|
||||||
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,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "harmony_cli"
|
|
||||||
edition = "2024"
|
|
||||||
version.workspace = true
|
|
||||||
readme.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
assert_cmd = "2.0.17"
|
|
||||||
clap = { version = "4.5.35", features = ["derive"] }
|
|
||||||
harmony = { path = "../harmony" }
|
|
||||||
harmony_tui = { path = "../harmony_tui", optional = true }
|
|
||||||
inquire.workspace = true
|
|
||||||
tokio.workspace = true
|
|
||||||
env_logger.workspace = true
|
|
||||||
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["tui"]
|
|
||||||
tui = ["dep:harmony_tui"]
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
use clap::Parser;
|
|
||||||
use clap::builder::ArgPredicate;
|
|
||||||
use harmony;
|
|
||||||
use harmony::{score::Score, topology::Topology};
|
|
||||||
use inquire::Confirm;
|
|
||||||
|
|
||||||
#[cfg(feature = "tui")]
|
|
||||||
use harmony_tui;
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(version, about, long_about = None)]
|
|
||||||
pub struct Args {
|
|
||||||
/// Run score(s) without prompt
|
|
||||||
#[arg(short, long, default_value_t = false, conflicts_with = "interactive")]
|
|
||||||
yes: bool,
|
|
||||||
|
|
||||||
/// Filter query
|
|
||||||
#[arg(short, long, conflicts_with = "interactive")]
|
|
||||||
filter: Option<String>,
|
|
||||||
|
|
||||||
/// Run interactive TUI or not
|
|
||||||
#[arg(short, long, default_value_t = false)]
|
|
||||||
interactive: bool,
|
|
||||||
|
|
||||||
/// Run all or nth, defaults to all
|
|
||||||
#[arg(
|
|
||||||
short,
|
|
||||||
long,
|
|
||||||
default_value_t = true,
|
|
||||||
default_value_if("number", ArgPredicate::IsPresent, "false"),
|
|
||||||
conflicts_with = "number",
|
|
||||||
conflicts_with = "interactive"
|
|
||||||
)]
|
|
||||||
all: bool,
|
|
||||||
|
|
||||||
/// Run nth matching, zero indexed
|
|
||||||
#[arg(short, long, default_value_t = 0, conflicts_with = "interactive")]
|
|
||||||
number: usize,
|
|
||||||
|
|
||||||
/// list scores, will also be affected by run filter
|
|
||||||
#[arg(short, long, default_value_t = false, conflicts_with = "interactive")]
|
|
||||||
list: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn maestro_scores_filter<T: Topology>(
|
|
||||||
maestro: &harmony::maestro::Maestro<T>,
|
|
||||||
all: bool,
|
|
||||||
filter: Option<String>,
|
|
||||||
number: usize,
|
|
||||||
) -> Vec<Box<dyn Score<T>>> {
|
|
||||||
let scores = maestro.scores();
|
|
||||||
let scores_read = scores.read().expect("Should be able to read scores");
|
|
||||||
let mut scores_vec: Vec<Box<dyn Score<T>>> = match filter {
|
|
||||||
Some(f) => scores_read
|
|
||||||
.iter()
|
|
||||||
.filter(|s| s.name().contains(&f))
|
|
||||||
.map(|s| s.clone_box())
|
|
||||||
.collect(),
|
|
||||||
None => scores_read.iter().map(|s| s.clone_box()).collect(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if !all {
|
|
||||||
let score = scores_vec.get(number);
|
|
||||||
match score {
|
|
||||||
Some(s) => scores_vec = vec![s.clone_box()],
|
|
||||||
None => return vec![],
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return scores_vec;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: consider adding doctest for this function
|
|
||||||
fn list_scores_with_index<T: Topology>(scores_vec: &Vec<Box<dyn Score<T>>>) -> String {
|
|
||||||
let mut display_str = String::new();
|
|
||||||
for (i, s) in scores_vec.iter().enumerate() {
|
|
||||||
let name = s.name();
|
|
||||||
display_str.push_str(&format!("\n{i}: {name}"));
|
|
||||||
}
|
|
||||||
return display_str;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init<T: Topology + Send + Sync + 'static>(
|
|
||||||
maestro: harmony::maestro::Maestro<T>,
|
|
||||||
args_struct: Option<Args>,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let args = match args_struct {
|
|
||||||
Some(args) => args,
|
|
||||||
None => Args::parse(),
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "tui")]
|
|
||||||
if args.interactive {
|
|
||||||
return harmony_tui::init(maestro).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "tui"))]
|
|
||||||
if args.interactive {
|
|
||||||
return Err("Not compiled with interactive support".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = env_logger::builder().try_init();
|
|
||||||
|
|
||||||
let scores_vec = maestro_scores_filter(&maestro, args.all, args.filter, args.number);
|
|
||||||
|
|
||||||
if scores_vec.len() == 0 {
|
|
||||||
return Err("No score found".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// if list option is specified, print filtered list and exit
|
|
||||||
if args.list {
|
|
||||||
println!("Available scores:");
|
|
||||||
println!("{}", list_scores_with_index(&scores_vec));
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// prompt user if --yes is not specified
|
|
||||||
if !args.yes {
|
|
||||||
let confirmation = Confirm::new(
|
|
||||||
format!(
|
|
||||||
"This will run the following scores: {}\n",
|
|
||||||
list_scores_with_index(&scores_vec)
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
)
|
|
||||||
.with_default(true)
|
|
||||||
.prompt()
|
|
||||||
.expect("Unexpected prompt error");
|
|
||||||
|
|
||||||
if !confirmation {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run filtered scores
|
|
||||||
for s in scores_vec {
|
|
||||||
println!("Running: {}", s.name());
|
|
||||||
maestro.interpret(s).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use harmony::{
|
|
||||||
inventory::Inventory,
|
|
||||||
maestro::Maestro,
|
|
||||||
modules::dummy::{ErrorScore, PanicScore, SuccessScore},
|
|
||||||
topology::HAClusterTopology,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn init_test_maestro() -> Maestro<HAClusterTopology> {
|
|
||||||
let inventory = Inventory::autoload();
|
|
||||||
let topology = HAClusterTopology::autoload();
|
|
||||||
let mut maestro = Maestro::new(inventory, topology);
|
|
||||||
|
|
||||||
maestro.register_all(vec![
|
|
||||||
Box::new(SuccessScore {}),
|
|
||||||
Box::new(ErrorScore {}),
|
|
||||||
Box::new(PanicScore {}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
maestro
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_init_success_score() {
|
|
||||||
let maestro = init_test_maestro();
|
|
||||||
let res = crate::init(
|
|
||||||
maestro,
|
|
||||||
Some(crate::Args {
|
|
||||||
yes: true,
|
|
||||||
filter: Some("SuccessScore".to_owned()),
|
|
||||||
interactive: false,
|
|
||||||
all: true,
|
|
||||||
number: 0,
|
|
||||||
list: false,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(res.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_init_error_score() {
|
|
||||||
let maestro = init_test_maestro();
|
|
||||||
|
|
||||||
let res = crate::init(
|
|
||||||
maestro,
|
|
||||||
Some(crate::Args {
|
|
||||||
yes: true,
|
|
||||||
filter: Some("ErrorScore".to_owned()),
|
|
||||||
interactive: false,
|
|
||||||
all: true,
|
|
||||||
number: 0,
|
|
||||||
list: false,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(res.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_init_number_score() {
|
|
||||||
let maestro = init_test_maestro();
|
|
||||||
|
|
||||||
let res = crate::init(
|
|
||||||
maestro,
|
|
||||||
Some(crate::Args {
|
|
||||||
yes: true,
|
|
||||||
filter: None,
|
|
||||||
interactive: false,
|
|
||||||
all: false,
|
|
||||||
number: 0,
|
|
||||||
list: false,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(res.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_filter_fn_all() {
|
|
||||||
let maestro = init_test_maestro();
|
|
||||||
|
|
||||||
let res = crate::maestro_scores_filter(&maestro, true, None, 0);
|
|
||||||
|
|
||||||
assert!(res.len() == 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_filter_fn_all_success() {
|
|
||||||
let maestro = init_test_maestro();
|
|
||||||
|
|
||||||
let res = crate::maestro_scores_filter(&maestro, true, Some("Success".to_owned()), 0);
|
|
||||||
|
|
||||||
assert!(res.len() == 1);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
maestro
|
|
||||||
.interpret(res.get(0).unwrap().clone_box())
|
|
||||||
.await
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_filter_fn_all_error() {
|
|
||||||
let maestro = init_test_maestro();
|
|
||||||
|
|
||||||
let res = crate::maestro_scores_filter(&maestro, true, Some("Error".to_owned()), 0);
|
|
||||||
|
|
||||||
assert!(res.len() == 1);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
maestro
|
|
||||||
.interpret(res.get(0).unwrap().clone_box())
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_filter_fn_all_score() {
|
|
||||||
let maestro = init_test_maestro();
|
|
||||||
|
|
||||||
let res = crate::maestro_scores_filter(&maestro, true, Some("Score".to_owned()), 0);
|
|
||||||
|
|
||||||
assert!(res.len() == 3);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
maestro
|
|
||||||
.interpret(res.get(0).unwrap().clone_box())
|
|
||||||
.await
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
maestro
|
|
||||||
.interpret(res.get(1).unwrap().clone_box())
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_filter_fn_number() {
|
|
||||||
let maestro = init_test_maestro();
|
|
||||||
|
|
||||||
let res = crate::maestro_scores_filter(&maestro, false, None, 0);
|
|
||||||
|
|
||||||
assert!(res.len() == 1);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
maestro
|
|
||||||
.interpret(res.get(0).unwrap().clone_box())
|
|
||||||
.await
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_filter_fn_number_invalid() {
|
|
||||||
let maestro = init_test_maestro();
|
|
||||||
|
|
||||||
let res = crate::maestro_scores_filter(&maestro, false, None, 11);
|
|
||||||
|
|
||||||
assert!(res.len() == 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ mod widget;
|
|||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use tui_logger::{TuiLoggerFile, TuiWidgetEvent, TuiWidgetState};
|
use tui_logger::{TuiWidgetEvent, TuiWidgetState};
|
||||||
use widget::{help::HelpWidget, score::ScoreListWidget};
|
use widget::{help::HelpWidget, score::ScoreListWidget};
|
||||||
|
|
||||||
use std::{panic, sync::Arc, time::Duration};
|
use std::{panic, sync::Arc, time::Duration};
|
||||||
@@ -36,13 +36,13 @@ pub mod tui {
|
|||||||
/// modules::dummy::{ErrorScore, PanicScore, SuccessScore},
|
/// modules::dummy::{ErrorScore, PanicScore, SuccessScore},
|
||||||
/// topology::HAClusterTopology,
|
/// topology::HAClusterTopology,
|
||||||
/// };
|
/// };
|
||||||
///
|
///
|
||||||
/// #[tokio::main]
|
/// #[tokio::main]
|
||||||
/// async fn main() {
|
/// async fn main() {
|
||||||
/// let inventory = Inventory::autoload();
|
/// let inventory = Inventory::autoload();
|
||||||
/// let topology = HAClusterTopology::autoload();
|
/// let topology = HAClusterTopology::autoload();
|
||||||
/// let mut maestro = Maestro::new(inventory, topology, "local");
|
/// let mut maestro = Maestro::new(inventory, topology);
|
||||||
///
|
///
|
||||||
/// maestro.register_all(vec![
|
/// maestro.register_all(vec![
|
||||||
/// Box::new(SuccessScore {}),
|
/// Box::new(SuccessScore {}),
|
||||||
/// Box::new(ErrorScore {}),
|
/// Box::new(ErrorScore {}),
|
||||||
@@ -51,7 +51,7 @@ pub mod tui {
|
|||||||
/// harmony_tui::init(maestro).await.unwrap();
|
/// harmony_tui::init(maestro).await.unwrap();
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn init<T: Topology + Send + Sync + 'static>(
|
pub async fn init<T: Topology + std::fmt::Debug + Send + Sync + 'static>(
|
||||||
maestro: Maestro<T>,
|
maestro: Maestro<T>,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
HarmonyTUI::new(maestro).init().await
|
HarmonyTUI::new(maestro).init().await
|
||||||
@@ -63,21 +63,12 @@ pub struct HarmonyTUI<T: Topology> {
|
|||||||
tui_state: TuiWidgetState,
|
tui_state: TuiWidgetState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
enum HarmonyTuiEvent<T: Topology> {
|
enum HarmonyTuiEvent<T: Topology> {
|
||||||
LaunchScore(Box<dyn Score<T>>),
|
LaunchScore(Box<dyn Score<T>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Topology> std::fmt::Display for HarmonyTuiEvent<T> {
|
impl<T: Topology + std::fmt::Debug + Send + Sync + 'static> HarmonyTUI<T> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let output = match self {
|
|
||||||
HarmonyTuiEvent::LaunchScore(score) => format!("LaunchScore({})", score.name()),
|
|
||||||
};
|
|
||||||
|
|
||||||
f.write_str(&output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Topology + Send + Sync + 'static> HarmonyTUI<T> {
|
|
||||||
pub fn new(maestro: Maestro<T>) -> Self {
|
pub fn new(maestro: Maestro<T>) -> Self {
|
||||||
let maestro = Arc::new(maestro);
|
let maestro = Arc::new(maestro);
|
||||||
let (_handle, sender) = Self::start_channel(maestro.clone());
|
let (_handle, sender) = Self::start_channel(maestro.clone());
|
||||||
@@ -100,7 +91,7 @@ impl<T: Topology + Send + Sync + 'static> HarmonyTUI<T> {
|
|||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
info!("Starting message channel receiver loop");
|
info!("Starting message channel receiver loop");
|
||||||
while let Some(event) = receiver.recv().await {
|
while let Some(event) = receiver.recv().await {
|
||||||
info!("Received event {event}");
|
info!("Received event {event:#?}");
|
||||||
match event {
|
match event {
|
||||||
HarmonyTuiEvent::LaunchScore(score_item) => {
|
HarmonyTuiEvent::LaunchScore(score_item) => {
|
||||||
let maestro = maestro.clone();
|
let maestro = maestro.clone();
|
||||||
@@ -132,7 +123,7 @@ impl<T: Topology + Send + Sync + 'static> HarmonyTUI<T> {
|
|||||||
// Set default level for unknown targets to Trace
|
// Set default level for unknown targets to Trace
|
||||||
tui_logger::set_default_level(log::LevelFilter::Info);
|
tui_logger::set_default_level(log::LevelFilter::Info);
|
||||||
std::fs::create_dir_all("log")?;
|
std::fs::create_dir_all("log")?;
|
||||||
tui_logger::set_log_file(TuiLoggerFile::new("log/harmony.log"));
|
tui_logger::set_log_file("log/harmony.log").unwrap();
|
||||||
|
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
let mut terminal = ratatui::init();
|
let mut terminal = ratatui::init();
|
||||||
|
|||||||
@@ -19,21 +19,13 @@ enum ExecutionState {
|
|||||||
CANCELED,
|
CANCELED,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
struct Execution<T: Topology> {
|
struct Execution<T: Topology> {
|
||||||
state: ExecutionState,
|
state: ExecutionState,
|
||||||
score: Box<dyn Score<T>>,
|
score: Box<dyn Score<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Topology> std::fmt::Display for Execution<T> {
|
#[derive(Debug)]
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_fmt(format_args!(
|
|
||||||
"Execution of {} status {:?}",
|
|
||||||
self.score.name(),
|
|
||||||
self.state
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct ScoreListWidget<T: Topology> {
|
pub(crate) struct ScoreListWidget<T: Topology> {
|
||||||
list_state: Arc<RwLock<ListState>>,
|
list_state: Arc<RwLock<ListState>>,
|
||||||
scores: Vec<Box<dyn Score<T>>>,
|
scores: Vec<Box<dyn Score<T>>>,
|
||||||
@@ -42,7 +34,7 @@ pub(crate) struct ScoreListWidget<T: Topology> {
|
|||||||
sender: mpsc::Sender<HarmonyTuiEvent<T>>,
|
sender: mpsc::Sender<HarmonyTuiEvent<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Topology> ScoreListWidget<T> {
|
impl<T: Topology + std::fmt::Debug> ScoreListWidget<T> {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
scores: Vec<Box<dyn Score<T>>>,
|
scores: Vec<Box<dyn Score<T>>>,
|
||||||
sender: mpsc::Sender<HarmonyTuiEvent<T>>,
|
sender: mpsc::Sender<HarmonyTuiEvent<T>>,
|
||||||
@@ -65,13 +57,13 @@ impl<T: Topology> ScoreListWidget<T> {
|
|||||||
state: ExecutionState::INITIATED,
|
state: ExecutionState::INITIATED,
|
||||||
score: score.clone_box(),
|
score: score.clone_box(),
|
||||||
});
|
});
|
||||||
info!("{}\n\nConfirm Execution (Press y/n)", score.name());
|
info!("{:#?}\n\nConfirm Execution (Press y/n)", score);
|
||||||
info!("{}", score.print_score_details());
|
info!("{}", score.print_score_details());
|
||||||
} else {
|
} else {
|
||||||
warn!("No Score selected, nothing to launch");
|
warn!("No Score selected, nothing to launch");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_selected_score(&self) -> Option<Box<dyn Score<T>>> {
|
pub(crate) fn get_selected_score(&self) -> Option<Box<dyn Score<T>>> {
|
||||||
let list_read = self.list_state.read().unwrap();
|
let list_read = self.list_state.read().unwrap();
|
||||||
if let Some(index) = list_read.selected() {
|
if let Some(index) = list_read.selected() {
|
||||||
@@ -107,7 +99,7 @@ impl<T: Topology> ScoreListWidget<T> {
|
|||||||
match confirm {
|
match confirm {
|
||||||
true => {
|
true => {
|
||||||
execution.state = ExecutionState::RUNNING;
|
execution.state = ExecutionState::RUNNING;
|
||||||
info!("Launch execution {execution}");
|
info!("Launch execution {:?}", execution);
|
||||||
self.sender
|
self.sender
|
||||||
.send(HarmonyTuiEvent::LaunchScore(execution.score.clone_box()))
|
.send(HarmonyTuiEvent::LaunchScore(execution.score.clone_box()))
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "k3d-rs"
|
|
||||||
edition = "2021"
|
|
||||||
version.workspace = true
|
|
||||||
readme.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
log = { workspace = true }
|
|
||||||
async-trait = { workspace = true }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
octocrab = "0.44.0"
|
|
||||||
regex = "1.11.1"
|
|
||||||
reqwest = { version = "0.12", features = ["stream"] }
|
|
||||||
url.workspace = true
|
|
||||||
sha2 = "0.10.8"
|
|
||||||
futures-util = "0.3.31"
|
|
||||||
kube.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
env_logger = { workspace = true }
|
|
||||||
httptest = "0.16.3"
|
|
||||||
pretty_assertions = "1.4.1"
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
use futures_util::StreamExt;
|
|
||||||
use log::{debug, info, warn};
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
use std::io::Read;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use tokio::fs;
|
|
||||||
use tokio::fs::File;
|
|
||||||
use tokio::io::AsyncWriteExt;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
const CHECKSUM_FAILED_MSG: &str = "Downloaded file failed checksum verification";
|
|
||||||
|
|
||||||
/// Represents an asset that can be downloaded from a URL with checksum verification.
|
|
||||||
///
|
|
||||||
/// This struct facilitates secure downloading of files from remote URLs by
|
|
||||||
/// verifying the integrity of the downloaded content using SHA-256 checksums.
|
|
||||||
/// It handles downloading the file, saving it to disk, and verifying the checksum matches
|
|
||||||
/// the expected value.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```compile_fail
|
|
||||||
/// # use url::Url;
|
|
||||||
/// # use std::path::PathBuf;
|
|
||||||
///
|
|
||||||
/// # async fn example() -> Result<(), String> {
|
|
||||||
/// let asset = DownloadableAsset {
|
|
||||||
/// url: Url::parse("https://example.com/file.zip").unwrap(),
|
|
||||||
/// file_name: "file.zip".to_string(),
|
|
||||||
/// checksum: "a1b2c3d4e5f6...".to_string(),
|
|
||||||
/// };
|
|
||||||
///
|
|
||||||
/// let download_dir = PathBuf::from("/tmp/downloads");
|
|
||||||
/// let file_path = asset.download_to_path(download_dir).await?;
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) struct DownloadableAsset {
|
|
||||||
pub(crate) url: Url,
|
|
||||||
pub(crate) file_name: String,
|
|
||||||
pub(crate) checksum: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DownloadableAsset {
|
|
||||||
fn verify_checksum(&self, file: PathBuf) -> bool {
|
|
||||||
if !file.exists() {
|
|
||||||
warn!("File does not exist: {:?}", file);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut file = match std::fs::File::open(&file) {
|
|
||||||
Ok(file) => file,
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Failed to open file for checksum verification: {:?}", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
let mut buffer = [0; 1024 * 1024]; // 1MB buffer
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let bytes_read = match file.read(&mut buffer) {
|
|
||||||
Ok(0) => break,
|
|
||||||
Ok(n) => n,
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Error reading file for checksum: {:?}", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
hasher.update(&buffer[..bytes_read]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = hasher.finalize();
|
|
||||||
let calculated_hash = format!("{:x}", result);
|
|
||||||
|
|
||||||
debug!("Expected checksum: {}", self.checksum);
|
|
||||||
debug!("Calculated checksum: {}", calculated_hash);
|
|
||||||
|
|
||||||
calculated_hash == self.checksum
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Downloads the asset to the specified directory, verifying its checksum.
|
|
||||||
///
|
|
||||||
/// This function will:
|
|
||||||
/// 1. Create the target directory if it doesn't exist
|
|
||||||
/// 2. Check if the file already exists with the correct checksum
|
|
||||||
/// 3. If not, download the file from the URL
|
|
||||||
/// 4. Verify the downloaded file's checksum matches the expected value
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `folder` - The directory path where the file should be saved
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Ok(PathBuf)` - The path to the downloaded file on success
|
|
||||||
/// * `Err(String)` - A descriptive error message if the download or verification fails
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// This function will return an error if:
|
|
||||||
/// - The network request fails
|
|
||||||
/// - The server responds with a non-success status code
|
|
||||||
/// - Writing to disk fails
|
|
||||||
/// - The checksum verification fails
|
|
||||||
pub(crate) async fn download_to_path(&self, folder: PathBuf) -> Result<PathBuf, String> {
|
|
||||||
if !folder.exists() {
|
|
||||||
fs::create_dir_all(&folder)
|
|
||||||
.await
|
|
||||||
.expect("Failed to create download directory");
|
|
||||||
}
|
|
||||||
|
|
||||||
let target_file_path = folder.join(&self.file_name);
|
|
||||||
debug!("Downloading to path: {:?}", target_file_path);
|
|
||||||
|
|
||||||
if self.verify_checksum(target_file_path.clone()) {
|
|
||||||
debug!("File already exists with correct checksum, skipping download");
|
|
||||||
return Ok(target_file_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("Downloading from URL: {}", self.url);
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let response = client
|
|
||||||
.get(self.url.clone())
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to download file: {e}"))?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
return Err(format!(
|
|
||||||
"Failed to download file, status: {}",
|
|
||||||
response.status()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut file = File::create(&target_file_path)
|
|
||||||
.await
|
|
||||||
.expect("Failed to create target file");
|
|
||||||
|
|
||||||
let mut stream = response.bytes_stream();
|
|
||||||
while let Some(chunk_result) = stream.next().await {
|
|
||||||
let chunk = chunk_result.expect("Error while downloading file");
|
|
||||||
file.write_all(&chunk)
|
|
||||||
.await
|
|
||||||
.expect("Failed to write data to file");
|
|
||||||
}
|
|
||||||
|
|
||||||
file.flush().await.expect("Failed to flush file");
|
|
||||||
drop(file);
|
|
||||||
|
|
||||||
if !self.verify_checksum(target_file_path.clone()) {
|
|
||||||
return Err(CHECKSUM_FAILED_MSG.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"File downloaded and verified successfully: {}",
|
|
||||||
target_file_path.to_string_lossy()
|
|
||||||
);
|
|
||||||
Ok(target_file_path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use httptest::{
|
|
||||||
matchers::{self, request},
|
|
||||||
responders, Expectation, Server,
|
|
||||||
};
|
|
||||||
|
|
||||||
const BASE_TEST_PATH: &str = "/tmp/harmony-test-k3d-download";
|
|
||||||
const TEST_CONTENT: &str = "This is a test file.";
|
|
||||||
const TEST_CONTENT_HASH: &str =
|
|
||||||
"f29bc64a9d3732b4b9035125fdb3285f5b6455778edca72414671e0ca3b2e0de";
|
|
||||||
|
|
||||||
fn setup_test() -> (PathBuf, Server) {
|
|
||||||
let _ = env_logger::builder().try_init();
|
|
||||||
|
|
||||||
// Create unique test directory
|
|
||||||
let test_id = std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_millis();
|
|
||||||
let download_path = format!("{}/test_{}", BASE_TEST_PATH, test_id);
|
|
||||||
std::fs::create_dir_all(&download_path).unwrap();
|
|
||||||
|
|
||||||
(PathBuf::from(download_path), Server::run())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_download_to_path_success() {
|
|
||||||
let (folder, server) = setup_test();
|
|
||||||
|
|
||||||
server.expect(
|
|
||||||
Expectation::matching(request::method_path("GET", "/test.txt"))
|
|
||||||
.respond_with(responders::status_code(200).body(TEST_CONTENT)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let asset = DownloadableAsset {
|
|
||||||
url: Url::parse(&server.url("/test.txt").to_string()).unwrap(),
|
|
||||||
file_name: "test.txt".to_string(),
|
|
||||||
checksum: TEST_CONTENT_HASH.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = asset
|
|
||||||
.download_to_path(folder.join("success"))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let downloaded_content = std::fs::read_to_string(result).unwrap();
|
|
||||||
assert_eq!(downloaded_content, TEST_CONTENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_download_to_path_already_exists() {
|
|
||||||
let (folder, server) = setup_test();
|
|
||||||
|
|
||||||
server.expect(
|
|
||||||
Expectation::matching(matchers::any())
|
|
||||||
.times(0)
|
|
||||||
.respond_with(responders::status_code(200).body(TEST_CONTENT)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let asset = DownloadableAsset {
|
|
||||||
url: Url::parse(&server.url("/test.txt").to_string()).unwrap(),
|
|
||||||
file_name: "test.txt".to_string(),
|
|
||||||
checksum: TEST_CONTENT_HASH.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let target_file_path = folder.join(&asset.file_name);
|
|
||||||
std::fs::write(&target_file_path, TEST_CONTENT).unwrap();
|
|
||||||
|
|
||||||
let result = asset.download_to_path(folder).await.unwrap();
|
|
||||||
let content = std::fs::read_to_string(result).unwrap();
|
|
||||||
assert_eq!(content, TEST_CONTENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_download_to_path_server_error() {
|
|
||||||
let (folder, server) = setup_test();
|
|
||||||
|
|
||||||
server.expect(
|
|
||||||
Expectation::matching(matchers::any()).respond_with(responders::status_code(404)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let asset = DownloadableAsset {
|
|
||||||
url: Url::parse(&server.url("/test.txt").to_string()).unwrap(),
|
|
||||||
file_name: "test.txt".to_string(),
|
|
||||||
checksum: TEST_CONTENT_HASH.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = asset.download_to_path(folder.join("error")).await;
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(result.unwrap_err().contains("status: 404"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_download_to_path_checksum_failure() {
|
|
||||||
let (folder, server) = setup_test();
|
|
||||||
|
|
||||||
let invalid_content = "This is NOT the expected content";
|
|
||||||
server.expect(
|
|
||||||
Expectation::matching(matchers::any())
|
|
||||||
.respond_with(responders::status_code(200).body(invalid_content)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let asset = DownloadableAsset {
|
|
||||||
url: Url::parse(&server.url("/test.txt").to_string()).unwrap(),
|
|
||||||
file_name: "test.txt".to_string(),
|
|
||||||
checksum: TEST_CONTENT_HASH.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let join_handle =
|
|
||||||
tokio::spawn(async move { asset.download_to_path(folder.join("failure")).await });
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
join_handle.await.unwrap().err().unwrap(),
|
|
||||||
CHECKSUM_FAILED_MSG
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_download_with_specific_path_matcher() {
|
|
||||||
let (folder, server) = setup_test();
|
|
||||||
|
|
||||||
server.expect(
|
|
||||||
Expectation::matching(matchers::request::path("/specific/path.txt"))
|
|
||||||
.respond_with(responders::status_code(200).body(TEST_CONTENT)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let asset = DownloadableAsset {
|
|
||||||
url: Url::parse(&server.url("/specific/path.txt").to_string()).unwrap(),
|
|
||||||
file_name: "path.txt".to_string(),
|
|
||||||
checksum: TEST_CONTENT_HASH.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = asset.download_to_path(folder).await.unwrap();
|
|
||||||
let downloaded_content = std::fs::read_to_string(result).unwrap();
|
|
||||||
assert_eq!(downloaded_content, TEST_CONTENT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
410
k3d/src/lib.rs
410
k3d/src/lib.rs
@@ -1,410 +0,0 @@
|
|||||||
mod downloadable_asset;
|
|
||||||
use downloadable_asset::*;
|
|
||||||
|
|
||||||
use kube::Client;
|
|
||||||
use log::{debug, info, warn};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
const K3D_BIN_FILE_NAME: &str = "k3d";
|
|
||||||
|
|
||||||
pub struct K3d {
|
|
||||||
base_dir: PathBuf,
|
|
||||||
cluster_name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl K3d {
|
|
||||||
pub fn new(base_dir: PathBuf, cluster_name: Option<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
base_dir,
|
|
||||||
cluster_name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_binary_for_current_platform(
|
|
||||||
&self,
|
|
||||||
latest_release: octocrab::models::repos::Release,
|
|
||||||
) -> DownloadableAsset {
|
|
||||||
let os = std::env::consts::OS;
|
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
|
|
||||||
debug!("Detecting platform: OS={}, ARCH={}", os, arch);
|
|
||||||
|
|
||||||
let binary_pattern = match (os, arch) {
|
|
||||||
("linux", "x86") => "k3d-linux-386",
|
|
||||||
("linux", "x86_64") => "k3d-linux-amd64",
|
|
||||||
("linux", "arm") => "k3d-linux-arm",
|
|
||||||
("linux", "aarch64") => "k3d-linux-arm64",
|
|
||||||
("windows", "x86_64") => "k3d-windows-amd64.exe",
|
|
||||||
("macos", "x86_64") => "k3d-darwin-amd64",
|
|
||||||
("macos", "aarch64") => "k3d-darwin-arm64",
|
|
||||||
_ => panic!("Unsupported platform: {}-{}", os, arch),
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!("Looking for binary matching pattern: {}", binary_pattern);
|
|
||||||
|
|
||||||
let binary_asset = latest_release
|
|
||||||
.assets
|
|
||||||
.iter()
|
|
||||||
.find(|asset| asset.name == binary_pattern)
|
|
||||||
.unwrap_or_else(|| panic!("No matching binary found for {}", binary_pattern));
|
|
||||||
|
|
||||||
let binary_url = binary_asset.browser_download_url.clone();
|
|
||||||
|
|
||||||
let checksums_asset = latest_release
|
|
||||||
.assets
|
|
||||||
.iter()
|
|
||||||
.find(|asset| asset.name == "checksums.txt")
|
|
||||||
.expect("Checksums file not found in release assets");
|
|
||||||
|
|
||||||
let checksums_url = checksums_asset.browser_download_url.clone();
|
|
||||||
|
|
||||||
let body = reqwest::get(checksums_url)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
println!("body: {body}");
|
|
||||||
|
|
||||||
let checksum = body
|
|
||||||
.lines()
|
|
||||||
.find_map(|line| {
|
|
||||||
if line.ends_with(&binary_pattern) {
|
|
||||||
Some(line.split_whitespace().next().unwrap_or("").to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| panic!("Checksum not found for {}", binary_pattern));
|
|
||||||
|
|
||||||
debug!("Found binary at {} with checksum {}", binary_url, checksum);
|
|
||||||
|
|
||||||
DownloadableAsset {
|
|
||||||
url: binary_url,
|
|
||||||
file_name: K3D_BIN_FILE_NAME.to_string(),
|
|
||||||
checksum,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn download_latest_release(&self) -> Result<PathBuf, String> {
|
|
||||||
let latest_release = self.get_latest_release_tag().await.unwrap();
|
|
||||||
|
|
||||||
let release_binary = self.get_binary_for_current_platform(latest_release).await;
|
|
||||||
info!("Foudn K3d binary to install : {release_binary:#?}");
|
|
||||||
release_binary.download_to_path(self.base_dir.clone()).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO : Make sure this will only find actual released versions, no prereleases or test
|
|
||||||
// builds
|
|
||||||
pub async fn get_latest_release_tag(&self) -> Result<octocrab::models::repos::Release, String> {
|
|
||||||
let octo = octocrab::instance();
|
|
||||||
let latest_release = octo
|
|
||||||
.repos("k3d-io", "k3d")
|
|
||||||
.releases()
|
|
||||||
.get_latest()
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
// debug!("Got k3d releases {releases:#?}");
|
|
||||||
println!("Got k3d first releases {latest_release:#?}");
|
|
||||||
|
|
||||||
Ok(latest_release)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if k3d binary exists and is executable
|
|
||||||
///
|
|
||||||
/// Verifies that:
|
|
||||||
/// 1. The k3d binary exists in the base directory
|
|
||||||
/// 2. It has proper executable permissions (on Unix systems)
|
|
||||||
/// 3. It responds correctly to a simple command (`k3d --version`)
|
|
||||||
pub fn is_installed(&self) -> bool {
|
|
||||||
let binary_path = self.get_k3d_binary_path();
|
|
||||||
|
|
||||||
if !binary_path.exists() {
|
|
||||||
debug!("K3d binary not found at {:?}", binary_path);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.ensure_binary_executable(&binary_path) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.can_execute_binary_check(&binary_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verifies if the specified cluster is already created
|
|
||||||
///
|
|
||||||
/// Executes `k3d cluster list <cluster_name>` and checks for a successful response,
|
|
||||||
/// indicating that the cluster exists and is registered with k3d.
|
|
||||||
pub fn is_cluster_initialized(&self) -> bool {
|
|
||||||
let cluster_name = match self.get_cluster_name() {
|
|
||||||
Ok(name) => name,
|
|
||||||
Err(_) => {
|
|
||||||
debug!("Could not get cluster name, can't verify if cluster is initialized");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let binary_path = self.base_dir.join(K3D_BIN_FILE_NAME);
|
|
||||||
if !binary_path.exists() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.verify_cluster_exists(&binary_path, cluster_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_cluster_name(&self) -> Result<&String, String> {
|
|
||||||
match &self.cluster_name {
|
|
||||||
Some(name) => Ok(name),
|
|
||||||
None => Err("No cluster name available".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new k3d cluster with the specified name
|
|
||||||
///
|
|
||||||
/// This method:
|
|
||||||
/// 1. Creates a new k3d cluster using `k3d cluster create <cluster_name>`
|
|
||||||
/// 2. Waits for the cluster to initialize
|
|
||||||
/// 3. Returns a configured Kubernetes client connected to the cluster
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// - `Ok(Client)` - Successfully created cluster and connected client
|
|
||||||
/// - `Err(String)` - Error message detailing what went wrong
|
|
||||||
pub async fn initialize_cluster(&self) -> Result<Client, String> {
|
|
||||||
let cluster_name = match self.get_cluster_name() {
|
|
||||||
Ok(name) => name,
|
|
||||||
Err(_) => return Err("Could not get cluster_name, cannot initialize".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
info!("Initializing k3d cluster '{}'", cluster_name);
|
|
||||||
|
|
||||||
self.create_cluster(cluster_name)?;
|
|
||||||
self.create_kubernetes_client().await
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_k3d_binary_path(&self) -> PathBuf {
|
|
||||||
self.base_dir.join(K3D_BIN_FILE_NAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_k3d_binary(&self) -> Result<PathBuf, String> {
|
|
||||||
let path = self.get_k3d_binary_path();
|
|
||||||
if !path.exists() {
|
|
||||||
return Err(format!("K3d binary not found at {:?}", path));
|
|
||||||
}
|
|
||||||
Ok(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensures k3d is installed and the cluster is initialized
|
|
||||||
///
|
|
||||||
/// This method provides a complete setup flow:
|
|
||||||
/// 1. Checks if k3d is installed, downloads and installs it if needed
|
|
||||||
/// 2. Verifies if the specified cluster exists, creates it if not
|
|
||||||
/// 3. Returns a Kubernetes client connected to the cluster
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// - `Ok(Client)` - Successfully ensured k3d and cluster are ready
|
|
||||||
/// - `Err(String)` - Error message if any step failed
|
|
||||||
pub async fn ensure_installed(&self) -> Result<Client, String> {
|
|
||||||
if !self.is_installed() {
|
|
||||||
info!("K3d is not installed, downloading latest release");
|
|
||||||
self.download_latest_release()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to download k3d: {}", e))?;
|
|
||||||
|
|
||||||
if !self.is_installed() {
|
|
||||||
return Err("Failed to install k3d properly".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.is_cluster_initialized() {
|
|
||||||
info!("Cluster is not initialized, initializing now");
|
|
||||||
return self.initialize_cluster().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.start_cluster().await?;
|
|
||||||
|
|
||||||
info!("K3d and cluster are already properly set up");
|
|
||||||
self.create_kubernetes_client().await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private helper methods
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn ensure_binary_executable(&self, binary_path: &PathBuf) -> bool {
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
|
|
||||||
let mut perms = match std::fs::metadata(binary_path) {
|
|
||||||
Ok(metadata) => metadata.permissions(),
|
|
||||||
Err(e) => {
|
|
||||||
debug!("Failed to get binary metadata: {}", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
perms.set_mode(0o755);
|
|
||||||
|
|
||||||
if let Err(e) = std::fs::set_permissions(binary_path, perms) {
|
|
||||||
debug!("Failed to set executable permissions on k3d binary: {}", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn ensure_binary_executable(&self, _binary_path: &PathBuf) -> bool {
|
|
||||||
// Windows doesn't use executable file permissions
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn can_execute_binary_check(&self, binary_path: &PathBuf) -> bool {
|
|
||||||
match std::process::Command::new(binary_path)
|
|
||||||
.arg("--version")
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
Ok(output) => {
|
|
||||||
if output.status.success() {
|
|
||||||
debug!("K3d binary is installed and working");
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
debug!("K3d binary check failed: {:?}", output);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
debug!("Failed to execute K3d binary: {}", e);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_cluster_exists(&self, binary_path: &PathBuf, cluster_name: &str) -> bool {
|
|
||||||
match std::process::Command::new(binary_path)
|
|
||||||
.args(["cluster", "list", cluster_name, "--no-headers"])
|
|
||||||
.output()
|
|
||||||
{
|
|
||||||
Ok(output) => {
|
|
||||||
if output.status.success() && !output.stdout.is_empty() {
|
|
||||||
debug!("Cluster '{}' is initialized", cluster_name);
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
debug!("Cluster '{}' is not initialized", cluster_name);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
debug!("Failed to check cluster initialization: {}", e);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_k3d_command<I, S>(&self, args: I) -> Result<std::process::Output, String>
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = S>,
|
|
||||||
S: AsRef<std::ffi::OsStr>,
|
|
||||||
{
|
|
||||||
let binary_path = self.get_k3d_binary()?;
|
|
||||||
let output = std::process::Command::new(binary_path).args(args).output();
|
|
||||||
match output {
|
|
||||||
Ok(output) => {
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
debug!("stderr : {}", stderr);
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
debug!("stdout : {}", stdout);
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
Err(e) => Err(format!("Failed to execute k3d command: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_cluster(&self, cluster_name: &str) -> Result<(), String> {
|
|
||||||
let output = self.run_k3d_command(["cluster", "create", cluster_name])?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
return Err(format!("Failed to create cluster: {}", stderr));
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Successfully created k3d cluster '{}'", cluster_name);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_kubernetes_client(&self) -> Result<Client, String> {
|
|
||||||
warn!("TODO this method is way too dumb, it should make sure that the client is connected to the k3d cluster actually represented by this instance, not just any default client");
|
|
||||||
Client::try_default()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to create Kubernetes client: {}", e))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_client(&self) -> Result<Client, String> {
|
|
||||||
match self.is_cluster_initialized() {
|
|
||||||
true => Ok(self.create_kubernetes_client().await?),
|
|
||||||
false => Err("Cannot get client! Cluster not initialized yet".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_cluster(&self) -> Result<(), String> {
|
|
||||||
let cluster_name = self.get_cluster_name()?;
|
|
||||||
let output = self.run_k3d_command(["cluster", "start", cluster_name])?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
return Err(format!("Failed to start cluster: {}", stderr));
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Successfully started k3d cluster '{}'", cluster_name);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use regex::Regex;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::{K3d, K3D_BIN_FILE_NAME};
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn k3d_latest_release_should_get_latest() {
|
|
||||||
let dir = get_clean_test_directory();
|
|
||||||
|
|
||||||
assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), false);
|
|
||||||
|
|
||||||
let k3d = K3d::new(dir.clone(), None);
|
|
||||||
let latest_release = k3d.get_latest_release_tag().await.unwrap();
|
|
||||||
|
|
||||||
let tag_regex = Regex::new(r"^v\d+\.\d+\.\d+$").unwrap();
|
|
||||||
assert!(tag_regex.is_match(&latest_release.tag_name));
|
|
||||||
assert!(!latest_release.tag_name.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn k3d_download_latest_release_should_get_latest_bin() {
|
|
||||||
let dir = get_clean_test_directory();
|
|
||||||
|
|
||||||
assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), false);
|
|
||||||
|
|
||||||
let k3d = K3d::new(dir.clone(), None);
|
|
||||||
let bin_file_path = k3d.download_latest_release().await.unwrap();
|
|
||||||
assert_eq!(bin_file_path, dir.join(K3D_BIN_FILE_NAME));
|
|
||||||
assert_eq!(dir.join(K3D_BIN_FILE_NAME).exists(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_clean_test_directory() -> PathBuf {
|
|
||||||
let dir = PathBuf::from("/tmp/harmony-k3d-test-dir");
|
|
||||||
|
|
||||||
if dir.exists() {
|
|
||||||
if let Err(e) = std::fs::remove_dir_all(&dir) {
|
|
||||||
// TODO sometimes this fails because of the race when running multiple tests at
|
|
||||||
// once
|
|
||||||
panic!("Failed to clean up test directory: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = std::fs::create_dir_all(&dir) {
|
|
||||||
panic!("Failed to create test directory: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
dir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,7 @@ pub struct Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for Config {
|
impl Serialize for Config {
|
||||||
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,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,11 +10,10 @@ mod test {
|
|||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[cfg(opnsenseendtoend)]
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_public_sdk() {
|
async fn test_public_sdk() {
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
let mac = "11:22:33:44:55:66";
|
let mac = "11:22:33:44:55:66";
|
||||||
let ip = Ipv4Addr::new(10, 100, 8, 200);
|
let ip = Ipv4Addr::new(10, 100, 8, 200);
|
||||||
let hostname = "test_hostname";
|
let hostname = "test_hostname";
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "example"
|
name = "example"
|
||||||
edition = "2024"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user