Compare commits
	
		
			31 Commits
		
	
	
		
			feat/multi
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6e6f57e38c | |||
| 6f55f79281 | |||
| 19f87fdaf7 | |||
| 49370af176 | |||
| cf0b8326dc | |||
| 1e2563f7d1 | |||
| 7f50c36f11 | |||
| 4df451bc41 | |||
| 49dad343ad | |||
| 9961e8b79d | |||
| 9b889f71da | |||
| 7514ebfb5c | |||
| b3ae4e6611 | |||
| 8424778871 | |||
| 7bc083701e | |||
| 4fa2b8deb6 | |||
|  | f3639c604c | ||
| 258cfa279e | |||
| ceafabf430 | |||
| 11481b16cd | |||
| 21dcb75408 | |||
| a5f9ecfcf7 | |||
| 849bd79710 | |||
| c5101e096a | |||
| cd0720f43e | |||
| b9e04d21da | |||
| a0884950d7 | |||
| 29d22a611f | |||
| 3bf5cb0526 | |||
| 54803c40a2 | |||
| 288129b0c1 | 
							
								
								
									
										69
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @ -36,48 +36,59 @@ These principles surface as simple, ergonomic Rust APIs that let teams focus on | ||||
| 
 | ||||
| ## 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 | ||||
| use harmony::{ | ||||
|     data::Version, | ||||
|     inventory::Inventory, | ||||
|     maestro::Maestro, | ||||
|     modules::{ | ||||
|         lamp::{LAMPConfig, LAMPScore}, | ||||
|         monitoring::monitoring_alerting::MonitoringAlertingStackScore, | ||||
|         application::{ | ||||
|             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] | ||||
| async fn main() { | ||||
|     // 1. Describe what you want | ||||
|     let lamp_stack = LAMPScore { | ||||
|         name: "harmony-lamp-demo".into(), | ||||
|         domain: Url::Url(url::Url::parse("https://lampdemo.example.com").unwrap()), | ||||
|         php_version: Version::from("8.3.0").unwrap(), | ||||
|         config: LAMPConfig { | ||||
|             project_root: "./php".into(), | ||||
|             database_size: "4Gi".into(), | ||||
|             ..Default::default() | ||||
|         }, | ||||
|     let application = Arc::new(RustWebapp { | ||||
|         name: "harmony-example-leptos".to_string(), | ||||
|         project_root: PathBuf::from(".."), // <== Your project root, usually .. if you use the standard `/harmony` folder | ||||
|         framework: Some(RustWebFramework::Leptos), | ||||
|         service_port: 8080, | ||||
|     }); | ||||
| 
 | ||||
|     // Define your Application deployment and the features you want | ||||
|     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( | ||||
|         Inventory::autoload(),                // auto-detect hardware / kube-config | ||||
|         K8sAnywhereTopology::from_env(),      // local k3d, CI, staging, prod… | ||||
|         vec![ | ||||
|           Box::new(lamp_stack), | ||||
|           Box::new(monitoring) | ||||
|         ], | ||||
|         None | ||||
|     ).await.unwrap(); | ||||
|         Inventory::autoload(), | ||||
|         K8sAnywhereTopology::from_env(), // <== Deploy to local automatically provisioned local k3d by default or connect to any kubernetes cluster | ||||
|         vec![Box::new(app)], | ||||
|         None, | ||||
|     ) | ||||
|     .await | ||||
|     .unwrap(); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										3
									
								
								demos/cncf-k8s-quebec-meetup-september-2025/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,3 @@ | ||||
| .terraform | ||||
| *.tfstate | ||||
| venv | ||||
							
								
								
									
										
											BIN
										
									
								
								demos/cncf-k8s-quebec-meetup-september-2025/75_years_later.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 72 KiB | 
							
								
								
									
										
											BIN
										
									
								
								demos/cncf-k8s-quebec-meetup-september-2025/Happy_swimmer.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 38 KiB | 
| After Width: | Height: | Size: 38 KiB | 
| After Width: | Height: | Size: 52 KiB | 
| After Width: | Height: | Size: 62 KiB | 
| After Width: | Height: | Size: 64 KiB | 
| After Width: | Height: | Size: 100 KiB | 
							
								
								
									
										5
									
								
								demos/cncf-k8s-quebec-meetup-september-2025/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,5 @@ | ||||
| To build : | ||||
| 
 | ||||
| ```bash | ||||
| npx @marp-team/marp-cli@latest -w slides.md | ||||
| ``` | ||||
							
								
								
									
										
											BIN
										
									
								
								demos/cncf-k8s-quebec-meetup-september-2025/ansible.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 11 KiB | 
| @ -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 | ||||
| ``` | ||||
| @ -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' | ||||
| After Width: | Height: | Size: 22 KiB | 
							
								
								
									
										
											BIN
										
									
								
								demos/cncf-k8s-quebec-meetup-september-2025/ansible_fail.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 275 KiB | 
| After Width: | Height: | Size: 212 KiB | 
| After Width: | Height: | Size: 384 KiB | 
| After Width: | Height: | Size: 8.3 KiB | 
							
								
								
									
										195
									
								
								demos/cncf-k8s-quebec-meetup-september-2025/slides.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										241
									
								
								demos/cncf-k8s-quebec-meetup-september-2025/slides.md
									
									
									
									
									
										Normal 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) | ||||
							
								
								
									
										
											BIN
										
									
								
								demos/cncf-k8s-quebec-meetup-september-2025/terraform.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 11 KiB | 
| @ -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", | ||||
|   ] | ||||
| } | ||||
| @ -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" | ||||
| } | ||||
| @ -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", | ||||
|   ] | ||||
| } | ||||
| @ -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" | ||||
|   } | ||||
| } | ||||
| After Width: | Height: | Size: 14 KiB | 
							
								
								
									
										
											BIN
										
									
								
								demos/cncf-k8s-quebec-meetup-september-2025/terraform_fail.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 144 KiB | 
| After Width: | Height: | Size: 58 KiB | 
| After Width: | Height: | Size: 56 KiB | 
| After Width: | Height: | Size: 71 KiB | 
| After Width: | Height: | Size: 81 KiB | 
| After Width: | Height: | Size: 87 KiB | 
| After Width: | Height: | Size: 88 KiB | 
| After Width: | Height: | Size: 48 KiB | 
							
								
								
									
										
											BIN
										
									
								
								demos/cncf-k8s-quebec-meetup-september-2025/tryrust.org.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 325 KiB | 
| @ -27,7 +27,6 @@ async fn main() { | ||||
|     }; | ||||
|     let application = Arc::new(RustWebapp { | ||||
|         name: "example-monitoring".to_string(), | ||||
|         domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()), | ||||
|         project_root: PathBuf::from("./examples/rust/webapp"), | ||||
|         framework: Some(RustWebFramework::Leptos), | ||||
|         service_port: 3000, | ||||
|  | ||||
| @ -4,8 +4,7 @@ use harmony::{ | ||||
|     inventory::Inventory, | ||||
|     modules::{ | ||||
|         application::{ | ||||
|             ApplicationScore, RustWebFramework, RustWebapp, | ||||
|             features::rhob_monitoring::RHOBMonitoring, | ||||
|             ApplicationScore, RustWebFramework, RustWebapp, features::rhob_monitoring::Monitoring, | ||||
|         }, | ||||
|         monitoring::alert_channel::discord_alert_channel::DiscordWebhook, | ||||
|     }, | ||||
| @ -17,7 +16,6 @@ use harmony_types::net::Url; | ||||
| async fn main() { | ||||
|     let application = Arc::new(RustWebapp { | ||||
|         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
 | ||||
|         framework: Some(RustWebFramework::Leptos), | ||||
|         service_port: 3000, | ||||
| @ -30,7 +28,7 @@ async fn main() { | ||||
| 
 | ||||
|     let app = ApplicationScore { | ||||
|         features: vec![ | ||||
|             Box::new(RHOBMonitoring { | ||||
|             Box::new(Monitoring { | ||||
|                 application: application.clone(), | ||||
|                 alert_receiver: vec![Box::new(discord_receiver)], | ||||
|             }), | ||||
|  | ||||
| @ -5,7 +5,7 @@ use harmony::{ | ||||
|     modules::{ | ||||
|         application::{ | ||||
|             ApplicationScore, RustWebFramework, RustWebapp, | ||||
|             features::{ContinuousDelivery, Monitoring}, | ||||
|             features::{Monitoring, PackagingDeployment}, | ||||
|         }, | ||||
|         monitoring::alert_channel::{ | ||||
|             discord_alert_channel::DiscordWebhook, webhook_receiver::WebhookReceiver, | ||||
| @ -19,7 +19,6 @@ use harmony_macros::hurl; | ||||
| async fn main() { | ||||
|     let application = Arc::new(RustWebapp { | ||||
|         name: "harmony-example-rust-webapp".to_string(), | ||||
|         domain: hurl!("https://rustapp.harmony.example.com"), | ||||
|         project_root: PathBuf::from("./webapp"), | ||||
|         framework: Some(RustWebFramework::Leptos), | ||||
|         service_port: 3000, | ||||
| @ -37,7 +36,7 @@ async fn main() { | ||||
| 
 | ||||
|     let app = ApplicationScore { | ||||
|         features: vec![ | ||||
|             Box::new(ContinuousDelivery { | ||||
|             Box::new(PackagingDeployment { | ||||
|                 application: application.clone(), | ||||
|             }), | ||||
|             Box::new(Monitoring { | ||||
|  | ||||
							
								
								
									
										1
									
								
								examples/try_rust_webapp/files_to_add/.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1 @@ | ||||
| harmony | ||||
							
								
								
									
										20
									
								
								examples/try_rust_webapp/files_to_add/Cargo.toml.to_add
									
									
									
									
									
										Normal 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" | ||||
							
								
								
									
										50
									
								
								examples/try_rust_webapp/files_to_add/main.rs
									
									
									
									
									
										Normal 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(); | ||||
| } | ||||
| @ -1,41 +1,39 @@ | ||||
| use std::{path::PathBuf, sync::Arc}; | ||||
| 
 | ||||
| use harmony::{ | ||||
|     inventory::Inventory, | ||||
|     modules::{ | ||||
|         application::{ | ||||
|             ApplicationScore, RustWebFramework, RustWebapp, | ||||
|             features::{ContinuousDelivery, Monitoring}, | ||||
|             features::{PackagingDeployment, rhob_monitoring::Monitoring}, | ||||
|         }, | ||||
|         monitoring::alert_channel::discord_alert_channel::DiscordWebhook, | ||||
|     }, | ||||
|     topology::K8sAnywhereTopology, | ||||
| }; | ||||
| use harmony_types::net::Url; | ||||
| use harmony_macros::hurl; | ||||
| use std::{path::PathBuf, sync::Arc}; | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|     let application = Arc::new(RustWebapp { | ||||
|         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: PathBuf::from("./tryrust.org"), // <== Project root, in this case it is a
 | ||||
|         // submodule
 | ||||
|         framework: Some(RustWebFramework::Leptos), | ||||
|         service_port: 8080, | ||||
|     }); | ||||
| 
 | ||||
|     let discord_receiver = DiscordWebhook { | ||||
|         name: "test-discord".to_string(), | ||||
|         url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()), | ||||
|     }; | ||||
| 
 | ||||
|     // Define your Application deployment and the features you want
 | ||||
|     let app = ApplicationScore { | ||||
|         features: vec![ | ||||
|             Box::new(ContinuousDelivery { | ||||
|             Box::new(PackagingDeployment { | ||||
|                 application: application.clone(), | ||||
|             }), | ||||
|             Box::new(Monitoring { | ||||
|                 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, | ||||
| @ -43,7 +41,7 @@ async fn main() { | ||||
| 
 | ||||
|     harmony_cli::run( | ||||
|         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)], | ||||
|         None, | ||||
|     ) | ||||
|  | ||||
| @ -10,7 +10,11 @@ testing = [] | ||||
| 
 | ||||
| [dependencies] | ||||
| 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" | ||||
| rust-ipmi = "0.1.1" | ||||
| semver = "1.0.23" | ||||
|  | ||||
| @ -34,6 +34,7 @@ pub enum InterpretName { | ||||
|     CephClusterHealth, | ||||
|     Custom(&'static str), | ||||
|     RHOBAlerting, | ||||
|     K8sIngress, | ||||
| } | ||||
| 
 | ||||
| impl std::fmt::Display for InterpretName { | ||||
| @ -64,6 +65,7 @@ impl std::fmt::Display for InterpretName { | ||||
|             InterpretName::CephClusterHealth => f.write_str("CephClusterHealth"), | ||||
|             InterpretName::Custom(name) => f.write_str(name), | ||||
|             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 status: InterpretStatus, | ||||
|     pub message: String, | ||||
|     pub details: Vec<String>, | ||||
| } | ||||
| 
 | ||||
| impl Outcome { | ||||
|     pub fn noop() -> Self { | ||||
|     pub fn noop(message: String) -> Self { | ||||
|         Self { | ||||
|             status: InterpretStatus::NOOP, | ||||
|             message: String::new(), | ||||
|             message, | ||||
|             details: vec![], | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -96,6 +100,23 @@ impl Outcome { | ||||
|         Self { | ||||
|             status: InterpretStatus::SUCCESS, | ||||
|             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![], | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										7
									
								
								harmony/src/domain/topology/ingress.rs
									
									
									
									
									
										Normal 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>; | ||||
| } | ||||
| @ -1,6 +1,7 @@ | ||||
| use std::{process::Command, sync::Arc}; | ||||
| 
 | ||||
| use async_trait::async_trait; | ||||
| use kube::api::GroupVersionKind; | ||||
| use log::{debug, info, warn}; | ||||
| use serde::Serialize; | ||||
| use tokio::sync::OnceCell; | ||||
| @ -22,6 +23,7 @@ use crate::{ | ||||
|         }, | ||||
|     }, | ||||
|     score::Score, | ||||
|     topology::ingress::Ingress, | ||||
| }; | ||||
| 
 | ||||
| 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> { | ||||
|         let version_result = Command::new("helm") | ||||
|             .arg("version") | ||||
| @ -350,6 +372,10 @@ impl K8sAnywhereTopology { | ||||
|             if let Some(Some(k8s_state)) = self.k8s_state.get() { | ||||
|                 match k8s_state.source { | ||||
|                     K8sSource::LocalK3d => { | ||||
|                         warn!( | ||||
|                             "Installing observability operator is not supported on LocalK3d source" | ||||
|                         ); | ||||
|                         return Ok(PreparationOutcome::Noop); | ||||
|                         debug!("installing cluster observability operator"); | ||||
|                         todo!(); | ||||
|                         let op_score = | ||||
| @ -528,7 +554,7 @@ impl MultiTargetTopology for K8sAnywhereTopology { | ||||
|         match self.config.harmony_profile.to_lowercase().as_str() { | ||||
|             "staging" => DeploymentTarget::Staging, | ||||
|             "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 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[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(), | ||||
|             )) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,9 +1,10 @@ | ||||
| use async_trait::async_trait; | ||||
| use derive_new::new; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| use super::{HelmCommand, PreparationError, PreparationOutcome, Topology}; | ||||
| 
 | ||||
| #[derive(new)] | ||||
| #[derive(new, Clone, Debug, Serialize, Deserialize)] | ||||
| pub struct LocalhostTopology; | ||||
| 
 | ||||
| #[async_trait] | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| mod ha_cluster; | ||||
| pub mod ingress; | ||||
| use harmony_types::net::IpAddress; | ||||
| mod host_binding; | ||||
| mod http; | ||||
|  | ||||
| @ -1,7 +1,10 @@ | ||||
| use std::error::Error; | ||||
| 
 | ||||
| use async_trait::async_trait; | ||||
| use derive_new::new; | ||||
| use serde::Serialize; | ||||
| 
 | ||||
| use crate::topology::Topology; | ||||
| use crate::{executors::ExecutorError, topology::Topology}; | ||||
| 
 | ||||
| /// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability,
 | ||||
| /// ContinuousIntegration, ContinuousDelivery
 | ||||
| @ -9,7 +12,10 @@ use crate::topology::Topology; | ||||
| pub trait ApplicationFeature<T: Topology>: | ||||
|     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; | ||||
| } | ||||
| 
 | ||||
| @ -40,3 +46,60 @@ impl<T: Topology> Clone for Box<dyn ApplicationFeature<T>> { | ||||
|         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}"), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -2,7 +2,7 @@ use async_trait::async_trait; | ||||
| use log::info; | ||||
| 
 | ||||
| use crate::{ | ||||
|     modules::application::ApplicationFeature, | ||||
|     modules::application::{ApplicationFeature, InstallationError, InstallationOutcome}, | ||||
|     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
 | ||||
| #[async_trait] | ||||
| 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!( | ||||
|             "Making sure public endpoint is installed for port {}", | ||||
|             self.application_port | ||||
|  | ||||
| @ -13,7 +13,8 @@ use crate::{ | ||||
|     modules::helm::chart::{HelmChartScore, HelmRepository}, | ||||
|     score::Score, | ||||
|     topology::{ | ||||
|         HelmCommand, K8sclient, PreparationError, PreparationOutcome, Topology, k8s::K8sClient, | ||||
|         HelmCommand, K8sclient, PreparationError, PreparationOutcome, Topology, ingress::Ingress, | ||||
|         k8s::K8sClient, | ||||
|     }, | ||||
| }; | ||||
| use harmony_types::id::Id; | ||||
| @ -27,7 +28,7 @@ pub struct ArgoHelmScore { | ||||
|     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>> { | ||||
|         Box::new(ArgoInterpret { | ||||
|             score: self.clone(), | ||||
| @ -47,17 +48,15 @@ pub struct ArgoInterpret { | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for ArgoInterpret { | ||||
| impl<T: Topology + K8sclient + HelmCommand + Ingress> Interpret<T> for ArgoInterpret { | ||||
|     async fn execute( | ||||
|         &self, | ||||
|         inventory: &Inventory, | ||||
|         topology: &T, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         let k8s_client = topology.k8s_client().await?; | ||||
|         let domain = self | ||||
|             .get_host_domain(k8s_client.clone(), self.score.openshift) | ||||
|             .await?; | ||||
|         let domain = format!("argo.{domain}"); | ||||
|         let svc = format!("argo-{}", self.score.namespace.clone()); | ||||
|         let domain = topology.get_domain(&svc).await?; | ||||
|         let helm_score = | ||||
|             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 | ||||
|             .unwrap(); | ||||
| 
 | ||||
|         Ok(Outcome::success(format!( | ||||
|             "ArgoCD installed with {} {}", | ||||
|             self.argo_apps.len(), | ||||
|             match self.argo_apps.len() { | ||||
|                 1 => "application", | ||||
|                 _ => "applications", | ||||
|             } | ||||
|         ))) | ||||
|         Ok(Outcome::success_with_details( | ||||
|             format!( | ||||
|                 "ArgoCD {} {}", | ||||
|                 self.argo_apps.len(), | ||||
|                 match self.argo_apps.len() { | ||||
|                     1 => "application", | ||||
|                     _ => "applications", | ||||
|                 } | ||||
|             ), | ||||
|             vec![format!("argo application: http://{}", domain)], | ||||
|         )) | ||||
|     } | ||||
| 
 | ||||
|     fn get_name(&self) -> InterpretName { | ||||
|  | ||||
| @ -5,8 +5,8 @@ pub use endpoint::*; | ||||
| mod monitoring; | ||||
| pub use monitoring::*; | ||||
| 
 | ||||
| mod continuous_delivery; | ||||
| pub use continuous_delivery::*; | ||||
| mod packaging_deployment; | ||||
| pub use packaging_deployment::*; | ||||
| 
 | ||||
| mod helm_argocd_score; | ||||
| pub use helm_argocd_score::*; | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| 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::kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus; | ||||
| 
 | ||||
| use crate::topology::MultiTargetTopology; | ||||
| use crate::topology::ingress::Ingress; | ||||
| use crate::{ | ||||
|     inventory::Inventory, | ||||
|     modules::monitoring::{ | ||||
| @ -19,8 +19,12 @@ use crate::{ | ||||
| }; | ||||
| use async_trait::async_trait; | ||||
| use base64::{Engine as _, engine::general_purpose}; | ||||
| use harmony_secret::SecretManager; | ||||
| use harmony_secret_derive::Secret; | ||||
| use harmony_types::net::Url; | ||||
| use log::{debug, info}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::sync::Arc; | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct Monitoring { | ||||
| @ -36,17 +40,22 @@ impl< | ||||
|         + TenantManager | ||||
|         + K8sclient | ||||
|         + MultiTargetTopology | ||||
|         + std::fmt::Debug | ||||
|         + PrometheusApplicationMonitoring<CRDPrometheus>, | ||||
|         + PrometheusApplicationMonitoring<CRDPrometheus> | ||||
|         + Ingress | ||||
|         + std::fmt::Debug, | ||||
| > 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"); | ||||
|         let namespace = topology | ||||
|             .get_tenant_config() | ||||
|             .await | ||||
|             .map(|ns| ns.name.clone()) | ||||
|             .unwrap_or_else(|| self.application.name()); | ||||
|         let domain = topology.get_domain("ntfy").await.unwrap(); | ||||
| 
 | ||||
|         let mut alerting_score = ApplicationMonitoringScore { | ||||
|             sender: CRDPrometheus { | ||||
| @ -58,19 +67,17 @@ impl< | ||||
|         }; | ||||
|         let ntfy = NtfyScore { | ||||
|             namespace: namespace.clone(), | ||||
|             host: "ntfy.harmonydemo.apps.ncd0.harmony.mcd".to_string(), | ||||
|             host: domain, | ||||
|         }; | ||||
|         ntfy.interpret(&Inventory::empty(), topology) | ||||
|             .await | ||||
|             .map_err(|e| e.to_string())?; | ||||
| 
 | ||||
|         let ntfy_default_auth_username = "harmony"; | ||||
|         let ntfy_default_auth_password = "harmony"; | ||||
|         let config = SecretManager::get_or_prompt::<NtfyAuth>().await.unwrap(); | ||||
| 
 | ||||
|         let ntfy_default_auth_header = format!( | ||||
|             "Basic {}", | ||||
|             general_purpose::STANDARD.encode(format!( | ||||
|                 "{ntfy_default_auth_username}:{ntfy_default_auth_password}" | ||||
|             )) | ||||
|             general_purpose::STANDARD.encode(format!("{}:{}", config.username, config.password)) | ||||
|         ); | ||||
| 
 | ||||
|         debug!("ntfy_default_auth_header: {ntfy_default_auth_header}"); | ||||
| @ -100,9 +107,17 @@ impl< | ||||
|             .interpret(&Inventory::empty(), topology) | ||||
|             .await | ||||
|             .map_err(|e| e.to_string())?; | ||||
|         Ok(()) | ||||
| 
 | ||||
|         Ok(InstallationOutcome::success()) | ||||
|     } | ||||
| 
 | ||||
|     fn name(&self) -> String { | ||||
|         "Monitoring".to_string() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Secret, Serialize, Deserialize, Clone, Debug)] | ||||
| struct NtfyAuth { | ||||
|     username: String, | ||||
|     password: String, | ||||
| } | ||||
|  | ||||
| @ -10,11 +10,13 @@ use crate::{ | ||||
|     data::Version, | ||||
|     inventory::Inventory, | ||||
|     modules::application::{ | ||||
|         ApplicationFeature, HelmPackage, OCICompliant, | ||||
|         ApplicationFeature, HelmPackage, InstallationError, InstallationOutcome, OCICompliant, | ||||
|         features::{ArgoApplication, ArgoHelmScore}, | ||||
|     }, | ||||
|     score::Score, | ||||
|     topology::{DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology}, | ||||
|     topology::{ | ||||
|         DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology, ingress::Ingress, | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| /// ContinuousDelivery in Harmony provides this functionality :
 | ||||
| @ -45,11 +47,11 @@ use crate::{ | ||||
| /// - ArgoCD to install/upgrade/rollback/inspect k8s resources
 | ||||
| /// - Kubernetes for runtime orchestration
 | ||||
| #[derive(Debug, Default, Clone)] | ||||
| pub struct ContinuousDelivery<A: OCICompliant + HelmPackage> { | ||||
| pub struct PackagingDeployment<A: OCICompliant + HelmPackage> { | ||||
|     pub application: Arc<A>, | ||||
| } | ||||
| 
 | ||||
| impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> { | ||||
| impl<A: OCICompliant + HelmPackage> PackagingDeployment<A> { | ||||
|     async fn deploy_to_local_k3d( | ||||
|         &self, | ||||
|         app_name: String, | ||||
| @ -136,18 +138,28 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> { | ||||
| #[async_trait] | ||||
| impl< | ||||
|     A: OCICompliant + HelmPackage + Clone + 'static, | ||||
|     T: Topology + HelmCommand + MultiTargetTopology + K8sclient + 'static, | ||||
| > ApplicationFeature<T> for ContinuousDelivery<A> | ||||
|     T: Topology + HelmCommand + MultiTargetTopology + K8sclient + Ingress + 'static, | ||||
| > 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 domain = topology | ||||
|             .get_domain(&self.application.name()) | ||||
|             .await | ||||
|             .map_err(|e| e.to_string())?; | ||||
| 
 | ||||
|         // TODO Write CI/CD workflow files
 | ||||
|         // we can autotedect the CI type using the remote url (default to github action for github
 | ||||
|         // url, etc..)
 | ||||
|         // 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)")
 | ||||
|         // https://git.nationtech.io/NationTech/harmony/issues/104
 | ||||
| @ -196,7 +208,11 @@ impl< | ||||
|                     .unwrap(); | ||||
|             } | ||||
|         }; | ||||
|         Ok(()) | ||||
| 
 | ||||
|         Ok(InstallationOutcome::success_with_details(vec![format!( | ||||
|             "{}: http://{domain}", | ||||
|             self.application.name() | ||||
|         )])) | ||||
|     } | ||||
|     fn name(&self) -> String { | ||||
|         "ContinuousDelivery".to_string() | ||||
| @ -1,11 +1,14 @@ | ||||
| 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::rhobs_application_monitoring_score::ApplicationRHOBMonitoringScore; | ||||
| 
 | ||||
| use crate::modules::monitoring::kube_prometheus::crd::rhob_alertmanager_config::RHOBObservability; | ||||
| use crate::topology::MultiTargetTopology; | ||||
| use crate::topology::ingress::Ingress; | ||||
| use crate::{ | ||||
|     inventory::Inventory, | ||||
|     modules::monitoring::{ | ||||
| @ -24,7 +27,7 @@ use harmony_types::net::Url; | ||||
| use log::{debug, info}; | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct RHOBMonitoring { | ||||
| pub struct Monitoring { | ||||
|     pub application: Arc<dyn Application>, | ||||
|     pub alert_receiver: Vec<Box<dyn AlertReceiver<RHOBObservability>>>, | ||||
| } | ||||
| @ -37,11 +40,15 @@ impl< | ||||
|         + TenantManager | ||||
|         + K8sclient | ||||
|         + MultiTargetTopology | ||||
|         + Ingress | ||||
|         + std::fmt::Debug | ||||
|         + 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"); | ||||
|         let namespace = topology | ||||
|             .get_tenant_config() | ||||
| @ -57,9 +64,13 @@ impl< | ||||
|             application: self.application.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 { | ||||
|             namespace: namespace.clone(), | ||||
|             host: "ntfy.harmonydemo.apps.ncd0.harmony.mcd".to_string(), | ||||
|             host: domain.clone(), | ||||
|         }; | ||||
|         ntfy.interpret(&Inventory::empty(), topology) | ||||
|             .await | ||||
| @ -81,27 +92,33 @@ impl< | ||||
|             .replace("=", ""); | ||||
| 
 | ||||
|         debug!("ntfy_default_auth_param: {ntfy_default_auth_param}"); | ||||
| 
 | ||||
|         let ntfy_receiver = WebhookReceiver { | ||||
|             name: "ntfy-webhook".to_string(), | ||||
|             url: Url::Url( | ||||
|                 url::Url::parse( | ||||
|                     format!( | ||||
|                         "http://ntfy.{}.svc.cluster.local/rust-web-app?auth={ntfy_default_auth_param}", | ||||
|                         namespace.clone() | ||||
|                         "http://{domain}/{}?auth={ntfy_default_auth_param}", | ||||
|                         self.application.name() | ||||
|                     ) | ||||
|                     .as_str(), | ||||
|                 ) | ||||
|                 .unwrap(), | ||||
|             ), | ||||
|         }; | ||||
| 
 | ||||
|         debug!( | ||||
|             "ntfy webhook receiver \n{:#?}\nntfy topic: {}", | ||||
|             ntfy_receiver.clone(), | ||||
|             self.application.name() | ||||
|         ); | ||||
|         alerting_score.receivers.push(Box::new(ntfy_receiver)); | ||||
|         alerting_score | ||||
|             .interpret(&Inventory::empty(), topology) | ||||
|             .await | ||||
|             .map_err(|e| e.to_string())?; | ||||
|         Ok(()) | ||||
|         Ok(InstallationOutcome::success_with_details(vec![format!( | ||||
|             "ntfy topic: {}", | ||||
|             self.application.name() | ||||
|         )])) | ||||
|     } | ||||
|     fn name(&self) -> String { | ||||
|         "Monitoring".to_string() | ||||
|  | ||||
| @ -24,8 +24,8 @@ use harmony_types::id::Id; | ||||
| #[derive(Clone, Debug)] | ||||
| pub enum ApplicationFeatureStatus { | ||||
|     Installing, | ||||
|     Installed, | ||||
|     Failed { details: String }, | ||||
|     Installed { details: Vec<String> }, | ||||
|     Failed { message: String }, | ||||
| } | ||||
| 
 | ||||
| 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(); | ||||
| 
 | ||||
|             let _ = match feature.ensure_installed(topology).await { | ||||
|                 Ok(()) => { | ||||
|                 Ok(outcome) => { | ||||
|                     instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged { | ||||
|                         topology: topology.name().into(), | ||||
|                         application: self.application.name(), | ||||
|                         feature: feature.name(), | ||||
|                         status: ApplicationFeatureStatus::Installed, | ||||
|                         status: ApplicationFeatureStatus::Installed { | ||||
|                             details: match outcome { | ||||
|                                 InstallationOutcome::Success { details } => details, | ||||
|                                 InstallationOutcome::Noop => vec![], | ||||
|                             }, | ||||
|                         }, | ||||
|                     }) | ||||
|                     .unwrap(); | ||||
|                 } | ||||
|                 Err(msg) => { | ||||
|                 Err(error) => { | ||||
|                     instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged { | ||||
|                         topology: topology.name().into(), | ||||
|                         application: self.application.name(), | ||||
|                         feature: feature.name(), | ||||
|                         status: ApplicationFeatureStatus::Failed { | ||||
|                             details: msg.clone(), | ||||
|                             message: error.to_string(), | ||||
|                         }, | ||||
|                     }) | ||||
|                     .unwrap(); | ||||
|                     return Err(InterpretError::new(format!( | ||||
|                         "Application Interpret failed to install feature : {msg}" | ||||
|                         "Application Interpret failed to install feature : {error}" | ||||
|                     ))); | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| use async_trait::async_trait; | ||||
| 
 | ||||
| use super::Application; | ||||
| use async_trait::async_trait; | ||||
| 
 | ||||
| #[async_trait] | ||||
| pub trait OCICompliant: Application { | ||||
| @ -17,5 +16,10 @@ pub trait HelmPackage: Application { | ||||
|     ///
 | ||||
|     /// # Arguments
 | ||||
|     /// * `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>; | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| use std::fs::{self, File}; | ||||
| use std::io::Read; | ||||
| use std::fs::{self}; | ||||
| use std::path::{Path, PathBuf}; | ||||
| use std::process; | ||||
| 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_builder::CopyBuilder; | ||||
| use futures_util::StreamExt; | ||||
| use log::{debug, info, log_enabled}; | ||||
| use log::{debug, error, info, log_enabled, trace, warn}; | ||||
| use serde::Serialize; | ||||
| use tar::{Archive, Builder, Header}; | ||||
| use tar::{Builder, Header}; | ||||
| use walkdir::WalkDir; | ||||
| 
 | ||||
| use crate::config::{REGISTRY_PROJECT, REGISTRY_URL}; | ||||
| use crate::{score::Score, topology::Topology}; | ||||
| use harmony_types::net::Url; | ||||
| 
 | ||||
| use super::{Application, ApplicationFeature, ApplicationInterpret, HelmPackage, OCICompliant}; | ||||
| 
 | ||||
| @ -58,7 +56,6 @@ pub enum RustWebFramework { | ||||
| #[derive(Debug, Clone, Serialize)] | ||||
| pub struct RustWebapp { | ||||
|     pub name: String, | ||||
|     pub domain: Url, | ||||
|     /// The path to the root of the Rust project to be containerized.
 | ||||
|     pub project_root: PathBuf, | ||||
|     pub service_port: u32, | ||||
| @ -73,12 +70,17 @@ impl Application for RustWebapp { | ||||
| 
 | ||||
| #[async_trait] | ||||
| 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); | ||||
| 
 | ||||
|         // 1. Create the Helm chart files on disk.
 | ||||
|         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))?; | ||||
|         info!("Successfully created Helm chart files in {:?}", chart_dir); | ||||
| 
 | ||||
| @ -160,7 +162,7 @@ impl RustWebapp { | ||||
|         &self, | ||||
|         image_name: &str, | ||||
|     ) -> 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 quiet = !log_enabled!(log::Level::Debug); | ||||
|         match dockerfile | ||||
| @ -192,8 +194,41 @@ impl RustWebapp { | ||||
|                     Some(body_full(tar_data.into())), | ||||
|                 ); | ||||
| 
 | ||||
|                 while let Some(msg) = image_build_stream.next().await { | ||||
|                     debug!("Message: {msg:?}"); | ||||
|                 while let Some(mut msg) = image_build_stream.next().await { | ||||
|                     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()) | ||||
| @ -220,7 +255,9 @@ impl RustWebapp { | ||||
|                 ".git", | ||||
|                 ".github", | ||||
|                 ".harmony_generated", | ||||
|                 "harmony", | ||||
|                 "node_modules", | ||||
|                 "Dockerfile.harmony", | ||||
|             ]; | ||||
|             let mut entries: Vec<_> = WalkDir::new(project_root) | ||||
|                 .into_iter() | ||||
| @ -265,8 +302,6 @@ impl RustWebapp { | ||||
| 
 | ||||
|         let docker = Docker::connect_with_socket_defaults().unwrap(); | ||||
| 
 | ||||
|         // let push_options = PushImageOptionsBuilder::new().tag(tag);
 | ||||
| 
 | ||||
|         let mut push_image_stream = docker.push_image( | ||||
|             image_tag, | ||||
|             Some(PushImageOptionsBuilder::new().build()), | ||||
| @ -274,6 +309,8 @@ impl RustWebapp { | ||||
|         ); | ||||
| 
 | ||||
|         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:?}"); | ||||
|         } | ||||
| 
 | ||||
| @ -408,9 +445,10 @@ impl RustWebapp { | ||||
|     } | ||||
| 
 | ||||
|     /// Creates all necessary files for a basic Helm chart.
 | ||||
|     fn create_helm_chart_files( | ||||
|     async fn create_helm_chart_files( | ||||
|         &self, | ||||
|         image_url: &str, | ||||
|         domain: &str, | ||||
|     ) -> Result<PathBuf, Box<dyn std::error::Error>> { | ||||
|         let chart_name = format!("{}-chart", self.name); | ||||
|         let chart_dir = self | ||||
| @ -460,21 +498,15 @@ ingress: | ||||
|   enabled: true | ||||
|   # Annotations for cert-manager to handle SSL. | ||||
|   annotations: | ||||
|     cert-manager.io/cluster-issuer: "letsencrypt-prod" | ||||
|     # Add other annotations like nginx ingress class if needed | ||||
|     # kubernetes.io/ingress.class: nginx | ||||
|   hosts: | ||||
|     - host: chart-example.local | ||||
|     - host: {} | ||||
|       paths: | ||||
|         - path: / | ||||
|           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)?; | ||||
| 
 | ||||
|  | ||||
| @ -69,17 +69,14 @@ impl DhcpInterpret { | ||||
| 
 | ||||
|         dhcp_server.set_pxe_options(pxe_options).await?; | ||||
| 
 | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             format!( | ||||
|                 "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.filename64, | ||||
|                 self.score.filenameipxe | ||||
|             ), | ||||
|         )) | ||||
|         Ok(Outcome::success(format!( | ||||
|             "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.filename64, | ||||
|             self.score.filenameipxe | ||||
|         ))) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -122,8 +119,7 @@ impl<T: Topology + DhcpServer> Interpret<T> for DhcpInterpret { | ||||
| 
 | ||||
|         topology.commit_config().await?; | ||||
| 
 | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|         Ok(Outcome::success( | ||||
|             "Dhcp Interpret execution successful".to_string(), | ||||
|         )) | ||||
|     } | ||||
| @ -197,10 +193,10 @@ impl DhcpHostBindingInterpret { | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             format!("Dhcp Interpret registered {} entries", number_new_entries), | ||||
|         )) | ||||
|         Ok(Outcome::success(format!( | ||||
|             "Dhcp Interpret registered {} entries", | ||||
|             number_new_entries | ||||
|         ))) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -236,12 +232,9 @@ impl<T: DhcpServer> Interpret<T> for DhcpHostBindingInterpret { | ||||
| 
 | ||||
|         topology.commit_config().await?; | ||||
| 
 | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             format!( | ||||
|                 "Dhcp Host Binding Interpret execution successful on {} hosts", | ||||
|                 self.score.host_binding.len() | ||||
|             ), | ||||
|         )) | ||||
|         Ok(Outcome::success(format!( | ||||
|             "Dhcp Host Binding Interpret execution successful on {} hosts", | ||||
|             self.score.host_binding.len() | ||||
|         ))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -55,8 +55,7 @@ impl DnsInterpret { | ||||
|             dns.register_dhcp_leases(register).await?; | ||||
|         } | ||||
| 
 | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|         Ok(Outcome::success( | ||||
|             "DNS Interpret execution successfull".to_string(), | ||||
|         )) | ||||
|     } | ||||
| @ -68,13 +67,10 @@ impl DnsInterpret { | ||||
|         let entries = &self.score.dns_entries; | ||||
|         dns_server.ensure_hosts_registered(entries.clone()).await?; | ||||
| 
 | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             format!( | ||||
|                 "DnsInterpret registered {} hosts successfully", | ||||
|                 entries.len() | ||||
|             ), | ||||
|         )) | ||||
|         Ok(Outcome::success(format!( | ||||
|             "DnsInterpret registered {} hosts successfully", | ||||
|             entries.len() | ||||
|         ))) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -111,8 +107,7 @@ impl<T: Topology + DnsServer> Interpret<T> for DnsInterpret { | ||||
| 
 | ||||
|         topology.commit_config().await?; | ||||
| 
 | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|         Ok(Outcome::success( | ||||
|             "Dns Interpret execution successful".to_string(), | ||||
|         )) | ||||
|     } | ||||
|  | ||||
| @ -153,6 +153,10 @@ impl<T: Topology + HelmCommand> Interpret<T> for HelmChartInterpret { | ||||
|         let yaml_path: Option<&Path> = match self.score.values_yaml.as_ref() { | ||||
|             Some(yaml_str) => { | ||||
|                 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()) | ||||
|             } | ||||
|             None => None, | ||||
| @ -193,13 +197,10 @@ impl<T: Topology + HelmCommand> Interpret<T> for HelmChartInterpret { | ||||
|                     self.score.release_name, ns | ||||
|                 ); | ||||
| 
 | ||||
|                 return Ok(Outcome::new( | ||||
|                     InterpretStatus::SUCCESS, | ||||
|                     format!( | ||||
|                         "Helm Chart '{}' already installed to namespace {ns} and install_only=true", | ||||
|                         self.score.release_name | ||||
|                     ), | ||||
|                 )); | ||||
|                 return Ok(Outcome::success(format!( | ||||
|                     "Helm Chart '{}' already installed to namespace {ns} and install_only=true", | ||||
|                     self.score.release_name | ||||
|                 ))); | ||||
|             } else { | ||||
|                 info!( | ||||
|                     "Release '{}' not found in namespace '{}'. Proceeding with installation.", | ||||
| @ -224,18 +225,18 @@ impl<T: Topology + HelmCommand> Interpret<T> for HelmChartInterpret { | ||||
|         }; | ||||
| 
 | ||||
|         match status { | ||||
|             helm_wrapper_rs::HelmDeployStatus::Deployed => Ok(Outcome::new( | ||||
|                 InterpretStatus::SUCCESS, | ||||
|                 format!("Helm Chart {} deployed", self.score.release_name), | ||||
|             )), | ||||
|             helm_wrapper_rs::HelmDeployStatus::PendingInstall => Ok(Outcome::new( | ||||
|                 InterpretStatus::RUNNING, | ||||
|                 format!("Helm Chart {} pending install...", self.score.release_name), | ||||
|             )), | ||||
|             helm_wrapper_rs::HelmDeployStatus::PendingUpgrade => Ok(Outcome::new( | ||||
|                 InterpretStatus::RUNNING, | ||||
|                 format!("Helm Chart {} pending upgrade...", self.score.release_name), | ||||
|             )), | ||||
|             helm_wrapper_rs::HelmDeployStatus::Deployed => Ok(Outcome::success(format!( | ||||
|                 "Helm Chart {} deployed", | ||||
|                 self.score.release_name | ||||
|             ))), | ||||
|             helm_wrapper_rs::HelmDeployStatus::PendingInstall => Ok(Outcome::running(format!( | ||||
|                 "Helm Chart {} pending install...", | ||||
|                 self.score.release_name | ||||
|             ))), | ||||
|             helm_wrapper_rs::HelmDeployStatus::PendingUpgrade => Ok(Outcome::running(format!( | ||||
|                 "Helm Chart {} pending upgrade...", | ||||
|                 self.score.release_name | ||||
|             ))), | ||||
|             helm_wrapper_rs::HelmDeployStatus::Failed => Err(InterpretError::new(format!( | ||||
|                 "Helm Chart {} installation failed", | ||||
|                 self.score.release_name | ||||
|  | ||||
| @ -133,10 +133,9 @@ impl<T: Topology> Interpret<T> for DiscoverInventoryAgentInterpret { | ||||
|             }, | ||||
|         ) | ||||
|         .await; | ||||
|         Ok(Outcome { | ||||
|             status: InterpretStatus::SUCCESS, | ||||
|             message: "Discovery process completed successfully".to_string(), | ||||
|         }) | ||||
|         Ok(Outcome::success( | ||||
|             "Discovery process completed successfully".to_string(), | ||||
|         )) | ||||
|     } | ||||
| 
 | ||||
|     fn get_name(&self) -> InterpretName { | ||||
|  | ||||
| @ -1,11 +1,15 @@ | ||||
| use async_trait::async_trait; | ||||
| use harmony_macros::ingress_path; | ||||
| use harmony_types::id::Id; | ||||
| use k8s_openapi::api::networking::v1::Ingress; | ||||
| use log::{debug, trace}; | ||||
| use serde::Serialize; | ||||
| use serde_json::json; | ||||
| 
 | ||||
| use crate::{ | ||||
|     interpret::Interpret, | ||||
|     data::Version, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::Inventory, | ||||
|     score::Score, | ||||
|     topology::{K8sclient, Topology}, | ||||
| }; | ||||
| @ -40,6 +44,7 @@ pub struct K8sIngressScore { | ||||
|     pub path: Option<IngressPath>, | ||||
|     pub path_type: Option<PathType>, | ||||
|     pub namespace: Option<fqdn::FQDN>, | ||||
|     pub ingress_class_name: Option<String>, | ||||
| } | ||||
| 
 | ||||
| impl<T: Topology + K8sclient> Score<T> for K8sIngressScore { | ||||
| @ -54,12 +59,18 @@ impl<T: Topology + K8sclient> Score<T> for K8sIngressScore { | ||||
|             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!( | ||||
|             { | ||||
|                 "metadata": { | ||||
|                     "name": self.name.to_string(), | ||||
|                 }, | ||||
|                 "spec": { | ||||
|                     "ingressClassName": ingress_class.as_str(), | ||||
|                     "rules": [ | ||||
|                         {   "host": self.host.to_string(), | ||||
|                             "http": { | ||||
| @ -90,11 +101,12 @@ impl<T: Topology + K8sclient> Score<T> for K8sIngressScore { | ||||
|             "Successfully built Ingress for host {:?}", | ||||
|             ingress.metadata.name | ||||
|         ); | ||||
|         Box::new(K8sResourceInterpret { | ||||
|             score: K8sResourceScore::single( | ||||
|                 ingress.clone(), | ||||
|                 self.namespace.clone().map(|f| f.to_string()), | ||||
|             ), | ||||
| 
 | ||||
|         Box::new(K8sIngressInterpret { | ||||
|             ingress, | ||||
|             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) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[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![] | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -147,6 +147,7 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for LAMPInterpret { | ||||
|             port: 8080, | ||||
|             path: Some(ingress_path), | ||||
|             path_type: None, | ||||
|             ingress_class_name: None, | ||||
|             namespace: self | ||||
|                 .get_namespace() | ||||
|                 .map(|nbs| fqdn!(nbs.to_string().as_str())), | ||||
|  | ||||
| @ -35,6 +35,24 @@ pub struct DiscordWebhook { | ||||
| #[async_trait] | ||||
| impl AlertReceiver<RHOBObservability> for DiscordWebhook { | ||||
|     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 { | ||||
|             data: json!({ | ||||
|                 "route": { | ||||
| @ -43,9 +61,14 @@ impl AlertReceiver<RHOBObservability> for DiscordWebhook { | ||||
|                 "receivers": [ | ||||
|                     { | ||||
|                         "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\" . }}" | ||||
|                             } | ||||
|                         ] | ||||
|                     } | ||||
|  | ||||
| @ -43,6 +43,11 @@ impl AlertReceiver<RHOBObservability> for WebhookReceiver { | ||||
|                         "webhookConfigs": [ | ||||
|                             { | ||||
|                             "url": self.url, | ||||
|                             "httpConfig": { | ||||
|                                 "tlsConfig": { | ||||
|                                     "insecureSkipVerify": true | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         ] | ||||
|                     } | ||||
|  | ||||
| @ -68,7 +68,9 @@ impl<T: Topology + PrometheusApplicationMonitoring<CRDPrometheus>> Interpret<T> | ||||
|                 PreparationOutcome::Success { details: _ } => { | ||||
|                     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)), | ||||
|         } | ||||
|  | ||||
| @ -70,7 +70,9 @@ impl<T: Topology + PrometheusApplicationMonitoring<RHOBObservability>> Interpret | ||||
|                 PreparationOutcome::Success { details: _ } => { | ||||
|                     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)), | ||||
|         } | ||||
|  | ||||
| @ -4,7 +4,9 @@ use kube::CustomResource; | ||||
| use schemars::JsonSchema; | ||||
| 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
 | ||||
| #[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)] | ||||
|  | ||||
| @ -45,6 +45,12 @@ service: | ||||
| 
 | ||||
| ingress: | ||||
|   enabled: {ingress_enabled} | ||||
|   hosts: | ||||
|     - host: {host} | ||||
|       paths: | ||||
|         - path: / | ||||
|           pathType: ImplementationSpecific | ||||
| 
 | ||||
| 
 | ||||
| route: | ||||
|   enabled: {route_enabled} | ||||
|  | ||||
| @ -113,7 +113,13 @@ impl<T: Topology + HelmCommand + K8sclient + MultiTargetTopology> Interpret<T> f | ||||
|             .await?; | ||||
|         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 { | ||||
|  | ||||
| @ -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::{ | ||||
|     data::Version, | ||||
|     hardware::PhysicalHost, | ||||
|     infra::inventory::InventoryRepositoryFactory, | ||||
|     interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, | ||||
|     inventory::{HostRole, Inventory}, | ||||
|     modules::inventory::{DiscoverHostForRoleScore, LaunchDiscoverInventoryAgentScore}, | ||||
|     modules::inventory::DiscoverHostForRoleScore, | ||||
|     score::Score, | ||||
|     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)
 | ||||
| // - 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?; | ||||
|         } | ||||
| 
 | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             format!( | ||||
|                 "Found and assigned bootstrap node: {}", | ||||
|                 bootstrap_host.unwrap().summary() | ||||
|             ), | ||||
|         )) | ||||
|         Ok(Outcome::success(format!( | ||||
|             "Found and assigned bootstrap node: {}", | ||||
|             bootstrap_host.unwrap().summary() | ||||
|         ))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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::{ | ||||
|     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}, | ||||
| @ -28,6 +16,15 @@ use crate::{ | ||||
|     score::Score, | ||||
|     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
 | ||||
| // - Select bootstrap node (from discovered set).
 | ||||
| @ -313,7 +310,7 @@ impl OKDSetup02BootstrapInterpret { | ||||
|         info!("[Bootstrap] Rebooting bootstrap node via SSH"); | ||||
|         // TODO reboot programatically, there are some logical checks and refactoring to do such as
 | ||||
|         // 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.", | ||||
|         ) | ||||
|         .prompt() | ||||
| @ -379,9 +376,6 @@ impl Interpret<HAClusterTopology> for OKDSetup02BootstrapInterpret { | ||||
|         self.reboot_target().await?; | ||||
|         self.wait_for_bootstrap_complete().await?; | ||||
| 
 | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             "Bootstrap phase complete".into(), | ||||
|         )) | ||||
|         Ok(Outcome::success("Bootstrap phase complete".into())) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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::{ | ||||
|     data::Version, | ||||
|     hardware::PhysicalHost, | ||||
| @ -19,6 +11,12 @@ use crate::{ | ||||
|     score::Score, | ||||
|     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
 | ||||
| // - Render per-MAC PXE & ignition for cp0/cp1/cp2.
 | ||||
| @ -269,8 +267,7 @@ impl Interpret<HAClusterTopology> for OKDSetup03ControlPlaneInterpret { | ||||
|         // the `wait-for bootstrap-complete` command.
 | ||||
|         info!("[ControlPlane] Provisioning initiated. Monitor the cluster convergence manually."); | ||||
| 
 | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|         Ok(Outcome::success( | ||||
|             "Control plane provisioning has been successfully initiated.".into(), | ||||
|         )) | ||||
|     } | ||||
|  | ||||
| @ -1,33 +1,17 @@ | ||||
| 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 log::info; | ||||
| use serde::Serialize; | ||||
| 
 | ||||
| use crate::{ | ||||
|     config::secret::{RedhatSecret, SshKeyPair}, | ||||
|     data::{FileContent, FilePath, Version}, | ||||
|     hardware::PhysicalHost, | ||||
|     infra::inventory::InventoryRepositoryFactory, | ||||
|     instrumentation::{HarmonyEvent, instrument}, | ||||
|     data::Version, | ||||
|     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}, | ||||
|         }, | ||||
|     }, | ||||
|     inventory::Inventory, | ||||
|     score::Score, | ||||
|     topology::{HAClusterTopology, HostBinding}, | ||||
|     topology::HAClusterTopology, | ||||
| }; | ||||
| 
 | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| // Step 04: Workers
 | ||||
| // - Render per-MAC PXE & ignition for workers; join nodes.
 | ||||
| @ -94,9 +78,6 @@ impl Interpret<HAClusterTopology> for OKDSetup04WorkersInterpret { | ||||
|         _topology: &HAClusterTopology, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         self.render_and_reboot().await?; | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             "Workers provisioned".into(), | ||||
|         )) | ||||
|         Ok(Outcome::success("Workers provisioned".into())) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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 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 log::info; | ||||
| use serde::Serialize; | ||||
| 
 | ||||
| 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
 | ||||
| // - Validate API reachability, ClusterOperators, ingress, and SDN status.
 | ||||
| @ -93,9 +76,6 @@ impl Interpret<HAClusterTopology> for OKDSetup05SanityCheckInterpret { | ||||
|         _topology: &HAClusterTopology, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         self.run_checks().await?; | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             "Sanity checks passed".into(), | ||||
|         )) | ||||
|         Ok(Outcome::success("Sanity checks passed".into())) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,32 +1,15 @@ | ||||
| // -------------------------------------------------------------------------------------------------
 | ||||
| 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 std::{fmt::Write, path::PathBuf}; | ||||
| use tokio::{fs::File, io::AsyncWriteExt, process::Command}; | ||||
| use log::info; | ||||
| use serde::Serialize; | ||||
| 
 | ||||
| use crate::{ | ||||
|     config::secret::{RedhatSecret, SshKeyPair}, | ||||
|     data::{FileContent, FilePath, Version}, | ||||
|     hardware::PhysicalHost, | ||||
|     infra::inventory::InventoryRepositoryFactory, | ||||
|     instrumentation::{HarmonyEvent, instrument}, | ||||
|     data::Version, | ||||
|     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}, | ||||
|         }, | ||||
|     }, | ||||
|     inventory::Inventory, | ||||
|     score::Score, | ||||
|     topology::{HAClusterTopology, HostBinding}, | ||||
|     topology::HAClusterTopology, | ||||
| }; | ||||
| 
 | ||||
| // Step 06: Installation Report
 | ||||
| @ -93,9 +76,6 @@ impl Interpret<HAClusterTopology> for OKDSetup06InstallationReportInterpret { | ||||
|         _topology: &HAClusterTopology, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         self.generate().await?; | ||||
|         Ok(Outcome::new( | ||||
|             InterpretStatus::SUCCESS, | ||||
|             "Installation report generated".into(), | ||||
|         )) | ||||
|         Ok(Outcome::success("Installation report generated".into())) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -21,8 +21,8 @@ pub fn pod_failed() -> PrometheusAlertRule { | ||||
| pub fn alert_container_restarting() -> PrometheusAlertRule { | ||||
|     PrometheusAlertRule { | ||||
|         alert: "ContainerRestarting".into(), | ||||
|         expr: "increase(kube_pod_container_status_restarts_total[5m]) > 3".into(), | ||||
|         r#for: Some("5m".into()), | ||||
|         expr: "increase(kube_pod_container_status_restarts_total[30s]) > 3".into(), | ||||
|         r#for: Some("30s".into()), | ||||
|         labels: HashMap::from([("severity".into(), "warning".into())]), | ||||
|         annotations: HashMap::from([ | ||||
|             ( | ||||
| @ -42,7 +42,7 @@ pub fn alert_pod_not_ready() -> PrometheusAlertRule { | ||||
|     PrometheusAlertRule { | ||||
|         alert: "PodNotReady".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())]), | ||||
|         annotations: HashMap::from([ | ||||
|             ("summary".into(), "Pod is not ready".into()), | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| use fqdn::fqdn; | ||||
| use std::fs; | ||||
| use std::{collections::BTreeMap, sync::Arc}; | ||||
| use tempfile::tempdir; | ||||
| @ -8,6 +9,7 @@ use log::{debug, info}; | ||||
| use serde::Serialize; | ||||
| 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::rhob_alertmanager_config::RHOBObservability; | ||||
| 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::{ | ||||
|     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::{ | ||||
|     ServiceMonitor, ServiceMonitorSpec, | ||||
| }; | ||||
| use crate::score::Score; | ||||
| use crate::topology::ingress::Ingress; | ||||
| use crate::topology::oberservability::monitoring::AlertReceiver; | ||||
| use crate::topology::{K8sclient, Topology, k8s::K8sClient}; | ||||
| use crate::{ | ||||
| @ -48,8 +56,8 @@ pub struct RHOBAlertingScore { | ||||
|     pub prometheus_rules: Vec<RuleGroup>, | ||||
| } | ||||
| 
 | ||||
| impl<T: Topology + K8sclient + PrometheusApplicationMonitoring<RHOBObservability>> Score<T> | ||||
|     for RHOBAlertingScore | ||||
| impl<T: Topology + K8sclient + Ingress + PrometheusApplicationMonitoring<RHOBObservability>> | ||||
|     Score<T> for RHOBAlertingScore | ||||
| { | ||||
|     fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { | ||||
|         Box::new(RHOBAlertingInterpret { | ||||
| @ -74,19 +82,20 @@ pub struct RHOBAlertingInterpret { | ||||
| } | ||||
| 
 | ||||
| #[async_trait] | ||||
| impl<T: Topology + K8sclient + PrometheusApplicationMonitoring<RHOBObservability>> Interpret<T> | ||||
|     for RHOBAlertingInterpret | ||||
| impl<T: Topology + K8sclient + Ingress + PrometheusApplicationMonitoring<RHOBObservability>> | ||||
|     Interpret<T> for RHOBAlertingInterpret | ||||
| { | ||||
|     async fn execute( | ||||
|         &self, | ||||
|         _inventory: &Inventory, | ||||
|         inventory: &Inventory, | ||||
|         topology: &T, | ||||
|     ) -> Result<Outcome, InterpretError> { | ||||
|         let client = topology.k8s_client().await.unwrap(); | ||||
|         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_grafana(&client).await?; | ||||
|         self.install_grafana(inventory, topology, &client).await?; | ||||
|         self.install_receivers(&self.sender, &self.receivers) | ||||
|             .await?; | ||||
|         self.install_rules(&self.prometheus_rules, &client).await?; | ||||
| @ -212,7 +221,8 @@ impl RHOBAlertingInterpret { | ||||
| 
 | ||||
|         let output = Command::new("helm") | ||||
|             .args([ | ||||
|                 "install", | ||||
|                 "upgrade", | ||||
|                 "--install", | ||||
|                 "grafana-operator", | ||||
|                 "grafana-operator/grafana-operator", | ||||
|                 "--namespace", | ||||
| @ -226,7 +236,7 @@ impl RHOBAlertingInterpret { | ||||
| 
 | ||||
|         if !output.status.success() { | ||||
|             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.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!( | ||||
|             "installing crd-prometheuses in namespace {}", | ||||
|             self.sender.namespace.clone() | ||||
|         ); | ||||
|         debug!("building role/rolebinding/serviceaccount for crd-prometheus"); | ||||
| 
 | ||||
|         let stack = MonitoringStack { | ||||
|             metadata: ObjectMeta { | ||||
|                 name: Some(format!("{}-monitoring", self.sender.namespace.clone()).into()), | ||||
|                 namespace: Some(self.sender.namespace.clone()), | ||||
|                 labels: Some([("coo".into(), "example".into())].into()), | ||||
|                 labels: Some([("monitoring-stack".into(), "true".into())].into()), | ||||
|                 ..Default::default() | ||||
|             }, | ||||
|             spec: MonitoringStackSpec { | ||||
|                 log_level: Some("debug".into()), | ||||
|                 retention: Some("1d".into()), | ||||
|                 resource_selector: Some(LabelSelector { | ||||
|                     match_labels: [("app".into(), "demo".into())].into(), | ||||
|                     ..Default::default() | ||||
|                     match_labels: Default::default(), | ||||
|                     match_expressions: vec![], | ||||
|                 }), | ||||
|             }, | ||||
|         }; | ||||
| @ -265,6 +281,42 @@ impl RHOBAlertingInterpret { | ||||
|             .apply(&stack, Some(&self.sender.namespace.clone())) | ||||
|             .await | ||||
|             .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",); | ||||
|         Ok(Outcome::success(format!( | ||||
|             "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( | ||||
|         &self, | ||||
|         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(); | ||||
|         label.insert("dashboards".to_string(), "grafana".to_string()); | ||||
|         let labels = LabelSelector { | ||||
| @ -465,6 +497,23 @@ impl RHOBAlertingInterpret { | ||||
|             .apply(&grafana, Some(&self.sender.namespace.clone())) | ||||
|             .await | ||||
|             .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!( | ||||
|             "successfully deployed grafana instance {:#?}", | ||||
|             grafana.metadata.name | ||||
|  | ||||
| @ -178,10 +178,10 @@ fn handle_events() { | ||||
|                     ApplicationFeatureStatus::Installing => { | ||||
|                         info!("Installing feature '{feature}' for '{application}'..."); | ||||
|                     } | ||||
|                     ApplicationFeatureStatus::Installed => { | ||||
|                     ApplicationFeatureStatus::Installed { details: _ } => { | ||||
|                         info!(status = "finished"; "Feature '{feature}' installed"); | ||||
|                     } | ||||
|                     ApplicationFeatureStatus::Failed { details } => { | ||||
|                     ApplicationFeatureStatus::Failed { message: details } => { | ||||
|                         error!(status = "failed"; "Feature '{feature}' installation failed: {details}"); | ||||
|                     } | ||||
|                 }, | ||||
|  | ||||
							
								
								
									
										56
									
								
								harmony_cli/src/cli_reporter.rs
									
									
									
									
									
										Normal 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!(); | ||||
|                     } | ||||
|                 } | ||||
|                 _ => {} | ||||
|             }; | ||||
|         } | ||||
|     }); | ||||
| } | ||||
| @ -8,6 +8,7 @@ use inquire::Confirm; | ||||
| use log::debug; | ||||
| 
 | ||||
| pub mod cli_logger; // FIXME: Don't make me pub
 | ||||
| mod cli_reporter; | ||||
| pub mod progress; | ||||
| pub mod theme; | ||||
| 
 | ||||
| @ -116,6 +117,7 @@ pub async fn run_cli<T: Topology + Send + Sync + 'static>( | ||||
|     args: Args, | ||||
| ) -> Result<(), Box<dyn std::error::Error>> { | ||||
|     cli_logger::init(); | ||||
|     cli_reporter::init(); | ||||
| 
 | ||||
|     let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); | ||||
|     maestro.register_all(scores); | ||||
|  | ||||
| @ -9,6 +9,7 @@ pub static EMOJI_ERROR: Emoji<'_, '_> = Emoji("⚠️", ""); | ||||
| pub static EMOJI_DEPLOY: Emoji<'_, '_> = Emoji("🚀", ""); | ||||
| pub static EMOJI_TOPOLOGY: Emoji<'_, '_> = Emoji("📦", ""); | ||||
| pub static EMOJI_SCORE: Emoji<'_, '_> = Emoji("🎶", ""); | ||||
| pub static EMOJI_SUMMARY: Emoji<'_, '_> = Emoji("🚀", ""); | ||||
| 
 | ||||
| lazy_static! { | ||||
|     pub static ref SECTION_STYLE: ProgressStyle = ProgressStyle::default_spinner() | ||||
|  | ||||
| @ -21,7 +21,6 @@ pub fn handle_events() { | ||||
| 
 | ||||
|     instrumentation::subscribe("Harmony Composer Logger", { | ||||
|         move |event| match event { | ||||
|             HarmonyComposerEvent::HarmonyComposerStarted => {} | ||||
|             HarmonyComposerEvent::ProjectInitializationStarted => { | ||||
|                 progress_tracker.add_section( | ||||
|                     SETUP_SECTION, | ||||
|  | ||||
| @ -5,7 +5,6 @@ use crate::{HarmonyProfile, HarmonyTarget}; | ||||
| 
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub enum HarmonyComposerEvent { | ||||
|     HarmonyComposerStarted, | ||||
|     ProjectInitializationStarted, | ||||
|     ProjectInitialized, | ||||
|     ProjectCompilationStarted { | ||||
|  | ||||
| @ -2,8 +2,8 @@ mod downloadable_asset; | ||||
| use downloadable_asset::*; | ||||
| 
 | ||||
| use kube::Client; | ||||
| use log::debug; | ||||
| use std::path::PathBuf; | ||||
| use log::{debug, info}; | ||||
| use std::{ffi::OsStr, path::PathBuf}; | ||||
| 
 | ||||
| const K3D_BIN_FILE_NAME: &str = "k3d"; | ||||
| 
 | ||||
| @ -213,15 +213,19 @@ impl K3d { | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         let client; | ||||
|         if !self.is_cluster_initialized() { | ||||
|             debug!("Cluster is not initialized, initializing now"); | ||||
|             return self.initialize_cluster().await; | ||||
|             client = self.initialize_cluster().await?; | ||||
|         } else { | ||||
|             self.start_cluster().await?; | ||||
| 
 | ||||
|             debug!("K3d and cluster are already properly set up"); | ||||
|             client = self.create_kubernetes_client().await?; | ||||
|         } | ||||
| 
 | ||||
|         self.start_cluster().await?; | ||||
| 
 | ||||
|         debug!("K3d and cluster are already properly set up"); | ||||
|         self.create_kubernetes_client().await | ||||
|         self.ensure_k3d_config_is_default(self.get_cluster_name()?)?; | ||||
|         Ok(client) | ||||
|     } | ||||
| 
 | ||||
|     // Private helper methods
 | ||||
| @ -302,7 +306,16 @@ impl K3d { | ||||
|         S: AsRef<std::ffi::OsStr>, | ||||
|     { | ||||
|         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 { | ||||
|             Ok(output) => { | ||||
|                 let stderr = String::from_utf8_lossy(&output.stderr); | ||||
| @ -311,7 +324,7 @@ impl K3d { | ||||
|                 debug!("stdout : {}", stdout); | ||||
|                 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)); | ||||
|         } | ||||
| 
 | ||||
|         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(()) | ||||
|     } | ||||
| 
 | ||||
|     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() | ||||
|             .await | ||||
|             .map_err(|e| format!("Failed to create Kubernetes client: {}", e)) | ||||
|  | ||||