Compare commits

..

24 Commits

Author SHA1 Message Date
1802b10ddf fix:translated documentaion notes into English 2025-10-23 15:31:45 -04:00
dd3f07e5b7 doc for removing worker flag from cp on UPI
All checks were successful
Run Check Script / check (pull_request) Successful in 1m13s
2025-10-09 15:28:42 -04:00
cbbaae2ac8 okd_enable_user_workload_monitoring (#160)
Reviewed-on: #160
Co-authored-by: Willem <wrolleman@nationtech.io>
Co-committed-by: Willem <wrolleman@nationtech.io>
2025-09-29 14:32:38 +00:00
c84b2413ed Merge pull request 'fix: added securityContext.runAsUser:null to argo-cd helm chart so that in okd user group will be randomly assigned within the uid range for the designated namespace' (#156) from fix/argo-cd-redis into master
All checks were successful
Run Check Script / check (push) Successful in 57s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 6m35s
Reviewed-on: #156
2025-09-12 13:54:02 +00:00
f83fd09f11 fix(monitoring): returned namespaced kube metrics
All checks were successful
Run Check Script / check (pull_request) Successful in 55s
2025-09-12 09:49:20 -04:00
c15bd53331 fix: added securityContext.runAsUser:null to argo-cd helm chart so that in okd user group will be randomly assigned within the uid range for the designated namespace
All checks were successful
Run Check Script / check (pull_request) Successful in 59s
2025-09-12 09:29:27 -04:00
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
All checks were successful
Run Check Script / check (push) Successful in 59s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 6m27s
Reviewed-on: #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
All checks were successful
Run Check Script / check (pull_request) Successful in 58s
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
All checks were successful
Run Check Script / check (pull_request) Successful in 1m1s
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
All checks were successful
Run Check Script / check (push) Successful in 1m4s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 6m55s
Reviewed-on: #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
Some checks failed
Compile and package harmony_composer / package_harmony_composer (push) Waiting to run
Run Check Script / check (push) Has been cancelled
Reviewed-on: #154
2025-09-10 17:13:31 +00:00
1e2563f7d1 fix: added reporting to output ntfy topic
All checks were successful
Run Check Script / check (pull_request) Successful in 1m2s
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
All checks were successful
Run Check Script / check (push) Successful in 1m1s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 6m50s
Reviewed-on: #152
2025-09-10 17:01:15 +00:00
4df451bc41 Merge remote-tracking branch 'origin/master' into fix/demo
All checks were successful
Run Check Script / check (pull_request) Successful in 1m2s
2025-09-10 12:59:58 -04:00
49dad343ad fix: properly configured discord alert receiver corrected domain and topic name for ntfy
All checks were successful
Run Check Script / check (pull_request) Successful in 1m2s
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
Some checks failed
Run Check Script / check (push) Failing after 1m16s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 7m46s
Reviewed-on: #151
2025-09-10 02:50:45 +00:00
7514ebfb5c fix format
Some checks failed
Run Check Script / check (pull_request) Failing after 1m22s
2025-09-09 22:50:26 -04:00
b3ae4e6611 fix: Various demo fixe and rename : RHOBMonitoring -> Monitoring, ContinuousDelivery -> PackagingDeployment, Fix bollard logs
Some checks failed
Run Check Script / check (pull_request) Failing after 1m21s
2025-09-09 22:46:14 -04:00
8424778871 add http
Some checks failed
Run Check Script / check (pull_request) Failing after 50s
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
Some checks failed
Run Check Script / check (push) Failing after 4s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 6m56s
2025-09-09 20:46:33 -04:00
Ian Letourneau
f3639c604c report Ntfy endpoint 2025-09-09 20:12:24 -04:00
ceafabf430 wip: Report harmony execution outcome 2025-09-09 17:59:14 -04:00
85 changed files with 1431 additions and 340 deletions

1
Cargo.lock generated
View File

@@ -3124,7 +3124,6 @@ dependencies = [
"fxhash", "fxhash",
"newline-converter", "newline-converter",
"once_cell", "once_cell",
"tempfile",
"unicode-segmentation", "unicode-segmentation",
"unicode-width 0.1.14", "unicode-width 0.1.14",
] ]

View File

@@ -14,8 +14,7 @@ members = [
"harmony_composer", "harmony_composer",
"harmony_inventory_agent", "harmony_inventory_agent",
"harmony_secret_derive", "harmony_secret_derive",
"harmony_secret", "harmony_secret", "adr/agent_discovery/mdns",
"adr/agent_discovery/mdns",
] ]
[workspace.package] [workspace.package]
@@ -51,7 +50,7 @@ k8s-openapi = { version = "0.25", features = ["v1_30"] }
serde_yaml = "0.9" serde_yaml = "0.9"
serde-value = "0.7" serde-value = "0.7"
http = "1.2" http = "1.2"
inquire = { version = "0.7", features = ["editor"] } inquire = "0.7"
convert_case = "0.8" convert_case = "0.8"
chrono = "0.4" chrono = "0.4"
similar = "2" similar = "2"
@@ -67,11 +66,5 @@ thiserror = "2.0.14"
serde = { version = "1.0.209", features = ["derive", "rc"] } serde = { version = "1.0.209", features = ["derive", "rc"] }
serde_json = "1.0.127" serde_json = "1.0.127"
askama = "0.14" askama = "0.14"
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite" ] }
reqwest = { version = "0.12", features = [ reqwest = { version = "0.12", features = ["blocking", "stream", "rustls-tls", "http2", "json"], default-features = false }
"blocking",
"stream",
"rustls-tls",
"http2",
"json",
], default-features = false }

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},
},
monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
}, },
topology::{K8sAnywhereTopology, Url}, 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

@@ -0,0 +1,56 @@
## **Remove Worker flag from OKD Control Planes**
### **Context**
On OKD user provisioned infrastructure the control plane nodes can have the flag node-role.kubernetes.io/worker which allows non critical workloads to be scheduled on the control-planes
### **Observed Symptoms**
- After adding HAProxy servers to the backend each back end appears down
- Traffic is redirected to the control planes instead of workers
- The pods router-default are incorrectly applied on the control planes rather than on the workers
- Pods are being scheduled on the control planes causing cluster instability
```
ss -tlnp | grep 80
```
- shows process haproxy is listening at 0.0.0.0:80 on cps
- same problem for port 443
- In namespace rook-ceph certain pods are deploted on cps rather than on worker nodes
### **Cause**
- when intalling UPI, the roles (master, worker) are not managed by the Machine Config operator and the cps are made schedulable by default.
### **Diagnostic**
check node labels:
```
oc get nodes --show-labels | grep control-plane
```
Inspecter kubelet configuration:
```
cat /etc/systemd/system/kubelet.service
```
find the line:
```
--node-labels=node-role.kubernetes.io/control-plane,node-role.kubernetes.io/master,node-role.kubernetes.io/worker
```
→ presence of label worker confirms the problem.
Verify the flag doesnt come from MCO
```
oc get machineconfig | grep rendered-master
```
**Solution:**
To make the control planes non schedulable you must patch the cluster scheduler resource
```
oc patch scheduler cluster --type merge -p '{"spec":{"mastersSchedulable":false}}'
```
after the patch is applied the workloads can be deplaced by draining the nodes
```
oc adm cordon <cp-node>
oc adm drain <cp-node> --ignore-daemonsets delete-emptydir-data
```

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

@@ -3,7 +3,7 @@ use harmony::{
modules::{ modules::{
application::{ application::{
ApplicationScore, RustWebFramework, RustWebapp, ApplicationScore, RustWebFramework, RustWebapp,
features::{ContinuousDelivery, Monitoring, rhob_monitoring::RHOBMonitoring}, features::{PackagingDeployment, rhob_monitoring::Monitoring},
}, },
monitoring::alert_channel::discord_alert_channel::DiscordWebhook, monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
}, },
@@ -16,24 +16,24 @@ use std::{path::PathBuf, sync::Arc};
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(),
project_root: PathBuf::from("./tryrust.org"), project_root: PathBuf::from("./tryrust.org"), // <== Project root, in this case it is a
// 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: hurl!("https://discord.doesnt.exist.com"),
};
let app = ApplicationScore { let app = ApplicationScore {
features: vec![ features: vec![
Box::new(ContinuousDelivery { Box::new(PackagingDeployment {
application: application.clone(), application: application.clone(),
}), }),
Box::new(RHOBMonitoring { 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,
@@ -41,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

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

@@ -1,13 +1,19 @@
use std::time::Duration;
use derive_new::new; use derive_new::new;
use k8s_openapi::{ use k8s_openapi::{
ClusterResourceScope, NamespaceResourceScope, ClusterResourceScope, NamespaceResourceScope,
api::{apps::v1::Deployment, core::v1::Pod}, api::{
apps::v1::Deployment,
core::v1::{Pod, PodStatus},
},
}; };
use kube::{ use kube::{
Client, Config, Error, Resource, Client, Config, Error, Resource,
api::{Api, AttachParams, DeleteParams, ListParams, Patch, PatchParams, ResourceExt}, api::{Api, AttachParams, DeleteParams, ListParams, Patch, PatchParams, ResourceExt},
config::{KubeConfigOptions, Kubeconfig}, config::{KubeConfigOptions, Kubeconfig},
core::ErrorResponse, core::ErrorResponse,
error::DiscoveryError,
runtime::reflector::Lookup, runtime::reflector::Lookup,
}; };
use kube::{api::DynamicObject, runtime::conditions}; use kube::{api::DynamicObject, runtime::conditions};
@@ -19,7 +25,7 @@ use log::{debug, error, trace};
use serde::{Serialize, de::DeserializeOwned}; use serde::{Serialize, de::DeserializeOwned};
use serde_json::{Value, json}; use serde_json::{Value, json};
use similar::TextDiff; use similar::TextDiff;
use tokio::io::AsyncReadExt; use tokio::{io::AsyncReadExt, time::sleep};
#[derive(new, Clone)] #[derive(new, Clone)]
pub struct K8sClient { pub struct K8sClient {
@@ -153,6 +159,41 @@ impl K8sClient {
} }
} }
pub async fn wait_for_pod_ready(
&self,
pod_name: &str,
namespace: Option<&str>,
) -> Result<(), Error> {
let mut elapsed = 0;
let interval = 5; // seconds between checks
let timeout_secs = 120;
loop {
let pod = self.get_pod(pod_name, namespace).await?;
if let Some(p) = pod {
if let Some(status) = p.status {
if let Some(phase) = status.phase {
if phase.to_lowercase() == "running" {
return Ok(());
}
}
}
}
if elapsed >= timeout_secs {
return Err(Error::Discovery(DiscoveryError::MissingResource(format!(
"'{}' in ns '{}' did not become ready within {}s",
pod_name,
namespace.unwrap(),
timeout_secs
))));
}
sleep(Duration::from_secs(interval)).await;
elapsed += interval;
}
}
/// Will execute a commond in the first pod found that matches the specified label /// Will execute a commond in the first pod found that matches the specified label
/// '{label}={name}' /// '{label}={name}'
pub async fn exec_app_capture_output( pub async fn exec_app_capture_output(
@@ -419,9 +460,12 @@ impl K8sClient {
.as_str() .as_str()
.expect("couldn't get kind as str"); .expect("couldn't get kind as str");
let split: Vec<&str> = api_version.splitn(2, "/").collect(); let mut it = api_version.splitn(2, '/');
let g = split[0]; let first = it.next().unwrap();
let v = split[1]; let (g, v) = match it.next() {
Some(second) => (first, second),
None => ("", first),
};
let gvk = GroupVersionKind::gvk(g, v, kind); let gvk = GroupVersionKind::gvk(g, v, kind);
let api_resource = ApiResource::from_gvk(&gvk); let api_resource = ApiResource::from_gvk(&gvk);

View File

@@ -372,7 +372,9 @@ 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"); warn!(
"Installing observability operator is not supported on LocalK3d source"
);
return Ok(PreparationOutcome::Noop); return Ok(PreparationOutcome::Noop);
debug!("installing cluster observability operator"); debug!("installing cluster observability operator");
todo!(); todo!();

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

@@ -55,7 +55,8 @@ impl<T: Topology + K8sclient + HelmCommand + Ingress> Interpret<T> for ArgoInter
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 = topology.get_domain("argo").await?; let svc = format!("argo-{}", self.score.namespace.clone());
let domain = topology.get_domain(&svc).await?;
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);
@@ -66,14 +67,17 @@ impl<T: Topology + K8sclient + HelmCommand + Ingress> Interpret<T> for ArgoInter
.await .await
.unwrap(); .unwrap();
Ok(Outcome::success(format!( Ok(Outcome::success_with_details(
"ArgoCD installed with {} {}", format!(
self.argo_apps.len(), "ArgoCD {} {}",
match self.argo_apps.len() { self.argo_apps.len(),
1 => "application", match self.argo_apps.len() {
_ => "applications", 1 => "application",
} _ => "applications",
))) }
),
vec![format!("argo application: http://{}", domain)],
))
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {
@@ -156,6 +160,9 @@ global:
## Used for ingresses, certificates, SSO, notifications, etc. ## Used for ingresses, certificates, SSO, notifications, etc.
domain: {domain} domain: {domain}
securityContext:
runAsUser: null
# -- Runtime class name for all components # -- Runtime class name for all components
runtimeClassName: "" runtimeClassName: ""
@@ -467,6 +474,13 @@ redis:
# -- Redis name # -- Redis name
name: redis name: redis
serviceAccount:
create: true
securityContext:
runAsUser: null
## Redis image ## Redis image
image: image:
# -- Redis repository # -- Redis repository

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,4 +1,6 @@
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::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;
@@ -43,7 +45,10 @@ impl<
+ std::fmt::Debug, + 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()
@@ -103,7 +108,7 @@ impl<
.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 {

View File

@@ -10,7 +10,7 @@ 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,
@@ -47,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,
@@ -139,9 +139,12 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
impl< impl<
A: OCICompliant + HelmPackage + Clone + 'static, A: OCICompliant + HelmPackage + Clone + 'static,
T: Topology + HelmCommand + MultiTargetTopology + K8sclient + Ingress + '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 let domain = topology
.get_domain(&self.application.name()) .get_domain(&self.application.name())
@@ -205,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,6 +1,8 @@
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;
@@ -25,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>>>,
} }
@@ -41,9 +43,12 @@ impl<
+ Ingress + 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()
@@ -59,12 +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: topology host: domain.clone(),
.get_domain("ntfy")
.await
.map_err(|e| format!("Could not get domain {e}"))?,
}; };
ntfy.interpret(&Inventory::empty(), topology) ntfy.interpret(&Inventory::empty(), topology)
.await .await
@@ -86,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

@@ -10,7 +10,7 @@ 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::{Builder, Header}; use tar::{Builder, Header};
use walkdir::WalkDir; use walkdir::WalkDir;
@@ -162,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
@@ -194,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())
@@ -224,6 +257,7 @@ impl RustWebapp {
".harmony_generated", ".harmony_generated",
"harmony", "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()

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, "Dhcp Interpret Set next boot to [{:?}], boot_filename to [{:?}], filename to [{:?}], filename64 to [{:?}], filenameipxe to [:{:?}]",
format!( self.score.boot_filename,
"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.filename,
self.score.boot_filename, self.score.filename64,
self.score.filename, self.score.filenameipxe
self.score.filename64, )))
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, "Dhcp Host Binding Interpret execution successful on {} hosts",
format!( self.score.host_binding.len()
"Dhcp Host Binding Interpret execution successful on {} hosts", )))
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, "DnsInterpret registered {} hosts successfully",
format!( entries.len()
"DnsInterpret registered {} hosts successfully", )))
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

@@ -197,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, "Helm Chart '{}' already installed to namespace {ns} and install_only=true",
format!( self.score.release_name
"Helm Chart '{}' already installed to namespace {ns} and install_only=true", )));
self.score.release_name
),
));
} else { } else {
info!( info!(
"Release '{}' not found in namespace '{}'. Proceeding with installation.", "Release '{}' not found in namespace '{}'. Proceeding with installation.",
@@ -228,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},
}; };
@@ -57,7 +61,7 @@ impl<T: Topology + K8sclient> Score<T> for K8sIngressScore {
let ingress_class = match self.ingress_class_name.clone() { let ingress_class = match self.ingress_class_name.clone() {
Some(ingress_class_name) => ingress_class_name, Some(ingress_class_name) => ingress_class_name,
None => format!("\"default\""), None => "\"default\"".to_string(),
}; };
let ingress = json!( let ingress = json!(
@@ -97,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(),
}) })
} }
@@ -109,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

@@ -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,4 +4,5 @@ pub mod application_monitoring;
pub mod grafana; pub mod grafana;
pub mod kube_prometheus; pub mod kube_prometheus;
pub mod ntfy; pub mod ntfy;
pub mod okd;
pub mod prometheus; pub mod prometheus;

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

@@ -0,0 +1,149 @@
use std::{collections::BTreeMap, sync::Arc};
use crate::{
data::Version,
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory,
score::Score,
topology::{K8sclient, Topology, k8s::K8sClient},
};
use async_trait::async_trait;
use harmony_types::id::Id;
use k8s_openapi::api::core::v1::ConfigMap;
use kube::api::ObjectMeta;
use serde::Serialize;
#[derive(Clone, Debug, Serialize)]
pub struct OpenshiftUserWorkloadMonitoring {}
impl<T: Topology + K8sclient> Score<T> for OpenshiftUserWorkloadMonitoring {
fn name(&self) -> String {
"OpenshiftUserWorkloadMonitoringScore".to_string()
}
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(OpenshiftUserWorkloadMonitoringInterpret {})
}
}
#[derive(Clone, Debug, Serialize)]
pub struct OpenshiftUserWorkloadMonitoringInterpret {}
#[async_trait]
impl<T: Topology + K8sclient> Interpret<T> for OpenshiftUserWorkloadMonitoringInterpret {
async fn execute(
&self,
_inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
let client = topology.k8s_client().await.unwrap();
self.update_cluster_monitoring_config_cm(&client).await?;
self.update_user_workload_monitoring_config_cm(&client)
.await?;
self.verify_user_workload(&client).await?;
Ok(Outcome::success(
"successfully enabled user-workload-monitoring".to_string(),
))
}
fn get_name(&self) -> InterpretName {
InterpretName::Custom("OpenshiftUserWorkloadMonitoring")
}
fn get_version(&self) -> Version {
todo!()
}
fn get_status(&self) -> InterpretStatus {
todo!()
}
fn get_children(&self) -> Vec<Id> {
todo!()
}
}
impl OpenshiftUserWorkloadMonitoringInterpret {
pub async fn update_cluster_monitoring_config_cm(
&self,
client: &Arc<K8sClient>,
) -> Result<Outcome, InterpretError> {
let mut data = BTreeMap::new();
data.insert(
"config.yaml".to_string(),
r#"
enableUserWorkload: true
alertmanagerMain:
enableUserAlertmanagerConfig: true
"#
.to_string(),
);
let cm = ConfigMap {
metadata: ObjectMeta {
name: Some("cluster-monitoring-config".to_string()),
namespace: Some("openshift-monitoring".to_string()),
..Default::default()
},
data: Some(data),
..Default::default()
};
client.apply(&cm, Some("openshift-monitoring")).await?;
Ok(Outcome::success(
"updated cluster-monitoring-config-map".to_string(),
))
}
pub async fn update_user_workload_monitoring_config_cm(
&self,
client: &Arc<K8sClient>,
) -> Result<Outcome, InterpretError> {
let mut data = BTreeMap::new();
data.insert(
"config.yaml".to_string(),
r#"
alertmanager:
enabled: true
enableAlertmanagerConfig: true
"#
.to_string(),
);
let cm = ConfigMap {
metadata: ObjectMeta {
name: Some("user-workload-monitoring-config".to_string()),
namespace: Some("openshift-user-workload-monitoring".to_string()),
..Default::default()
},
data: Some(data),
..Default::default()
};
client
.apply(&cm, Some("openshift-user-workload-monitoring"))
.await?;
Ok(Outcome::success(
"updated openshift-user-monitoring-config-map".to_string(),
))
}
pub async fn verify_user_workload(
&self,
client: &Arc<K8sClient>,
) -> Result<Outcome, InterpretError> {
let namespace = "openshift-user-workload-monitoring";
let alertmanager_name = "alertmanager-user-workload-0";
let prometheus_name = "prometheus-user-workload-0";
client
.wait_for_pod_ready(alertmanager_name, Some(namespace))
.await?;
client
.wait_for_pod_ready(prometheus_name, Some(namespace))
.await?;
Ok(Outcome::success(format!(
"pods: {}, {} ready in ns: {}",
alertmanager_name, prometheus_name, namespace
)))
}
}

View File

@@ -0,0 +1 @@
pub mod enable_user_workload;

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, "Found and assigned bootstrap node: {}",
format!( bootstrap_host.unwrap().summary()
"Found and assigned bootstrap node: {}", )))
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

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

@@ -12,9 +12,6 @@ use std::process::Command;
use crate::modules::k8s::ingress::{K8sIngressScore, PathType}; 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::{
Alertmanager, AlertmanagerSpec,
};
use crate::modules::monitoring::kube_prometheus::crd::rhob_grafana::{ use crate::modules::monitoring::kube_prometheus::crd::rhob_grafana::{
Grafana, GrafanaDashboard, GrafanaDashboardSpec, GrafanaDatasource, GrafanaDatasourceConfig, Grafana, GrafanaDashboard, GrafanaDashboardSpec, GrafanaDatasource, GrafanaDatasourceConfig,
GrafanaDatasourceSpec, GrafanaSpec, GrafanaDatasourceSpec, GrafanaSpec,
@@ -25,13 +22,8 @@ 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::{ use crate::modules::monitoring::kube_prometheus::crd::rhob_prometheuses::LabelSelector;
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,
}; };

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

@@ -120,26 +120,10 @@ impl SecretManager {
let ns = &manager.namespace; let ns = &manager.namespace;
let key = T::KEY; let key = T::KEY;
let secret_json = inquire::Editor::new(&format!( let secret_json = inquire::Text::new(&format!(
"Secret not found for {ns} {key}, paste the JSON here :", "Secret not found for {} {}, paste the JSON here :",
ns, key
)) ))
.with_formatter(&|data| {
let char_count = data.chars().count();
if char_count == 0 {
String::from("<skipped>")
} else if char_count <= 20 {
data.into()
} else {
let mut substr: String = data.chars().take(17).collect();
substr.push_str("...");
substr
}
})
.with_render_config(
inquire::ui::RenderConfig::default().with_canceled_prompt_indicator(
inquire::ui::Styled::new("<skipped>").with_fg(inquire::ui::Color::DarkYellow),
),
)
.prompt() .prompt()
.map_err(|e| { .map_err(|e| {
SecretStoreError::Store(format!("Failed to prompt secret {ns} {key} : {e}").into()) SecretStoreError::Store(format!("Failed to prompt secret {ns} {key} : {e}").into())