Compare commits
6 Commits
feat/okd_d
...
0070373714
| Author | SHA1 | Date | |
|---|---|---|---|
| 0070373714 | |||
| f6e665f990 | |||
| 241980ebec | |||
| 35a459f63c | |||
| f076d36297 | |||
| 138e414727 |
2
.gitattributes
vendored
@@ -2,5 +2,3 @@ bootx64.efi filter=lfs diff=lfs merge=lfs -text
|
||||
grubx64.efi filter=lfs diff=lfs merge=lfs -text
|
||||
initrd filter=lfs diff=lfs merge=lfs -text
|
||||
linux filter=lfs diff=lfs merge=lfs -text
|
||||
data/okd/bin/* filter=lfs diff=lfs merge=lfs -text
|
||||
data/okd/installer_image/* filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
1
.gitignore
vendored
@@ -3,7 +3,6 @@ private_repos/
|
||||
|
||||
### Harmony ###
|
||||
harmony.log
|
||||
data/okd/installation_files*
|
||||
|
||||
### Helm ###
|
||||
# Chart dependencies
|
||||
|
||||
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "examples/try_rust_webapp/tryrust.org"]
|
||||
path = examples/try_rust_webapp/tryrust.org
|
||||
url = https://github.com/rust-dd/tryrust.org.git
|
||||
725
Cargo.lock
generated
69
README.md
@@ -36,59 +36,48 @@ 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 Rust + Leptos Webapp** 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 LAMP stack** 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::{
|
||||
application::{
|
||||
ApplicationScore, RustWebFramework, RustWebapp,
|
||||
features::{PackagingDeployment, rhob_monitoring::Monitoring},
|
||||
},
|
||||
monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
|
||||
lamp::{LAMPConfig, LAMPScore},
|
||||
monitoring::monitoring_alerting::MonitoringAlertingStackScore,
|
||||
},
|
||||
topology::K8sAnywhereTopology,
|
||||
topology::{K8sAnywhereTopology, Url},
|
||||
};
|
||||
use harmony_macros::hurl;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
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,
|
||||
// 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()
|
||||
},
|
||||
};
|
||||
|
||||
// 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(),
|
||||
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();
|
||||
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();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use log::debug;
|
||||
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
||||
|
||||
use crate::SERVICE_TYPE;
|
||||
@@ -73,7 +74,7 @@ pub async fn discover() {
|
||||
// }
|
||||
}
|
||||
|
||||
async fn _discover_example() {
|
||||
async fn discover_example() {
|
||||
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
||||
|
||||
// Create a daemon
|
||||
|
||||
BIN
data/okd/bin/kubectl
(Stored with Git LFS)
BIN
data/okd/bin/oc
(Stored with Git LFS)
BIN
data/okd/bin/oc_README.md
(Stored with Git LFS)
BIN
data/okd/bin/openshift-install
(Stored with Git LFS)
BIN
data/okd/bin/openshift-install_README.md
(Stored with Git LFS)
BIN
data/okd/installer_image/scos-9.0.20250510-0-live-initramfs.x86_64.img
(Stored with Git LFS)
BIN
data/okd/installer_image/scos-9.0.20250510-0-live-kernel.x86_64
(Stored with Git LFS)
BIN
data/okd/installer_image/scos-9.0.20250510-0-live-rootfs.x86_64.img
(Stored with Git LFS)
@@ -1 +0,0 @@
|
||||
scos-9.0.20250510-0-live-initramfs.x86_64.img
|
||||
@@ -1 +0,0 @@
|
||||
scos-9.0.20250510-0-live-kernel.x86_64
|
||||
@@ -1 +0,0 @@
|
||||
scos-9.0.20250510-0-live-rootfs.x86_64.img
|
||||
@@ -1,3 +0,0 @@
|
||||
.terraform
|
||||
*.tfstate
|
||||
venv
|
||||
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 100 KiB |
@@ -1,5 +0,0 @@
|
||||
To build :
|
||||
|
||||
```bash
|
||||
npx @marp-team/marp-cli@latest -w slides.md
|
||||
```
|
||||
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,9 +0,0 @@
|
||||
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
|
||||
```
|
||||
@@ -1,8 +0,0 @@
|
||||
- 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'
|
||||
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
@@ -1,241 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -1,132 +0,0 @@
|
||||
# Harmony, Orchestrateur d'infrastructure open-source
|
||||
|
||||
**Target Duration:** 25 minutes\
|
||||
**Tone:** Friendly, expert-to-expert, inspiring.
|
||||
|
||||
---
|
||||
|
||||
#### **Slide 1: Title Slide**
|
||||
|
||||
- **Visual:** Clean and simple. Your company logo (NationTech) and the Harmony logo.
|
||||
|
||||
---
|
||||
|
||||
#### **Slide 2: The YAML Labyrinth**
|
||||
|
||||
**Goal:** Get every head in the room nodding in agreement. Start with their world, not yours.
|
||||
|
||||
- **Visual:**
|
||||
- Option A: "The Pull Request from Hell". A screenshot of a GitHub pull request for a seemingly minor change that touches dozens of YAML files across multiple directories. A sea of red and green diffs that is visually overwhelming.
|
||||
- Option B: A complex flowchart connecting dozens of logos: Terraform, Ansible, K8s, Helm, etc.
|
||||
- **Narration:**\
|
||||
[...ADD SOMETHING FOR INTRODUCTION...]\
|
||||
"We love the power that tools like Kubernetes and the CNCF landscape have given us. But let's be honest... when did our infrastructure code start looking like _this_?"\
|
||||
"We have GitOps, which is great. But it often means we're managing this fragile cathedral of YAML, Helm charts, and brittle scripts. We spend more time debugging indentation and tracing variables than we do building truly resilient systems."
|
||||
|
||||
---
|
||||
|
||||
#### **Slide 3: The Real Cost of Infrastructure**
|
||||
|
||||
- **Visual:** "The Jenga Tower of Tools". A tall, precarious Jenga tower where each block is the logo of a different tool (Terraform, K8s, Helm, Ansible, Prometheus, ArgoCD, etc.). One block near the bottom is being nervously pulled out.
|
||||
- **Narration:**
|
||||
"The real cost isn't just complexity; it's the constant need to choose, learn, integrate, and operate a dozen different tools, each with its own syntax and failure modes. It's the nagging fear that a tiny typo in a config file could bring everything down. Click-ops isn't the answer, but the current state of IaC feels like we've traded one problem for another."
|
||||
|
||||
---
|
||||
|
||||
#### **Slide 4: The Broken Promise of "Code"**
|
||||
|
||||
**Goal:** Introduce the core idea before introducing the product. This makes the solution feel inevitable.
|
||||
|
||||
- **(Initial Visual):** A two-panel slide.
|
||||
- **Left Panel Title: "The Plan"** - A terminal showing a green, successful `terraform plan` output.
|
||||
- **Right Panel Title: "The Reality"** - The _next_ screen in the terminal, showing the `terraform apply` failing with a cascade of red error text.
|
||||
- **Narration:**
|
||||
"We call our discipline **Infrastructure as Code**. And we've all been here. Our 'compiler' is a `terraform plan` that says everything looks perfect. We get the green light."
|
||||
(Pause for a beat)
|
||||
"And then we `apply`, and reality hits. It fails halfway through, at runtime, when it's most expensive and painful to fix."
|
||||
|
||||
**(Click to transition the slide)**
|
||||
|
||||
- **(New Visual):** The entire slide is replaced by a clean screenshot of a code editor (like nvim 😉) showing Harmony's Rust DSL. A red squiggly line is under a config line. The error message is clear in the "Problems" panel: `error: Incompatible deployment. Production target 'gcp-prod-cluster' requires a StorageClass with 'snapshots' capability, but 'standard-sc' does not provide it.`
|
||||
- **Narration (continued):**
|
||||
"In software development, we solved these problems years ago. We don't accept 'it compiled, but crashed on startup'. We have real tools, type systems, compilers, test frameworks, and IDEs that catch our mistakes before they ever reach production. **So, what if we could treat our entire infrastructure... like a modern, compiled application?**"
|
||||
"What if your infrastructure code could get compile-time checks, straight into the editor... instead of runtime panics and failures at 3 AM in production?"
|
||||
|
||||
---
|
||||
|
||||
#### **Slide 5: Introducing Harmony**
|
||||
|
||||
**Goal:** Introduce Harmony as the answer to the "What If?" question.
|
||||
|
||||
- **Visual:** The Harmony logo, large and centered.
|
||||
- **Tagline:** `Infrastructure in type-safe Rust. No YAML required.`
|
||||
- **Narration:**
|
||||
"This is Harmony. It's an open-source orchestrator that lets you define your entire stack — from a dev laptop to a multi-site bare-metal cluster—in a single, type-safe Rust codebase."
|
||||
|
||||
---
|
||||
|
||||
#### **Slide 6: Before & After**
|
||||
|
||||
- **Visual:** A side-by-side comparison. Left side: A screen full of complex, nested YAML. Right side: 10-15 lines of clean, readable Harmony Rust DSL that accomplishes the same thing.
|
||||
- **Narration:**
|
||||
"This is the difference. On the left, the fragile world of strings and templates. On the right, a portable, verifiable program that describes your apps, your infra, and your operations. We unify scaffolding, provisioning, and Day-2 ops, all verified by the Rust compiler. But enough slides... let's see it in action."
|
||||
|
||||
---
|
||||
|
||||
#### **Slide 7: Live Demo: Zero to Monitored App**
|
||||
|
||||
**Goal:** Show, don't just tell. Make it look effortless. This is where you build the "dream."
|
||||
|
||||
- **Visual:** Your terminal/IDE, ready to go.
|
||||
- **Narration Guide:**
|
||||
"Okay, for this demo, we're going to take a standard web app from GitHub. Nothing special about it."
|
||||
_(Show the repo)_
|
||||
"Now, let's bring it into Harmony. This is the entire definition we need to describe the application and its needs."
|
||||
_(Show the Rust DSL)_
|
||||
"First, let's run it locally on k3d. The exact same definition for dev as for prod."
|
||||
_(Deploy locally, show it works)_
|
||||
"Cool. But a real app needs monitoring. In Harmony, that's just adding a feature to our code."
|
||||
_(Uncomment one line: `.with_feature(Monitoring)` and redeploy)_
|
||||
"And just like that, we have a fully configured Prometheus and Grafana stack, scraping our app. No YAML, no extra config."
|
||||
"Finally, let's push this to our production staging cluster. We just change the target and specify our multi-site Ceph storage."
|
||||
_(Deploy to the remote cluster)_
|
||||
"And there it is. We've gone from a simple web app to a monitored, enterprise-grade service in minutes."
|
||||
|
||||
---
|
||||
|
||||
#### **Slide 8: Live Demo: Embracing Chaos**
|
||||
|
||||
**Goal:** Prove the "predictable" and "resilient" claims in the most dramatic way possible.
|
||||
|
||||
- **Visual:** A slide showing a map or diagram of your distributed infrastructure (the different data centers). Then switch back to your terminal.
|
||||
- **Narration Guide:**
|
||||
"This is great when things are sunny. But production is chaos. So... let's break things. On purpose."
|
||||
"First, a network failure." _(Kill a switch/link, show app is still up)_
|
||||
"Now, let's power off a storage server." _(Force off a server, show Ceph healing and the app is unaffected)_
|
||||
"How about a control plane node?" _(Force off a k8s control plane, show the cluster is still running)_
|
||||
"Okay, for the grand finale. What if we have a cascading failure? I'm going to kill _another_ storage server. This should cause a total failure in this data center."
|
||||
_(Force off the second server, narrate what's happening)_
|
||||
"And there it is... Ceph has lost quorum in this site... and Harmony has automatically failed everything over to our other datacenter. The app is still running."
|
||||
|
||||
---
|
||||
|
||||
#### **Slide 9: The New Reality**
|
||||
|
||||
**Goal:** Summarize the dream and tell the audience what you want them to do.
|
||||
|
||||
- **Visual:** The clean, simple Harmony Rust DSL code from Slide 6. A summary of what was just accomplished is listed next to it: `✓ GitHub to Prod in minutes`, `✓ Type-Safe Validation`, `✓ Built-in Monitoring`, `✓ Automated Multi-Site Failover`.
|
||||
- **Narration:**
|
||||
"So, in just a few minutes, we went from a simple web app to a multi-site, monitored, and chaos-proof production deployment. We did it with a small amount of code that is easy to read, easy to verify, and completely portable. This is our vision: to offload the complexity, and make infrastructure simple, predictable, and even fun again."
|
||||
|
||||
---
|
||||
|
||||
#### **Slide 10: Join Us**
|
||||
|
||||
- **Visual:** A clean, final slide with QR codes and links.
|
||||
- GitHub Repo (`github.com/nation-tech/harmony`)
|
||||
- Website (`harmony.sh` or similar)
|
||||
- Your contact info (`jg@nation.tech` / LinkedIn / Twitter)
|
||||
- **Narration:**
|
||||
"Harmony is open-source, AGPLv3. We believe this is the future, but we're just getting started. We know this crowd has great infrastructure minds out there, and we need your feedback. Please, check out the project on GitHub. Star it if you like what you see. Tell us what's missing. Let's build this future together. Thank you."
|
||||
|
||||
**(Open for Q&A)**
|
||||
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,40 +0,0 @@
|
||||
# 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",
|
||||
]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
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"
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
# 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",
|
||||
]
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 325 KiB |
@@ -1,8 +0,0 @@
|
||||
## Bios settings
|
||||
|
||||
1. CSM : Disabled (compatibility support to boot gpt formatted drives)
|
||||
2. Secure boot : disabled
|
||||
3. Boot order :
|
||||
1. Local Hard drive
|
||||
2. PXE IPv4
|
||||
4. System clock, make sure it is adjusted, otherwise you will get invalid certificates error
|
||||
@@ -27,9 +27,9 @@ 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,
|
||||
});
|
||||
|
||||
let webhook_receiver = WebhookReceiver {
|
||||
|
||||
@@ -13,7 +13,6 @@ harmony_types = { path = "../../harmony_types" }
|
||||
cidr = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
harmony_macros = { path = "../../harmony_macros" }
|
||||
harmony_secret = { path = "../../harmony_secret" }
|
||||
log = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
@@ -5,8 +5,6 @@ use std::{
|
||||
|
||||
use cidr::Ipv4Cidr;
|
||||
use harmony::{
|
||||
config::secret::SshKeyPair,
|
||||
data::{FileContent, FilePath},
|
||||
hardware::{HostCategory, Location, PhysicalHost, SwitchGroup},
|
||||
infra::opnsense::OPNSenseManagementInterface,
|
||||
inventory::Inventory,
|
||||
@@ -15,14 +13,13 @@ use harmony::{
|
||||
okd::{
|
||||
bootstrap_dhcp::OKDBootstrapDhcpScore,
|
||||
bootstrap_load_balancer::OKDBootstrapLoadBalancerScore, dhcp::OKDDhcpScore,
|
||||
dns::OKDDnsScore, ipxe::OKDIpxeScore,
|
||||
dns::OKDDnsScore, ipxe::OkdIpxeScore,
|
||||
},
|
||||
tftp::TftpScore,
|
||||
},
|
||||
topology::{LogicalHost, UnmanagedRouter},
|
||||
};
|
||||
use harmony_macros::{ip, mac_address};
|
||||
use harmony_secret::SecretManager;
|
||||
use harmony_types::net::Url;
|
||||
|
||||
#[tokio::main]
|
||||
@@ -126,27 +123,22 @@ async fn main() {
|
||||
let load_balancer_score =
|
||||
harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
|
||||
|
||||
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await.unwrap();
|
||||
|
||||
let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string()));
|
||||
let http_score = StaticFilesHttpScore {
|
||||
folder_to_serve: Some(Url::LocalFolder(
|
||||
"./data/watchguard/pxe-http-files".to_string(),
|
||||
)),
|
||||
files: vec![],
|
||||
remote_path: None,
|
||||
};
|
||||
|
||||
let kickstart_filename = "inventory.kickstart".to_string();
|
||||
let cluster_pubkey_filename = "cluster_ssh_key.pub".to_string();
|
||||
let harmony_inventory_agent = "harmony_inventory_agent".to_string();
|
||||
|
||||
let ipxe_score = OKDIpxeScore {
|
||||
let ipxe_score = OkdIpxeScore {
|
||||
kickstart_filename,
|
||||
harmony_inventory_agent,
|
||||
cluster_pubkey: FileContent {
|
||||
path: FilePath::Relative("cluster_ssh_key.pub".to_string()),
|
||||
content: ssh_key.public,
|
||||
},
|
||||
cluster_pubkey_filename,
|
||||
};
|
||||
|
||||
harmony_tui::run(
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export HARMONY_SECRET_NAMESPACE=example-vms
|
||||
export HARMONY_SECRET_STORE=file
|
||||
export HARMONY_DATABASE_URL=sqlite://harmony_vms.sqlite RUST_LOG=info
|
||||
export RUST_LOG=info
|
||||
@@ -1,34 +1,20 @@
|
||||
mod topology;
|
||||
|
||||
use crate::topology::{get_inventory, get_topology};
|
||||
use harmony::{
|
||||
config::secret::SshKeyPair,
|
||||
data::{FileContent, FilePath},
|
||||
modules::okd::{installation::OKDInstallationPipeline, ipxe::OKDIpxeScore},
|
||||
score::Score,
|
||||
topology::HAClusterTopology,
|
||||
};
|
||||
use harmony_secret::SecretManager;
|
||||
use harmony::modules::okd::{installation::OKDInstallationScore, ipxe::OkdIpxeScore};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let inventory = get_inventory();
|
||||
let topology = get_topology().await;
|
||||
|
||||
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await.unwrap();
|
||||
let kickstart_filename = "inventory.kickstart".to_string();
|
||||
let cluster_pubkey_filename = "cluster_ssh_key.pub".to_string();
|
||||
let harmony_inventory_agent = "harmony_inventory_agent".to_string();
|
||||
|
||||
let mut scores: Vec<Box<dyn Score<HAClusterTopology>>> = vec![Box::new(OKDIpxeScore {
|
||||
kickstart_filename: "inventory.kickstart".to_string(),
|
||||
harmony_inventory_agent: "harmony_inventory_agent".to_string(),
|
||||
cluster_pubkey: FileContent {
|
||||
path: FilePath::Relative("cluster_ssh_key.pub".to_string()),
|
||||
content: ssh_key.public,
|
||||
},
|
||||
})];
|
||||
let okd_install = Box::new(OKDInstallationScore {});
|
||||
|
||||
scores.append(&mut OKDInstallationPipeline::get_all_scores().await);
|
||||
|
||||
harmony_cli::run(inventory, topology, scores, None)
|
||||
harmony_cli::run(inventory, topology, vec![okd_install], None)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ pub async fn get_topology() -> HAClusterTopology {
|
||||
name: String::from("opnsense-1"),
|
||||
};
|
||||
|
||||
let config = SecretManager::get_or_prompt::<OPNSenseFirewallConfig>().await;
|
||||
let config = SecretManager::get::<OPNSenseFirewallConfig>().await;
|
||||
let config = config.unwrap();
|
||||
|
||||
let opnsense = Arc::new(
|
||||
@@ -51,10 +51,10 @@ pub async fn get_topology() -> HAClusterTopology {
|
||||
dns_server: opnsense.clone(),
|
||||
control_plane: vec![LogicalHost {
|
||||
ip: ip!("192.168.1.20"),
|
||||
name: "master".to_string(),
|
||||
name: "cp0".to_string(),
|
||||
}],
|
||||
bootstrap_host: LogicalHost {
|
||||
ip: ip!("192.168.1.10"),
|
||||
ip: ip!("192.168.1.20"),
|
||||
name: "bootstrap".to_string(),
|
||||
},
|
||||
workers: vec![],
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
mod topology;
|
||||
|
||||
use crate::topology::{get_inventory, get_topology};
|
||||
use harmony::{
|
||||
config::secret::SshKeyPair,
|
||||
data::{FileContent, FilePath},
|
||||
modules::okd::ipxe::OKDIpxeScore,
|
||||
};
|
||||
use harmony_secret::SecretManager;
|
||||
use harmony::modules::okd::ipxe::OkdIpxeScore;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -14,16 +9,13 @@ async fn main() {
|
||||
let topology = get_topology().await;
|
||||
|
||||
let kickstart_filename = "inventory.kickstart".to_string();
|
||||
let cluster_pubkey_filename = "cluster_ssh_key.pub".to_string();
|
||||
let harmony_inventory_agent = "harmony_inventory_agent".to_string();
|
||||
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await.unwrap();
|
||||
|
||||
let ipxe_score = OKDIpxeScore {
|
||||
let ipxe_score = OkdIpxeScore {
|
||||
kickstart_filename,
|
||||
harmony_inventory_agent,
|
||||
cluster_pubkey: FileContent {
|
||||
path: FilePath::Relative("cluster_ssh_key.pub".to_string()),
|
||||
content: ssh_key.public,
|
||||
},
|
||||
cluster_pubkey_filename,
|
||||
};
|
||||
|
||||
harmony_cli::run(inventory, topology, vec![Box::new(ipxe_score)], None)
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
use cidr::Ipv4Cidr;
|
||||
use harmony::{
|
||||
config::secret::OPNSenseFirewallCredentials,
|
||||
hardware::{Location, SwitchGroup},
|
||||
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
|
||||
infra::opnsense::OPNSenseManagementInterface,
|
||||
inventory::Inventory,
|
||||
topology::{HAClusterTopology, LogicalHost, UnmanagedRouter},
|
||||
};
|
||||
use harmony_macros::{ip, ipv4};
|
||||
use harmony_secret::SecretManager;
|
||||
use harmony_secret::{Secret, SecretManager};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{net::IpAddr, sync::Arc};
|
||||
|
||||
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
||||
struct OPNSenseFirewallConfig {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
pub async fn get_topology() -> HAClusterTopology {
|
||||
let firewall = harmony::topology::LogicalHost {
|
||||
ip: ip!("192.168.1.1"),
|
||||
name: String::from("opnsense-1"),
|
||||
};
|
||||
|
||||
let config = SecretManager::get_or_prompt::<OPNSenseFirewallCredentials>().await;
|
||||
let config = SecretManager::get::<OPNSenseFirewallConfig>().await;
|
||||
let config = config.unwrap();
|
||||
|
||||
let opnsense = Arc::new(
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
|
||||
use cidr::Ipv4Cidr;
|
||||
use harmony::{
|
||||
hardware::{HostCategory, Location, PhysicalHost, SwitchGroup},
|
||||
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
|
||||
infra::opnsense::OPNSenseManagementInterface,
|
||||
inventory::Inventory,
|
||||
modules::{
|
||||
@@ -85,7 +85,6 @@ async fn main() {
|
||||
"./data/watchguard/pxe-http-files".to_string(),
|
||||
)),
|
||||
files: vec![],
|
||||
remote_path: None,
|
||||
};
|
||||
|
||||
harmony_tui::run(
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "rhob-application-monitoring"
|
||||
edition = "2024"
|
||||
version.workspace = true
|
||||
readme.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
harmony = { path = "../../harmony" }
|
||||
harmony_cli = { path = "../../harmony_cli" }
|
||||
harmony_types = { path = "../../harmony_types" }
|
||||
harmony_macros = { path = "../../harmony_macros" }
|
||||
tokio = { workspace = true }
|
||||
log = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
url = { workspace = true }
|
||||
base64.workspace = true
|
||||
@@ -1,48 +0,0 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use harmony::{
|
||||
inventory::Inventory,
|
||||
modules::{
|
||||
application::{
|
||||
ApplicationScore, RustWebFramework, RustWebapp, features::rhob_monitoring::Monitoring,
|
||||
},
|
||||
monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
|
||||
},
|
||||
topology::K8sAnywhereTopology,
|
||||
};
|
||||
use harmony_types::net::Url;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let application = Arc::new(RustWebapp {
|
||||
name: "test-rhob-monitoring".to_string(),
|
||||
project_root: PathBuf::from("./webapp"), // Relative from 'harmony-path' param
|
||||
framework: Some(RustWebFramework::Leptos),
|
||||
service_port: 3000,
|
||||
});
|
||||
|
||||
let discord_receiver = DiscordWebhook {
|
||||
name: "test-discord".to_string(),
|
||||
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
||||
};
|
||||
|
||||
let app = ApplicationScore {
|
||||
features: vec![
|
||||
Box::new(Monitoring {
|
||||
application: application.clone(),
|
||||
alert_receiver: vec![Box::new(discord_receiver)],
|
||||
}),
|
||||
// TODO add backups, multisite ha, etc
|
||||
],
|
||||
application,
|
||||
};
|
||||
|
||||
harmony_cli::run(
|
||||
Inventory::autoload(),
|
||||
K8sAnywhereTopology::from_env(),
|
||||
vec![Box::new(app)],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use harmony::{
|
||||
modules::{
|
||||
application::{
|
||||
ApplicationScore, RustWebFramework, RustWebapp,
|
||||
features::{Monitoring, PackagingDeployment},
|
||||
features::{ContinuousDelivery, Monitoring},
|
||||
},
|
||||
monitoring::alert_channel::{
|
||||
discord_alert_channel::DiscordWebhook, webhook_receiver::WebhookReceiver,
|
||||
@@ -13,30 +13,30 @@ use harmony::{
|
||||
},
|
||||
topology::K8sAnywhereTopology,
|
||||
};
|
||||
use harmony_macros::hurl;
|
||||
use harmony_types::net::Url;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let application = Arc::new(RustWebapp {
|
||||
name: "harmony-example-rust-webapp".to_string(),
|
||||
project_root: PathBuf::from("./webapp"),
|
||||
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
|
||||
project_root: PathBuf::from("./webapp"), // Relative from 'harmony-path' param
|
||||
framework: Some(RustWebFramework::Leptos),
|
||||
service_port: 3000,
|
||||
});
|
||||
|
||||
let discord_receiver = DiscordWebhook {
|
||||
name: "test-discord".to_string(),
|
||||
url: hurl!("https://discord.doesnt.exist.com"),
|
||||
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
||||
};
|
||||
|
||||
let webhook_receiver = WebhookReceiver {
|
||||
name: "sample-webhook-receiver".to_string(),
|
||||
url: hurl!("https://webhook-doesnt-exist.com"),
|
||||
url: Url::Url(url::Url::parse("https://webhook-doesnt-exist.com").unwrap()),
|
||||
};
|
||||
|
||||
let app = ApplicationScore {
|
||||
features: vec![
|
||||
Box::new(PackagingDeployment {
|
||||
Box::new(ContinuousDelivery {
|
||||
application: application.clone(),
|
||||
}),
|
||||
Box::new(Monitoring {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "example-try-rust-webapp"
|
||||
edition = "2024"
|
||||
version.workspace = true
|
||||
readme.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
harmony = { path = "../../harmony" }
|
||||
harmony_cli = { path = "../../harmony_cli" }
|
||||
harmony_types = { path = "../../harmony_types" }
|
||||
harmony_macros = { path = "../../harmony_macros" }
|
||||
tokio = { workspace = true }
|
||||
log = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
url = { workspace = true }
|
||||
base64.workspace = true
|
||||
@@ -1 +0,0 @@
|
||||
harmony
|
||||
@@ -1,20 +0,0 @@
|
||||
[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"
|
||||
@@ -1,50 +0,0 @@
|
||||
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,50 +0,0 @@
|
||||
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: "harmony-example-tryrust".to_string(),
|
||||
project_root: PathBuf::from("./tryrust.org"), // <== Project root, in this case it is a
|
||||
// submodule
|
||||
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"),
|
||||
})],
|
||||
}),
|
||||
],
|
||||
application,
|
||||
};
|
||||
|
||||
harmony_cli::run(
|
||||
Inventory::autoload(),
|
||||
K8sAnywhereTopology::from_env(), // <== Deploy to local automatically provisioned k3d by default or connect to any kubernetes cluster
|
||||
vec![Box::new(app)],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -9,7 +9,6 @@ use harmony::{
|
||||
},
|
||||
topology::{
|
||||
BackendServer, DummyInfra, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancerService,
|
||||
SSL,
|
||||
},
|
||||
};
|
||||
use harmony_macros::ipv4;
|
||||
@@ -48,7 +47,6 @@ fn build_large_score() -> LoadBalancerScore {
|
||||
.to_string(),
|
||||
HttpMethod::GET,
|
||||
HttpStatusCode::Success2xx,
|
||||
SSL::Disabled,
|
||||
)),
|
||||
};
|
||||
LoadBalancerScore {
|
||||
|
||||
@@ -10,11 +10,7 @@ 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"
|
||||
@@ -70,10 +66,8 @@ tar.workspace = true
|
||||
base64.workspace = true
|
||||
thiserror.workspace = true
|
||||
once_cell = "1.21.3"
|
||||
walkdir = "2.5.0"
|
||||
harmony_inventory_agent = { path = "../harmony_inventory_agent" }
|
||||
harmony_secret_derive = { path = "../harmony_secret_derive" }
|
||||
harmony_secret = { path = "../harmony_secret" }
|
||||
harmony_secret_derive = { version = "0.1.0", path = "../harmony_secret_derive" }
|
||||
askama.workspace = true
|
||||
sqlx.workspace = true
|
||||
inquire.workspace = true
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
pub mod secret;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
use harmony_secret_derive::Secret;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct OPNSenseFirewallCredentials {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
// TODO we need a better way to handle multiple "instances" of the same secret structure.
|
||||
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct SshKeyPair {
|
||||
pub private: String,
|
||||
pub public: String,
|
||||
}
|
||||
|
||||
#[derive(Secret, Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct RedhatSecret {
|
||||
pub pull_secret: String,
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use derive_new::new;
|
||||
use harmony_inventory_agent::hwinfo::{CPU, MemoryModule, NetworkInterface, StorageDrive};
|
||||
use harmony_types::net::MacAddress;
|
||||
@@ -149,98 +151,6 @@ impl PhysicalHost {
|
||||
parts.join(" | ")
|
||||
}
|
||||
|
||||
pub fn parts_list(&self) -> String {
|
||||
let PhysicalHost {
|
||||
id,
|
||||
category,
|
||||
network,
|
||||
storage,
|
||||
labels,
|
||||
memory_modules,
|
||||
cpus,
|
||||
} = self;
|
||||
|
||||
let mut parts_list = String::new();
|
||||
parts_list.push_str("\n\n=====================");
|
||||
parts_list.push_str(&format!("\nHost ID {id}"));
|
||||
parts_list.push_str("\n=====================");
|
||||
parts_list.push_str("\n\n=====================");
|
||||
parts_list.push_str(&format!("\nCPU count {}", cpus.len()));
|
||||
parts_list.push_str("\n=====================");
|
||||
cpus.iter().for_each(|c| {
|
||||
let CPU {
|
||||
model,
|
||||
vendor,
|
||||
cores,
|
||||
threads,
|
||||
frequency_mhz,
|
||||
} = c;
|
||||
parts_list.push_str(&format!(
|
||||
"\n{vendor} {model}, {cores}/{threads} {}Ghz",
|
||||
*frequency_mhz as f64 / 1000.0
|
||||
));
|
||||
});
|
||||
|
||||
parts_list.push_str("\n\n=====================");
|
||||
parts_list.push_str(&format!("\nNetwork Interfaces count {}", network.len()));
|
||||
parts_list.push_str("\n=====================");
|
||||
network.iter().for_each(|nic| {
|
||||
parts_list.push_str(&format!(
|
||||
"\nNic({} {}Gbps mac({}) ipv4({}), ipv6({})",
|
||||
nic.name,
|
||||
nic.speed_mbps.unwrap_or(0) / 1000,
|
||||
nic.mac_address,
|
||||
nic.ipv4_addresses.join(","),
|
||||
nic.ipv6_addresses.join(",")
|
||||
));
|
||||
});
|
||||
|
||||
parts_list.push_str("\n\n=====================");
|
||||
parts_list.push_str(&format!("\nStorage drives count {}", storage.len()));
|
||||
parts_list.push_str("\n=====================");
|
||||
storage.iter().for_each(|drive| {
|
||||
let StorageDrive {
|
||||
name,
|
||||
model,
|
||||
serial,
|
||||
size_bytes,
|
||||
logical_block_size: _,
|
||||
physical_block_size: _,
|
||||
rotational: _,
|
||||
wwn: _,
|
||||
interface_type,
|
||||
smart_status,
|
||||
} = drive;
|
||||
parts_list.push_str(&format!(
|
||||
"\n{name} {}Gb {model} {interface_type} smart({smart_status:?}) {serial}",
|
||||
size_bytes / 1000 / 1000 / 1000
|
||||
));
|
||||
});
|
||||
|
||||
parts_list.push_str("\n\n=====================");
|
||||
parts_list.push_str(&format!("\nMemory modules count {}", memory_modules.len()));
|
||||
parts_list.push_str("\n=====================");
|
||||
memory_modules.iter().for_each(|mem| {
|
||||
let MemoryModule {
|
||||
size_bytes,
|
||||
speed_mhz,
|
||||
manufacturer,
|
||||
part_number,
|
||||
serial_number,
|
||||
rank,
|
||||
} = mem;
|
||||
parts_list.push_str(&format!(
|
||||
"\n{}Gb, {}Mhz, Manufacturer ({}), Part Number ({})",
|
||||
size_bytes / 1000 / 1000 / 1000,
|
||||
speed_mhz.unwrap_or(0),
|
||||
manufacturer.as_ref().unwrap_or(&String::new()),
|
||||
part_number.as_ref().unwrap_or(&String::new()),
|
||||
));
|
||||
});
|
||||
|
||||
parts_list
|
||||
}
|
||||
|
||||
pub fn cluster_mac(&self) -> MacAddress {
|
||||
self.network
|
||||
.first()
|
||||
@@ -365,6 +275,9 @@ pub enum HostCategory {
|
||||
Switch,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
use harmony_macros::mac_address;
|
||||
|
||||
use harmony_types::id::Id;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
|
||||
@@ -33,8 +33,6 @@ pub enum InterpretName {
|
||||
DiscoverInventoryAgent,
|
||||
CephClusterHealth,
|
||||
Custom(&'static str),
|
||||
RHOBAlerting,
|
||||
K8sIngress,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for InterpretName {
|
||||
@@ -64,8 +62,6 @@ impl std::fmt::Display for InterpretName {
|
||||
InterpretName::DiscoverInventoryAgent => f.write_str("DiscoverInventoryAgent"),
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,15 +80,13 @@ 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(message: String) -> Self {
|
||||
pub fn noop() -> Self {
|
||||
Self {
|
||||
status: InterpretStatus::NOOP,
|
||||
message,
|
||||
details: vec![],
|
||||
message: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,23 +94,6 @@ 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![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,12 +142,6 @@ impl From<PreparationError> for InterpretError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<harmony_secret::SecretStoreError> for InterpretError {
|
||||
fn from(value: harmony_secret::SecretStoreError) -> Self {
|
||||
InterpretError::new(format!("Interpret error : {value}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ExecutorError> for InterpretError {
|
||||
fn from(value: ExecutorError) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -18,13 +18,12 @@ impl InventoryFilter {
|
||||
use derive_new::new;
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::EnumIter;
|
||||
|
||||
use crate::hardware::{ManagementInterface, ManualManagementInterface};
|
||||
|
||||
use super::{
|
||||
filter::Filter,
|
||||
hardware::{HostGroup, Location, SwitchGroup},
|
||||
hardware::{FirewallGroup, HostGroup, Location, SwitchGroup},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -64,7 +63,7 @@ impl Inventory {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::Type, Clone, EnumIter)]
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::Type)]
|
||||
pub enum HostRole {
|
||||
Bootstrap,
|
||||
ControlPlane,
|
||||
|
||||
@@ -29,7 +29,7 @@ pub trait InventoryRepository: Send + Sync + 'static {
|
||||
async fn save(&self, host: &PhysicalHost) -> Result<(), RepoError>;
|
||||
async fn get_latest_by_id(&self, host_id: &str) -> Result<Option<PhysicalHost>, RepoError>;
|
||||
async fn get_all_hosts(&self) -> Result<Vec<PhysicalHost>, RepoError>;
|
||||
async fn get_host_for_role(&self, role: &HostRole) -> Result<Vec<PhysicalHost>, RepoError>;
|
||||
async fn get_host_for_role(&self, role: HostRole) -> Result<Vec<PhysicalHost>, RepoError>;
|
||||
async fn save_role_mapping(
|
||||
&self,
|
||||
role: &HostRole,
|
||||
|
||||
@@ -69,26 +69,6 @@ impl K8sclient for HAClusterTopology {
|
||||
}
|
||||
|
||||
impl HAClusterTopology {
|
||||
// TODO this is a hack to avoid refactoring
|
||||
pub fn get_cluster_name(&self) -> String {
|
||||
self.domain_name
|
||||
.split(".")
|
||||
.next()
|
||||
.expect("Cluster domain name must not be empty")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn get_cluster_base_domain(&self) -> String {
|
||||
let base_domain = self
|
||||
.domain_name
|
||||
.strip_prefix(&self.get_cluster_name())
|
||||
.expect("cluster domain must start with cluster name");
|
||||
base_domain
|
||||
.strip_prefix(".")
|
||||
.unwrap_or(base_domain)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn autoload() -> Self {
|
||||
let dummy_infra = Arc::new(DummyInfra {});
|
||||
let dummy_host = LogicalHost {
|
||||
@@ -237,12 +217,8 @@ impl Router for HAClusterTopology {
|
||||
|
||||
#[async_trait]
|
||||
impl HttpServer for HAClusterTopology {
|
||||
async fn serve_files(
|
||||
&self,
|
||||
url: &Url,
|
||||
remote_path: &Option<String>,
|
||||
) -> Result<(), ExecutorError> {
|
||||
self.http_server.serve_files(url, remote_path).await
|
||||
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError> {
|
||||
self.http_server.serve_files(url).await
|
||||
}
|
||||
|
||||
async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError> {
|
||||
@@ -401,11 +377,7 @@ impl TftpServer for DummyInfra {
|
||||
|
||||
#[async_trait]
|
||||
impl HttpServer for DummyInfra {
|
||||
async fn serve_files(
|
||||
&self,
|
||||
_url: &Url,
|
||||
_remote_path: &Option<String>,
|
||||
) -> Result<(), ExecutorError> {
|
||||
async fn serve_files(&self, _url: &Url) -> Result<(), ExecutorError> {
|
||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
||||
}
|
||||
async fn serve_file_content(&self, _file: &FileContent) -> Result<(), ExecutorError> {
|
||||
|
||||
@@ -5,11 +5,7 @@ use harmony_types::net::IpAddress;
|
||||
use harmony_types::net::Url;
|
||||
#[async_trait]
|
||||
pub trait HttpServer: Send + Sync {
|
||||
async fn serve_files(
|
||||
&self,
|
||||
url: &Url,
|
||||
remote_path: &Option<String>,
|
||||
) -> Result<(), ExecutorError>;
|
||||
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError>;
|
||||
async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError>;
|
||||
fn get_ip(&self) -> IpAddress;
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
use crate::topology::PreparationError;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Ingress {
|
||||
async fn get_domain(&self, service: &str) -> Result<String, PreparationError>;
|
||||
}
|
||||
@@ -8,7 +8,6 @@ use kube::{
|
||||
api::{Api, AttachParams, DeleteParams, ListParams, Patch, PatchParams, ResourceExt},
|
||||
config::{KubeConfigOptions, Kubeconfig},
|
||||
core::ErrorResponse,
|
||||
error::DiscoveryError,
|
||||
runtime::reflector::Lookup,
|
||||
};
|
||||
use kube::{api::DynamicObject, runtime::conditions};
|
||||
@@ -18,12 +17,10 @@ use kube::{
|
||||
};
|
||||
use log::{debug, error, trace};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::json;
|
||||
use similar::TextDiff;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::interpret::Outcome;
|
||||
|
||||
#[derive(new, Clone)]
|
||||
pub struct K8sClient {
|
||||
client: Client,
|
||||
@@ -56,72 +53,6 @@ impl K8sClient {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn ensure_deployment(
|
||||
&self,
|
||||
resource_name: &str,
|
||||
resource_namespace: &str,
|
||||
) -> Result<Outcome, Error> {
|
||||
match self
|
||||
.get_deployment(resource_name, Some(&resource_namespace))
|
||||
.await
|
||||
{
|
||||
Ok(Some(deployment)) => {
|
||||
if let Some(status) = deployment.status {
|
||||
let ready_count = status.ready_replicas.unwrap_or(0);
|
||||
if ready_count >= 1 {
|
||||
Ok(Outcome::success(format!(
|
||||
"'{}' is ready with {} replica(s).",
|
||||
resource_name, ready_count
|
||||
)))
|
||||
} else {
|
||||
Err(Error::Discovery(DiscoveryError::MissingResource(format!(
|
||||
"Deployment '{}' in namespace '{}' has 0 ready replicas",
|
||||
resource_name, resource_namespace
|
||||
))))
|
||||
}
|
||||
} else {
|
||||
Err(Error::Api(ErrorResponse {
|
||||
status: "Failure".to_string(),
|
||||
message: format!(
|
||||
"No status found for deployment '{}' in namespace '{}'",
|
||||
resource_name, resource_namespace
|
||||
),
|
||||
reason: "MissingStatus".to_string(),
|
||||
code: 404,
|
||||
}))
|
||||
}
|
||||
}
|
||||
Ok(None) => Err(Error::Discovery(DiscoveryError::MissingResource(format!(
|
||||
"Deployment '{}' not found in namespace '{}'",
|
||||
resource_name, resource_namespace
|
||||
)))),
|
||||
Err(e) => Err(Error::Api(ErrorResponse {
|
||||
status: "Failure".to_string(),
|
||||
message: format!(
|
||||
"Failed to fetch deployment '{}' in namespace '{}': {}",
|
||||
resource_name, resource_namespace, e
|
||||
),
|
||||
reason: "ApiError".to_string(),
|
||||
code: 500,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_resource_json_value(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace: Option<&str>,
|
||||
gvk: &GroupVersionKind,
|
||||
) -> Result<DynamicObject, Error> {
|
||||
let gvk = ApiResource::from_gvk(gvk);
|
||||
let resource: Api<DynamicObject> = if let Some(ns) = namespace {
|
||||
Api::namespaced_with(self.client.clone(), ns, &gvk)
|
||||
} else {
|
||||
Api::default_namespaced_with(self.client.clone(), &gvk)
|
||||
};
|
||||
Ok(resource.get(name).await?)
|
||||
}
|
||||
|
||||
pub async fn get_deployment(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -144,25 +75,6 @@ impl K8sClient {
|
||||
Ok(pods.get_opt(name).await?)
|
||||
}
|
||||
|
||||
pub async fn patch_resource_by_merge(
|
||||
&self,
|
||||
name: &str,
|
||||
namespace: Option<&str>,
|
||||
gvk: &GroupVersionKind,
|
||||
patch: Value,
|
||||
) -> Result<(), Error> {
|
||||
let gvk = ApiResource::from_gvk(gvk);
|
||||
let resource: Api<DynamicObject> = if let Some(ns) = namespace {
|
||||
Api::namespaced_with(self.client.clone(), ns, &gvk)
|
||||
} else {
|
||||
Api::default_namespaced_with(self.client.clone(), &gvk)
|
||||
};
|
||||
let pp = PatchParams::default();
|
||||
let merge = Patch::Merge(&patch);
|
||||
resource.patch(name, &pp, &merge).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn scale_deployment(
|
||||
&self,
|
||||
name: &str,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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;
|
||||
@@ -15,15 +14,13 @@ use crate::{
|
||||
monitoring::kube_prometheus::crd::{
|
||||
crd_alertmanager_config::CRDPrometheus,
|
||||
prometheus_operator::prometheus_operator_helm_chart_score,
|
||||
rhob_alertmanager_config::RHOBObservability,
|
||||
},
|
||||
prometheus::{
|
||||
k8s_prometheus_alerting_score::K8sPrometheusCRDAlertingScore,
|
||||
prometheus::PrometheusApplicationMonitoring, rhob_alerting_score::RHOBAlertingScore,
|
||||
prometheus::PrometheusApplicationMonitoring,
|
||||
},
|
||||
},
|
||||
score::Score,
|
||||
topology::ingress::Ingress,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -111,43 +108,6 @@ impl PrometheusApplicationMonitoring<CRDPrometheus> for K8sAnywhereTopology {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PrometheusApplicationMonitoring<RHOBObservability> for K8sAnywhereTopology {
|
||||
async fn install_prometheus(
|
||||
&self,
|
||||
sender: &RHOBObservability,
|
||||
inventory: &Inventory,
|
||||
receivers: Option<Vec<Box<dyn AlertReceiver<RHOBObservability>>>>,
|
||||
) -> Result<PreparationOutcome, PreparationError> {
|
||||
let po_result = self.ensure_cluster_observability_operator(sender).await?;
|
||||
|
||||
if po_result == PreparationOutcome::Noop {
|
||||
debug!("Skipping Prometheus CR installation due to missing operator.");
|
||||
return Ok(po_result);
|
||||
}
|
||||
|
||||
let result = self
|
||||
.get_cluster_observability_operator_prometheus_application_score(
|
||||
sender.clone(),
|
||||
receivers,
|
||||
)
|
||||
.await
|
||||
.interpret(inventory, self)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(outcome) => match outcome.status {
|
||||
InterpretStatus::SUCCESS => Ok(PreparationOutcome::Success {
|
||||
details: outcome.message,
|
||||
}),
|
||||
InterpretStatus::NOOP => Ok(PreparationOutcome::Noop),
|
||||
_ => Err(PreparationError::new(outcome.message)),
|
||||
},
|
||||
Err(err) => Err(PreparationError::new(err.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for K8sAnywhereTopology {
|
||||
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
@@ -174,19 +134,6 @@ impl K8sAnywhereTopology {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cluster_observability_operator_prometheus_application_score(
|
||||
&self,
|
||||
sender: RHOBObservability,
|
||||
receivers: Option<Vec<Box<dyn AlertReceiver<RHOBObservability>>>>,
|
||||
) -> RHOBAlertingScore {
|
||||
RHOBAlertingScore {
|
||||
sender,
|
||||
receivers: receivers.unwrap_or_default(),
|
||||
service_monitors: vec![],
|
||||
prometheus_rules: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_k8s_prometheus_application_score(
|
||||
&self,
|
||||
sender: CRDPrometheus,
|
||||
@@ -200,26 +147,6 @@ 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")
|
||||
@@ -359,64 +286,6 @@ impl K8sAnywhereTopology {
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_cluster_observability_operator(
|
||||
&self,
|
||||
sender: &RHOBObservability,
|
||||
) -> Result<PreparationOutcome, PreparationError> {
|
||||
let status = Command::new("sh")
|
||||
.args(["-c", "kubectl get crd -A | grep -i rhobs"])
|
||||
.status()
|
||||
.map_err(|e| PreparationError::new(format!("could not connect to cluster: {}", e)))?;
|
||||
|
||||
if !status.success() {
|
||||
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 =
|
||||
prometheus_operator_helm_chart_score(sender.namespace.clone());
|
||||
let result = op_score.interpret(&Inventory::empty(), self).await;
|
||||
|
||||
return match result {
|
||||
Ok(outcome) => match outcome.status {
|
||||
InterpretStatus::SUCCESS => Ok(PreparationOutcome::Success {
|
||||
details: "installed cluster observability operator".into(),
|
||||
}),
|
||||
InterpretStatus::NOOP => Ok(PreparationOutcome::Noop),
|
||||
_ => Err(PreparationError::new(
|
||||
"failed to install cluster observability operator (unknown error)".into(),
|
||||
)),
|
||||
},
|
||||
Err(err) => Err(PreparationError::new(err.to_string())),
|
||||
};
|
||||
}
|
||||
K8sSource::Kubeconfig => {
|
||||
debug!(
|
||||
"unable to install cluster observability operator, contact cluster admin"
|
||||
);
|
||||
return Ok(PreparationOutcome::Noop);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"Unable to detect k8s_state. Skipping Cluster Observability Operator install."
|
||||
);
|
||||
return Ok(PreparationOutcome::Noop);
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Cluster Observability Operator is already present, skipping install");
|
||||
|
||||
Ok(PreparationOutcome::Success {
|
||||
details: "cluster observability operator present in cluster".into(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn ensure_prometheus_operator(
|
||||
&self,
|
||||
sender: &CRDPrometheus,
|
||||
@@ -554,7 +423,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 false"),
|
||||
_ => todo!("HARMONY_PROFILE must be set when use_local_k3d is not set"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -576,45 +445,3 @@ 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(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,17 +102,8 @@ pub enum HttpStatusCode {
|
||||
ServerError5xx,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
pub enum SSL {
|
||||
SSL,
|
||||
Disabled,
|
||||
Default,
|
||||
SNI,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
pub enum HealthCheck {
|
||||
HTTP(String, HttpMethod, HttpStatusCode, SSL),
|
||||
HTTP(String, HttpMethod, HttpStatusCode),
|
||||
TCP(Option<u16>),
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use async_trait::async_trait;
|
||||
use derive_new::new;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{HelmCommand, PreparationError, PreparationOutcome, Topology};
|
||||
|
||||
#[derive(new, Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(new)]
|
||||
pub struct LocalhostTopology;
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
mod ha_cluster;
|
||||
pub mod ingress;
|
||||
use harmony_types::net::IpAddress;
|
||||
mod host_binding;
|
||||
mod http;
|
||||
|
||||
@@ -11,21 +11,15 @@ use super::{LogicalHost, k8s::K8sClient};
|
||||
#[derive(Debug)]
|
||||
pub struct DHCPStaticEntry {
|
||||
pub name: String,
|
||||
pub mac: Vec<MacAddress>,
|
||||
pub mac: MacAddress,
|
||||
pub ip: Ipv4Addr,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DHCPStaticEntry {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mac = self
|
||||
.mac
|
||||
.iter()
|
||||
.map(|m| m.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
f.write_fmt(format_args!(
|
||||
"DHCPStaticEntry : name {}, mac {}, ip {}",
|
||||
self.name, mac, self.ip
|
||||
self.name, self.mac, self.ip
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -47,7 +41,6 @@ impl std::fmt::Debug for dyn Firewall {
|
||||
pub struct NetworkDomain {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait K8sclient: Send + Sync {
|
||||
async fn k8s_client(&self) -> Result<Arc<K8sClient>, String>;
|
||||
|
||||
@@ -108,8 +108,7 @@ impl InventoryRepository for SqliteInventoryRepository {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_host_for_role(&self, role: &HostRole) -> Result<Vec<PhysicalHost>, RepoError> {
|
||||
async fn get_host_for_role(&self, role: HostRole) -> Result<Vec<PhysicalHost>, RepoError> {
|
||||
struct HostIdRow {
|
||||
host_id: String,
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@ impl DhcpServer for OPNSenseFirewall {
|
||||
}
|
||||
|
||||
async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError> {
|
||||
let mac: Vec<String> = entry.mac.iter().map(MacAddress::to_string).collect();
|
||||
let mac: String = String::from(&entry.mac);
|
||||
|
||||
{
|
||||
let mut writable_opnsense = self.opnsense_config.write().await;
|
||||
writable_opnsense
|
||||
.dhcp()
|
||||
.add_static_mapping(&mac, &entry.ip, &entry.name)
|
||||
.add_static_mapping(&mac, entry.ip, &entry.name)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::infra::opnsense::Host;
|
||||
use crate::infra::opnsense::LogicalHost;
|
||||
use crate::{
|
||||
executors::ExecutorError,
|
||||
@@ -11,22 +12,21 @@ use super::OPNSenseFirewall;
|
||||
#[async_trait]
|
||||
impl DnsServer for OPNSenseFirewall {
|
||||
async fn register_hosts(&self, hosts: Vec<DnsRecord>) -> Result<(), ExecutorError> {
|
||||
todo!("Refactor this to use dnsmasq")
|
||||
// let mut writable_opnsense = self.opnsense_config.write().await;
|
||||
// let mut dns = writable_opnsense.dns();
|
||||
// let hosts = hosts
|
||||
// .iter()
|
||||
// .map(|h| {
|
||||
// Host::new(
|
||||
// h.host.clone(),
|
||||
// h.domain.clone(),
|
||||
// h.record_type.to_string(),
|
||||
// h.value.to_string(),
|
||||
// )
|
||||
// })
|
||||
// .collect();
|
||||
// dns.add_static_mapping(hosts);
|
||||
// Ok(())
|
||||
let mut writable_opnsense = self.opnsense_config.write().await;
|
||||
let mut dns = writable_opnsense.dns();
|
||||
let hosts = hosts
|
||||
.iter()
|
||||
.map(|h| {
|
||||
Host::new(
|
||||
h.host.clone(),
|
||||
h.domain.clone(),
|
||||
h.record_type.to_string(),
|
||||
h.value.to_string(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
dns.register_hosts(hosts);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_record(
|
||||
@@ -38,26 +38,25 @@ impl DnsServer for OPNSenseFirewall {
|
||||
}
|
||||
|
||||
async fn list_records(&self) -> Vec<crate::topology::DnsRecord> {
|
||||
todo!("Refactor this to use dnsmasq")
|
||||
// self.opnsense_config
|
||||
// .write()
|
||||
// .await
|
||||
// .dns()
|
||||
// .get_hosts()
|
||||
// .iter()
|
||||
// .map(|h| DnsRecord {
|
||||
// host: h.hostname.clone(),
|
||||
// domain: h.domain.clone(),
|
||||
// record_type: h
|
||||
// .rr
|
||||
// .parse()
|
||||
// .expect("received invalid record type {h.rr} from opnsense"),
|
||||
// value: h
|
||||
// .server
|
||||
// .parse()
|
||||
// .expect("received invalid ipv4 record from opnsense {h.server}"),
|
||||
// })
|
||||
// .collect()
|
||||
self.opnsense_config
|
||||
.write()
|
||||
.await
|
||||
.dns()
|
||||
.get_hosts()
|
||||
.iter()
|
||||
.map(|h| DnsRecord {
|
||||
host: h.hostname.clone(),
|
||||
domain: h.domain.clone(),
|
||||
record_type: h
|
||||
.rr
|
||||
.parse()
|
||||
.expect("received invalid record type {h.rr} from opnsense"),
|
||||
value: h
|
||||
.server
|
||||
.parse()
|
||||
.expect("received invalid ipv4 record from opnsense {h.server}"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_ip(&self) -> IpAddress {
|
||||
@@ -69,12 +68,11 @@ impl DnsServer for OPNSenseFirewall {
|
||||
}
|
||||
|
||||
async fn register_dhcp_leases(&self, register: bool) -> Result<(), ExecutorError> {
|
||||
todo!("Refactor this to use dnsmasq")
|
||||
// let mut writable_opnsense = self.opnsense_config.write().await;
|
||||
// let mut dns = writable_opnsense.dns();
|
||||
// dns.register_dhcp_leases(register);
|
||||
//
|
||||
// Ok(())
|
||||
let mut writable_opnsense = self.opnsense_config.write().await;
|
||||
let mut dns = writable_opnsense.dns();
|
||||
dns.register_dhcp_leases(register);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn commit_config(&self) -> Result<(), ExecutorError> {
|
||||
|
||||
@@ -10,21 +10,13 @@ const OPNSENSE_HTTP_ROOT_PATH: &str = "/usr/local/http";
|
||||
|
||||
#[async_trait]
|
||||
impl HttpServer for OPNSenseFirewall {
|
||||
async fn serve_files(
|
||||
&self,
|
||||
url: &Url,
|
||||
remote_path: &Option<String>,
|
||||
) -> Result<(), ExecutorError> {
|
||||
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError> {
|
||||
let config = self.opnsense_config.read().await;
|
||||
info!("Uploading files from url {url} to {OPNSENSE_HTTP_ROOT_PATH}");
|
||||
let remote_upload_path = remote_path
|
||||
.clone()
|
||||
.map(|r| format!("{OPNSENSE_HTTP_ROOT_PATH}/{r}"))
|
||||
.unwrap_or(OPNSENSE_HTTP_ROOT_PATH.to_string());
|
||||
match url {
|
||||
Url::LocalFolder(path) => {
|
||||
config
|
||||
.upload_files(path, &remote_upload_path)
|
||||
.upload_files(path, OPNSENSE_HTTP_ROOT_PATH)
|
||||
.await
|
||||
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, error, info, warn};
|
||||
use opnsense_config_xml::{
|
||||
Frontend, HAProxy, HAProxyBackend, HAProxyHealthCheck, HAProxyServer, MaybeString,
|
||||
};
|
||||
use log::{debug, info, warn};
|
||||
use opnsense_config_xml::{Frontend, HAProxy, HAProxyBackend, HAProxyHealthCheck, HAProxyServer};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
executors::ExecutorError,
|
||||
topology::{
|
||||
BackendServer, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancer, LoadBalancerService,
|
||||
LogicalHost, SSL,
|
||||
LogicalHost,
|
||||
},
|
||||
};
|
||||
use harmony_types::net::IpAddress;
|
||||
@@ -208,22 +206,7 @@ pub(crate) fn get_health_check_for_backend(
|
||||
.unwrap_or_default()
|
||||
.into();
|
||||
let status_code: HttpStatusCode = HttpStatusCode::Success2xx;
|
||||
let ssl = match haproxy_health_check
|
||||
.ssl
|
||||
.content_string()
|
||||
.to_uppercase()
|
||||
.as_str()
|
||||
{
|
||||
"SSL" => SSL::SSL,
|
||||
"SSLNI" => SSL::SNI,
|
||||
"NOSSL" => SSL::Disabled,
|
||||
"" => SSL::Default,
|
||||
other => {
|
||||
error!("Unknown haproxy health check ssl config {other}");
|
||||
SSL::Other(other.to_string())
|
||||
}
|
||||
};
|
||||
Some(HealthCheck::HTTP(path, method, status_code, ssl))
|
||||
Some(HealthCheck::HTTP(path, method, status_code))
|
||||
}
|
||||
_ => panic!("Received unsupported health check type {}", uppercase),
|
||||
}
|
||||
@@ -258,14 +241,7 @@ pub(crate) fn harmony_load_balancer_service_to_haproxy_xml(
|
||||
// frontend points to backend
|
||||
let healthcheck = if let Some(health_check) = &service.health_check {
|
||||
match health_check {
|
||||
HealthCheck::HTTP(path, http_method, _http_status_code, ssl) => {
|
||||
let ssl: MaybeString = match ssl {
|
||||
SSL::SSL => "ssl".into(),
|
||||
SSL::SNI => "sslni".into(),
|
||||
SSL::Disabled => "nossl".into(),
|
||||
SSL::Default => "".into(),
|
||||
SSL::Other(other) => other.as_str().into(),
|
||||
};
|
||||
HealthCheck::HTTP(path, http_method, _http_status_code) => {
|
||||
let haproxy_check = HAProxyHealthCheck {
|
||||
name: format!("HTTP_{http_method}_{path}"),
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
@@ -273,7 +249,6 @@ pub(crate) fn harmony_load_balancer_service_to_haproxy_xml(
|
||||
health_check_type: "http".to_string(),
|
||||
http_uri: path.clone().into(),
|
||||
interval: "2s".to_string(),
|
||||
ssl,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use std::error::Error;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use derive_new::new;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{executors::ExecutorError, topology::Topology};
|
||||
use crate::topology::Topology;
|
||||
|
||||
/// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability,
|
||||
/// ContinuousIntegration, ContinuousDelivery
|
||||
@@ -12,10 +9,7 @@ use crate::{executors::ExecutorError, topology::Topology};
|
||||
pub trait ApplicationFeature<T: Topology>:
|
||||
std::fmt::Debug + Send + Sync + ApplicationFeatureClone<T>
|
||||
{
|
||||
async fn ensure_installed(
|
||||
&self,
|
||||
topology: &T,
|
||||
) -> Result<InstallationOutcome, InstallationError>;
|
||||
async fn ensure_installed(&self, topology: &T) -> Result<(), String>;
|
||||
fn name(&self) -> String;
|
||||
}
|
||||
|
||||
@@ -46,60 +40,3 @@ 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}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,11 @@ use crate::{
|
||||
data::Version,
|
||||
inventory::Inventory,
|
||||
modules::application::{
|
||||
ApplicationFeature, HelmPackage, InstallationError, InstallationOutcome, OCICompliant,
|
||||
ApplicationFeature, HelmPackage, OCICompliant,
|
||||
features::{ArgoApplication, ArgoHelmScore},
|
||||
},
|
||||
score::Score,
|
||||
topology::{
|
||||
DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology, ingress::Ingress,
|
||||
},
|
||||
topology::{DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology},
|
||||
};
|
||||
|
||||
/// ContinuousDelivery in Harmony provides this functionality :
|
||||
@@ -47,11 +45,11 @@ use crate::{
|
||||
/// - ArgoCD to install/upgrade/rollback/inspect k8s resources
|
||||
/// - Kubernetes for runtime orchestration
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct PackagingDeployment<A: OCICompliant + HelmPackage> {
|
||||
pub struct ContinuousDelivery<A: OCICompliant + HelmPackage> {
|
||||
pub application: Arc<A>,
|
||||
}
|
||||
|
||||
impl<A: OCICompliant + HelmPackage> PackagingDeployment<A> {
|
||||
impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
|
||||
async fn deploy_to_local_k3d(
|
||||
&self,
|
||||
app_name: String,
|
||||
@@ -138,28 +136,18 @@ impl<A: OCICompliant + HelmPackage> PackagingDeployment<A> {
|
||||
#[async_trait]
|
||||
impl<
|
||||
A: OCICompliant + HelmPackage + Clone + 'static,
|
||||
T: Topology + HelmCommand + MultiTargetTopology + K8sclient + Ingress + 'static,
|
||||
> ApplicationFeature<T> for PackagingDeployment<A>
|
||||
T: Topology + HelmCommand + MultiTargetTopology + K8sclient + 'static,
|
||||
> ApplicationFeature<T> for ContinuousDelivery<A>
|
||||
{
|
||||
async fn ensure_installed(
|
||||
&self,
|
||||
topology: &T,
|
||||
) -> Result<InstallationOutcome, InstallationError> {
|
||||
async fn ensure_installed(&self, topology: &T) -> Result<(), String> {
|
||||
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, &domain)
|
||||
.await?;
|
||||
let helm_chart = self.application.build_push_helm_package(&image).await?;
|
||||
|
||||
// TODO: Make building image configurable/skippable if image already exists (prompt)")
|
||||
// https://git.nationtech.io/NationTech/harmony/issues/104
|
||||
@@ -188,18 +176,18 @@ impl<
|
||||
}
|
||||
target => {
|
||||
info!("Deploying {} to target {target:?}", self.application.name());
|
||||
|
||||
let score = ArgoHelmScore {
|
||||
namespace: format!("{}", self.application.name()),
|
||||
namespace: "harmony-example-rust-webapp".to_string(),
|
||||
openshift: true,
|
||||
domain: "argo.harmonydemo.apps.ncd0.harmony.mcd".to_string(),
|
||||
argo_apps: vec![ArgoApplication::from(CDApplicationConfig {
|
||||
// helm pull oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart --version 0.1.0
|
||||
version: Version::from("0.1.0").unwrap(),
|
||||
helm_chart_repo_url: "hub.nationtech.io/harmony".to_string(),
|
||||
helm_chart_name: format!("{}-chart", self.application.name()),
|
||||
helm_chart_name: "harmony-example-rust-webapp-chart".to_string(),
|
||||
values_overrides: None,
|
||||
name: format!("{}", self.application.name()),
|
||||
namespace: format!("{}", self.application.name()),
|
||||
name: "harmony-demo-rust-webapp".to_string(),
|
||||
namespace: "harmony-example-rust-webapp".to_string(),
|
||||
})],
|
||||
};
|
||||
score
|
||||
@@ -208,11 +196,7 @@ impl<
|
||||
.unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
Ok(InstallationOutcome::success_with_details(vec![format!(
|
||||
"{}: http://{domain}",
|
||||
self.application.name()
|
||||
)]))
|
||||
Ok(())
|
||||
}
|
||||
fn name(&self) -> String {
|
||||
"ContinuousDelivery".to_string()
|
||||
@@ -2,7 +2,7 @@ use async_trait::async_trait;
|
||||
use log::info;
|
||||
|
||||
use crate::{
|
||||
modules::application::{ApplicationFeature, InstallationError, InstallationOutcome},
|
||||
modules::application::ApplicationFeature,
|
||||
topology::{K8sclient, Topology},
|
||||
};
|
||||
|
||||
@@ -29,10 +29,7 @@ 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<InstallationOutcome, InstallationError> {
|
||||
async fn ensure_installed(&self, _topology: &T) -> Result<(), String> {
|
||||
info!(
|
||||
"Making sure public endpoint is installed for port {}",
|
||||
self.application_port
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
use kube::{Api, api::GroupVersionKind};
|
||||
use log::{debug, warn};
|
||||
use non_blank_string_rs::NonBlankString;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::{process::Command, str::FromStr, sync::Arc};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::{
|
||||
data::Version,
|
||||
@@ -12,10 +9,7 @@ use crate::{
|
||||
inventory::Inventory,
|
||||
modules::helm::chart::{HelmChartScore, HelmRepository},
|
||||
score::Score,
|
||||
topology::{
|
||||
HelmCommand, K8sclient, PreparationError, PreparationOutcome, Topology, ingress::Ingress,
|
||||
k8s::K8sClient,
|
||||
},
|
||||
topology::{HelmCommand, K8sclient, Topology},
|
||||
};
|
||||
use harmony_types::id::Id;
|
||||
|
||||
@@ -25,13 +19,15 @@ use super::ArgoApplication;
|
||||
pub struct ArgoHelmScore {
|
||||
pub namespace: String,
|
||||
pub openshift: bool,
|
||||
pub domain: String,
|
||||
pub argo_apps: Vec<ArgoApplication>,
|
||||
}
|
||||
|
||||
impl<T: Topology + HelmCommand + K8sclient + Ingress> Score<T> for ArgoHelmScore {
|
||||
impl<T: Topology + HelmCommand + K8sclient> Score<T> for ArgoHelmScore {
|
||||
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
|
||||
let helm_score = argo_helm_chart_score(&self.namespace, self.openshift, &self.domain);
|
||||
Box::new(ArgoInterpret {
|
||||
score: self.clone(),
|
||||
score: helm_score,
|
||||
argo_apps: self.argo_apps.clone(),
|
||||
})
|
||||
}
|
||||
@@ -43,41 +39,33 @@ impl<T: Topology + HelmCommand + K8sclient + Ingress> Score<T> for ArgoHelmScore
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArgoInterpret {
|
||||
score: ArgoHelmScore,
|
||||
score: HelmChartScore,
|
||||
argo_apps: Vec<ArgoApplication>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Topology + K8sclient + HelmCommand + Ingress> Interpret<T> for ArgoInterpret {
|
||||
impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for ArgoInterpret {
|
||||
async fn execute(
|
||||
&self,
|
||||
inventory: &Inventory,
|
||||
topology: &T,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
self.score.interpret(inventory, topology).await?;
|
||||
|
||||
let k8s_client = topology.k8s_client().await?;
|
||||
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);
|
||||
|
||||
helm_score.interpret(inventory, topology).await?;
|
||||
|
||||
k8s_client
|
||||
.apply_yaml_many(&self.argo_apps.iter().map(|a| a.to_yaml()).collect(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
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)],
|
||||
))
|
||||
Ok(Outcome::success(format!(
|
||||
"ArgoCD installed with {} {}",
|
||||
self.argo_apps.len(),
|
||||
match self.argo_apps.len() {
|
||||
1 => "application",
|
||||
_ => "applications",
|
||||
}
|
||||
)))
|
||||
}
|
||||
|
||||
fn get_name(&self) -> InterpretName {
|
||||
@@ -97,38 +85,6 @@ impl<T: Topology + K8sclient + HelmCommand + Ingress> Interpret<T> for ArgoInter
|
||||
}
|
||||
}
|
||||
|
||||
impl ArgoInterpret {
|
||||
pub async fn get_host_domain(
|
||||
&self,
|
||||
client: Arc<K8sClient>,
|
||||
openshift: bool,
|
||||
) -> Result<String, InterpretError> {
|
||||
//This should be the job of the topology to determine if we are in
|
||||
//openshift, potentially we need on openshift topology the same way we create a
|
||||
//localhosttopology
|
||||
match openshift {
|
||||
true => {
|
||||
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?;
|
||||
|
||||
match ic.data["status"]["domain"].as_str() {
|
||||
Some(domain) => return Ok(domain.to_string()),
|
||||
None => return Err(InterpretError::new("Could not find domain".to_string())),
|
||||
}
|
||||
}
|
||||
false => {
|
||||
todo!()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn argo_helm_chart_score(namespace: &str, openshift: bool, domain: &str) -> HelmChartScore {
|
||||
let values = format!(
|
||||
r#"
|
||||
@@ -160,9 +116,6 @@ global:
|
||||
## Used for ingresses, certificates, SSO, notifications, etc.
|
||||
domain: {domain}
|
||||
|
||||
securityContext:
|
||||
runAsUser: null
|
||||
|
||||
# -- Runtime class name for all components
|
||||
runtimeClassName: ""
|
||||
|
||||
@@ -474,13 +427,6 @@ redis:
|
||||
# -- Redis name
|
||||
name: redis
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
|
||||
securityContext:
|
||||
runAsUser: null
|
||||
|
||||
|
||||
## Redis image
|
||||
image:
|
||||
# -- Redis repository
|
||||
@@ -714,7 +660,7 @@ server:
|
||||
# nginx.ingress.kubernetes.io/ssl-passthrough: "true"
|
||||
|
||||
# -- Defines which ingress controller will implement the resource
|
||||
ingressClassName: "openshift-default"
|
||||
ingressClassName: ""
|
||||
|
||||
# -- Argo CD server hostname
|
||||
# @default -- `""` (defaults to global.domain)
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
mod endpoint;
|
||||
pub mod rhob_monitoring;
|
||||
pub use endpoint::*;
|
||||
|
||||
mod monitoring;
|
||||
pub use monitoring::*;
|
||||
|
||||
mod packaging_deployment;
|
||||
pub use packaging_deployment::*;
|
||||
mod continuous_delivery;
|
||||
pub use continuous_delivery::*;
|
||||
|
||||
mod helm_argocd_score;
|
||||
pub use helm_argocd_score::*;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::modules::application::{
|
||||
Application, ApplicationFeature, InstallationError, InstallationOutcome,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::modules::application::{Application, ApplicationFeature};
|
||||
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,12 +19,8 @@ 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 {
|
||||
@@ -40,22 +36,17 @@ impl<
|
||||
+ TenantManager
|
||||
+ K8sclient
|
||||
+ MultiTargetTopology
|
||||
+ PrometheusApplicationMonitoring<CRDPrometheus>
|
||||
+ Ingress
|
||||
+ std::fmt::Debug,
|
||||
+ std::fmt::Debug
|
||||
+ PrometheusApplicationMonitoring<CRDPrometheus>,
|
||||
> ApplicationFeature<T> for Monitoring
|
||||
{
|
||||
async fn ensure_installed(
|
||||
&self,
|
||||
topology: &T,
|
||||
) -> Result<InstallationOutcome, InstallationError> {
|
||||
async fn ensure_installed(&self, topology: &T) -> Result<(), String> {
|
||||
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 {
|
||||
@@ -67,17 +58,19 @@ impl<
|
||||
};
|
||||
let ntfy = NtfyScore {
|
||||
namespace: namespace.clone(),
|
||||
host: domain,
|
||||
host: "ntfy.harmonydemo.apps.ncd0.harmony.mcd".to_string(),
|
||||
};
|
||||
ntfy.interpret(&Inventory::empty(), topology)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let config = SecretManager::get_or_prompt::<NtfyAuth>().await.unwrap();
|
||||
|
||||
let ntfy_default_auth_username = "harmony";
|
||||
let ntfy_default_auth_password = "harmony";
|
||||
let ntfy_default_auth_header = format!(
|
||||
"Basic {}",
|
||||
general_purpose::STANDARD.encode(format!("{}:{}", config.username, config.password))
|
||||
general_purpose::STANDARD.encode(format!(
|
||||
"{ntfy_default_auth_username}:{ntfy_default_auth_password}"
|
||||
))
|
||||
);
|
||||
|
||||
debug!("ntfy_default_auth_header: {ntfy_default_auth_header}");
|
||||
@@ -107,17 +100,9 @@ impl<
|
||||
.interpret(&Inventory::empty(), topology)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(InstallationOutcome::success())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
"Monitoring".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Secret, Serialize, Deserialize, Clone, Debug)]
|
||||
struct NtfyAuth {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
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::{
|
||||
alert_channel::webhook_receiver::WebhookReceiver, ntfy::ntfy::NtfyScore,
|
||||
},
|
||||
score::Score,
|
||||
topology::{HelmCommand, K8sclient, Topology, tenant::TenantManager},
|
||||
};
|
||||
use crate::{
|
||||
modules::prometheus::prometheus::PrometheusApplicationMonitoring,
|
||||
topology::oberservability::monitoring::AlertReceiver,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use harmony_types::net::Url;
|
||||
use log::{debug, info};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Monitoring {
|
||||
pub application: Arc<dyn Application>,
|
||||
pub alert_receiver: Vec<Box<dyn AlertReceiver<RHOBObservability>>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<
|
||||
T: Topology
|
||||
+ HelmCommand
|
||||
+ 'static
|
||||
+ TenantManager
|
||||
+ K8sclient
|
||||
+ MultiTargetTopology
|
||||
+ Ingress
|
||||
+ std::fmt::Debug
|
||||
+ PrometheusApplicationMonitoring<RHOBObservability>,
|
||||
> ApplicationFeature<T> for Monitoring
|
||||
{
|
||||
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 mut alerting_score = ApplicationRHOBMonitoringScore {
|
||||
sender: RHOBObservability {
|
||||
namespace: namespace.clone(),
|
||||
client: topology.k8s_client().await.unwrap(),
|
||||
},
|
||||
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: domain.clone(),
|
||||
};
|
||||
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 ntfy_default_auth_header = format!(
|
||||
"Basic {}",
|
||||
general_purpose::STANDARD.encode(format!(
|
||||
"{ntfy_default_auth_username}:{ntfy_default_auth_password}"
|
||||
))
|
||||
);
|
||||
|
||||
debug!("ntfy_default_auth_header: {ntfy_default_auth_header}");
|
||||
|
||||
let ntfy_default_auth_param = general_purpose::STANDARD
|
||||
.encode(ntfy_default_auth_header)
|
||||
.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://{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(InstallationOutcome::success_with_details(vec![format!(
|
||||
"ntfy topic: {}",
|
||||
self.application.name()
|
||||
)]))
|
||||
}
|
||||
fn name(&self) -> String {
|
||||
"Monitoring".to_string()
|
||||
}
|
||||
}
|
||||