Compare commits

...

31 Commits

Author SHA1 Message Date
6e6f57e38c Merge pull request 'fix: added routes to domain name for prometheus, grafana, alertmanageradded argo cd to the reporting after successfull build' (#155) from fix/add_routes_to_domain into master
Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/155
2025-09-10 19:44:53 +00:00
6f55f79281 feat: Update readme with newer UX/DX Rust Leptos app, update slides and misc stuff 2025-09-10 15:40:32 -04:00
19f87fdaf7 fix: added routes to domain name for prometheus, grafana, alertmanageradded argo cd to the reporting after successfull build 2025-09-10 15:08:13 -04:00
49370af176 Merge pull request 'doc: Slides demo 10 sept' (#153) from feat/slides_demo_10sept into master
Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/153
2025-09-10 17:14:48 +00:00
cf0b8326dc Merge pull request 'fix: properly configured discord alert receiver corrected domain and topic name for ntfy' (#154) from fix/alertreceivers into master
Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/154
2025-09-10 17:13:31 +00:00
1e2563f7d1 fix: added reporting to output ntfy topic 2025-09-10 13:10:06 -04:00
7f50c36f11 Merge pull request 'fix: Various demo fixe and rename : RHOBMonitoring -> Monitoring, ContinuousDelivery -> PackagingDeployment, Fix bollard logs' (#152) from fix/demo into master
Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/152
2025-09-10 17:01:15 +00:00
4df451bc41 Merge remote-tracking branch 'origin/master' into fix/demo 2025-09-10 12:59:58 -04:00
49dad343ad fix: properly configured discord alert receiver corrected domain and topic name for ntfy 2025-09-10 12:53:43 -04:00
9961e8b79d doc: Slides demo 10 sept 2025-09-10 12:38:25 -04:00
9b889f71da Merge pull request 'feat: Report execution outcome' (#151) from report-execution-outcome into master
Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/151
2025-09-10 02:50:45 +00:00
7514ebfb5c fix format 2025-09-09 22:50:26 -04:00
b3ae4e6611 fix: Various demo fixe and rename : RHOBMonitoring -> Monitoring, ContinuousDelivery -> PackagingDeployment, Fix bollard logs 2025-09-09 22:46:14 -04:00
8424778871 add http 2025-09-09 22:24:36 -04:00
7bc083701e report application deploy URL 2025-09-09 22:18:00 -04:00
4fa2b8deb6 chore: Add files to create in a leptos project in try_rust example 2025-09-09 20:46:33 -04:00
Ian Letourneau
f3639c604c report Ntfy endpoint 2025-09-09 20:12:24 -04:00
258cfa279e chore: Cleanup some logs and error message, also add a todo on bollard push failure to private registry 2025-09-09 19:58:49 -04:00
ceafabf430 wip: Report harmony execution outcome 2025-09-09 17:59:14 -04:00
11481b16cd fix: Multiple ingress fixes for localk3d, it works nicely now for Application and ntfy at least. Also fix k3d kubeconfig context by force switching to it every time. Not perfect but better and more intuitive for the user to view his resources. 2025-09-09 16:41:53 -04:00
21dcb75408 Merge pull request 'fix/connected_alert_receivers' (#150) from fix/connected_alert_receivers into master
Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/150
2025-09-09 20:23:59 +00:00
a5f9ecfcf7 cargo fmt 2025-09-09 15:36:49 -04:00
849bd79710 connected alert rules, grafana, etc 2025-09-09 15:35:28 -04:00
c5101e096a Merge pull request 'fix/ingress' (#145) from fix/ingress into master
Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/145
2025-09-09 18:25:52 +00:00
cd0720f43e connected ingress to servicemodified rust application helm chart deployment to not use tls and cert-manager annotation 2025-09-09 13:09:52 -04:00
b9e04d21da get domain for a service 2025-09-09 09:46:00 -04:00
a0884950d7 remove hardcoded domain and secrets in Ntfy 2025-09-09 08:27:43 -04:00
29d22a611f Merge branch 'master' into fix/ingress 2025-09-09 08:11:21 -04:00
3bf5cb0526 use topology domain to build & push helm package for continuous deliery 2025-09-08 21:53:44 -04:00
54803c40a2 ingress: check whether running as local k3d or kubeconfig 2025-09-08 20:43:12 -04:00
288129b0c1 wip: added ingress scores for install grafana and install prometheusadded ingress capability to k8s anywhere topology
need to get the domain name dynamically from the topology when building the app to insert into the helm chart
2025-09-08 16:16:01 -04:00
86 changed files with 1460 additions and 393 deletions

View File

@ -36,48 +36,59 @@ These principles surface as simple, ergonomic Rust APIs that let teams focus on
## 2 · Quick Start ## 2 · Quick Start
The snippet below spins up a complete **production-grade LAMP stack** with monitoring. Swap it for your own scores to deploy anything from microservices to machine-learning pipelines. The snippet below spins up a complete **production-grade Rust + Leptos Webapp** with monitoring. Swap it for your own scores to deploy anything from microservices to machine-learning pipelines.
```rust ```rust
use harmony::{ use harmony::{
data::Version,
inventory::Inventory, inventory::Inventory,
maestro::Maestro,
modules::{ modules::{
lamp::{LAMPConfig, LAMPScore}, application::{
monitoring::monitoring_alerting::MonitoringAlertingStackScore, ApplicationScore, RustWebFramework, RustWebapp,
features::{PackagingDeployment, rhob_monitoring::Monitoring},
}, },
topology::{K8sAnywhereTopology, Url}, monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
},
topology::K8sAnywhereTopology,
}; };
use harmony_macros::hurl;
use std::{path::PathBuf, sync::Arc};
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// 1. Describe what you want let application = Arc::new(RustWebapp {
let lamp_stack = LAMPScore { name: "harmony-example-leptos".to_string(),
name: "harmony-lamp-demo".into(), project_root: PathBuf::from(".."), // <== Your project root, usually .. if you use the standard `/harmony` folder
domain: Url::Url(url::Url::parse("https://lampdemo.example.com").unwrap()), framework: Some(RustWebFramework::Leptos),
php_version: Version::from("8.3.0").unwrap(), service_port: 8080,
config: LAMPConfig { });
project_root: "./php".into(),
database_size: "4Gi".into(), // Define your Application deployment and the features you want
..Default::default() let app = ApplicationScore {
}, features: vec![
Box::new(PackagingDeployment {
application: application.clone(),
}),
Box::new(Monitoring {
application: application.clone(),
alert_receiver: vec![
Box::new(DiscordWebhook {
name: "test-discord".to_string(),
url: hurl!("https://discord.doesnt.exist.com"), // <== Get your discord webhook url
}),
],
}),
],
application,
}; };
// 2. Enhance with extra scores (monitoring, CI/CD, …)
let mut monitoring = MonitoringAlertingStackScore::new();
monitoring.namespace = Some(lamp_stack.config.namespace.clone());
// 3. Run your scores on the desired topology & inventory
harmony_cli::run( harmony_cli::run(
Inventory::autoload(), // auto-detect hardware / kube-config Inventory::autoload(),
K8sAnywhereTopology::from_env(), // local k3d, CI, staging, prod… K8sAnywhereTopology::from_env(), // <== Deploy to local automatically provisioned local k3d by default or connect to any kubernetes cluster
vec![ vec![Box::new(app)],
Box::new(lamp_stack), None,
Box::new(monitoring) )
], .await
None .unwrap();
).await.unwrap();
} }
``` ```

View File

@ -0,0 +1,3 @@
.terraform
*.tfstate
venv

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -0,0 +1,5 @@
To build :
```bash
npx @marp-team/marp-cli@latest -w slides.md
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,9 @@
To run this :
```bash
virtualenv venv
source venv/bin/activate
pip install ansible ansible-dev-tools
ansible-lint download.yml
ansible-playbook -i localhost download.yml
```

View File

@ -0,0 +1,8 @@
- name: Test Ansible URL Validation
hosts: localhost
tasks:
- name: Download a file
ansible.builtin.get_url:
url: "http:/wikipedia.org/"
dest: "/tmp/ansible-test/wikipedia.html"
mode: '0900'

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,241 @@
---
theme: uncover
---
# Voici l'histoire de Petit Poisson
---
<img src="./Happy_swimmer.jpg" width="600"/>
---
<img src="./happy_landscape_swimmer.jpg" width="1000"/>
---
<img src="./Happy_swimmer.jpg" width="200"/>
<img src="./tryrust.org.png" width="600"/>
[https://tryrust.org](https://tryrust.org)
---
<img src="./texto_deploy_prod_1.png" width="600"/>
---
<img src="./texto_deploy_prod_2.png" width="600"/>
---
<img src="./texto_deploy_prod_3.png" width="600"/>
---
<img src="./texto_deploy_prod_4.png" width="600"/>
---
## Demo time
---
<img src="./Happy_swimmer_sunglasses.jpg" width="1000"/>
---
<img src="./texto_download_wikipedia.png" width="600"/>
---
<img src="./ansible.jpg" width="200"/>
## Ansible❓
---
<img src="./Happy_swimmer.jpg" width="200"/>
```yaml
- name: Download wikipedia
hosts: localhost
tasks:
- name: Download a file
ansible.builtin.get_url:
url: "https:/wikipedia.org/"
dest: "/tmp/ansible-test/wikipedia.html"
mode: '0900'
```
---
<img src="./Happy_swimmer.jpg" width="200"/>
```
ansible-lint download.yml
Passed: 0 failure(s), 0 warning(s) on 1 files. Last profile that met the validation criteria was 'production'.
```
---
```
git push
```
---
<img src="./75_years_later.jpg" width="1100"/>
---
<img src="./texto_download_wikipedia_fail.png" width="600"/>
---
<img src="./Happy_swimmer_reversed.jpg" width="600"/>
---
<img src="./ansible_output_fail.jpg" width="1100"/>
---
<img src="./Happy_swimmer_reversed_1hit.jpg" width="600"/>
---
<img src="./ansible_crossed_out.jpg" width="400"/>
---
<img src="./terraform.jpg" width="400"/>
## Terraform❓❗
---
<img src="./Happy_swimmer_reversed_1hit.jpg" width="200"/>
<img src="./terraform.jpg" width="200"/>
```tf
provider "docker" {}
resource "docker_network" "invalid_network" {
name = "my-invalid-network"
ipam_config {
subnet = "172.17.0.0/33"
}
}
```
---
<img src="./Happy_swimmer_reversed_1hit.jpg" width="100"/>
<img src="./terraform.jpg" width="200"/>
```
terraform plan
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# docker_network.invalid_network will be created
+ resource "docker_network" "invalid_network" {
+ driver = (known after apply)
+ id = (known after apply)
+ internal = (known after apply)
+ ipam_driver = "default"
+ name = "my-invalid-network"
+ options = (known after apply)
+ scope = (known after apply)
+ ipam_config {
+ subnet = "172.17.0.0/33"
# (2 unchanged attributes hidden)
}
}
Plan: 1 to add, 0 to change, 0 to destroy.
```
---
---
```
terraform apply
```
---
```
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
```
---
```
docker_network.invalid_network: Creating...
│ Error: Unable to create network: Error response from daemon: invalid network config:
│ invalid subnet 172.17.0.0/33: invalid CIDR block notation
│ with docker_network.invalid_network,
│ on main.tf line 11, in resource "docker_network" "invalid_network":
│ 11: resource "docker_network" "invalid_network" {
```
---
<img src="./Happy_swimmer_reversed_fullhit.jpg" width="1100"/>
---
<img src="./ansible_crossed_out.jpg" width="300"/>
<img src="./terraform_crossed_out.jpg" width="400"/>
<img src="./Happy_swimmer_reversed_fullhit.jpg" width="300"/>
---
## Harmony❓❗
---
Demo time
---
<img src="./Happy_swimmer.jpg" width="300"/>
---
# 🎼
Harmony : [https://git.nationtech.io/nationtech/harmony](https://git.nationtech.io/nationtech/harmony)
<img src="./qrcode_gitea_nationtech.png" width="120"/>
LinkedIn : [https://www.linkedin.com/in/jean-gabriel-gill-couture/](https://www.linkedin.com/in/jean-gabriel-gill-couture/)
Courriel : [jg@nationtech.io](mailto:jg@nationtech.io)

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,40 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/http" {
version = "3.5.0"
hashes = [
"h1:8bUoPwS4hahOvzCBj6b04ObLVFXCEmEN8T/5eOHmWOM=",
"zh:047c5b4920751b13425efe0d011b3a23a3be97d02d9c0e3c60985521c9c456b7",
"zh:157866f700470207561f6d032d344916b82268ecd0cf8174fb11c0674c8d0736",
"zh:1973eb9383b0d83dd4fd5e662f0f16de837d072b64a6b7cd703410d730499476",
"zh:212f833a4e6d020840672f6f88273d62a564f44acb0c857b5961cdb3bbc14c90",
"zh:2c8034bc039fffaa1d4965ca02a8c6d57301e5fa9fff4773e684b46e3f78e76a",
"zh:5df353fc5b2dd31577def9cc1a4ebf0c9a9c2699d223c6b02087a3089c74a1c6",
"zh:672083810d4185076c81b16ad13d1224b9e6ea7f4850951d2ab8d30fa6e41f08",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:7b4200f18abdbe39904b03537e1a78f21ebafe60f1c861a44387d314fda69da6",
"zh:843feacacd86baed820f81a6c9f7bd32cf302db3d7a0f39e87976ebc7a7cc2ee",
"zh:a9ea5096ab91aab260b22e4251c05f08dad2ed77e43e5e4fadcdfd87f2c78926",
"zh:d02b288922811739059e90184c7f76d45d07d3a77cc48d0b15fd3db14e928623",
]
}
provider "registry.terraform.io/hashicorp/local" {
version = "2.5.3"
hashes = [
"h1:1Nkh16jQJMp0EuDmvP/96f5Unnir0z12WyDuoR6HjMo=",
"zh:284d4b5b572eacd456e605e94372f740f6de27b71b4e1fd49b63745d8ecd4927",
"zh:40d9dfc9c549e406b5aab73c023aa485633c1b6b730c933d7bcc2fa67fd1ae6e",
"zh:6243509bb208656eb9dc17d3c525c89acdd27f08def427a0dce22d5db90a4c8b",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:885d85869f927853b6fe330e235cd03c337ac3b933b0d9ae827ec32fa1fdcdbf",
"zh:bab66af51039bdfcccf85b25fe562cbba2f54f6b3812202f4873ade834ec201d",
"zh:c505ff1bf9442a889ac7dca3ac05a8ee6f852e0118dd9a61796a2f6ff4837f09",
"zh:d36c0b5770841ddb6eaf0499ba3de48e5d4fc99f4829b6ab66b0fab59b1aaf4f",
"zh:ddb6a407c7f3ec63efb4dad5f948b54f7f4434ee1a2607a49680d494b1776fe1",
"zh:e0dafdd4500bec23d3ff221e3a9b60621c5273e5df867bc59ef6b7e41f5c91f6",
"zh:ece8742fd2882a8fc9d6efd20e2590010d43db386b920b2a9c220cfecc18de47",
"zh:f4c6b3eb8f39105004cf720e202f04f57e3578441cfb76ca27611139bc116a82",
]
}

View File

@ -0,0 +1,10 @@
provider "http" {}
data "http" "remote_file" {
url = "http:/example.com/file.txt"
}
resource "local_file" "downloaded_file" {
content = data.http.remote_file.body
filename = "${path.module}/downloaded_file.txt"
}

View File

@ -0,0 +1,24 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/kreuzwerker/docker" {
version = "3.0.2"
constraints = "~> 3.0.1"
hashes = [
"h1:cT2ccWOtlfKYBUE60/v2/4Q6Stk1KYTNnhxSck+VPlU=",
"zh:15b0a2b2b563d8d40f62f83057d91acb02cd0096f207488d8b4298a59203d64f",
"zh:23d919de139f7cd5ebfd2ff1b94e6d9913f0977fcfc2ca02e1573be53e269f95",
"zh:38081b3fe317c7e9555b2aaad325ad3fa516a886d2dfa8605ae6a809c1072138",
"zh:4a9c5065b178082f79ad8160243369c185214d874ff5048556d48d3edd03c4da",
"zh:5438ef6afe057945f28bce43d76c4401254073de01a774760169ac1058830ac2",
"zh:60b7fadc287166e5c9873dfe53a7976d98244979e0ab66428ea0dea1ebf33e06",
"zh:61c5ec1cb94e4c4a4fb1e4a24576d5f39a955f09afb17dab982de62b70a9bdd1",
"zh:a38fe9016ace5f911ab00c88e64b156ebbbbfb72a51a44da3c13d442cd214710",
"zh:c2c4d2b1fd9ebb291c57f524b3bf9d0994ff3e815c0cd9c9bcb87166dc687005",
"zh:d567bb8ce483ab2cf0602e07eae57027a1a53994aba470fa76095912a505533d",
"zh:e83bf05ab6a19dd8c43547ce9a8a511f8c331a124d11ac64687c764ab9d5a792",
"zh:e90c934b5cd65516fbcc454c89a150bfa726e7cf1fe749790c7480bbeb19d387",
"zh:f05f167d2eaf913045d8e7b88c13757e3cf595dd5cd333057fdafc7c4b7fed62",
"zh:fcc9c1cea5ce85e8bcb593862e699a881bd36dffd29e2e367f82d15368659c3d",
]
}

View File

@ -0,0 +1,17 @@
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0.1" # Adjust version as needed
}
}
}
provider "docker" {}
resource "docker_network" "invalid_network" {
name = "my-invalid-network"
ipam_config {
subnet = "172.17.0.0/33"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

View File

@ -27,7 +27,6 @@ async fn main() {
}; };
let application = Arc::new(RustWebapp { let application = Arc::new(RustWebapp {
name: "example-monitoring".to_string(), name: "example-monitoring".to_string(),
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
project_root: PathBuf::from("./examples/rust/webapp"), project_root: PathBuf::from("./examples/rust/webapp"),
framework: Some(RustWebFramework::Leptos), framework: Some(RustWebFramework::Leptos),
service_port: 3000, service_port: 3000,

View File

@ -4,8 +4,7 @@ use harmony::{
inventory::Inventory, inventory::Inventory,
modules::{ modules::{
application::{ application::{
ApplicationScore, RustWebFramework, RustWebapp, ApplicationScore, RustWebFramework, RustWebapp, features::rhob_monitoring::Monitoring,
features::rhob_monitoring::RHOBMonitoring,
}, },
monitoring::alert_channel::discord_alert_channel::DiscordWebhook, monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
}, },
@ -17,7 +16,6 @@ use harmony_types::net::Url;
async fn main() { async fn main() {
let application = Arc::new(RustWebapp { let application = Arc::new(RustWebapp {
name: "test-rhob-monitoring".to_string(), name: "test-rhob-monitoring".to_string(),
domain: Url::Url(url::Url::parse("htps://some-fake-url").unwrap()),
project_root: PathBuf::from("./webapp"), // Relative from 'harmony-path' param project_root: PathBuf::from("./webapp"), // Relative from 'harmony-path' param
framework: Some(RustWebFramework::Leptos), framework: Some(RustWebFramework::Leptos),
service_port: 3000, service_port: 3000,
@ -30,7 +28,7 @@ async fn main() {
let app = ApplicationScore { let app = ApplicationScore {
features: vec![ features: vec![
Box::new(RHOBMonitoring { Box::new(Monitoring {
application: application.clone(), application: application.clone(),
alert_receiver: vec![Box::new(discord_receiver)], alert_receiver: vec![Box::new(discord_receiver)],
}), }),

View File

@ -5,7 +5,7 @@ use harmony::{
modules::{ modules::{
application::{ application::{
ApplicationScore, RustWebFramework, RustWebapp, ApplicationScore, RustWebFramework, RustWebapp,
features::{ContinuousDelivery, Monitoring}, features::{Monitoring, PackagingDeployment},
}, },
monitoring::alert_channel::{ monitoring::alert_channel::{
discord_alert_channel::DiscordWebhook, webhook_receiver::WebhookReceiver, discord_alert_channel::DiscordWebhook, webhook_receiver::WebhookReceiver,
@ -19,7 +19,6 @@ use harmony_macros::hurl;
async fn main() { async fn main() {
let application = Arc::new(RustWebapp { let application = Arc::new(RustWebapp {
name: "harmony-example-rust-webapp".to_string(), name: "harmony-example-rust-webapp".to_string(),
domain: hurl!("https://rustapp.harmony.example.com"),
project_root: PathBuf::from("./webapp"), project_root: PathBuf::from("./webapp"),
framework: Some(RustWebFramework::Leptos), framework: Some(RustWebFramework::Leptos),
service_port: 3000, service_port: 3000,
@ -37,7 +36,7 @@ async fn main() {
let app = ApplicationScore { let app = ApplicationScore {
features: vec![ features: vec![
Box::new(ContinuousDelivery { Box::new(PackagingDeployment {
application: application.clone(), application: application.clone(),
}), }),
Box::new(Monitoring { Box::new(Monitoring {

View File

@ -0,0 +1 @@
harmony

View File

@ -0,0 +1,20 @@
[package]
name = "harmony-tryrust"
edition = "2024"
version = "0.1.0"
[dependencies]
harmony = { path = "../../../nationtech/harmony/harmony" }
harmony_cli = { path = "../../../nationtech/harmony/harmony_cli" }
harmony_types = { path = "../../../nationtech/harmony/harmony_types" }
harmony_macros = { path = "../../../nationtech/harmony/harmony_macros" }
tokio = { version = "1.40", features = [
"io-std",
"fs",
"macros",
"rt-multi-thread",
] }
log = { version = "0.4", features = ["kv"] }
env_logger = "0.11"
url = "2.5"
base64 = "0.22.1"

View File

@ -0,0 +1,50 @@
use harmony::{
inventory::Inventory,
modules::{
application::{
ApplicationScore, RustWebFramework, RustWebapp,
features::{PackagingDeployment, rhob_monitoring::Monitoring},
},
monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
},
topology::K8sAnywhereTopology,
};
use harmony_macros::hurl;
use std::{path::PathBuf, sync::Arc};
#[tokio::main]
async fn main() {
let application = Arc::new(RustWebapp {
name: "tryrust".to_string(),
project_root: PathBuf::from(".."),
framework: Some(RustWebFramework::Leptos),
service_port: 8080,
});
let discord_webhook = DiscordWebhook {
name: "harmony_demo".to_string(),
url: hurl!("http://not_a_url.com"),
};
let app = ApplicationScore {
features: vec![
Box::new(PackagingDeployment {
application: application.clone(),
}),
Box::new(Monitoring {
application: application.clone(),
alert_receiver: vec![Box::new(discord_webhook)],
}),
],
application,
};
harmony_cli::run(
Inventory::autoload(),
K8sAnywhereTopology::from_env(),
vec![Box::new(app)],
None,
)
.await
.unwrap();
}

View File

@ -1,41 +1,39 @@
use std::{path::PathBuf, sync::Arc};
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,
modules::{ modules::{
application::{ application::{
ApplicationScore, RustWebFramework, RustWebapp, ApplicationScore, RustWebFramework, RustWebapp,
features::{ContinuousDelivery, Monitoring}, features::{PackagingDeployment, rhob_monitoring::Monitoring},
}, },
monitoring::alert_channel::discord_alert_channel::DiscordWebhook, monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
}, },
topology::K8sAnywhereTopology, topology::K8sAnywhereTopology,
}; };
use harmony_types::net::Url; use harmony_macros::hurl;
use std::{path::PathBuf, sync::Arc};
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let application = Arc::new(RustWebapp { let application = Arc::new(RustWebapp {
name: "harmony-example-tryrust".to_string(), name: "harmony-example-tryrust".to_string(),
domain: Url::Url(url::Url::parse("https://tryrust.harmony.example.com").unwrap()), project_root: PathBuf::from("./tryrust.org"), // <== Project root, in this case it is a
project_root: PathBuf::from("./tryrust.org"), // submodule
framework: Some(RustWebFramework::Leptos), framework: Some(RustWebFramework::Leptos),
service_port: 8080, service_port: 8080,
}); });
let discord_receiver = DiscordWebhook { // Define your Application deployment and the features you want
name: "test-discord".to_string(),
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
};
let app = ApplicationScore { let app = ApplicationScore {
features: vec![ features: vec![
Box::new(ContinuousDelivery { Box::new(PackagingDeployment {
application: application.clone(), application: application.clone(),
}), }),
Box::new(Monitoring { Box::new(Monitoring {
application: application.clone(), application: application.clone(),
alert_receiver: vec![Box::new(discord_receiver)], alert_receiver: vec![Box::new(DiscordWebhook {
name: "test-discord".to_string(),
url: hurl!("https://discord.doesnt.exist.com"),
})],
}), }),
], ],
application, application,
@ -43,7 +41,7 @@ async fn main() {
harmony_cli::run( harmony_cli::run(
Inventory::autoload(), Inventory::autoload(),
K8sAnywhereTopology::from_env(), K8sAnywhereTopology::from_env(), // <== Deploy to local automatically provisioned k3d by default or connect to any kubernetes cluster
vec![Box::new(app)], vec![Box::new(app)],
None, None,
) )

View File

@ -10,7 +10,11 @@ testing = []
[dependencies] [dependencies]
hex = "0.4" hex = "0.4"
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features = false } reqwest = { version = "0.11", features = [
"blocking",
"json",
"rustls-tls",
], default-features = false }
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"

View File

@ -34,6 +34,7 @@ pub enum InterpretName {
CephClusterHealth, CephClusterHealth,
Custom(&'static str), Custom(&'static str),
RHOBAlerting, RHOBAlerting,
K8sIngress,
} }
impl std::fmt::Display for InterpretName { impl std::fmt::Display for InterpretName {
@ -64,6 +65,7 @@ impl std::fmt::Display for InterpretName {
InterpretName::CephClusterHealth => f.write_str("CephClusterHealth"), InterpretName::CephClusterHealth => f.write_str("CephClusterHealth"),
InterpretName::Custom(name) => f.write_str(name), InterpretName::Custom(name) => f.write_str(name),
InterpretName::RHOBAlerting => f.write_str("RHOBAlerting"), InterpretName::RHOBAlerting => f.write_str("RHOBAlerting"),
InterpretName::K8sIngress => f.write_str("K8sIngress"),
} }
} }
} }
@ -82,13 +84,15 @@ pub trait Interpret<T>: std::fmt::Debug + Send {
pub struct Outcome { pub struct Outcome {
pub status: InterpretStatus, pub status: InterpretStatus,
pub message: String, pub message: String,
pub details: Vec<String>,
} }
impl Outcome { impl Outcome {
pub fn noop() -> Self { pub fn noop(message: String) -> Self {
Self { Self {
status: InterpretStatus::NOOP, status: InterpretStatus::NOOP,
message: String::new(), message,
details: vec![],
} }
} }
@ -96,6 +100,23 @@ impl Outcome {
Self { Self {
status: InterpretStatus::SUCCESS, status: InterpretStatus::SUCCESS,
message, message,
details: vec![],
}
}
pub fn success_with_details(message: String, details: Vec<String>) -> Self {
Self {
status: InterpretStatus::SUCCESS,
message,
details,
}
}
pub fn running(message: String) -> Self {
Self {
status: InterpretStatus::RUNNING,
message,
details: vec![],
} }
} }
} }

View File

@ -0,0 +1,7 @@
use crate::topology::PreparationError;
use async_trait::async_trait;
#[async_trait]
pub trait Ingress {
async fn get_domain(&self, service: &str) -> Result<String, PreparationError>;
}

View File

@ -1,6 +1,7 @@
use std::{process::Command, sync::Arc}; use std::{process::Command, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use kube::api::GroupVersionKind;
use log::{debug, info, warn}; use log::{debug, info, warn};
use serde::Serialize; use serde::Serialize;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
@ -22,6 +23,7 @@ use crate::{
}, },
}, },
score::Score, score::Score,
topology::ingress::Ingress,
}; };
use super::{ use super::{
@ -198,6 +200,26 @@ impl K8sAnywhereTopology {
} }
} }
async fn openshift_ingress_operator_available(&self) -> Result<(), PreparationError> {
let client = self.k8s_client().await?;
let gvk = GroupVersionKind {
group: "operator.openshift.io".into(),
version: "v1".into(),
kind: "IngressController".into(),
};
let ic = client
.get_resource_json_value("default", Some("openshift-ingress-operator"), &gvk)
.await?;
let ready_replicas = ic.data["status"]["availableReplicas"].as_i64().unwrap_or(0);
if ready_replicas >= 1 {
return Ok(());
} else {
return Err(PreparationError::new(
"openshift-ingress-operator not available".to_string(),
));
}
}
fn is_helm_available(&self) -> Result<(), String> { fn is_helm_available(&self) -> Result<(), String> {
let version_result = Command::new("helm") let version_result = Command::new("helm")
.arg("version") .arg("version")
@ -350,6 +372,10 @@ impl K8sAnywhereTopology {
if let Some(Some(k8s_state)) = self.k8s_state.get() { if let Some(Some(k8s_state)) = self.k8s_state.get() {
match k8s_state.source { match k8s_state.source {
K8sSource::LocalK3d => { K8sSource::LocalK3d => {
warn!(
"Installing observability operator is not supported on LocalK3d source"
);
return Ok(PreparationOutcome::Noop);
debug!("installing cluster observability operator"); debug!("installing cluster observability operator");
todo!(); todo!();
let op_score = let op_score =
@ -528,7 +554,7 @@ impl MultiTargetTopology for K8sAnywhereTopology {
match self.config.harmony_profile.to_lowercase().as_str() { match self.config.harmony_profile.to_lowercase().as_str() {
"staging" => DeploymentTarget::Staging, "staging" => DeploymentTarget::Staging,
"production" => DeploymentTarget::Production, "production" => DeploymentTarget::Production,
_ => todo!("HARMONY_PROFILE must be set when use_local_k3d is not set"), _ => todo!("HARMONY_PROFILE must be set when use_local_k3d is false"),
} }
} }
} }
@ -550,3 +576,45 @@ impl TenantManager for K8sAnywhereTopology {
.await .await
} }
} }
#[async_trait]
impl Ingress for K8sAnywhereTopology {
//TODO this is specifically for openshift/okd which violates the k8sanywhere idea
async fn get_domain(&self, service: &str) -> Result<String, PreparationError> {
let client = self.k8s_client().await?;
if let Some(Some(k8s_state)) = self.k8s_state.get() {
match k8s_state.source {
K8sSource::LocalK3d => Ok(format!("{service}.local.k3d")),
K8sSource::Kubeconfig => {
self.openshift_ingress_operator_available().await?;
let gvk = GroupVersionKind {
group: "operator.openshift.io".into(),
version: "v1".into(),
kind: "IngressController".into(),
};
let ic = client
.get_resource_json_value(
"default",
Some("openshift-ingress-operator"),
&gvk,
)
.await
.map_err(|_| {
PreparationError::new("Failed to fetch IngressController".to_string())
})?;
match ic.data["status"]["domain"].as_str() {
Some(domain) => Ok(format!("{service}.{domain}")),
None => Err(PreparationError::new("Could not find domain".to_string())),
}
}
}
} else {
Err(PreparationError::new(
"Cannot get domain: unable to detect K8s state".to_string(),
))
}
}
}

View File

@ -1,9 +1,10 @@
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new; use derive_new::new;
use serde::{Deserialize, Serialize};
use super::{HelmCommand, PreparationError, PreparationOutcome, Topology}; use super::{HelmCommand, PreparationError, PreparationOutcome, Topology};
#[derive(new)] #[derive(new, Clone, Debug, Serialize, Deserialize)]
pub struct LocalhostTopology; pub struct LocalhostTopology;
#[async_trait] #[async_trait]

View File

@ -1,4 +1,5 @@
mod ha_cluster; mod ha_cluster;
pub mod ingress;
use harmony_types::net::IpAddress; use harmony_types::net::IpAddress;
mod host_binding; mod host_binding;
mod http; mod http;

View File

@ -1,7 +1,10 @@
use std::error::Error;
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new;
use serde::Serialize; use serde::Serialize;
use crate::topology::Topology; use crate::{executors::ExecutorError, topology::Topology};
/// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability, /// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability,
/// ContinuousIntegration, ContinuousDelivery /// ContinuousIntegration, ContinuousDelivery
@ -9,7 +12,10 @@ use crate::topology::Topology;
pub trait ApplicationFeature<T: Topology>: pub trait ApplicationFeature<T: Topology>:
std::fmt::Debug + Send + Sync + ApplicationFeatureClone<T> std::fmt::Debug + Send + Sync + ApplicationFeatureClone<T>
{ {
async fn ensure_installed(&self, topology: &T) -> Result<(), String>; async fn ensure_installed(
&self,
topology: &T,
) -> Result<InstallationOutcome, InstallationError>;
fn name(&self) -> String; fn name(&self) -> String;
} }
@ -40,3 +46,60 @@ impl<T: Topology> Clone for Box<dyn ApplicationFeature<T>> {
self.clone_box() self.clone_box()
} }
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InstallationOutcome {
Success { details: Vec<String> },
Noop,
}
impl InstallationOutcome {
pub fn success() -> Self {
Self::Success { details: vec![] }
}
pub fn success_with_details(details: Vec<String>) -> Self {
Self::Success { details }
}
pub fn noop() -> Self {
Self::Noop
}
}
#[derive(Debug, Clone, new)]
pub struct InstallationError {
msg: String,
}
impl std::fmt::Display for InstallationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.msg)
}
}
impl Error for InstallationError {}
impl From<ExecutorError> for InstallationError {
fn from(value: ExecutorError) -> Self {
Self {
msg: format!("InstallationError : {value}"),
}
}
}
impl From<kube::Error> for InstallationError {
fn from(value: kube::Error) -> Self {
Self {
msg: format!("InstallationError : {value}"),
}
}
}
impl From<String> for InstallationError {
fn from(value: String) -> Self {
Self {
msg: format!("PreparationError : {value}"),
}
}
}

View File

@ -2,7 +2,7 @@ use async_trait::async_trait;
use log::info; use log::info;
use crate::{ use crate::{
modules::application::ApplicationFeature, modules::application::{ApplicationFeature, InstallationError, InstallationOutcome},
topology::{K8sclient, Topology}, topology::{K8sclient, Topology},
}; };
@ -29,7 +29,10 @@ impl Default for PublicEndpoint {
/// For now we only suport K8s ingress, but we will support more stuff at some point /// For now we only suport K8s ingress, but we will support more stuff at some point
#[async_trait] #[async_trait]
impl<T: Topology + K8sclient + 'static> ApplicationFeature<T> for PublicEndpoint { impl<T: Topology + K8sclient + 'static> ApplicationFeature<T> for PublicEndpoint {
async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { async fn ensure_installed(
&self,
_topology: &T,
) -> Result<InstallationOutcome, InstallationError> {
info!( info!(
"Making sure public endpoint is installed for port {}", "Making sure public endpoint is installed for port {}",
self.application_port self.application_port

View File

@ -13,7 +13,8 @@ use crate::{
modules::helm::chart::{HelmChartScore, HelmRepository}, modules::helm::chart::{HelmChartScore, HelmRepository},
score::Score, score::Score,
topology::{ topology::{
HelmCommand, K8sclient, PreparationError, PreparationOutcome, Topology, k8s::K8sClient, HelmCommand, K8sclient, PreparationError, PreparationOutcome, Topology, ingress::Ingress,
k8s::K8sClient,
}, },
}; };
use harmony_types::id::Id; use harmony_types::id::Id;
@ -27,7 +28,7 @@ pub struct ArgoHelmScore {
pub argo_apps: Vec<ArgoApplication>, pub argo_apps: Vec<ArgoApplication>,
} }
impl<T: Topology + HelmCommand + K8sclient> Score<T> for ArgoHelmScore { impl<T: Topology + HelmCommand + K8sclient + Ingress> Score<T> for ArgoHelmScore {
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
Box::new(ArgoInterpret { Box::new(ArgoInterpret {
score: self.clone(), score: self.clone(),
@ -47,17 +48,15 @@ pub struct ArgoInterpret {
} }
#[async_trait] #[async_trait]
impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for ArgoInterpret { impl<T: Topology + K8sclient + HelmCommand + Ingress> Interpret<T> for ArgoInterpret {
async fn execute( async fn execute(
&self, &self,
inventory: &Inventory, inventory: &Inventory,
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
let k8s_client = topology.k8s_client().await?; let k8s_client = topology.k8s_client().await?;
let domain = self let svc = format!("argo-{}", self.score.namespace.clone());
.get_host_domain(k8s_client.clone(), self.score.openshift) let domain = topology.get_domain(&svc).await?;
.await?;
let domain = format!("argo.{domain}");
let helm_score = let helm_score =
argo_helm_chart_score(&self.score.namespace, self.score.openshift, &domain); argo_helm_chart_score(&self.score.namespace, self.score.openshift, &domain);
@ -68,14 +67,17 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for ArgoInterpret {
.await .await
.unwrap(); .unwrap();
Ok(Outcome::success(format!( Ok(Outcome::success_with_details(
"ArgoCD installed with {} {}", format!(
"ArgoCD {} {}",
self.argo_apps.len(), self.argo_apps.len(),
match self.argo_apps.len() { match self.argo_apps.len() {
1 => "application", 1 => "application",
_ => "applications", _ => "applications",
} }
))) ),
vec![format!("argo application: http://{}", domain)],
))
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {

View File

@ -5,8 +5,8 @@ pub use endpoint::*;
mod monitoring; mod monitoring;
pub use monitoring::*; pub use monitoring::*;
mod continuous_delivery; mod packaging_deployment;
pub use continuous_delivery::*; pub use packaging_deployment::*;
mod helm_argocd_score; mod helm_argocd_score;
pub use helm_argocd_score::*; pub use helm_argocd_score::*;

View File

@ -1,10 +1,10 @@
use std::sync::Arc; use crate::modules::application::{
Application, ApplicationFeature, InstallationError, InstallationOutcome,
use crate::modules::application::{Application, ApplicationFeature}; };
use crate::modules::monitoring::application_monitoring::application_monitoring_score::ApplicationMonitoringScore; use crate::modules::monitoring::application_monitoring::application_monitoring_score::ApplicationMonitoringScore;
use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus; use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus;
use crate::topology::MultiTargetTopology; use crate::topology::MultiTargetTopology;
use crate::topology::ingress::Ingress;
use crate::{ use crate::{
inventory::Inventory, inventory::Inventory,
modules::monitoring::{ modules::monitoring::{
@ -19,8 +19,12 @@ use crate::{
}; };
use async_trait::async_trait; use async_trait::async_trait;
use base64::{Engine as _, engine::general_purpose}; use base64::{Engine as _, engine::general_purpose};
use harmony_secret::SecretManager;
use harmony_secret_derive::Secret;
use harmony_types::net::Url; use harmony_types::net::Url;
use log::{debug, info}; use log::{debug, info};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Monitoring { pub struct Monitoring {
@ -36,17 +40,22 @@ impl<
+ TenantManager + TenantManager
+ K8sclient + K8sclient
+ MultiTargetTopology + MultiTargetTopology
+ std::fmt::Debug + PrometheusApplicationMonitoring<CRDPrometheus>
+ PrometheusApplicationMonitoring<CRDPrometheus>, + Ingress
+ std::fmt::Debug,
> ApplicationFeature<T> for Monitoring > ApplicationFeature<T> for Monitoring
{ {
async fn ensure_installed(&self, topology: &T) -> Result<(), String> { async fn ensure_installed(
&self,
topology: &T,
) -> Result<InstallationOutcome, InstallationError> {
info!("Ensuring monitoring is available for application"); info!("Ensuring monitoring is available for application");
let namespace = topology let namespace = topology
.get_tenant_config() .get_tenant_config()
.await .await
.map(|ns| ns.name.clone()) .map(|ns| ns.name.clone())
.unwrap_or_else(|| self.application.name()); .unwrap_or_else(|| self.application.name());
let domain = topology.get_domain("ntfy").await.unwrap();
let mut alerting_score = ApplicationMonitoringScore { let mut alerting_score = ApplicationMonitoringScore {
sender: CRDPrometheus { sender: CRDPrometheus {
@ -58,19 +67,17 @@ impl<
}; };
let ntfy = NtfyScore { let ntfy = NtfyScore {
namespace: namespace.clone(), namespace: namespace.clone(),
host: "ntfy.harmonydemo.apps.ncd0.harmony.mcd".to_string(), host: domain,
}; };
ntfy.interpret(&Inventory::empty(), topology) ntfy.interpret(&Inventory::empty(), topology)
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let ntfy_default_auth_username = "harmony"; let config = SecretManager::get_or_prompt::<NtfyAuth>().await.unwrap();
let ntfy_default_auth_password = "harmony";
let ntfy_default_auth_header = format!( let ntfy_default_auth_header = format!(
"Basic {}", "Basic {}",
general_purpose::STANDARD.encode(format!( general_purpose::STANDARD.encode(format!("{}:{}", config.username, config.password))
"{ntfy_default_auth_username}:{ntfy_default_auth_password}"
))
); );
debug!("ntfy_default_auth_header: {ntfy_default_auth_header}"); debug!("ntfy_default_auth_header: {ntfy_default_auth_header}");
@ -100,9 +107,17 @@ impl<
.interpret(&Inventory::empty(), topology) .interpret(&Inventory::empty(), topology)
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
Ok(())
Ok(InstallationOutcome::success())
} }
fn name(&self) -> String { fn name(&self) -> String {
"Monitoring".to_string() "Monitoring".to_string()
} }
} }
#[derive(Secret, Serialize, Deserialize, Clone, Debug)]
struct NtfyAuth {
username: String,
password: String,
}

View File

@ -10,11 +10,13 @@ use crate::{
data::Version, data::Version,
inventory::Inventory, inventory::Inventory,
modules::application::{ modules::application::{
ApplicationFeature, HelmPackage, OCICompliant, ApplicationFeature, HelmPackage, InstallationError, InstallationOutcome, OCICompliant,
features::{ArgoApplication, ArgoHelmScore}, features::{ArgoApplication, ArgoHelmScore},
}, },
score::Score, score::Score,
topology::{DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology}, topology::{
DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology, ingress::Ingress,
},
}; };
/// ContinuousDelivery in Harmony provides this functionality : /// ContinuousDelivery in Harmony provides this functionality :
@ -45,11 +47,11 @@ use crate::{
/// - ArgoCD to install/upgrade/rollback/inspect k8s resources /// - ArgoCD to install/upgrade/rollback/inspect k8s resources
/// - Kubernetes for runtime orchestration /// - Kubernetes for runtime orchestration
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct ContinuousDelivery<A: OCICompliant + HelmPackage> { pub struct PackagingDeployment<A: OCICompliant + HelmPackage> {
pub application: Arc<A>, pub application: Arc<A>,
} }
impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> { impl<A: OCICompliant + HelmPackage> PackagingDeployment<A> {
async fn deploy_to_local_k3d( async fn deploy_to_local_k3d(
&self, &self,
app_name: String, app_name: String,
@ -136,18 +138,28 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
#[async_trait] #[async_trait]
impl< impl<
A: OCICompliant + HelmPackage + Clone + 'static, A: OCICompliant + HelmPackage + Clone + 'static,
T: Topology + HelmCommand + MultiTargetTopology + K8sclient + 'static, T: Topology + HelmCommand + MultiTargetTopology + K8sclient + Ingress + 'static,
> ApplicationFeature<T> for ContinuousDelivery<A> > ApplicationFeature<T> for PackagingDeployment<A>
{ {
async fn ensure_installed(&self, topology: &T) -> Result<(), String> { async fn ensure_installed(
&self,
topology: &T,
) -> Result<InstallationOutcome, InstallationError> {
let image = self.application.image_name(); let image = self.application.image_name();
let domain = topology
.get_domain(&self.application.name())
.await
.map_err(|e| e.to_string())?;
// TODO Write CI/CD workflow files // TODO Write CI/CD workflow files
// we can autotedect the CI type using the remote url (default to github action for github // we can autotedect the CI type using the remote url (default to github action for github
// url, etc..) // url, etc..)
// Or ask for it when unknown // Or ask for it when unknown
let helm_chart = self.application.build_push_helm_package(&image).await?; let helm_chart = self
.application
.build_push_helm_package(&image, &domain)
.await?;
// TODO: Make building image configurable/skippable if image already exists (prompt)") // TODO: Make building image configurable/skippable if image already exists (prompt)")
// https://git.nationtech.io/NationTech/harmony/issues/104 // https://git.nationtech.io/NationTech/harmony/issues/104
@ -196,7 +208,11 @@ impl<
.unwrap(); .unwrap();
} }
}; };
Ok(())
Ok(InstallationOutcome::success_with_details(vec![format!(
"{}: http://{domain}",
self.application.name()
)]))
} }
fn name(&self) -> String { fn name(&self) -> String {
"ContinuousDelivery".to_string() "ContinuousDelivery".to_string()

View File

@ -1,11 +1,14 @@
use std::sync::Arc; use std::sync::Arc;
use crate::modules::application::{Application, ApplicationFeature}; use crate::modules::application::{
Application, ApplicationFeature, InstallationError, InstallationOutcome,
};
use crate::modules::monitoring::application_monitoring::application_monitoring_score::ApplicationMonitoringScore; use crate::modules::monitoring::application_monitoring::application_monitoring_score::ApplicationMonitoringScore;
use crate::modules::monitoring::application_monitoring::rhobs_application_monitoring_score::ApplicationRHOBMonitoringScore; use crate::modules::monitoring::application_monitoring::rhobs_application_monitoring_score::ApplicationRHOBMonitoringScore;
use crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::RHOBObservability; use crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::RHOBObservability;
use crate::topology::MultiTargetTopology; use crate::topology::MultiTargetTopology;
use crate::topology::ingress::Ingress;
use crate::{ use crate::{
inventory::Inventory, inventory::Inventory,
modules::monitoring::{ modules::monitoring::{
@ -24,7 +27,7 @@ use harmony_types::net::Url;
use log::{debug, info}; use log::{debug, info};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RHOBMonitoring { pub struct Monitoring {
pub application: Arc<dyn Application>, pub application: Arc<dyn Application>,
pub alert_receiver: Vec<Box<dyn AlertReceiver<RHOBObservability>>>, pub alert_receiver: Vec<Box<dyn AlertReceiver<RHOBObservability>>>,
} }
@ -37,11 +40,15 @@ impl<
+ TenantManager + TenantManager
+ K8sclient + K8sclient
+ MultiTargetTopology + MultiTargetTopology
+ Ingress
+ std::fmt::Debug + std::fmt::Debug
+ PrometheusApplicationMonitoring<RHOBObservability>, + PrometheusApplicationMonitoring<RHOBObservability>,
> ApplicationFeature<T> for RHOBMonitoring > ApplicationFeature<T> for Monitoring
{ {
async fn ensure_installed(&self, topology: &T) -> Result<(), String> { async fn ensure_installed(
&self,
topology: &T,
) -> Result<InstallationOutcome, InstallationError> {
info!("Ensuring monitoring is available for application"); info!("Ensuring monitoring is available for application");
let namespace = topology let namespace = topology
.get_tenant_config() .get_tenant_config()
@ -57,9 +64,13 @@ impl<
application: self.application.clone(), application: self.application.clone(),
receivers: self.alert_receiver.clone(), receivers: self.alert_receiver.clone(),
}; };
let domain = topology
.get_domain("ntfy")
.await
.map_err(|e| format!("could not get domain {e}"))?;
let ntfy = NtfyScore { let ntfy = NtfyScore {
namespace: namespace.clone(), namespace: namespace.clone(),
host: "ntfy.harmonydemo.apps.ncd0.harmony.mcd".to_string(), host: domain.clone(),
}; };
ntfy.interpret(&Inventory::empty(), topology) ntfy.interpret(&Inventory::empty(), topology)
.await .await
@ -81,27 +92,33 @@ impl<
.replace("=", ""); .replace("=", "");
debug!("ntfy_default_auth_param: {ntfy_default_auth_param}"); debug!("ntfy_default_auth_param: {ntfy_default_auth_param}");
let ntfy_receiver = WebhookReceiver { let ntfy_receiver = WebhookReceiver {
name: "ntfy-webhook".to_string(), name: "ntfy-webhook".to_string(),
url: Url::Url( url: Url::Url(
url::Url::parse( url::Url::parse(
format!( format!(
"http://ntfy.{}.svc.cluster.local/rust-web-app?auth={ntfy_default_auth_param}", "http://{domain}/{}?auth={ntfy_default_auth_param}",
namespace.clone() self.application.name()
) )
.as_str(), .as_str(),
) )
.unwrap(), .unwrap(),
), ),
}; };
debug!(
"ntfy webhook receiver \n{:#?}\nntfy topic: {}",
ntfy_receiver.clone(),
self.application.name()
);
alerting_score.receivers.push(Box::new(ntfy_receiver)); alerting_score.receivers.push(Box::new(ntfy_receiver));
alerting_score alerting_score
.interpret(&Inventory::empty(), topology) .interpret(&Inventory::empty(), topology)
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
Ok(()) Ok(InstallationOutcome::success_with_details(vec![format!(
"ntfy topic: {}",
self.application.name()
)]))
} }
fn name(&self) -> String { fn name(&self) -> String {
"Monitoring".to_string() "Monitoring".to_string()

View File

@ -24,8 +24,8 @@ use harmony_types::id::Id;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum ApplicationFeatureStatus { pub enum ApplicationFeatureStatus {
Installing, Installing,
Installed, Installed { details: Vec<String> },
Failed { details: String }, Failed { message: String },
} }
pub trait Application: std::fmt::Debug + Send + Sync { pub trait Application: std::fmt::Debug + Send + Sync {
@ -65,27 +65,32 @@ impl<A: Application, T: Topology + std::fmt::Debug> Interpret<T> for Application
.unwrap(); .unwrap();
let _ = match feature.ensure_installed(topology).await { let _ = match feature.ensure_installed(topology).await {
Ok(()) => { Ok(outcome) => {
instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged { instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged {
topology: topology.name().into(), topology: topology.name().into(),
application: self.application.name(), application: self.application.name(),
feature: feature.name(), feature: feature.name(),
status: ApplicationFeatureStatus::Installed, status: ApplicationFeatureStatus::Installed {
details: match outcome {
InstallationOutcome::Success { details } => details,
InstallationOutcome::Noop => vec![],
},
},
}) })
.unwrap(); .unwrap();
} }
Err(msg) => { Err(error) => {
instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged { instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged {
topology: topology.name().into(), topology: topology.name().into(),
application: self.application.name(), application: self.application.name(),
feature: feature.name(), feature: feature.name(),
status: ApplicationFeatureStatus::Failed { status: ApplicationFeatureStatus::Failed {
details: msg.clone(), message: error.to_string(),
}, },
}) })
.unwrap(); .unwrap();
return Err(InterpretError::new(format!( return Err(InterpretError::new(format!(
"Application Interpret failed to install feature : {msg}" "Application Interpret failed to install feature : {error}"
))); )));
} }
}; };

View File

@ -1,6 +1,5 @@
use async_trait::async_trait;
use super::Application; use super::Application;
use async_trait::async_trait;
#[async_trait] #[async_trait]
pub trait OCICompliant: Application { pub trait OCICompliant: Application {
@ -17,5 +16,10 @@ pub trait HelmPackage: Application {
/// ///
/// # Arguments /// # Arguments
/// * `image_url` - The full URL of the OCI container image to be used in the Deployment. /// * `image_url` - The full URL of the OCI container image to be used in the Deployment.
async fn build_push_helm_package(&self, image_url: &str) -> Result<String, String>; /// * `domain` - The domain where the application is hosted.
async fn build_push_helm_package(
&self,
image_url: &str,
domain: &str,
) -> Result<String, String>;
} }

View File

@ -1,5 +1,4 @@
use std::fs::{self, File}; use std::fs::{self};
use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process; use std::process;
use std::sync::Arc; use std::sync::Arc;
@ -11,14 +10,13 @@ use dockerfile_builder::Dockerfile;
use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR}; use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR};
use dockerfile_builder::instruction_builder::CopyBuilder; use dockerfile_builder::instruction_builder::CopyBuilder;
use futures_util::StreamExt; use futures_util::StreamExt;
use log::{debug, info, log_enabled}; use log::{debug, error, info, log_enabled, trace, warn};
use serde::Serialize; use serde::Serialize;
use tar::{Archive, Builder, Header}; use tar::{Builder, Header};
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL}; use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
use crate::{score::Score, topology::Topology}; use crate::{score::Score, topology::Topology};
use harmony_types::net::Url;
use super::{Application, ApplicationFeature, ApplicationInterpret, HelmPackage, OCICompliant}; use super::{Application, ApplicationFeature, ApplicationInterpret, HelmPackage, OCICompliant};
@ -58,7 +56,6 @@ pub enum RustWebFramework {
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct RustWebapp { pub struct RustWebapp {
pub name: String, pub name: String,
pub domain: Url,
/// The path to the root of the Rust project to be containerized. /// The path to the root of the Rust project to be containerized.
pub project_root: PathBuf, pub project_root: PathBuf,
pub service_port: u32, pub service_port: u32,
@ -73,12 +70,17 @@ impl Application for RustWebapp {
#[async_trait] #[async_trait]
impl HelmPackage for RustWebapp { impl HelmPackage for RustWebapp {
async fn build_push_helm_package(&self, image_url: &str) -> Result<String, String> { async fn build_push_helm_package(
&self,
image_url: &str,
domain: &str,
) -> Result<String, String> {
info!("Starting Helm chart build and push for '{}'", self.name); info!("Starting Helm chart build and push for '{}'", self.name);
// 1. Create the Helm chart files on disk. // 1. Create the Helm chart files on disk.
let chart_dir = self let chart_dir = self
.create_helm_chart_files(image_url) .create_helm_chart_files(image_url, domain)
.await
.map_err(|e| format!("Failed to create Helm chart files: {}", e))?; .map_err(|e| format!("Failed to create Helm chart files: {}", e))?;
info!("Successfully created Helm chart files in {:?}", chart_dir); info!("Successfully created Helm chart files in {:?}", chart_dir);
@ -160,7 +162,7 @@ impl RustWebapp {
&self, &self,
image_name: &str, image_name: &str,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
debug!("Generating Dockerfile for '{}'", self.name); info!("Generating Dockerfile for '{}'", self.name);
let dockerfile = self.get_or_build_dockerfile(); let dockerfile = self.get_or_build_dockerfile();
let quiet = !log_enabled!(log::Level::Debug); let quiet = !log_enabled!(log::Level::Debug);
match dockerfile match dockerfile
@ -192,8 +194,41 @@ impl RustWebapp {
Some(body_full(tar_data.into())), Some(body_full(tar_data.into())),
); );
while let Some(msg) = image_build_stream.next().await { while let Some(mut msg) = image_build_stream.next().await {
debug!("Message: {msg:?}"); trace!("Got bollard msg {msg:?}");
match msg {
Ok(mut msg) => {
if let Some(progress) = msg.progress_detail {
info!(
"Build progress {}/{}",
progress.current.unwrap_or(0),
progress.total.unwrap_or(0)
);
}
if let Some(mut log) = msg.stream {
if log.ends_with('\n') {
log.pop();
if log.ends_with('\r') {
log.pop();
}
}
info!("{log}");
}
if let Some(error) = msg.error {
warn!("Build error : {error:?}");
}
if let Some(error) = msg.error_detail {
warn!("Build error : {error:?}");
}
}
Err(e) => {
error!("Build failed : {e}");
return Err(format!("Build failed : {e}").into());
}
}
} }
Ok(image_name.to_string()) Ok(image_name.to_string())
@ -220,7 +255,9 @@ impl RustWebapp {
".git", ".git",
".github", ".github",
".harmony_generated", ".harmony_generated",
"harmony",
"node_modules", "node_modules",
"Dockerfile.harmony",
]; ];
let mut entries: Vec<_> = WalkDir::new(project_root) let mut entries: Vec<_> = WalkDir::new(project_root)
.into_iter() .into_iter()
@ -265,8 +302,6 @@ impl RustWebapp {
let docker = Docker::connect_with_socket_defaults().unwrap(); let docker = Docker::connect_with_socket_defaults().unwrap();
// let push_options = PushImageOptionsBuilder::new().tag(tag);
let mut push_image_stream = docker.push_image( let mut push_image_stream = docker.push_image(
image_tag, image_tag,
Some(PushImageOptionsBuilder::new().build()), Some(PushImageOptionsBuilder::new().build()),
@ -274,6 +309,8 @@ impl RustWebapp {
); );
while let Some(msg) = push_image_stream.next().await { while let Some(msg) = push_image_stream.next().await {
// let msg = msg?;
// TODO this fails silently, for some reason bollard cannot push to hub.nationtech.io
debug!("Message: {msg:?}"); debug!("Message: {msg:?}");
} }
@ -408,9 +445,10 @@ impl RustWebapp {
} }
/// Creates all necessary files for a basic Helm chart. /// Creates all necessary files for a basic Helm chart.
fn create_helm_chart_files( async fn create_helm_chart_files(
&self, &self,
image_url: &str, image_url: &str,
domain: &str,
) -> Result<PathBuf, Box<dyn std::error::Error>> { ) -> Result<PathBuf, Box<dyn std::error::Error>> {
let chart_name = format!("{}-chart", self.name); let chart_name = format!("{}-chart", self.name);
let chart_dir = self let chart_dir = self
@ -460,21 +498,15 @@ ingress:
enabled: true enabled: true
# Annotations for cert-manager to handle SSL. # Annotations for cert-manager to handle SSL.
annotations: annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
# Add other annotations like nginx ingress class if needed # Add other annotations like nginx ingress class if needed
# kubernetes.io/ingress.class: nginx # kubernetes.io/ingress.class: nginx
hosts: hosts:
- host: chart-example.local - host: {}
paths: paths:
- path: / - path: /
pathType: ImplementationSpecific pathType: ImplementationSpecific
tls:
- secretName: {}-tls
hosts:
- chart-example.local
"#, "#,
chart_name, image_repo, image_tag, self.service_port, self.name chart_name, image_repo, image_tag, self.service_port, domain,
); );
fs::write(chart_dir.join("values.yaml"), values_yaml)?; fs::write(chart_dir.join("values.yaml"), values_yaml)?;

View File

@ -69,17 +69,14 @@ impl DhcpInterpret {
dhcp_server.set_pxe_options(pxe_options).await?; dhcp_server.set_pxe_options(pxe_options).await?;
Ok(Outcome::new( Ok(Outcome::success(format!(
InterpretStatus::SUCCESS,
format!(
"Dhcp Interpret Set next boot to [{:?}], boot_filename to [{:?}], filename to [{:?}], filename64 to [{:?}], filenameipxe to [:{:?}]", "Dhcp Interpret Set next boot to [{:?}], boot_filename to [{:?}], filename to [{:?}], filename64 to [{:?}], filenameipxe to [:{:?}]",
self.score.boot_filename, self.score.boot_filename,
self.score.boot_filename, self.score.boot_filename,
self.score.filename, self.score.filename,
self.score.filename64, self.score.filename64,
self.score.filenameipxe self.score.filenameipxe
), )))
))
} }
} }
@ -122,8 +119,7 @@ impl<T: Topology + DhcpServer> Interpret<T> for DhcpInterpret {
topology.commit_config().await?; topology.commit_config().await?;
Ok(Outcome::new( Ok(Outcome::success(
InterpretStatus::SUCCESS,
"Dhcp Interpret execution successful".to_string(), "Dhcp Interpret execution successful".to_string(),
)) ))
} }
@ -197,10 +193,10 @@ impl DhcpHostBindingInterpret {
} }
} }
Ok(Outcome::new( Ok(Outcome::success(format!(
InterpretStatus::SUCCESS, "Dhcp Interpret registered {} entries",
format!("Dhcp Interpret registered {} entries", number_new_entries), number_new_entries
)) )))
} }
} }
@ -236,12 +232,9 @@ impl<T: DhcpServer> Interpret<T> for DhcpHostBindingInterpret {
topology.commit_config().await?; topology.commit_config().await?;
Ok(Outcome::new( Ok(Outcome::success(format!(
InterpretStatus::SUCCESS,
format!(
"Dhcp Host Binding Interpret execution successful on {} hosts", "Dhcp Host Binding Interpret execution successful on {} hosts",
self.score.host_binding.len() self.score.host_binding.len()
), )))
))
} }
} }

View File

@ -55,8 +55,7 @@ impl DnsInterpret {
dns.register_dhcp_leases(register).await?; dns.register_dhcp_leases(register).await?;
} }
Ok(Outcome::new( Ok(Outcome::success(
InterpretStatus::SUCCESS,
"DNS Interpret execution successfull".to_string(), "DNS Interpret execution successfull".to_string(),
)) ))
} }
@ -68,13 +67,10 @@ impl DnsInterpret {
let entries = &self.score.dns_entries; let entries = &self.score.dns_entries;
dns_server.ensure_hosts_registered(entries.clone()).await?; dns_server.ensure_hosts_registered(entries.clone()).await?;
Ok(Outcome::new( Ok(Outcome::success(format!(
InterpretStatus::SUCCESS,
format!(
"DnsInterpret registered {} hosts successfully", "DnsInterpret registered {} hosts successfully",
entries.len() entries.len()
), )))
))
} }
} }
@ -111,8 +107,7 @@ impl<T: Topology + DnsServer> Interpret<T> for DnsInterpret {
topology.commit_config().await?; topology.commit_config().await?;
Ok(Outcome::new( Ok(Outcome::success(
InterpretStatus::SUCCESS,
"Dns Interpret execution successful".to_string(), "Dns Interpret execution successful".to_string(),
)) ))
} }

View File

@ -153,6 +153,10 @@ impl<T: Topology + HelmCommand> Interpret<T> for HelmChartInterpret {
let yaml_path: Option<&Path> = match self.score.values_yaml.as_ref() { let yaml_path: Option<&Path> = match self.score.values_yaml.as_ref() {
Some(yaml_str) => { Some(yaml_str) => {
tf = temp_file::with_contents(yaml_str.as_bytes()); tf = temp_file::with_contents(yaml_str.as_bytes());
debug!(
"values yaml string for chart {} :\n {yaml_str}",
self.score.chart_name
);
Some(tf.path()) Some(tf.path())
} }
None => None, None => None,
@ -193,13 +197,10 @@ impl<T: Topology + HelmCommand> Interpret<T> for HelmChartInterpret {
self.score.release_name, ns self.score.release_name, ns
); );
return Ok(Outcome::new( return Ok(Outcome::success(format!(
InterpretStatus::SUCCESS,
format!(
"Helm Chart '{}' already installed to namespace {ns} and install_only=true", "Helm Chart '{}' already installed to namespace {ns} and install_only=true",
self.score.release_name self.score.release_name
), )));
));
} else { } else {
info!( info!(
"Release '{}' not found in namespace '{}'. Proceeding with installation.", "Release '{}' not found in namespace '{}'. Proceeding with installation.",
@ -224,18 +225,18 @@ impl<T: Topology + HelmCommand> Interpret<T> for HelmChartInterpret {
}; };
match status { match status {
helm_wrapper_rs::HelmDeployStatus::Deployed => Ok(Outcome::new( helm_wrapper_rs::HelmDeployStatus::Deployed => Ok(Outcome::success(format!(
InterpretStatus::SUCCESS, "Helm Chart {} deployed",
format!("Helm Chart {} deployed", self.score.release_name), self.score.release_name
)), ))),
helm_wrapper_rs::HelmDeployStatus::PendingInstall => Ok(Outcome::new( helm_wrapper_rs::HelmDeployStatus::PendingInstall => Ok(Outcome::running(format!(
InterpretStatus::RUNNING, "Helm Chart {} pending install...",
format!("Helm Chart {} pending install...", self.score.release_name), self.score.release_name
)), ))),
helm_wrapper_rs::HelmDeployStatus::PendingUpgrade => Ok(Outcome::new( helm_wrapper_rs::HelmDeployStatus::PendingUpgrade => Ok(Outcome::running(format!(
InterpretStatus::RUNNING, "Helm Chart {} pending upgrade...",
format!("Helm Chart {} pending upgrade...", self.score.release_name), self.score.release_name
)), ))),
helm_wrapper_rs::HelmDeployStatus::Failed => Err(InterpretError::new(format!( helm_wrapper_rs::HelmDeployStatus::Failed => Err(InterpretError::new(format!(
"Helm Chart {} installation failed", "Helm Chart {} installation failed",
self.score.release_name self.score.release_name

View File

@ -133,10 +133,9 @@ impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret {
}, },
) )
.await; .await;
Ok(Outcome { Ok(Outcome::success(
status: InterpretStatus::SUCCESS, "Discovery process completed successfully".to_string(),
message: "Discovery process completed successfully".to_string(), ))
})
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {

View File

@ -1,11 +1,15 @@
use async_trait::async_trait;
use harmony_macros::ingress_path; use harmony_macros::ingress_path;
use harmony_types::id::Id;
use k8s_openapi::api::networking::v1::Ingress; use k8s_openapi::api::networking::v1::Ingress;
use log::{debug, trace}; use log::{debug, trace};
use serde::Serialize; use serde::Serialize;
use serde_json::json; use serde_json::json;
use crate::{ use crate::{
interpret::Interpret, data::Version,
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory,
score::Score, score::Score,
topology::{K8sclient, Topology}, topology::{K8sclient, Topology},
}; };
@ -40,6 +44,7 @@ pub struct K8sIngressScore {
pub path: Option<IngressPath>, pub path: Option<IngressPath>,
pub path_type: Option<PathType>, pub path_type: Option<PathType>,
pub namespace: Option<fqdn::FQDN>, pub namespace: Option<fqdn::FQDN>,
pub ingress_class_name: Option<String>,
} }
impl<T: Topology + K8sclient> Score<T> for K8sIngressScore { impl<T: Topology + K8sclient> Score<T> for K8sIngressScore {
@ -54,12 +59,18 @@ impl<T: Topology + K8sclient> Score<T> for K8sIngressScore {
None => PathType::Prefix, None => PathType::Prefix,
}; };
let ingress_class = match self.ingress_class_name.clone() {
Some(ingress_class_name) => ingress_class_name,
None => "\"default\"".to_string(),
};
let ingress = json!( let ingress = json!(
{ {
"metadata": { "metadata": {
"name": self.name.to_string(), "name": self.name.to_string(),
}, },
"spec": { "spec": {
"ingressClassName": ingress_class.as_str(),
"rules": [ "rules": [
{ "host": self.host.to_string(), { "host": self.host.to_string(),
"http": { "http": {
@ -90,11 +101,12 @@ impl<T: Topology + K8sclient> Score<T> for K8sIngressScore {
"Successfully built Ingress for host {:?}", "Successfully built Ingress for host {:?}",
ingress.metadata.name ingress.metadata.name
); );
Box::new(K8sResourceInterpret {
score: K8sResourceScore::single( Box::new(K8sIngressInterpret {
ingress.clone(), ingress,
self.namespace.clone().map(|f| f.to_string()), service: self.name.to_string(),
), namespace: self.namespace.clone().map(|f| f.to_string()),
host: self.host.clone(),
}) })
} }
@ -102,3 +114,62 @@ impl<T: Topology + K8sclient> Score<T> for K8sIngressScore {
format!("{} K8sIngressScore", self.name) format!("{} K8sIngressScore", self.name)
} }
} }
#[derive(std::fmt::Debug)]
struct K8sIngressInterpret {
ingress: Ingress,
service: String,
namespace: Option<String>,
host: fqdn::FQDN,
}
#[async_trait]
impl<T: Topology + K8sclient> Interpret<T> for K8sIngressInterpret {
async fn execute(
&self,
inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
let result = K8sResourceInterpret {
score: K8sResourceScore::single(self.ingress.clone(), self.namespace.clone()),
}
.execute(inventory, topology)
.await;
match result {
Ok(outcome) => match outcome.status {
InterpretStatus::SUCCESS => {
let details = match &self.namespace {
Some(namespace) => {
vec![format!(
"{} ({namespace}): http://{}",
self.service, self.host
)]
}
None => vec![format!("{}: {}", self.service, self.host)],
};
Ok(Outcome::success_with_details(outcome.message, details))
}
_ => Ok(outcome),
},
Err(e) => Err(e),
}
}
fn get_name(&self) -> InterpretName {
InterpretName::K8sIngress
}
fn get_version(&self) -> Version {
Version::from("0.0.1").unwrap()
}
fn get_status(&self) -> InterpretStatus {
todo!()
}
fn get_children(&self) -> Vec<Id> {
vec![]
}
}

View File

@ -147,6 +147,7 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for LAMPInterpret {
port: 8080, port: 8080,
path: Some(ingress_path), path: Some(ingress_path),
path_type: None, path_type: None,
ingress_class_name: None,
namespace: self namespace: self
.get_namespace() .get_namespace()
.map(|nbs| fqdn!(nbs.to_string().as_str())), .map(|nbs| fqdn!(nbs.to_string().as_str())),

View File

@ -35,6 +35,24 @@ pub struct DiscordWebhook {
#[async_trait] #[async_trait]
impl AlertReceiver<RHOBObservability> for DiscordWebhook { impl AlertReceiver<RHOBObservability> for DiscordWebhook {
async fn install(&self, sender: &RHOBObservability) -> Result<Outcome, InterpretError> { async fn install(&self, sender: &RHOBObservability) -> Result<Outcome, InterpretError> {
let ns = sender.namespace.clone();
let secret_name = format!("{}-secret", self.name.clone());
let webhook_key = format!("{}", self.url.clone());
let mut string_data = BTreeMap::new();
string_data.insert("webhook-url".to_string(), webhook_key.clone());
let secret = Secret {
metadata: kube::core::ObjectMeta {
name: Some(secret_name.clone()),
..Default::default()
},
string_data: Some(string_data),
type_: Some("Opaque".to_string()),
..Default::default()
};
let _ = sender.client.apply(&secret, Some(&ns)).await;
let spec = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfigSpec { let spec = crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::AlertmanagerConfigSpec {
data: json!({ data: json!({
"route": { "route": {
@ -43,9 +61,14 @@ impl AlertReceiver<RHOBObservability> for DiscordWebhook {
"receivers": [ "receivers": [
{ {
"name": self.name, "name": self.name,
"webhookConfigs": [ "discordConfigs": [
{ {
"url": self.url, "apiURL": {
"name": secret_name,
"key": "webhook-url",
},
"title": "{{ template \"discord.default.title\" . }}",
"message": "{{ template \"discord.default.message\" . }}"
} }
] ]
} }

View File

@ -43,6 +43,11 @@ impl AlertReceiver<RHOBObservability> for WebhookReceiver {
"webhookConfigs": [ "webhookConfigs": [
{ {
"url": self.url, "url": self.url,
"httpConfig": {
"tlsConfig": {
"insecureSkipVerify": true
}
}
} }
] ]
} }

View File

@ -68,7 +68,9 @@ impl<T: Topology + PrometheusApplicationMonitoring<CRDPrometheus>> Interpret<T>
PreparationOutcome::Success { details: _ } => { PreparationOutcome::Success { details: _ } => {
Ok(Outcome::success("Prometheus installed".into())) Ok(Outcome::success("Prometheus installed".into()))
} }
PreparationOutcome::Noop => Ok(Outcome::noop()), PreparationOutcome::Noop => {
Ok(Outcome::noop("Prometheus installation skipped".into()))
}
}, },
Err(err) => Err(InterpretError::from(err)), Err(err) => Err(InterpretError::from(err)),
} }

View File

@ -70,7 +70,9 @@ impl<T: Topology + PrometheusApplicationMonitoring<RHOBObservability>> Interpret
PreparationOutcome::Success { details: _ } => { PreparationOutcome::Success { details: _ } => {
Ok(Outcome::success("Prometheus installed".into())) Ok(Outcome::success("Prometheus installed".into()))
} }
PreparationOutcome::Noop => Ok(Outcome::noop()), PreparationOutcome::Noop => {
Ok(Outcome::noop("Prometheus installation skipped".into()))
}
}, },
Err(err) => Err(InterpretError::from(err)), Err(err) => Err(InterpretError::from(err)),
} }

View File

@ -4,7 +4,9 @@ use kube::CustomResource;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::modules::monitoring::kube_prometheus::crd::rhob_prometheuses::LabelSelector; use crate::modules::monitoring::kube_prometheus::crd::rhob_prometheuses::{
LabelSelector, PrometheusSpec,
};
/// MonitoringStack CRD for monitoring.rhobs/v1alpha1 /// MonitoringStack CRD for monitoring.rhobs/v1alpha1
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] #[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]

View File

@ -45,6 +45,12 @@ service:
ingress: ingress:
enabled: {ingress_enabled} enabled: {ingress_enabled}
hosts:
- host: {host}
paths:
- path: /
pathType: ImplementationSpecific
route: route:
enabled: {route_enabled} enabled: {route_enabled}

View File

@ -113,7 +113,13 @@ impl<T: Topology + HelmCommand + K8sclient + MultiTargetTopology> Interpret<T> f
.await?; .await?;
info!("user added"); info!("user added");
Ok(Outcome::success("Ntfy installed".to_string())) Ok(Outcome::success_with_details(
"Ntfy installed".to_string(),
vec![format!(
"Ntfy ({}): http://{}",
self.score.namespace, self.score.host
)],
))
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {

View File

@ -1,19 +1,19 @@
use async_trait::async_trait;
use derive_new::new;
use harmony_types::id::Id;
use log::{error, info, warn};
use serde::Serialize;
use crate::{ use crate::{
data::Version, data::Version,
hardware::PhysicalHost, hardware::PhysicalHost,
infra::inventory::InventoryRepositoryFactory, infra::inventory::InventoryRepositoryFactory,
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::{HostRole, Inventory}, inventory::{HostRole, Inventory},
modules::inventory::{DiscoverHostForRoleScore, LaunchDiscoverInventoryAgentScore}, modules::inventory::DiscoverHostForRoleScore,
score::Score, score::Score,
topology::HAClusterTopology, topology::HAClusterTopology,
}; };
use async_trait::async_trait;
use derive_new::new;
use harmony_types::id::Id;
use log::info;
use serde::Serialize;
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
// Step 01: Inventory (default PXE + Kickstart in RAM + Rust agent) // Step 01: Inventory (default PXE + Kickstart in RAM + Rust agent)
// - This score exposes/ensures the default inventory assets and waits for discoveries. // - This score exposes/ensures the default inventory assets and waits for discoveries.
@ -109,12 +109,9 @@ When you can dig them, confirm to continue.
.await?; .await?;
} }
Ok(Outcome::new( Ok(Outcome::success(format!(
InterpretStatus::SUCCESS,
format!(
"Found and assigned bootstrap node: {}", "Found and assigned bootstrap node: {}",
bootstrap_host.unwrap().summary() bootstrap_host.unwrap().summary()
), )))
))
} }
} }

View File

@ -1,25 +1,13 @@
use std::{fmt::Write, path::PathBuf};
use async_trait::async_trait;
use derive_new::new;
use harmony_secret::SecretManager;
use harmony_types::id::Id;
use log::{debug, error, info, warn};
use serde::{Deserialize, Serialize};
use tokio::{fs::File, io::AsyncWriteExt, process::Command};
use crate::{ use crate::{
config::secret::{RedhatSecret, SshKeyPair}, config::secret::{RedhatSecret, SshKeyPair},
data::{FileContent, FilePath, Version}, data::{FileContent, FilePath, Version},
hardware::PhysicalHost, hardware::PhysicalHost,
infra::inventory::InventoryRepositoryFactory, infra::inventory::InventoryRepositoryFactory,
instrumentation::{HarmonyEvent, instrument},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::{HostRole, Inventory}, inventory::{HostRole, Inventory},
modules::{ modules::{
dhcp::DhcpHostBindingScore, dhcp::DhcpHostBindingScore,
http::{IPxeMacBootFileScore, StaticFilesHttpScore}, http::{IPxeMacBootFileScore, StaticFilesHttpScore},
inventory::LaunchDiscoverInventoryAgentScore,
okd::{ okd::{
bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, bootstrap_load_balancer::OKDBootstrapLoadBalancerScore,
templates::{BootstrapIpxeTpl, InstallConfigYaml}, templates::{BootstrapIpxeTpl, InstallConfigYaml},
@ -28,6 +16,15 @@ use crate::{
score::Score, score::Score,
topology::{HAClusterTopology, HostBinding}, topology::{HAClusterTopology, HostBinding},
}; };
use async_trait::async_trait;
use derive_new::new;
use harmony_secret::SecretManager;
use harmony_types::id::Id;
use log::{debug, info};
use serde::Serialize;
use std::path::PathBuf;
use tokio::{fs::File, io::AsyncWriteExt, process::Command};
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
// Step 02: Bootstrap // Step 02: Bootstrap
// - Select bootstrap node (from discovered set). // - Select bootstrap node (from discovered set).
@ -313,7 +310,7 @@ impl OKDSetup02BootstrapInterpret {
info!("[Bootstrap] Rebooting bootstrap node via SSH"); info!("[Bootstrap] Rebooting bootstrap node via SSH");
// TODO reboot programatically, there are some logical checks and refactoring to do such as // TODO reboot programatically, there are some logical checks and refactoring to do such as
// accessing the bootstrap node config (ip address) from the inventory // accessing the bootstrap node config (ip address) from the inventory
let confirmation = inquire::Confirm::new( let _ = inquire::Confirm::new(
"Now reboot the bootstrap node so it picks up its pxe boot file. Press enter when ready.", "Now reboot the bootstrap node so it picks up its pxe boot file. Press enter when ready.",
) )
.prompt() .prompt()
@ -379,9 +376,6 @@ impl Interpret<HAClusterTopology> for OKDSetup02BootstrapInterpret {
self.reboot_target().await?; self.reboot_target().await?;
self.wait_for_bootstrap_complete().await?; self.wait_for_bootstrap_complete().await?;
Ok(Outcome::new( Ok(Outcome::success("Bootstrap phase complete".into()))
InterpretStatus::SUCCESS,
"Bootstrap phase complete".into(),
))
} }
} }

View File

@ -1,11 +1,3 @@
use std::{fmt::Write, path::PathBuf};
use async_trait::async_trait;
use derive_new::new;
use harmony_types::id::Id;
use log::{debug, info};
use serde::Serialize;
use crate::{ use crate::{
data::Version, data::Version,
hardware::PhysicalHost, hardware::PhysicalHost,
@ -19,6 +11,12 @@ use crate::{
score::Score, score::Score,
topology::{HAClusterTopology, HostBinding}, topology::{HAClusterTopology, HostBinding},
}; };
use async_trait::async_trait;
use derive_new::new;
use harmony_types::id::Id;
use log::{debug, info};
use serde::Serialize;
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
// Step 03: Control Plane // Step 03: Control Plane
// - Render per-MAC PXE & ignition for cp0/cp1/cp2. // - Render per-MAC PXE & ignition for cp0/cp1/cp2.
@ -269,8 +267,7 @@ impl Interpret<HAClusterTopology> for OKDSetup03ControlPlaneInterpret {
// the `wait-for bootstrap-complete` command. // the `wait-for bootstrap-complete` command.
info!("[ControlPlane] Provisioning initiated. Monitor the cluster convergence manually."); info!("[ControlPlane] Provisioning initiated. Monitor the cluster convergence manually.");
Ok(Outcome::new( Ok(Outcome::success(
InterpretStatus::SUCCESS,
"Control plane provisioning has been successfully initiated.".into(), "Control plane provisioning has been successfully initiated.".into(),
)) ))
} }

View File

@ -1,33 +1,17 @@
use std::{fmt::Write, path::PathBuf};
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new; use derive_new::new;
use harmony_secret::SecretManager;
use harmony_types::id::Id; use harmony_types::id::Id;
use log::{debug, error, info, warn}; use log::info;
use serde::{Deserialize, Serialize}; use serde::Serialize;
use tokio::{fs::File, io::AsyncWriteExt, process::Command};
use crate::{ use crate::{
config::secret::{RedhatSecret, SshKeyPair}, data::Version,
data::{FileContent, FilePath, Version},
hardware::PhysicalHost,
infra::inventory::InventoryRepositoryFactory,
instrumentation::{HarmonyEvent, instrument},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::{HostRole, Inventory}, inventory::Inventory,
modules::{
dhcp::DhcpHostBindingScore,
http::{IPxeMacBootFileScore, StaticFilesHttpScore},
inventory::LaunchDiscoverInventoryAgentScore,
okd::{
bootstrap_load_balancer::OKDBootstrapLoadBalancerScore,
templates::{BootstrapIpxeTpl, InstallConfigYaml},
},
},
score::Score, score::Score,
topology::{HAClusterTopology, HostBinding}, topology::HAClusterTopology,
}; };
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
// Step 04: Workers // Step 04: Workers
// - Render per-MAC PXE & ignition for workers; join nodes. // - Render per-MAC PXE & ignition for workers; join nodes.
@ -94,9 +78,6 @@ impl Interpret<HAClusterTopology> for OKDSetup04WorkersInterpret {
_topology: &HAClusterTopology, _topology: &HAClusterTopology,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
self.render_and_reboot().await?; self.render_and_reboot().await?;
Ok(Outcome::new( Ok(Outcome::success("Workers provisioned".into()))
InterpretStatus::SUCCESS,
"Workers provisioned".into(),
))
} }
} }

View File

@ -1,33 +1,16 @@
use std::{fmt::Write, path::PathBuf}; use crate::{
data::Version,
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory,
score::Score,
topology::HAClusterTopology,
};
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new; use derive_new::new;
use harmony_secret::SecretManager;
use harmony_types::id::Id; use harmony_types::id::Id;
use log::{debug, error, info, warn}; use log::info;
use serde::{Deserialize, Serialize}; use serde::Serialize;
use tokio::{fs::File, io::AsyncWriteExt, process::Command};
use crate::{
config::secret::{RedhatSecret, SshKeyPair},
data::{FileContent, FilePath, Version},
hardware::PhysicalHost,
infra::inventory::InventoryRepositoryFactory,
instrumentation::{HarmonyEvent, instrument},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::{HostRole, Inventory},
modules::{
dhcp::DhcpHostBindingScore,
http::{IPxeMacBootFileScore, StaticFilesHttpScore},
inventory::LaunchDiscoverInventoryAgentScore,
okd::{
bootstrap_load_balancer::OKDBootstrapLoadBalancerScore,
templates::{BootstrapIpxeTpl, InstallConfigYaml},
},
},
score::Score,
topology::{HAClusterTopology, HostBinding},
};
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
// Step 05: Sanity Check // Step 05: Sanity Check
// - Validate API reachability, ClusterOperators, ingress, and SDN status. // - Validate API reachability, ClusterOperators, ingress, and SDN status.
@ -93,9 +76,6 @@ impl Interpret<HAClusterTopology> for OKDSetup05SanityCheckInterpret {
_topology: &HAClusterTopology, _topology: &HAClusterTopology,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
self.run_checks().await?; self.run_checks().await?;
Ok(Outcome::new( Ok(Outcome::success("Sanity checks passed".into()))
InterpretStatus::SUCCESS,
"Sanity checks passed".into(),
))
} }
} }

View File

@ -1,32 +1,15 @@
// -------------------------------------------------------------------------------------------------
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new; use derive_new::new;
use harmony_secret::SecretManager;
use harmony_types::id::Id; use harmony_types::id::Id;
use log::{debug, error, info, warn}; use log::info;
use serde::{Deserialize, Serialize}; use serde::Serialize;
use std::{fmt::Write, path::PathBuf};
use tokio::{fs::File, io::AsyncWriteExt, process::Command};
use crate::{ use crate::{
config::secret::{RedhatSecret, SshKeyPair}, data::Version,
data::{FileContent, FilePath, Version},
hardware::PhysicalHost,
infra::inventory::InventoryRepositoryFactory,
instrumentation::{HarmonyEvent, instrument},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::{HostRole, Inventory}, inventory::Inventory,
modules::{
dhcp::DhcpHostBindingScore,
http::{IPxeMacBootFileScore, StaticFilesHttpScore},
inventory::LaunchDiscoverInventoryAgentScore,
okd::{
bootstrap_load_balancer::OKDBootstrapLoadBalancerScore,
templates::{BootstrapIpxeTpl, InstallConfigYaml},
},
},
score::Score, score::Score,
topology::{HAClusterTopology, HostBinding}, topology::HAClusterTopology,
}; };
// Step 06: Installation Report // Step 06: Installation Report
@ -93,9 +76,6 @@ impl Interpret<HAClusterTopology> for OKDSetup06InstallationReportInterpret {
_topology: &HAClusterTopology, _topology: &HAClusterTopology,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
self.generate().await?; self.generate().await?;
Ok(Outcome::new( Ok(Outcome::success("Installation report generated".into()))
InterpretStatus::SUCCESS,
"Installation report generated".into(),
))
} }
} }

View File

@ -21,8 +21,8 @@ pub fn pod_failed() -> PrometheusAlertRule {
pub fn alert_container_restarting() -> PrometheusAlertRule { pub fn alert_container_restarting() -> PrometheusAlertRule {
PrometheusAlertRule { PrometheusAlertRule {
alert: "ContainerRestarting".into(), alert: "ContainerRestarting".into(),
expr: "increase(kube_pod_container_status_restarts_total[5m]) > 3".into(), expr: "increase(kube_pod_container_status_restarts_total[30s]) > 3".into(),
r#for: Some("5m".into()), r#for: Some("30s".into()),
labels: HashMap::from([("severity".into(), "warning".into())]), labels: HashMap::from([("severity".into(), "warning".into())]),
annotations: HashMap::from([ annotations: HashMap::from([
( (
@ -42,7 +42,7 @@ pub fn alert_pod_not_ready() -> PrometheusAlertRule {
PrometheusAlertRule { PrometheusAlertRule {
alert: "PodNotReady".into(), alert: "PodNotReady".into(),
expr: "kube_pod_status_ready{condition=\"true\"} == 0".into(), expr: "kube_pod_status_ready{condition=\"true\"} == 0".into(),
r#for: Some("2m".into()), r#for: Some("30s".into()),
labels: HashMap::from([("severity".into(), "warning".into())]), labels: HashMap::from([("severity".into(), "warning".into())]),
annotations: HashMap::from([ annotations: HashMap::from([
("summary".into(), "Pod is not ready".into()), ("summary".into(), "Pod is not ready".into()),

View File

@ -1,3 +1,4 @@
use fqdn::fqdn;
use std::fs; use std::fs;
use std::{collections::BTreeMap, sync::Arc}; use std::{collections::BTreeMap, sync::Arc};
use tempfile::tempdir; use tempfile::tempdir;
@ -8,6 +9,7 @@ use log::{debug, info};
use serde::Serialize; use serde::Serialize;
use std::process::Command; use std::process::Command;
use crate::modules::k8s::ingress::{K8sIngressScore, PathType};
use crate::modules::monitoring::kube_prometheus::crd::grafana_default_dashboard::build_default_dashboard; use crate::modules::monitoring::kube_prometheus::crd::grafana_default_dashboard::build_default_dashboard;
use crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::RHOBObservability; use crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::RHOBObservability;
use crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanagers::{ use crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanagers::{
@ -23,12 +25,18 @@ use crate::modules::monitoring::kube_prometheus::crd::rhob_monitoring_stack::{
use crate::modules::monitoring::kube_prometheus::crd::rhob_prometheus_rules::{ use crate::modules::monitoring::kube_prometheus::crd::rhob_prometheus_rules::{
PrometheusRule, PrometheusRuleSpec, RuleGroup, PrometheusRule, PrometheusRuleSpec, RuleGroup,
}; };
use crate::modules::monitoring::kube_prometheus::crd::rhob_prometheuses::LabelSelector; use crate::modules::monitoring::kube_prometheus::crd::rhob_prometheuses::{
AlertmanagerEndpoints, LabelSelector, PrometheusSpec, PrometheusSpecAlerting,
};
use crate::modules::monitoring::kube_prometheus::crd::rhob_role::{
build_prom_role, build_prom_rolebinding, build_prom_service_account,
};
use crate::modules::monitoring::kube_prometheus::crd::rhob_service_monitor::{ use crate::modules::monitoring::kube_prometheus::crd::rhob_service_monitor::{
ServiceMonitor, ServiceMonitorSpec, ServiceMonitor, ServiceMonitorSpec,
}; };
use crate::score::Score; use crate::score::Score;
use crate::topology::ingress::Ingress;
use crate::topology::oberservability::monitoring::AlertReceiver; use crate::topology::oberservability::monitoring::AlertReceiver;
use crate::topology::{K8sclient, Topology, k8s::K8sClient}; use crate::topology::{K8sclient, Topology, k8s::K8sClient};
use crate::{ use crate::{
@ -48,8 +56,8 @@ pub struct RHOBAlertingScore {
pub prometheus_rules: Vec<RuleGroup>, pub prometheus_rules: Vec<RuleGroup>,
} }
impl<T: Topology + K8sclient + PrometheusApplicationMonitoring<RHOBObservability>> Score<T> impl<T: Topology + K8sclient + Ingress + PrometheusApplicationMonitoring<RHOBObservability>>
for RHOBAlertingScore Score<T> for RHOBAlertingScore
{ {
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
Box::new(RHOBAlertingInterpret { Box::new(RHOBAlertingInterpret {
@ -74,19 +82,20 @@ pub struct RHOBAlertingInterpret {
} }
#[async_trait] #[async_trait]
impl<T: Topology + K8sclient + PrometheusApplicationMonitoring<RHOBObservability>> Interpret<T> impl<T: Topology + K8sclient + Ingress + PrometheusApplicationMonitoring<RHOBObservability>>
for RHOBAlertingInterpret Interpret<T> for RHOBAlertingInterpret
{ {
async fn execute( async fn execute(
&self, &self,
_inventory: &Inventory, inventory: &Inventory,
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
let client = topology.k8s_client().await.unwrap(); let client = topology.k8s_client().await.unwrap();
self.ensure_grafana_operator().await?; self.ensure_grafana_operator().await?;
self.install_prometheus(&client).await?; self.install_prometheus(inventory, topology, &client)
.await?;
self.install_client_kube_metrics().await?; self.install_client_kube_metrics().await?;
self.install_grafana(&client).await?; self.install_grafana(inventory, topology, &client).await?;
self.install_receivers(&self.sender, &self.receivers) self.install_receivers(&self.sender, &self.receivers)
.await?; .await?;
self.install_rules(&self.prometheus_rules, &client).await?; self.install_rules(&self.prometheus_rules, &client).await?;
@ -212,7 +221,8 @@ impl RHOBAlertingInterpret {
let output = Command::new("helm") let output = Command::new("helm")
.args([ .args([
"install", "upgrade",
"--install",
"grafana-operator", "grafana-operator",
"grafana-operator/grafana-operator", "grafana-operator/grafana-operator",
"--namespace", "--namespace",
@ -226,7 +236,7 @@ impl RHOBAlertingInterpret {
if !output.status.success() { if !output.status.success() {
return Err(InterpretError::new(format!( return Err(InterpretError::new(format!(
"helm install failed:\nstdout: {}\nstderr: {}", "helm upgrade --install failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr) String::from_utf8_lossy(&output.stderr)
))); )));
@ -238,25 +248,31 @@ impl RHOBAlertingInterpret {
))) )))
} }
async fn install_prometheus(&self, client: &Arc<K8sClient>) -> Result<Outcome, InterpretError> { async fn install_prometheus<T: Topology + K8sclient + Ingress>(
&self,
inventory: &Inventory,
topology: &T,
client: &Arc<K8sClient>,
) -> Result<Outcome, InterpretError> {
debug!( debug!(
"installing crd-prometheuses in namespace {}", "installing crd-prometheuses in namespace {}",
self.sender.namespace.clone() self.sender.namespace.clone()
); );
debug!("building role/rolebinding/serviceaccount for crd-prometheus");
let stack = MonitoringStack { let stack = MonitoringStack {
metadata: ObjectMeta { metadata: ObjectMeta {
name: Some(format!("{}-monitoring", self.sender.namespace.clone()).into()), name: Some(format!("{}-monitoring", self.sender.namespace.clone()).into()),
namespace: Some(self.sender.namespace.clone()), namespace: Some(self.sender.namespace.clone()),
labels: Some([("coo".into(), "example".into())].into()), labels: Some([("monitoring-stack".into(), "true".into())].into()),
..Default::default() ..Default::default()
}, },
spec: MonitoringStackSpec { spec: MonitoringStackSpec {
log_level: Some("debug".into()), log_level: Some("debug".into()),
retention: Some("1d".into()), retention: Some("1d".into()),
resource_selector: Some(LabelSelector { resource_selector: Some(LabelSelector {
match_labels: [("app".into(), "demo".into())].into(), match_labels: Default::default(),
..Default::default() match_expressions: vec![],
}), }),
}, },
}; };
@ -265,6 +281,42 @@ impl RHOBAlertingInterpret {
.apply(&stack, Some(&self.sender.namespace.clone())) .apply(&stack, Some(&self.sender.namespace.clone()))
.await .await
.map_err(|e| InterpretError::new(e.to_string()))?; .map_err(|e| InterpretError::new(e.to_string()))?;
let alert_manager_domain = topology
.get_domain(&format!("alert-manager-{}", self.sender.namespace.clone()))
.await?;
let name = format!("{}-alert-manager", self.sender.namespace.clone());
let backend_service = format!("alertmanager-operated");
let namespace = self.sender.namespace.clone();
let alert_manager_ingress = K8sIngressScore {
name: fqdn!(&name),
host: fqdn!(&alert_manager_domain),
backend_service: fqdn!(&backend_service),
port: 9093,
path: Some("/".to_string()),
path_type: Some(PathType::Prefix),
namespace: Some(fqdn!(&namespace)),
ingress_class_name: Some("openshift-default".to_string()),
};
let prometheus_domain = topology
.get_domain(&format!("prometheus-{}", self.sender.namespace.clone()))
.await?;
let name = format!("{}-prometheus", self.sender.namespace.clone());
let backend_service = format!("prometheus-operated");
let prometheus_ingress = K8sIngressScore {
name: fqdn!(&name),
host: fqdn!(&prometheus_domain),
backend_service: fqdn!(&backend_service),
port: 9090,
path: Some("/".to_string()),
path_type: Some(PathType::Prefix),
namespace: Some(fqdn!(&namespace)),
ingress_class_name: Some("openshift-default".to_string()),
};
alert_manager_ingress.interpret(inventory, topology).await?;
prometheus_ingress.interpret(inventory, topology).await?;
info!("installed rhob monitoring stack",); info!("installed rhob monitoring stack",);
Ok(Outcome::success(format!( Ok(Outcome::success(format!(
"successfully deployed rhob-prometheus {:#?}", "successfully deployed rhob-prometheus {:#?}",
@ -272,31 +324,6 @@ impl RHOBAlertingInterpret {
))) )))
} }
async fn install_alert_manager(
&self,
client: &Arc<K8sClient>,
) -> Result<Outcome, InterpretError> {
let am = Alertmanager {
metadata: ObjectMeta {
name: Some(self.sender.namespace.clone()),
labels: Some(std::collections::BTreeMap::from([(
"alertmanagerConfig".to_string(),
"enabled".to_string(),
)])),
namespace: Some(self.sender.namespace.clone()),
..Default::default()
},
spec: AlertmanagerSpec::default(),
};
client
.apply(&am, Some(&self.sender.namespace.clone()))
.await
.map_err(|e| InterpretError::new(e.to_string()))?;
Ok(Outcome::success(format!(
"successfully deployed service monitor {:#?}",
am.metadata.name
)))
}
async fn install_monitors( async fn install_monitors(
&self, &self,
mut monitors: Vec<ServiceMonitor>, mut monitors: Vec<ServiceMonitor>,
@ -379,7 +406,12 @@ impl RHOBAlertingInterpret {
))) )))
} }
async fn install_grafana(&self, client: &Arc<K8sClient>) -> Result<Outcome, InterpretError> { async fn install_grafana<T: Topology + K8sclient + Ingress>(
&self,
inventory: &Inventory,
topology: &T,
client: &Arc<K8sClient>,
) -> Result<Outcome, InterpretError> {
let mut label = BTreeMap::new(); let mut label = BTreeMap::new();
label.insert("dashboards".to_string(), "grafana".to_string()); label.insert("dashboards".to_string(), "grafana".to_string());
let labels = LabelSelector { let labels = LabelSelector {
@ -465,6 +497,23 @@ impl RHOBAlertingInterpret {
.apply(&grafana, Some(&self.sender.namespace.clone())) .apply(&grafana, Some(&self.sender.namespace.clone()))
.await .await
.map_err(|e| InterpretError::new(e.to_string()))?; .map_err(|e| InterpretError::new(e.to_string()))?;
let domain = topology
.get_domain(&format!("grafana-{}", self.sender.namespace.clone()))
.await?;
let name = format!("{}-grafana", self.sender.namespace.clone());
let backend_service = format!("grafana-{}-service", self.sender.namespace.clone());
let grafana_ingress = K8sIngressScore {
name: fqdn!(&name),
host: fqdn!(&domain),
backend_service: fqdn!(&backend_service),
port: 3000,
path: Some("/".to_string()),
path_type: Some(PathType::Prefix),
namespace: Some(fqdn!(&namespace)),
ingress_class_name: Some("openshift-default".to_string()),
};
grafana_ingress.interpret(inventory, topology).await?;
Ok(Outcome::success(format!( Ok(Outcome::success(format!(
"successfully deployed grafana instance {:#?}", "successfully deployed grafana instance {:#?}",
grafana.metadata.name grafana.metadata.name

View File

@ -178,10 +178,10 @@ fn handle_events() {
ApplicationFeatureStatus::Installing => { ApplicationFeatureStatus::Installing => {
info!("Installing feature '{feature}' for '{application}'..."); info!("Installing feature '{feature}' for '{application}'...");
} }
ApplicationFeatureStatus::Installed => { ApplicationFeatureStatus::Installed { details: _ } => {
info!(status = "finished"; "Feature '{feature}' installed"); info!(status = "finished"; "Feature '{feature}' installed");
} }
ApplicationFeatureStatus::Failed { details } => { ApplicationFeatureStatus::Failed { message: details } => {
error!(status = "failed"; "Feature '{feature}' installation failed: {details}"); error!(status = "failed"; "Feature '{feature}' installation failed: {details}");
} }
}, },

View File

@ -0,0 +1,56 @@
use std::sync::Mutex;
use harmony::{
instrumentation::{self, HarmonyEvent},
modules::application::ApplicationFeatureStatus,
};
use crate::theme;
pub fn init() {
let details: Mutex<Vec<String>> = Mutex::new(vec![]);
instrumentation::subscribe("Harmony CLI Reporter", {
move |event| {
let mut details = details.lock().unwrap();
match event {
HarmonyEvent::InterpretExecutionFinished {
execution_id: _,
topology: _,
interpret: _,
score: _,
outcome: Ok(outcome),
} => {
if outcome.status == harmony::interpret::InterpretStatus::SUCCESS {
details.extend(outcome.details.clone());
}
}
HarmonyEvent::ApplicationFeatureStateChanged {
topology: _,
application: _,
feature: _,
status:
ApplicationFeatureStatus::Installed {
details: feature_details,
},
} => {
details.extend(feature_details.clone());
}
HarmonyEvent::HarmonyFinished => {
if !details.is_empty() {
println!(
"\n{} All done! Here's what's next for you:",
theme::EMOJI_SUMMARY
);
for detail in details.iter() {
println!("- {detail}");
}
println!();
}
}
_ => {}
};
}
});
}

View File

@ -8,6 +8,7 @@ use inquire::Confirm;
use log::debug; use log::debug;
pub mod cli_logger; // FIXME: Don't make me pub pub mod cli_logger; // FIXME: Don't make me pub
mod cli_reporter;
pub mod progress; pub mod progress;
pub mod theme; pub mod theme;
@ -116,6 +117,7 @@ pub async fn run_cli<T: Topology + Send + Sync + 'static>(
args: Args, args: Args,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
cli_logger::init(); cli_logger::init();
cli_reporter::init();
let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); let mut maestro = Maestro::initialize(inventory, topology).await.unwrap();
maestro.register_all(scores); maestro.register_all(scores);

View File

@ -9,6 +9,7 @@ pub static EMOJI_ERROR: Emoji<'_, '_> = Emoji("⚠️", "");
pub static EMOJI_DEPLOY: Emoji<'_, '_> = Emoji("🚀", ""); pub static EMOJI_DEPLOY: Emoji<'_, '_> = Emoji("🚀", "");
pub static EMOJI_TOPOLOGY: Emoji<'_, '_> = Emoji("📦", ""); pub static EMOJI_TOPOLOGY: Emoji<'_, '_> = Emoji("📦", "");
pub static EMOJI_SCORE: Emoji<'_, '_> = Emoji("🎶", ""); pub static EMOJI_SCORE: Emoji<'_, '_> = Emoji("🎶", "");
pub static EMOJI_SUMMARY: Emoji<'_, '_> = Emoji("🚀", "");
lazy_static! { lazy_static! {
pub static ref SECTION_STYLE: ProgressStyle = ProgressStyle::default_spinner() pub static ref SECTION_STYLE: ProgressStyle = ProgressStyle::default_spinner()

View File

@ -21,7 +21,6 @@ pub fn handle_events() {
instrumentation::subscribe("Harmony Composer Logger", { instrumentation::subscribe("Harmony Composer Logger", {
move |event| match event { move |event| match event {
HarmonyComposerEvent::HarmonyComposerStarted => {}
HarmonyComposerEvent::ProjectInitializationStarted => { HarmonyComposerEvent::ProjectInitializationStarted => {
progress_tracker.add_section( progress_tracker.add_section(
SETUP_SECTION, SETUP_SECTION,

View File

@ -5,7 +5,6 @@ use crate::{HarmonyProfile, HarmonyTarget};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum HarmonyComposerEvent { pub enum HarmonyComposerEvent {
HarmonyComposerStarted,
ProjectInitializationStarted, ProjectInitializationStarted,
ProjectInitialized, ProjectInitialized,
ProjectCompilationStarted { ProjectCompilationStarted {

View File

@ -2,8 +2,8 @@ mod downloadable_asset;
use downloadable_asset::*; use downloadable_asset::*;
use kube::Client; use kube::Client;
use log::debug; use log::{debug, info};
use std::path::PathBuf; use std::{ffi::OsStr, path::PathBuf};
const K3D_BIN_FILE_NAME: &str = "k3d"; const K3D_BIN_FILE_NAME: &str = "k3d";
@ -213,15 +213,19 @@ impl K3d {
} }
} }
let client;
if !self.is_cluster_initialized() { if !self.is_cluster_initialized() {
debug!("Cluster is not initialized, initializing now"); debug!("Cluster is not initialized, initializing now");
return self.initialize_cluster().await; client = self.initialize_cluster().await?;
} } else {
self.start_cluster().await?; self.start_cluster().await?;
debug!("K3d and cluster are already properly set up"); debug!("K3d and cluster are already properly set up");
self.create_kubernetes_client().await client = self.create_kubernetes_client().await?;
}
self.ensure_k3d_config_is_default(self.get_cluster_name()?)?;
Ok(client)
} }
// Private helper methods // Private helper methods
@ -302,7 +306,16 @@ impl K3d {
S: AsRef<std::ffi::OsStr>, S: AsRef<std::ffi::OsStr>,
{ {
let binary_path = self.get_k3d_binary()?; let binary_path = self.get_k3d_binary()?;
let output = std::process::Command::new(binary_path).args(args).output(); self.run_command(binary_path, args)
}
pub fn run_command<I, S, C>(&self, cmd: C, args: I) -> Result<std::process::Output, String>
where
I: IntoIterator<Item = S>,
S: AsRef<std::ffi::OsStr>,
C: AsRef<OsStr>,
{
let output = std::process::Command::new(cmd).args(args).output();
match output { match output {
Ok(output) => { Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr); let stderr = String::from_utf8_lossy(&output.stderr);
@ -311,7 +324,7 @@ impl K3d {
debug!("stdout : {}", stdout); debug!("stdout : {}", stdout);
Ok(output) Ok(output)
} }
Err(e) => Err(format!("Failed to execute k3d command: {}", e)), Err(e) => Err(format!("Failed to execute command: {}", e)),
} }
} }
@ -323,12 +336,38 @@ impl K3d {
return Err(format!("Failed to create cluster: {}", stderr)); return Err(format!("Failed to create cluster: {}", stderr));
} }
debug!("Successfully created k3d cluster '{}'", cluster_name); info!("Successfully created k3d cluster '{}'", cluster_name);
Ok(())
}
fn ensure_k3d_config_is_default(&self, cluster_name: &str) -> Result<(), String> {
let output = self.run_k3d_command(["kubeconfig", "merge", "-d", cluster_name])?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Failed to setup k3d kubeconfig : {}", stderr));
}
let output = self.run_command(
"kubectl",
["config", "use-context", &format!("k3d-{cluster_name}")],
)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"Failed to switch kubectl context to k3d : {}",
stderr
));
}
info!(
"kubectl is now using 'k3d-{}' as default context",
cluster_name
);
Ok(()) Ok(())
} }
async fn create_kubernetes_client(&self) -> Result<Client, String> { async fn create_kubernetes_client(&self) -> Result<Client, String> {
// TODO: Connect the client to the right k3d cluster (see https://git.nationtech.io/NationTech/harmony/issues/92)
Client::try_default() Client::try_default()
.await .await
.map_err(|e| format!("Failed to create Kubernetes client: {}", e)) .map_err(|e| format!("Failed to create Kubernetes client: {}", e))