Compare commits

..

37 Commits

Author SHA1 Message Date
39208c5e86 Merge branch 'master' into better-cli
All checks were successful
Run Check Script / check (pull_request) Successful in -44s
2025-07-31 13:24:34 +00:00
Ian Letourneau
6b36b1c7e9 rename some event attributes
All checks were successful
Run Check Script / check (pull_request) Successful in -38s
2025-07-31 09:24:05 -04:00
1ab66af718 Merge pull request 'refactor(topo/k8s_anywhere): simplify local installation of k3d' (#90) from simply-k3d-installation into master
Some checks failed
Run Check Script / check (push) Failing after -1m14s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 3m23s
Reviewed-on: #90
Reviewed-by: wjro <wrolleman@nationtech.io>
2025-07-31 13:22:25 +00:00
Ian Letourneau
507556969a add k3d todo
All checks were successful
Run Check Script / check (pull_request) Successful in -35s
2025-07-31 08:05:06 -04:00
Ian Letourneau
68fde23f2c remove unused inquire dependency for Harmony
All checks were successful
Run Check Script / check (pull_request) Successful in -36s
2025-07-30 21:42:09 -04:00
Ian Letourneau
49f1e56599 add event to track progress of interprets, change a bunch of info! to debug!
All checks were successful
Run Check Script / check (pull_request) Successful in -34s
2025-07-30 21:34:27 -04:00
Ian Letourneau
ff7801a7c1 ensure event handlers are properly subscribed on init, extract duplicated progress functions, cleanup duplication
All checks were successful
Run Check Script / check (pull_request) Successful in -31s
2025-07-30 12:22:04 -04:00
Ian Letourneau
8fae9cf8c8 split instrumentation in 2 different places: harmony domain (for domain observability) & harmoy composer (for build/commands observability)
All checks were successful
Run Check Script / check (pull_request) Successful in -34s
2025-07-27 20:52:24 -04:00
Ian Letourneau
6f7e1640c1 fix(cli): reduce noise & better track progress within Harmony
All checks were successful
Run Check Script / check (pull_request) Successful in -35s
2025-07-27 17:41:43 -04:00
Ian Letourneau
0fff4ef566 refactor(topo/k8s_anywhere): simplify local installation of k3d
All checks were successful
Run Check Script / check (pull_request) Successful in -37s
A Maestro was initialized with a new inventory simply to provide a
localhost topology to install K3D locally. But in practice, the K3D
installation wasn't actually using the topology nor the inventory.

Directly installing K3D within the K8s Anywhere topology makes things
simpler and actually enforce the topology to provide the capabilities
required to install K3D.
2025-07-27 11:50:48 -04:00
d95e84d6fc Merge pull request 'fix(apps/rust): build & push using image tag instead of local VS remote image name' (#87) from fix-image-tag into master
All checks were successful
Run Check Script / check (push) Successful in -37s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 13m2s
Reviewed-on: #87
Reviewed-by: johnride <jg@nationtech.io>
2025-07-27 14:10:19 +00:00
a47be890de Merge branch 'master' into fix-image-tag
All checks were successful
Run Check Script / check (pull_request) Successful in -38s
2025-07-27 14:09:24 +00:00
ee8dfa4a93 Merge pull request 'chore: cleanup of unnecessary files & adjust gitignores' (#88) from quick-cleanup into master
Some checks failed
Run Check Script / check (push) Successful in -37s
Compile and package harmony_composer / package_harmony_composer (push) Has been cancelled
Reviewed-on: #88
Reviewed-by: johnride <jg@nationtech.io>
2025-07-27 14:08:57 +00:00
5d41cc8380 Merge branch 'master' into quick-cleanup
All checks were successful
Run Check Script / check (pull_request) Successful in -34s
2025-07-27 14:07:55 +00:00
cef745b642 Merge pull request 'log(composer): Log check_path_str value when error' (#77) from log/composer into master
All checks were successful
Run Check Script / check (push) Successful in -31s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 11m59s
Reviewed-on: #77
2025-07-21 18:04:57 +00:00
d9959378a6 log(composer): Log check_path_str value when error
All checks were successful
Run Check Script / check (pull_request) Successful in -37s
2025-07-21 09:15:41 -04:00
Ian Letourneau
07f1151e4c chore: cleanup of unncessary files & adjust gitignores
All checks were successful
Run Check Script / check (pull_request) Successful in -31s
2025-07-20 20:03:26 -04:00
Ian Letourneau
f7625f0484 fix(rust): push only the actual image tag
All checks were successful
Run Check Script / check (pull_request) Successful in -22s
2025-07-16 13:51:02 -04:00
tahahawa
537da5800f uncomment docker image build
All checks were successful
Run Check Script / check (push) Successful in 2m49s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 5m20s
2025-07-11 10:34:37 -04:00
3be2fa246c fix: unjank the demo (#85)
Some checks failed
Run Check Script / check (push) Has been cancelled
Compile and package harmony_composer / package_harmony_composer (push) Has been cancelled
Co-authored-by: tahahawa <tahahawa@gmail.com>
Reviewed-on: #85
Reviewed-by: wjro <wrolleman@nationtech.io>
2025-07-11 14:32:16 +00:00
9452cf5616 Merge pull request 'fix/argoApplication' (#84) from fix/argoApplication into master
All checks were successful
Run Check Script / check (push) Successful in 1m41s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 4m4s
Reviewed-on: #84
2025-07-05 01:19:05 +00:00
9b7456e148 Merge pull request 'feat/monitoring-application-feature' (#83) from feat/monitoring-application-feature into master
Some checks are pending
Compile and package harmony_composer / package_harmony_composer (push) Has started running
Run Check Script / check (push) Successful in 2s
Reviewed-on: #83
Reviewed-by: johnride <jg@nationtech.io>
2025-07-05 01:16:08 +00:00
98f3f82ad5 refact: Rename HttpScore into StaticFileHttpScore and add minimal documentation
All checks were successful
Run Check Script / check (pull_request) Successful in 1m43s
2025-07-04 21:05:32 -04:00
3eca409f8d Merge remote-tracking branch 'origin/feat/monitoring-application-feature' into fix/argoApplication 2025-07-04 16:44:03 -04:00
c11a31c7a9 wip: Fix ArgoApplication 2025-07-04 16:43:10 -04:00
1a6d72dc17 fix: uncommented example
All checks were successful
Run Check Script / check (pull_request) Successful in 1m37s
2025-07-04 16:30:13 -04:00
df9e21807e fix: git conflict
All checks were successful
Run Check Script / check (pull_request) Successful in -6s
2025-07-04 16:22:39 -04:00
b1bf4fd4d5 fix: cargo fmt
All checks were successful
Run Check Script / check (pull_request) Successful in 1m40s
2025-07-04 16:14:47 -04:00
f702ecd8c9 fix: deploys a lighter weight prometheus and grafana which is limited to their respective namespaces 2025-07-04 16:13:41 -04:00
a19b52e690 fix: properly append YAML in correct places in argoapplication (#80)
All checks were successful
Run Check Script / check (push) Successful in -7s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 3m56s
Co-authored-by: tahahawa <tahahawa@gmail.com>
Reviewed-on: #80
2025-07-04 15:32:02 +00:00
b73f2e76d0 Merge pull request 'refact: Make RustWebappScore generic, it is now Application score and takes an application and list of features to attach to the application' (#81) from refact/application into master
All checks were successful
Run Check Script / check (push) Successful in -1s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 3m38s
Reviewed-on: #81
Reviewed-by: wjro <wrolleman@nationtech.io>
2025-07-04 14:31:38 +00:00
b4534c6ee0 refact: Make RustWebappScore generic, it is now Application score and takes an application and list of features to attach to the application
All checks were successful
Run Check Script / check (pull_request) Successful in -8s
2025-07-04 10:27:16 -04:00
6149249a6c feat: create Argo interpret and kube client apply_yaml to install Argo Applications. Very messy implementation though, must be refactored/improved
All checks were successful
Run Check Script / check (push) Successful in -5s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 4m13s
2025-07-04 09:49:43 -04:00
d9935e20cb Merge pull request 'feat: harmony now defaults to using local k3d cluster. Also created OCICompliant: Application trait to make building images cleaner' (#76) from feat/oci into master
All checks were successful
Run Check Script / check (push) Successful in -9s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 4m4s
Reviewed-on: #76
2025-07-03 19:37:46 +00:00
7b0f3b79b1 Merge remote-tracking branch 'origin/master' into feat/oci
All checks were successful
Run Check Script / check (pull_request) Successful in -8s
2025-07-03 15:36:52 -04:00
e6612245a5 Merge pull request 'feat/cd/localdeploymentdemo' (#79) from feat/cd/localdeploymentdemo into feat/oci
All checks were successful
Run Check Script / check (pull_request) Successful in -9s
Reviewed-on: #79
2025-07-03 19:31:45 +00:00
b4f5b91a57 feat: WIP argocd_score (#78)
Some checks are pending
Compile and package harmony_composer / package_harmony_composer (push) Waiting to run
Run Check Script / check (push) Successful in -8s
Co-authored-by: tahahawa <tahahawa@gmail.com>
Reviewed-on: #78
Reviewed-by: johnride <jg@nationtech.io>
Co-authored-by: Taha Hawa <taha@taha.dev>
Co-committed-by: Taha Hawa <taha@taha.dev>
2025-07-03 19:30:00 +00:00
61 changed files with 3059 additions and 343 deletions

29
.gitignore vendored
View File

@@ -1,4 +1,25 @@
target ### General ###
private_repos private_repos/
log/
*.tgz ### Harmony ###
harmony.log
### Helm ###
# Chart dependencies
**/charts/*.tgz
### Rust ###
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

133
Cargo.lock generated
View File

@@ -650,6 +650,19 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "console"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width 0.2.0",
"windows-sys 0.60.2",
]
[[package]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.6" version = "0.9.6"
@@ -1137,6 +1150,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.35" version = "0.8.35"
@@ -1355,6 +1374,7 @@ dependencies = [
name = "example-rust" name = "example-rust"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64 0.22.1",
"env_logger", "env_logger",
"harmony", "harmony",
"harmony_cli", "harmony_cli",
@@ -1427,6 +1447,18 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "filetime"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.2" version = "1.1.2"
@@ -1726,6 +1758,8 @@ name = "harmony"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64 0.22.1",
"bollard",
"chrono", "chrono",
"cidr", "cidr",
"convert_case", "convert_case",
@@ -1742,7 +1776,6 @@ dependencies = [
"helm-wrapper-rs", "helm-wrapper-rs",
"hex", "hex",
"http 1.3.1", "http 1.3.1",
"inquire",
"k3d-rs", "k3d-rs",
"k8s-openapi", "k8s-openapi",
"kube", "kube",
@@ -1750,8 +1783,10 @@ dependencies = [
"libredfish", "libredfish",
"log", "log",
"non-blank-string-rs", "non-blank-string-rs",
"once_cell",
"opnsense-config", "opnsense-config",
"opnsense-config-xml", "opnsense-config-xml",
"pretty_assertions",
"rand 0.9.1", "rand 0.9.1",
"reqwest 0.11.27", "reqwest 0.11.27",
"russh", "russh",
@@ -1760,9 +1795,11 @@ dependencies = [
"serde", "serde",
"serde-value", "serde-value",
"serde_json", "serde_json",
"serde_with",
"serde_yaml", "serde_yaml",
"similar", "similar",
"strum 0.27.1", "strum 0.27.1",
"tar",
"temp-dir", "temp-dir",
"temp-file", "temp-file",
"tempfile", "tempfile",
@@ -1778,10 +1815,15 @@ version = "0.1.0"
dependencies = [ dependencies = [
"assert_cmd", "assert_cmd",
"clap", "clap",
"console",
"env_logger", "env_logger",
"harmony", "harmony",
"harmony_tui", "harmony_tui",
"indicatif",
"indicatif-log-bridge",
"inquire", "inquire",
"lazy_static",
"log",
"tokio", "tokio",
] ]
@@ -1792,10 +1834,16 @@ dependencies = [
"bollard", "bollard",
"cargo_metadata", "cargo_metadata",
"clap", "clap",
"console",
"current_platform", "current_platform",
"env_logger", "env_logger",
"futures-util", "futures-util",
"harmony_cli",
"indicatif",
"indicatif-log-bridge",
"lazy_static",
"log", "log",
"once_cell",
"serde_json", "serde_json",
"tokio", "tokio",
] ]
@@ -2391,6 +2439,29 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "indicatif"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd"
dependencies = [
"console",
"portable-atomic",
"unicode-width 0.2.0",
"unit-prefix",
"web-time",
]
[[package]]
name = "indicatif-log-bridge"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63703cf9069b85dbe6fe26e1c5230d013dee99d3559cd3d02ba39e099ef7ab02"
dependencies = [
"indicatif",
"log",
]
[[package]] [[package]]
name = "indoc" name = "indoc"
version = "2.0.6" version = "2.0.6"
@@ -2726,6 +2797,7 @@ checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"libc", "libc",
"redox_syscall",
] ]
[[package]] [[package]]
@@ -4080,6 +4152,18 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "schemars"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1375ba8ef45a6f15d83fa8748f1079428295d403d6ea991d09ab100155fbc06d"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@@ -4280,22 +4364,36 @@ dependencies = [
[[package]] [[package]]
name = "serde_with" name = "serde_with"
version = "3.13.0" version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42" checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
"hex", "hex",
"indexmap 1.9.3", "indexmap 1.9.3",
"indexmap 2.10.0", "indexmap 2.10.0",
"schemars", "schemars 0.9.0",
"schemars 1.0.3",
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"serde_with_macros",
"time", "time",
] ]
[[package]]
name = "serde_with_macros"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "serde_yaml" name = "serde_yaml"
version = "0.9.34+deprecated" version = "0.9.34+deprecated"
@@ -4668,6 +4766,17 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tar"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]] [[package]]
name = "temp-dir" name = "temp-dir"
version = "0.1.16" version = "0.1.16"
@@ -5125,6 +5234,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unit-prefix"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817"
[[package]] [[package]]
name = "universal-hash" name = "universal-hash"
version = "0.5.1" version = "0.5.1"
@@ -5713,6 +5828,16 @@ dependencies = [
"tap", "tap",
] ]
[[package]]
name = "xattr"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909"
dependencies = [
"libc",
"rustix 1.0.7",
]
[[package]] [[package]]
name = "xml-rs" name = "xml-rs"
version = "0.8.26" version = "0.8.26"

View File

@@ -52,3 +52,7 @@ convert_case = "0.8"
chrono = "0.4" chrono = "0.4"
similar = "2" similar = "2"
uuid = { version = "1.11", features = ["v4", "fast-rng", "macro-diagnostics"] } uuid = { version = "1.11", features = ["v4", "fast-rng", "macro-diagnostics"] }
pretty_assertions = "1.4.1"
bollard = "0.19.1"
base64 = "0.22.1"
tar = "0.4.44"

View File

@@ -10,7 +10,7 @@ use harmony::{
inventory::Inventory, inventory::Inventory,
maestro::Maestro, maestro::Maestro,
modules::{ modules::{
http::HttpScore, http::StaticFilesHttpScore,
ipxe::IpxeScore, ipxe::IpxeScore,
okd::{ okd::{
bootstrap_dhcp::OKDBootstrapDhcpScore, bootstrap_dhcp::OKDBootstrapDhcpScore,
@@ -126,7 +126,7 @@ async fn main() {
harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology); harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string())); let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string()));
let http_score = HttpScore::new(Url::LocalFolder( let http_score = StaticFilesHttpScore::new(Url::LocalFolder(
"./data/watchguard/pxe-http-files".to_string(), "./data/watchguard/pxe-http-files".to_string(),
)); ));
let ipxe_score = IpxeScore::new(); let ipxe_score = IpxeScore::new();

View File

@@ -14,6 +14,7 @@ async fn main() {
maestro.register_all(vec![Box::new(NtfyScore { maestro.register_all(vec![Box::new(NtfyScore {
namespace: "monitoring".to_string(), namespace: "monitoring".to_string(),
host: "localhost".to_string(),
})]); })]);
harmony_cli::init(maestro, None).await.unwrap(); harmony_cli::init(maestro, None).await.unwrap();
} }

View File

@@ -11,7 +11,7 @@ use harmony::{
maestro::Maestro, maestro::Maestro,
modules::{ modules::{
dummy::{ErrorScore, PanicScore, SuccessScore}, dummy::{ErrorScore, PanicScore, SuccessScore},
http::HttpScore, http::StaticFilesHttpScore,
okd::{dhcp::OKDDhcpScore, dns::OKDDnsScore, load_balancer::OKDLoadBalancerScore}, okd::{dhcp::OKDDhcpScore, dns::OKDDnsScore, load_balancer::OKDLoadBalancerScore},
opnsense::OPNsenseShellCommandScore, opnsense::OPNsenseShellCommandScore,
tftp::TftpScore, tftp::TftpScore,
@@ -81,7 +81,7 @@ async fn main() {
let load_balancer_score = OKDLoadBalancerScore::new(&topology); let load_balancer_score = OKDLoadBalancerScore::new(&topology);
let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string())); let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string()));
let http_score = HttpScore::new(Url::LocalFolder( let http_score = StaticFilesHttpScore::new(Url::LocalFolder(
"./data/watchguard/pxe-http-files".to_string(), "./data/watchguard/pxe-http-files".to_string(),
)); ));
let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); let mut maestro = Maestro::initialize(inventory, topology).await.unwrap();

3
examples/rust/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
Dockerfile.harmony
.harmony_generated
harmony

View File

@@ -12,3 +12,4 @@ tokio = { workspace = true }
log = { workspace = true } log = { workspace = true }
env_logger = { workspace = true } env_logger = { workspace = true }
url = { workspace = true } url = { workspace = true }
base64.workspace = true

View File

@@ -4,33 +4,43 @@ use harmony::{
inventory::Inventory, inventory::Inventory,
maestro::Maestro, maestro::Maestro,
modules::application::{ modules::application::{
RustWebFramework, RustWebapp, RustWebappScore, features::ContinuousDelivery, ApplicationScore, RustWebFramework, RustWebapp,
features::{ContinuousDelivery, Monitoring},
}, },
topology::{K8sAnywhereTopology, Url}, topology::{K8sAnywhereTopology, Url},
}; };
use harmony_cli::cli_logger;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
env_logger::init(); let cli_logger_handle = cli_logger::init();
let application = RustWebapp {
name: "harmony-example-rust-webapp".to_string(),
project_root: PathBuf::from("./examples/rust/webapp"),
framework: Some(RustWebFramework::Leptos),
};
// TODO RustWebappScore should simply take a RustWebApp as config
let app = RustWebappScore {
name: "Example Rust Webapp".to_string(),
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
features: vec![Box::new(ContinuousDelivery {
application: Arc::new(application.clone()),
})],
application,
};
let topology = K8sAnywhereTopology::from_env(); let topology = K8sAnywhereTopology::from_env();
let mut maestro = Maestro::initialize(Inventory::autoload(), topology) let mut maestro = Maestro::initialize(Inventory::autoload(), topology)
.await .await
.unwrap(); .unwrap();
let application = Arc::new(RustWebapp {
name: "harmony-example-rust-webapp".to_string(),
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),
});
let app = ApplicationScore {
features: vec![
Box::new(ContinuousDelivery {
application: application.clone(),
}),
Box::new(Monitoring {
application: application.clone(),
}), // TODO: add backups, multisite ha, etc.
],
application,
};
maestro.register_all(vec![Box::new(app)]); maestro.register_all(vec![Box::new(app)]);
harmony_cli::init(maestro, None).await.unwrap(); harmony_cli::init(maestro, None).await.unwrap();
let _ = tokio::try_join!(cli_logger_handle);
} }

View File

@@ -1,16 +0,0 @@
FROM rust:bookworm as builder
RUN apt-get update && apt-get install -y --no-install-recommends clang wget && wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz && tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz && cp cargo-binstall /usr/local/cargo/bin && rm cargo-binstall-x86_64-unknown-linux-musl.tgz cargo-binstall && apt-get clean && rm -rf /var/lib/apt/lists/*
RUN cargo binstall cargo-leptos -y
RUN rustup target add wasm32-unknown-unknown
WORKDIR /app
COPY . .
RUN cargo leptos build --release -vv
FROM debian:bookworm-slim
RUN groupadd -r appgroup && useradd -r -s /bin/false -g appgroup appuser
ENV LEPTOS_SITE_ADDR=0.0.0.0:3000
EXPOSE 3000/tcp
WORKDIR /home/appuser
COPY --from=builder /app/target/site/pkg /home/appuser/pkg
COPY --from=builder /app/target/release/harmony-example-rust-webapp /home/appuser/harmony-example-rust-webapp
USER appuser
CMD /home/appuser/harmony-example-rust-webapp

View File

@@ -13,7 +13,7 @@ reqwest = { version = "0.11", features = ["blocking", "json"] }
russh = "0.45.0" russh = "0.45.0"
rust-ipmi = "0.1.1" rust-ipmi = "0.1.1"
semver = "1.0.23" semver = "1.0.23"
serde = { version = "1.0.209", features = ["derive"] } serde = { version = "1.0.209", features = ["derive", "rc"] }
serde_json = "1.0.127" serde_json = "1.0.127"
tokio.workspace = true tokio.workspace = true
derive-new.workspace = true derive-new.workspace = true
@@ -32,7 +32,6 @@ k8s-openapi.workspace = true
serde_yaml.workspace = true serde_yaml.workspace = true
http.workspace = true http.workspace = true
serde-value.workspace = true serde-value.workspace = true
inquire.workspace = true
helm-wrapper-rs = "0.4.0" helm-wrapper-rs = "0.4.0"
non-blank-string-rs = "1.0.4" non-blank-string-rs = "1.0.4"
k3d-rs = { path = "../k3d" } k3d-rs = { path = "../k3d" }
@@ -44,12 +43,12 @@ convert_case.workspace = true
email_address = "0.2.9" email_address = "0.2.9"
chrono.workspace = true chrono.workspace = true
fqdn = { version = "0.4.6", features = [ fqdn = { version = "0.4.6", features = [
"domain-label-cannot-start-or-end-with-hyphen", "domain-label-cannot-start-or-end-with-hyphen",
"domain-label-length-limited-to-63", "domain-label-length-limited-to-63",
"domain-name-without-special-chars", "domain-name-without-special-chars",
"domain-name-length-limited-to-255", "domain-name-length-limited-to-255",
"punycode", "punycode",
"serde", "serde",
] } ] }
temp-dir = "0.1.14" temp-dir = "0.1.14"
dyn-clone = "1.0.19" dyn-clone = "1.0.19"
@@ -58,3 +57,11 @@ futures-util = "0.3.31"
tokio-util = "0.7.15" tokio-util = "0.7.15"
strum = { version = "0.27.1", features = ["derive"] } strum = { version = "0.27.1", features = ["derive"] }
tempfile = "3.20.0" tempfile = "3.20.0"
serde_with = "3.14.0"
bollard.workspace = true
tar.workspace = true
base64.workspace = true
once_cell = "1.21.3"
[dev-dependencies]
pretty_assertions.workspace = true

View File

@@ -0,0 +1,63 @@
use log::debug;
use once_cell::sync::Lazy;
use tokio::sync::broadcast;
use super::interpret::{InterpretError, Outcome};
#[derive(Debug, Clone)]
pub enum HarmonyEvent {
HarmonyStarted,
PrepareTopologyStarted {
topology: String,
},
TopologyPrepared {
topology: String,
outcome: Outcome,
},
InterpretExecutionStarted {
topology: String,
interpret: String,
message: String,
},
InterpretExecutionFinished {
topology: String,
interpret: String,
outcome: Result<Outcome, InterpretError>,
},
}
static HARMONY_EVENT_BUS: Lazy<broadcast::Sender<HarmonyEvent>> = Lazy::new(|| {
// TODO: Adjust channel capacity
let (tx, _rx) = broadcast::channel(100);
tx
});
pub fn instrument(event: HarmonyEvent) -> Result<(), &'static str> {
match HARMONY_EVENT_BUS.send(event) {
Ok(_) => Ok(()),
Err(_) => Err("send error: no subscribers"),
}
}
pub async fn subscribe<F, Fut>(name: &str, mut handler: F)
where
F: FnMut(HarmonyEvent) -> Fut + Send + 'static,
Fut: Future<Output = bool> + Send,
{
let mut rx = HARMONY_EVENT_BUS.subscribe();
debug!("[{name}] Service started. Listening for events...");
loop {
match rx.recv().await {
Ok(event) => {
if !handler(event).await {
debug!("[{name}] Handler requested exit.");
break;
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
debug!("[{name}] Lagged behind by {n} messages.");
}
Err(_) => break,
}
}
}

View File

@@ -22,6 +22,7 @@ pub enum InterpretName {
K3dInstallation, K3dInstallation,
TenantInterpret, TenantInterpret,
Application, Application,
ArgoCD,
} }
impl std::fmt::Display for InterpretName { impl std::fmt::Display for InterpretName {
@@ -39,6 +40,7 @@ impl std::fmt::Display for InterpretName {
InterpretName::K3dInstallation => f.write_str("K3dInstallation"), InterpretName::K3dInstallation => f.write_str("K3dInstallation"),
InterpretName::TenantInterpret => f.write_str("Tenant"), InterpretName::TenantInterpret => f.write_str("Tenant"),
InterpretName::Application => f.write_str("Application"), InterpretName::Application => f.write_str("Application"),
InterpretName::ArgoCD => f.write_str("ArgoCD"),
} }
} }
} }

View File

@@ -1,6 +1,8 @@
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex, RwLock};
use log::{info, warn}; use log::{debug, info, warn};
use crate::instrumentation::{self, HarmonyEvent};
use super::{ use super::{
interpret::{InterpretError, InterpretStatus, Outcome}, interpret::{InterpretError, InterpretStatus, Outcome},
@@ -40,13 +42,18 @@ impl<T: Topology> Maestro<T> {
/// Ensures the associated Topology is ready for operations. /// Ensures the associated Topology is ready for operations.
/// Delegates the readiness check and potential setup actions to the Topology. /// Delegates the readiness check and potential setup actions to the Topology.
pub async fn prepare_topology(&self) -> Result<Outcome, InterpretError> { pub async fn prepare_topology(&self) -> Result<Outcome, InterpretError> {
info!("Ensuring topology '{}' is ready...", self.topology.name()); instrumentation::instrument(HarmonyEvent::PrepareTopologyStarted {
topology: self.topology.name().to_string(),
})
.unwrap();
let outcome = self.topology.ensure_ready().await?; let outcome = self.topology.ensure_ready().await?;
info!(
"Topology '{}' readiness check complete: {}", instrumentation::instrument(HarmonyEvent::TopologyPrepared {
self.topology.name(), topology: self.topology.name().to_string(),
outcome.status outcome: outcome.clone(),
); })
.unwrap();
self.topology_preparation_result self.topology_preparation_result
.lock() .lock()
@@ -80,11 +87,11 @@ impl<T: Topology> Maestro<T> {
self.topology.name(), self.topology.name(),
); );
} }
info!("Running score {score:?}"); debug!("Running score {score:?}");
let interpret = score.create_interpret(); let interpret = score.create_interpret();
info!("Launching interpret {interpret:?}"); debug!("Launching interpret {interpret:?}");
let result = interpret.execute(&self.inventory, &self.topology).await; let result = interpret.execute(&self.inventory, &self.topology).await;
info!("Got result {result:?}"); debug!("Got result {result:?}");
result result
} }

View File

@@ -3,6 +3,7 @@ pub mod data;
pub mod executors; pub mod executors;
pub mod filter; pub mod filter;
pub mod hardware; pub mod hardware;
pub mod instrumentation;
pub mod interpret; pub mod interpret;
pub mod inventory; pub mod inventory;
pub mod maestro; pub mod maestro;

View File

@@ -0,0 +1,59 @@
////////////////////
/// Working idea
///
///
trait ScoreWithDep<T> {
fn create_interpret(&self) -> Box<dyn Interpret<T>>;
fn name(&self) -> String;
fn get_dependencies(&self) -> Vec<TypeId>; // Force T to impl Installer<TypeId> or something
// like that
}
struct PrometheusAlertScore;
impl <T> ScoreWithDep<T> for PrometheusAlertScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
todo!()
}
fn name(&self) -> String {
todo!()
}
fn get_dependencies(&self) -> Vec<TypeId> {
// We have to find a way to constrait here so at compile time we are only allowed to return
// TypeId for types which can be installed by T
//
// This means, for example that T must implement HelmCommand if the impl <T: HelmCommand> Installable<T> for
// KubePrometheus calls for HelmCommand.
vec![TypeId::of::<KubePrometheus>()]
}
}
trait Installable{}
struct KubePrometheus;
impl Installable for KubePrometheus;
struct Maestro<T> {
topology: T
}
impl <T>Maestro<T> {
fn execute_store(&self, score: ScoreWithDep<T>) {
score.get_dependencies().iter().for_each(|dep| {
self.topology.ensure_dependency_ready(dep);
});
}
}
struct TopologyWithDep {
}
impl TopologyWithDep {
fn ensure_dependency_ready(&self, type_id: TypeId) -> Result<(), String> {
self.installer
}
}

View File

@@ -4,8 +4,6 @@ use k8s_openapi::{
ClusterResourceScope, NamespaceResourceScope, ClusterResourceScope, NamespaceResourceScope,
api::{apps::v1::Deployment, core::v1::Pod}, api::{apps::v1::Deployment, core::v1::Pod},
}; };
use kube::runtime::conditions;
use kube::runtime::wait::await_condition;
use kube::{ use kube::{
Client, Config, Error, Resource, Client, Config, Error, Resource,
api::{Api, AttachParams, ListParams, Patch, PatchParams, ResourceExt}, api::{Api, AttachParams, ListParams, Patch, PatchParams, ResourceExt},
@@ -13,6 +11,11 @@ use kube::{
core::ErrorResponse, core::ErrorResponse,
runtime::reflector::Lookup, runtime::reflector::Lookup,
}; };
use kube::{api::DynamicObject, runtime::conditions};
use kube::{
api::{ApiResource, GroupVersionKind},
runtime::wait::await_condition,
};
use log::{debug, error, trace}; use log::{debug, error, trace};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use similar::{DiffableStr, TextDiff}; use similar::{DiffableStr, TextDiff};
@@ -239,6 +242,70 @@ impl K8sClient {
Ok(result) Ok(result)
} }
pub async fn apply_yaml_many(
&self,
yaml: &Vec<serde_yaml::Value>,
ns: Option<&str>,
) -> Result<(), Error> {
for y in yaml.iter() {
self.apply_yaml(y, ns).await?;
}
Ok(())
}
pub async fn apply_yaml(
&self,
yaml: &serde_yaml::Value,
ns: Option<&str>,
) -> Result<(), Error> {
let obj: DynamicObject = serde_yaml::from_value(yaml.clone()).expect("TODO do not unwrap");
let name = obj.metadata.name.as_ref().expect("YAML must have a name");
let api_version = yaml
.get("apiVersion")
.expect("couldn't get apiVersion from YAML")
.as_str()
.expect("couldn't get apiVersion as str");
let kind = yaml
.get("kind")
.expect("couldn't get kind from YAML")
.as_str()
.expect("couldn't get kind as str");
let split: Vec<&str> = api_version.splitn(2, "/").collect();
let g = split[0];
let v = split[1];
let gvk = GroupVersionKind::gvk(g, v, kind);
let api_resource = ApiResource::from_gvk(&gvk);
let namespace = match ns {
Some(n) => n,
None => obj
.metadata
.namespace
.as_ref()
.expect("YAML must have a namespace"),
};
// 5. Create a dynamic API client for this resource type.
let api: Api<DynamicObject> =
Api::namespaced_with(self.client.clone(), namespace, &api_resource);
// 6. Apply the object to the cluster using Server-Side Apply.
// This will create the resource if it doesn't exist, or update it if it does.
println!(
"Applying Argo Application '{}' in namespace '{}'...",
name, namespace
);
let patch_params = PatchParams::apply("harmony"); // Use a unique field manager name
let result = api.patch(name, &patch_params, &Patch::Apply(&obj)).await?;
println!("Successfully applied '{}'.", result.name_any());
Ok(())
}
pub(crate) async fn from_kubeconfig(path: &str) -> Option<K8sClient> { pub(crate) async fn from_kubeconfig(path: &str) -> Option<K8sClient> {
let k = match Kubeconfig::read_from(path) { let k = match Kubeconfig::read_from(path) {
Ok(k) => k, Ok(k) => k,

View File

@@ -1,7 +1,6 @@
use std::{process::Command, sync::Arc}; use std::{process::Command, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use inquire::Confirm;
use log::{debug, info, warn}; use log::{debug, info, warn};
use serde::Serialize; use serde::Serialize;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
@@ -10,9 +9,8 @@ use crate::{
executors::ExecutorError, executors::ExecutorError,
interpret::{InterpretError, Outcome}, interpret::{InterpretError, Outcome},
inventory::Inventory, inventory::Inventory,
maestro::Maestro,
modules::k3d::K3DInstallationScore, modules::k3d::K3DInstallationScore,
topology::LocalhostTopology, score::Score,
}; };
use super::{ use super::{
@@ -94,9 +92,8 @@ impl K8sAnywhereTopology {
return Err("Failed to run 'helm -version'".to_string()); return Err("Failed to run 'helm -version'".to_string());
} }
// Print the version output
let version_output = String::from_utf8_lossy(&version_result.stdout); let version_output = String::from_utf8_lossy(&version_result.stdout);
println!("Helm version: {}", version_output.trim()); debug!("Helm version: {}", version_output.trim());
Ok(()) Ok(())
} }
@@ -114,9 +111,10 @@ impl K8sAnywhereTopology {
} }
async fn try_install_k3d(&self) -> Result<(), InterpretError> { async fn try_install_k3d(&self) -> Result<(), InterpretError> {
let maestro = Maestro::initialize(Inventory::autoload(), LocalhostTopology::new()).await?; self.get_k3d_installation_score()
let k3d_score = self.get_k3d_installation_score(); .create_interpret()
maestro.interpret(Box::new(k3d_score)).await?; .execute(&Inventory::empty(), self)
.await?;
Ok(()) Ok(())
} }
@@ -126,7 +124,7 @@ impl K8sAnywhereTopology {
// TODO this deserves some refactoring, it is becoming a bit hard to figure out // TODO this deserves some refactoring, it is becoming a bit hard to figure out
// be careful when making modifications here // be careful when making modifications here
if k8s_anywhere_config.use_local_k3d { if k8s_anywhere_config.use_local_k3d {
info!("Using local k3d cluster because of use_local_k3d set to true"); debug!("Using local k3d cluster because of use_local_k3d set to true");
} else { } else {
if let Some(kubeconfig) = &k8s_anywhere_config.kubeconfig { if let Some(kubeconfig) = &k8s_anywhere_config.kubeconfig {
debug!("Loading kubeconfig {kubeconfig}"); debug!("Loading kubeconfig {kubeconfig}");
@@ -158,22 +156,13 @@ impl K8sAnywhereTopology {
} }
if !k8s_anywhere_config.autoinstall { if !k8s_anywhere_config.autoinstall {
debug!("Autoinstall confirmation prompt"); warn!(
let confirmation = Confirm::new( "Harmony autoinstallation is not activated, do you wish to launch autoinstallation? : ") "Installation cancelled, K8sAnywhere could not initialize a valid Kubernetes client"
.with_default(false) );
.prompt() return Ok(None);
.expect("Unexpected prompt error");
debug!("Autoinstall confirmation {confirmation}");
if !confirmation {
warn!(
"Installation cancelled, K8sAnywhere could not initialize a valid Kubernetes client"
);
return Ok(None);
}
} }
info!("Starting K8sAnywhere installation"); debug!("Starting K8sAnywhere installation");
self.try_install_k3d().await?; self.try_install_k3d().await?;
let k3d_score = self.get_k3d_installation_score(); let k3d_score = self.get_k3d_installation_score();
// I feel like having to rely on the k3d_rs crate here is a smell // I feel like having to rely on the k3d_rs crate here is a smell
@@ -186,7 +175,7 @@ impl K8sAnywhereTopology {
Ok(client) => K8sState { Ok(client) => K8sState {
client: Arc::new(K8sClient::new(client)), client: Arc::new(K8sClient::new(client)),
_source: K8sSource::LocalK3d, _source: K8sSource::LocalK3d,
message: "Successfully installed K3D cluster and acquired client".to_string(), message: "K8s client ready".to_string(),
}, },
Err(_) => todo!(), Err(_) => todo!(),
}; };
@@ -237,7 +226,7 @@ pub struct K8sAnywhereConfig {
/// ///
/// When enabled, autoinstall will setup a K3D cluster on the localhost. https://k3d.io/stable/ /// When enabled, autoinstall will setup a K3D cluster on the localhost. https://k3d.io/stable/
/// ///
/// Default: false /// Default: true
pub autoinstall: bool, pub autoinstall: bool,
/// Whether to use local k3d cluster. /// Whether to use local k3d cluster.
@@ -246,7 +235,7 @@ pub struct K8sAnywhereConfig {
/// ///
/// default: true /// default: true
pub use_local_k3d: bool, pub use_local_k3d: bool,
harmony_profile: String, pub harmony_profile: String,
} }
impl K8sAnywhereConfig { impl K8sAnywhereConfig {
@@ -256,7 +245,7 @@ impl K8sAnywhereConfig {
use_system_kubeconfig: std::env::var("HARMONY_USE_SYSTEM_KUBECONFIG") use_system_kubeconfig: std::env::var("HARMONY_USE_SYSTEM_KUBECONFIG")
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)), .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
autoinstall: std::env::var("HARMONY_AUTOINSTALL") autoinstall: std::env::var("HARMONY_AUTOINSTALL")
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)), .map_or_else(|_| true, |v| v.parse().ok().unwrap_or(false)),
// TODO harmony_profile should be managed at a more core level than this // TODO harmony_profile should be managed at a more core level than this
harmony_profile: std::env::var("HARMONY_PROFILE").map_or_else( harmony_profile: std::env::var("HARMONY_PROFILE").map_or_else(
|_| "dev".to_string(), |_| "dev".to_string(),

View File

@@ -3,7 +3,6 @@ use serde::Serialize;
use crate::topology::Topology; use crate::topology::Topology;
use super::Application;
/// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability, /// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability,
/// ContinuousIntegration, ContinuousDelivery /// ContinuousIntegration, ContinuousDelivery
#[async_trait] #[async_trait]

View File

@@ -0,0 +1,354 @@
use log::debug;
use serde::Serialize;
use serde_with::skip_serializing_none;
use serde_yaml::Value;
use crate::modules::application::features::CDApplicationConfig;
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Helm {
pub pass_credentials: Option<bool>,
pub parameters: Vec<Value>,
pub file_parameters: Vec<Value>,
pub release_name: Option<String>,
pub value_files: Vec<String>,
pub ignore_missing_value_files: Option<bool>,
pub values: Option<String>,
pub values_object: Option<Value>,
pub skip_crds: Option<bool>,
pub skip_schema_validation: Option<bool>,
pub version: Option<String>,
pub kube_version: Option<String>,
pub api_versions: Vec<String>,
pub namespace: Option<String>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Source {
// Using string for this because URL enforces a URL scheme at the beginning but Helm, ArgoCD, etc do not, and it can be counterproductive,
// as the only way I've found to get OCI working isn't by using oci:// but rather no scheme at all
#[serde(rename = "repoURL")]
pub repo_url: String,
pub target_revision: Option<String>,
pub chart: String,
pub helm: Helm,
pub path: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Automated {
pub prune: bool,
pub self_heal: bool,
pub allow_empty: bool,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Backoff {
pub duration: String,
pub factor: u32,
pub max_duration: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Retry {
pub limit: u32,
pub backoff: Backoff,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SyncPolicy {
pub automated: Automated,
pub sync_options: Vec<String>,
pub retry: Retry,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ArgoApplication {
pub name: String,
pub namespace: Option<String>,
pub project: String,
pub source: Source,
pub sync_policy: SyncPolicy,
pub revision_history_limit: u32,
}
impl Default for ArgoApplication {
fn default() -> Self {
Self {
name: Default::default(),
namespace: Default::default(),
project: Default::default(),
source: Source {
repo_url: "http://asdf".to_string(),
target_revision: None,
chart: "".to_string(),
helm: Helm {
pass_credentials: None,
parameters: vec![],
file_parameters: vec![],
release_name: None,
value_files: vec![],
ignore_missing_value_files: None,
values: None,
values_object: None,
skip_crds: None,
skip_schema_validation: None,
version: None,
kube_version: None,
api_versions: vec![],
namespace: None,
},
path: "".to_string(),
},
sync_policy: SyncPolicy {
automated: Automated {
prune: false,
self_heal: false,
allow_empty: false,
},
sync_options: vec![],
retry: Retry {
limit: 5,
backoff: Backoff {
duration: "5s".to_string(),
factor: 2,
max_duration: "3m".to_string(),
},
},
},
revision_history_limit: 10,
}
}
}
impl From<CDApplicationConfig> for ArgoApplication {
fn from(value: CDApplicationConfig) -> Self {
Self {
name: value.name,
namespace: Some(value.namespace),
project: "default".to_string(),
source: Source {
repo_url: value.helm_chart_repo_url,
target_revision: Some(value.version.to_string()),
chart: value.helm_chart_name.clone(),
path: value.helm_chart_name,
helm: Helm {
pass_credentials: None,
parameters: vec![],
file_parameters: vec![],
release_name: None,
value_files: vec![],
ignore_missing_value_files: None,
values: None,
values_object: value.values_overrides,
skip_crds: None,
skip_schema_validation: None,
version: None,
kube_version: None,
api_versions: vec![],
namespace: None,
},
},
sync_policy: SyncPolicy {
automated: Automated {
prune: false,
self_heal: false,
allow_empty: true,
},
sync_options: vec![],
retry: Retry {
limit: 5,
backoff: Backoff {
duration: "5s".to_string(),
factor: 2,
max_duration: "3m".to_string(),
},
},
},
..Self::default()
}
}
}
impl ArgoApplication {
pub fn to_yaml(&self) -> serde_yaml::Value {
let name = &self.name;
let namespace = if let Some(ns) = self.namespace.as_ref() {
&ns
} else {
"argocd"
};
let project = &self.project;
let source = &self.source;
let yaml_str = format!(
r#"
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: {name}
# You'll usually want to add your resources to the argocd namespace.
namespace: {namespace}
spec:
# The project the application belongs to.
project: {project}
# Destination cluster and namespace to deploy the application
destination:
# cluster API URL
server: https://kubernetes.default.svc
# or cluster name
# name: in-cluster
# The namespace will only be set for namespace-scoped resources that have not set a value for .metadata.namespace
namespace: {namespace}
"#
);
let mut yaml_value: Value =
serde_yaml::from_str(yaml_str.as_str()).expect("couldn't parse string to YAML");
let spec = yaml_value
.get_mut("spec")
.expect("couldn't get spec from yaml")
.as_mapping_mut()
.expect("couldn't unwrap spec as mutable mapping");
let source =
serde_yaml::to_value(&self.source).expect("couldn't serialize source to value");
let sync_policy = serde_yaml::to_value(&self.sync_policy)
.expect("couldn't serialize sync_policy to value");
let revision_history_limit = serde_yaml::to_value(&self.revision_history_limit)
.expect("couldn't serialize revision_history_limit to value");
spec.insert(
serde_yaml::to_value("source").expect("string to value failed"),
source,
);
spec.insert(
serde_yaml::to_value("syncPolicy").expect("string to value failed"),
sync_policy,
);
spec.insert(
serde_yaml::to_value("revisionHistoryLimit")
.expect("couldn't convert str to yaml value"),
revision_history_limit,
);
debug!("spec: {}", serde_yaml::to_string(spec).unwrap());
debug!(
"entire yaml_value: {}",
serde_yaml::to_string(&yaml_value).unwrap()
);
yaml_value
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::modules::application::features::{
ArgoApplication, Automated, Backoff, Helm, Retry, Source, SyncPolicy,
};
#[test]
fn test_argo_application_to_yaml_happy_path() {
let app = ArgoApplication {
name: "test".to_string(),
namespace: Some("test-ns".to_string()),
project: "test-project".to_string(),
source: Source {
repo_url: "http://test".to_string(),
target_revision: None,
chart: "test-chart".to_string(),
helm: Helm {
pass_credentials: None,
parameters: vec![],
file_parameters: vec![],
release_name: Some("test-release-neame".to_string()),
value_files: vec![],
ignore_missing_value_files: None,
values: None,
values_object: None,
skip_crds: None,
skip_schema_validation: None,
version: None,
kube_version: None,
api_versions: vec![],
namespace: None,
},
path: "".to_string(),
},
sync_policy: SyncPolicy {
automated: Automated {
prune: false,
self_heal: false,
allow_empty: false,
},
sync_options: vec![],
retry: Retry {
limit: 5,
backoff: Backoff {
duration: "5s".to_string(),
factor: 2,
max_duration: "3m".to_string(),
},
},
},
revision_history_limit: 10,
};
let expected_yaml_output = r#"apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: test
namespace: test-ns
spec:
project: test-project
destination:
server: https://kubernetes.default.svc
namespace: test-ns
source:
repoURL: http://test
chart: test-chart
helm:
parameters: []
fileParameters: []
releaseName: test-release-neame
valueFiles: []
apiVersions: []
path: ''
syncPolicy:
automated:
prune: false
selfHeal: false
allowEmpty: false
syncOptions: []
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 10"#;
assert_eq!(
expected_yaml_output.trim(),
serde_yaml::to_string(&app.clone().to_yaml())
.unwrap()
.trim()
);
}
}

View File

@@ -1,20 +1,20 @@
use std::{io::Write, process::Command, sync::Arc}; use std::{io::Write, process::Command, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use log::{error, info}; use log::{debug, error};
use serde_json::Value; use serde_yaml::Value;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use crate::{ use crate::{
config::HARMONY_DATA_DIR, config::HARMONY_DATA_DIR,
data::Version, data::Version,
inventory::Inventory, inventory::Inventory,
modules::{ modules::application::{
application::{Application, ApplicationFeature, HelmPackage, OCICompliant}, Application, ApplicationFeature, HelmPackage, OCICompliant,
helm::chart::HelmChartScore, features::{ArgoApplication, ArgoHelmScore},
}, },
score::Score, score::Score,
topology::{DeploymentTarget, HelmCommand, MultiTargetTopology, Topology, Url}, topology::{DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology},
}; };
/// ContinuousDelivery in Harmony provides this functionality : /// ContinuousDelivery in Harmony provides this functionality :
@@ -63,7 +63,7 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
error!("TODO hardcoded k3d bin path is wrong"); error!("TODO hardcoded k3d bin path is wrong");
let k3d_bin_path = (*HARMONY_DATA_DIR).join("k3d").join("k3d"); let k3d_bin_path = (*HARMONY_DATA_DIR).join("k3d").join("k3d");
// --- 1. Import the container image into the k3d cluster --- // --- 1. Import the container image into the k3d cluster ---
info!( debug!(
"Importing image '{}' into k3d cluster 'harmony'", "Importing image '{}' into k3d cluster 'harmony'",
image_name image_name
); );
@@ -80,7 +80,7 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
} }
// --- 2. Get the kubeconfig for the k3d cluster and write it to a temp file --- // --- 2. Get the kubeconfig for the k3d cluster and write it to a temp file ---
info!("Retrieving kubeconfig for k3d cluster 'harmony'"); debug!("Retrieving kubeconfig for k3d cluster 'harmony'");
let kubeconfig_output = Command::new(&k3d_bin_path) let kubeconfig_output = Command::new(&k3d_bin_path)
.args(["kubeconfig", "get", "harmony"]) .args(["kubeconfig", "get", "harmony"])
.output() .output()
@@ -101,7 +101,7 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
let kubeconfig_path = temp_kubeconfig.path().to_str().unwrap(); let kubeconfig_path = temp_kubeconfig.path().to_str().unwrap();
// --- 3. Install or upgrade the Helm chart in the cluster --- // --- 3. Install or upgrade the Helm chart in the cluster ---
info!( debug!(
"Deploying Helm chart '{}' to namespace '{}'", "Deploying Helm chart '{}' to namespace '{}'",
chart_url, app_name chart_url, app_name
); );
@@ -131,7 +131,7 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
)); ));
} }
info!("Successfully deployed '{}' to local k3d cluster.", app_name); debug!("Successfully deployed '{}' to local k3d cluster.", app_name);
Ok(()) Ok(())
} }
} }
@@ -139,25 +139,25 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
#[async_trait] #[async_trait]
impl< impl<
A: OCICompliant + HelmPackage + Clone + 'static, A: OCICompliant + HelmPackage + Clone + 'static,
T: Topology + HelmCommand + MultiTargetTopology + 'static, T: Topology + HelmCommand + MultiTargetTopology + K8sclient + 'static,
> ApplicationFeature<T> for ContinuousDelivery<A> > ApplicationFeature<T> for ContinuousDelivery<A>
{ {
async fn ensure_installed(&self, topology: &T) -> Result<(), String> { async fn ensure_installed(&self, topology: &T) -> Result<(), String> {
let image = self.application.image_name(); let image = self.application.image_name();
// TODO // TODO Write CI/CD workflow files
error!( // we can autotedect the CI type using the remote url (default to github action for github
"TODO reverse helm chart packaging and docker image build. I put helm package first for faster iterations" // url, etc..)
); // Or ask for it when unknown
let helm_chart = self.application.build_push_helm_package(&image).await?; let helm_chart = self.application.build_push_helm_package(&image).await?;
info!("Pushed new helm chart {helm_chart}"); debug!("Pushed new helm chart {helm_chart}");
// let image = self.application.build_push_oci_image().await?; error!("TODO Make building image configurable/skippable if image already exists (prompt)");
// info!("Pushed new docker image {image}"); let image = self.application.build_push_oci_image().await?;
error!("uncomment above"); debug!("Pushed new docker image {image}");
info!("Installing ContinuousDelivery feature"); debug!("Installing ContinuousDelivery feature");
// TODO this is a temporary hack for demo purposes, the deployment target should be driven // TODO this is a temporary hack for demo purposes, the deployment target should be driven
// by the topology only and we should not have to know how to perform tasks like this for // by the topology only and we should not have to know how to perform tasks like this for
// which the topology should be responsible. // which the topology should be responsible.
@@ -177,32 +177,29 @@ impl<
.await?; .await?;
} }
target => { target => {
info!("Deploying to target {target:?}"); debug!("Deploying to target {target:?}");
let cd_server = HelmChartScore { let score = ArgoHelmScore {
namespace: todo!( namespace: "harmonydemo-staging".to_string(),
"ArgoCD Helm chart with proper understanding of Tenant, see how Will did it for Monitoring for now" openshift: false,
), domain: "argo.harmonydemo.apps.st.mcd".to_string(),
release_name: todo!("argocd helm chart whatever"), argo_apps: vec![ArgoApplication::from(CDApplicationConfig {
chart_name: todo!(), // helm pull oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart --version 0.1.0
chart_version: todo!(), version: Version::from("0.1.0").unwrap(),
values_overrides: todo!(), helm_chart_repo_url: "hub.nationtech.io/harmony".to_string(),
values_yaml: todo!(), helm_chart_name: "harmony-example-rust-webapp-chart".to_string(),
create_namespace: todo!(), values_overrides: None,
install_only: todo!(), name: "harmony-demo-rust-webapp".to_string(),
repository: todo!(), namespace: "harmonydemo-staging".to_string(),
})],
}; };
let interpret = cd_server.create_interpret(); score
interpret.execute(&Inventory::empty(), topology); .create_interpret()
.execute(&Inventory::empty(), topology)
.await
.unwrap();
} }
}; };
Ok(())
todo!("1. Create ArgoCD score that installs argo using helm chart, see if Taha's already done it
- [X] Package app (docker image, helm chart)
- [X] Push to registry
- [ ] Push only if staging or prod
- [ ] Deploy to local k3d when target is local
- [ ] Poke Argo
- [ ] Ensure app is up")
} }
fn name(&self) -> String { fn name(&self) -> String {
"ContinuousDelivery".to_string() "ContinuousDelivery".to_string()
@@ -212,9 +209,12 @@ impl<
/// For now this is entirely bound to K8s / ArgoCD, will have to be revisited when we support /// For now this is entirely bound to K8s / ArgoCD, will have to be revisited when we support
/// more CD systems /// more CD systems
pub struct CDApplicationConfig { pub struct CDApplicationConfig {
version: Version, pub version: Version,
helm_chart_url: Url, pub helm_chart_repo_url: String,
values_overrides: Value, pub helm_chart_name: String,
pub values_overrides: Option<Value>,
pub name: String,
pub namespace: String,
} }
pub trait ContinuousDeliveryApplication { pub trait ContinuousDeliveryApplication {

View File

@@ -2,7 +2,7 @@ use async_trait::async_trait;
use log::info; use log::info;
use crate::{ use crate::{
modules::application::{Application, ApplicationFeature}, modules::application::ApplicationFeature,
topology::{K8sclient, Topology}, topology::{K8sclient, Topology},
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -6,3 +6,9 @@ pub use monitoring::*;
mod continuous_delivery; mod continuous_delivery;
pub use continuous_delivery::*; pub use continuous_delivery::*;
mod helm_argocd_score;
pub use helm_argocd_score::*;
mod argo_types;
pub use argo_types::*;

View File

@@ -1,19 +1,109 @@
use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use log::info; use base64::{Engine as _, engine::general_purpose};
use log::{debug, info};
use crate::{ use crate::{
modules::application::{Application, ApplicationFeature}, inventory::Inventory,
topology::{HelmCommand, Topology}, modules::{
application::{ApplicationFeature, OCICompliant},
monitoring::{
alert_channel::webhook_receiver::WebhookReceiver,
kube_prometheus::{
helm_prometheus_alert_score::HelmPrometheusAlertingScore,
types::{NamespaceSelector, ServiceMonitor},
},
ntfy::ntfy::NtfyScore,
},
},
score::Score,
topology::{HelmCommand, K8sclient, Topology, Url, tenant::TenantManager},
}; };
#[derive(Debug, Default, Clone)] #[derive(Debug, Clone)]
pub struct Monitoring {} pub struct Monitoring {
pub application: Arc<dyn OCICompliant>,
}
#[async_trait] #[async_trait]
impl<T: Topology + HelmCommand + 'static> ApplicationFeature<T> for Monitoring { impl<T: Topology + HelmCommand + K8sclient + 'static + TenantManager> ApplicationFeature<T>
async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { for Monitoring
{
async fn ensure_installed(&self, topology: &T) -> Result<(), String> {
info!("Ensuring monitoring is available for application"); info!("Ensuring monitoring is available for application");
todo!("create and execute k8s prometheus score, depends on Will's work")
let ntfy = NtfyScore {
// namespace: topology
// .get_tenant_config()
// .await
// .expect("couldn't get tenant config")
// .name,
namespace: self.application.name(),
host: "localhost".to_string(),
};
ntfy.create_interpret()
.execute(&Inventory::empty(), topology)
.await
.expect("couldn't create interpret for ntfy");
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://ntfy.{}.svc.cluster.local/rust-web-app?auth={ntfy_default_auth_param}",
self.application.name()
)
.as_str(),
)
.unwrap(),
),
};
let mut service_monitor = ServiceMonitor::default();
service_monitor.namespace_selector = Some(NamespaceSelector {
any: true,
match_names: vec![],
});
service_monitor.name = "rust-webapp".to_string();
// let alerting_score = ApplicationPrometheusMonitoringScore {
// receivers: vec![Box::new(ntfy_receiver)],
// rules: vec![],
// service_monitors: vec![service_monitor],
// };
let alerting_score = HelmPrometheusAlertingScore {
receivers: vec![Box::new(ntfy_receiver)],
rules: vec![],
service_monitors: vec![service_monitor],
};
alerting_score
.create_interpret()
.execute(&Inventory::empty(), topology)
.await
.unwrap();
Ok(())
} }
fn name(&self) -> String { fn name(&self) -> String {
"Monitoring".to_string() "Monitoring".to_string()

View File

@@ -5,7 +5,7 @@ mod rust;
use std::sync::Arc; use std::sync::Arc;
pub use feature::*; pub use feature::*;
use log::info; use log::debug;
pub use oci::*; pub use oci::*;
pub use rust::*; pub use rust::*;
@@ -23,20 +23,20 @@ pub trait Application: std::fmt::Debug + Send + Sync {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct ApplicationInterpret<T: Topology + std::fmt::Debug> { pub struct ApplicationInterpret<A: Application, T: Topology + std::fmt::Debug> {
features: Vec<Box<dyn ApplicationFeature<T>>>, features: Vec<Box<dyn ApplicationFeature<T>>>,
application: Arc<Box<dyn Application>>, application: Arc<A>,
} }
#[async_trait] #[async_trait]
impl<T: Topology + std::fmt::Debug> Interpret<T> for ApplicationInterpret<T> { impl<A: Application, T: Topology + std::fmt::Debug> Interpret<T> for ApplicationInterpret<A, T> {
async fn execute( async fn execute(
&self, &self,
_inventory: &Inventory, _inventory: &Inventory,
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
let app_name = self.application.name(); let app_name = self.application.name();
info!( debug!(
"Preparing {} features [{}] for application {app_name}", "Preparing {} features [{}] for application {app_name}",
self.features.len(), self.features.len(),
self.features self.features
@@ -46,7 +46,7 @@ impl<T: Topology + std::fmt::Debug> Interpret<T> for ApplicationInterpret<T> {
.join(", ") .join(", ")
); );
for feature in self.features.iter() { for feature in self.features.iter() {
info!( debug!(
"Installing feature {} for application {app_name}", "Installing feature {} for application {app_name}",
feature.name() feature.name()
); );
@@ -59,9 +59,7 @@ impl<T: Topology + std::fmt::Debug> Interpret<T> for ApplicationInterpret<T> {
} }
}; };
} }
todo!( Ok(Outcome::success("successfully created app".to_string()))
"Do I need to do anything more than this here?? I feel like the Application trait itself should expose something like ensure_ready but its becoming redundant. We'll see as this evolves."
)
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {

View File

@@ -4,11 +4,15 @@ use std::process;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use bollard::query_parameters::PushImageOptionsBuilder;
use bollard::{Docker, body_full};
use dockerfile_builder::Dockerfile; use dockerfile_builder::Dockerfile;
use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR}; use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR};
use dockerfile_builder::instruction_builder::CopyBuilder; use dockerfile_builder::instruction_builder::CopyBuilder;
use log::{debug, error, info}; use futures_util::StreamExt;
use log::{debug, error, log_enabled};
use serde::Serialize; use serde::Serialize;
use tar::Archive;
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL}; use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
use crate::{ use crate::{
@@ -19,23 +23,30 @@ use crate::{
use super::{Application, ApplicationFeature, ApplicationInterpret, HelmPackage, OCICompliant}; use super::{Application, ApplicationFeature, ApplicationInterpret, HelmPackage, OCICompliant};
#[derive(Debug, Serialize, Clone)] #[derive(Debug, Serialize, Clone)]
pub struct RustWebappScore<T: Topology + Clone + Serialize> { pub struct ApplicationScore<A: Application + Serialize, T: Topology + Clone + Serialize>
pub name: String, where
pub domain: Url, Arc<A>: Serialize + Clone,
{
pub features: Vec<Box<dyn ApplicationFeature<T>>>, pub features: Vec<Box<dyn ApplicationFeature<T>>>,
pub application: RustWebapp, pub application: Arc<A>,
} }
impl<T: Topology + std::fmt::Debug + Clone + Serialize + 'static> Score<T> for RustWebappScore<T> { impl<
A: Application + Serialize + Clone + 'static,
T: Topology + std::fmt::Debug + Clone + Serialize + 'static,
> Score<T> for ApplicationScore<A, T>
where
Arc<A>: Serialize,
{
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
Box::new(ApplicationInterpret { Box::new(ApplicationInterpret {
features: self.features.clone(), features: self.features.clone(),
application: Arc::new(Box::new(self.application.clone())), application: self.application.clone(),
}) })
} }
fn name(&self) -> String { fn name(&self) -> String {
format!("{}-RustWebapp", self.name) format!("Application: {}", self.application.name())
} }
} }
@@ -47,6 +58,7 @@ pub enum RustWebFramework {
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct RustWebapp { pub struct RustWebapp {
pub name: String, pub name: String,
pub domain: Url,
/// The path to the root of the Rust project to be containerized. /// The path to the root of the Rust project to be containerized.
pub project_root: PathBuf, pub project_root: PathBuf,
pub framework: Option<RustWebFramework>, pub framework: Option<RustWebFramework>,
@@ -61,19 +73,19 @@ impl Application for RustWebapp {
#[async_trait] #[async_trait]
impl HelmPackage for RustWebapp { impl HelmPackage for RustWebapp {
async fn build_push_helm_package(&self, image_url: &str) -> Result<String, String> { async fn build_push_helm_package(&self, image_url: &str) -> Result<String, String> {
info!("Starting Helm chart build and push for '{}'", self.name); debug!("Starting Helm chart build and push for '{}'", self.name);
// 1. Create the Helm chart files on disk. // 1. Create the Helm chart files on disk.
let chart_dir = self let chart_dir = self
.create_helm_chart_files(image_url) .create_helm_chart_files(image_url)
.map_err(|e| format!("Failed to create Helm chart files: {}", e))?; .map_err(|e| format!("Failed to create Helm chart files: {}", e))?;
info!("Successfully created Helm chart files in {:?}", chart_dir); debug!("Successfully created Helm chart files in {:?}", chart_dir);
// 2. Package the chart into a .tgz archive. // 2. Package the chart into a .tgz archive.
let packaged_chart_path = self let packaged_chart_path = self
.package_helm_chart(&chart_dir) .package_helm_chart(&chart_dir)
.map_err(|e| format!("Failed to package Helm chart: {}", e))?; .map_err(|e| format!("Failed to package Helm chart: {}", e))?;
info!( debug!(
"Successfully packaged Helm chart: {}", "Successfully packaged Helm chart: {}",
packaged_chart_path.to_string_lossy() packaged_chart_path.to_string_lossy()
); );
@@ -82,7 +94,7 @@ impl HelmPackage for RustWebapp {
let oci_chart_url = self let oci_chart_url = self
.push_helm_chart(&packaged_chart_path) .push_helm_chart(&packaged_chart_path)
.map_err(|e| format!("Failed to push Helm chart: {}", e))?; .map_err(|e| format!("Failed to push Helm chart: {}", e))?;
info!("Successfully pushed Helm chart to: {}", oci_chart_url); debug!("Successfully pushed Helm chart to: {}", oci_chart_url);
Ok(oci_chart_url) Ok(oci_chart_url)
} }
@@ -95,24 +107,22 @@ impl OCICompliant for RustWebapp {
async fn build_push_oci_image(&self) -> Result<String, String> { async fn build_push_oci_image(&self) -> Result<String, String> {
// This function orchestrates the build and push process. // This function orchestrates the build and push process.
// It's async to match the trait definition, though the underlying docker commands are blocking. // It's async to match the trait definition, though the underlying docker commands are blocking.
info!("Starting OCI image build and push for '{}'", self.name); debug!("Starting OCI image build and push for '{}'", self.name);
// 1. Build the local image by calling the synchronous helper function. // 1. Build the image by calling the synchronous helper function.
let local_image_name = self.local_image_name(); let image_tag = self.image_name();
self.build_docker_image(&local_image_name) self.build_docker_image(&image_tag)
.await
.map_err(|e| format!("Failed to build Docker image: {}", e))?; .map_err(|e| format!("Failed to build Docker image: {}", e))?;
info!( debug!("Successfully built Docker image: {}", image_tag);
"Successfully built local Docker image: {}",
local_image_name
);
let remote_image_name = self.image_name();
// 2. Push the image to the registry. // 2. Push the image to the registry.
self.push_docker_image(&local_image_name, &remote_image_name) self.push_docker_image(&image_tag)
.await
.map_err(|e| format!("Failed to push Docker image: {}", e))?; .map_err(|e| format!("Failed to push Docker image: {}", e))?;
info!("Successfully pushed Docker image to: {}", remote_image_name); debug!("Successfully pushed Docker image to: {}", image_tag);
Ok(remote_image_name) Ok(image_tag)
} }
fn local_image_name(&self) -> String { fn local_image_name(&self) -> String {
@@ -145,68 +155,74 @@ impl RustWebapp {
} }
/// Builds the Docker image using the generated Dockerfile. /// Builds the Docker image using the generated Dockerfile.
pub fn build_docker_image( pub async fn build_docker_image(
&self, &self,
image_name: &str, image_name: &str,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
info!("Generating Dockerfile for '{}'", self.name); debug!("Generating Dockerfile for '{}'", self.name);
let dockerfile_path = self.build_dockerfile()?; let _dockerfile_path = self.build_dockerfile()?;
info!( let docker = Docker::connect_with_socket_defaults().unwrap();
"Building Docker image with file {} from root {}",
dockerfile_path.to_string_lossy(), let quiet = !log_enabled!(log::Level::Debug);
self.project_root.to_string_lossy()
let build_image_options = bollard::query_parameters::BuildImageOptionsBuilder::default()
.dockerfile("Dockerfile.harmony")
.t(image_name)
.q(quiet)
.version(bollard::query_parameters::BuilderVersion::BuilderV1)
.platform("linux/x86_64");
let mut temp_tar_builder = tar::Builder::new(Vec::new());
let _ = temp_tar_builder
.append_dir_all("", self.project_root.clone())
.unwrap();
let archive = temp_tar_builder
.into_inner()
.expect("couldn't finish creating tar");
let archived_files = Archive::new(archive.as_slice())
.entries()
.unwrap()
.map(|entry| entry.unwrap().path().unwrap().into_owned())
.collect::<Vec<_>>();
debug!("files in docker tar: {:#?}", archived_files);
let mut image_build_stream = docker.build_image(
build_image_options.build(),
None,
Some(body_full(archive.into())),
); );
let output = process::Command::new("docker")
.args([
"build",
"--file",
dockerfile_path.to_str().unwrap(),
"-t",
&image_name,
self.project_root.to_str().unwrap(),
])
.spawn()?
.wait_with_output()?;
self.check_output(&output, "Failed to build Docker image")?; while let Some(msg) = image_build_stream.next().await {
println!("Message: {msg:?}");
}
Ok(image_name.to_string()) Ok(image_name.to_string())
} }
/// Tags and pushes a Docker image to the configured remote registry. /// Tags and pushes a Docker image to the configured remote registry.
fn push_docker_image( async fn push_docker_image(
&self, &self,
image_name: &str, image_tag: &str,
full_tag: &str,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
info!("Pushing docker image {full_tag}"); debug!("Pushing docker image {image_tag}");
// Tag the image for the remote registry. let docker = Docker::connect_with_socket_defaults().unwrap();
let output = process::Command::new("docker")
.args(["tag", image_name, &full_tag]) // let push_options = PushImageOptionsBuilder::new().tag(tag);
.spawn()?
.wait_with_output()?; let mut push_image_stream = docker.push_image(
self.check_output(&output, "Tagging docker image failed")?; image_tag,
debug!( Some(PushImageOptionsBuilder::new().build()),
"docker tag output: stdout: {}, stderr: {}", None,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
); );
// Push the image. while let Some(msg) = push_image_stream.next().await {
let output = process::Command::new("docker") println!("Message: {msg:?}");
.args(["push", &full_tag]) }
.spawn()?
.wait_with_output()?;
self.check_output(&output, "Pushing docker image failed")?;
debug!(
"docker push output: stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
Ok(full_tag.to_string()) Ok(image_tag.to_string())
} }
/// Checks the output of a process command for success. /// Checks the output of a process command for success.
@@ -341,7 +357,11 @@ impl RustWebapp {
image_url: &str, image_url: &str,
) -> Result<PathBuf, Box<dyn std::error::Error>> { ) -> Result<PathBuf, Box<dyn std::error::Error>> {
let chart_name = format!("{}-chart", self.name); let chart_name = format!("{}-chart", self.name);
let chart_dir = self.project_root.join("helm").join(&chart_name); let chart_dir = self
.project_root
.join(".harmony_generated")
.join("helm")
.join(&chart_name);
let templates_dir = chart_dir.join("templates"); let templates_dir = chart_dir.join("templates");
fs::create_dir_all(&templates_dir)?; fs::create_dir_all(&templates_dir)?;
@@ -408,7 +428,7 @@ ingress:
Expand the name of the chart. Expand the name of the chart.
*/}} */}}
{{- define "chart.name" -}} {{- define "chart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- default .Chart.Name $.Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }} {{- end }}
{{/* {{/*
@@ -416,7 +436,7 @@ Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
*/}} */}}
{{- define "chart.fullname" -}} {{- define "chart.fullname" -}}
{{- $name := default .Chart.Name .Values.nameOverride }} {{- $name := default .Chart.Name $.Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }} {{- end }}
"#; "#;
@@ -429,12 +449,12 @@ kind: Service
metadata: metadata:
name: {{ include "chart.fullname" . }} name: {{ include "chart.fullname" . }}
spec: spec:
type: {{ .Values.service.type }} type: {{ $.Values.service.type }}
ports: ports:
- port: {{ .Values.service.port }} - name: main
targetPort: 3000 port: {{ $.Values.service.port | default 3000 }}
targetPort: {{ $.Values.service.port | default 3000 }}
protocol: TCP protocol: TCP
name: http
selector: selector:
app: {{ include "chart.name" . }} app: {{ include "chart.name" . }}
"#; "#;
@@ -447,7 +467,7 @@ kind: Deployment
metadata: metadata:
name: {{ include "chart.fullname" . }} name: {{ include "chart.fullname" . }}
spec: spec:
replicas: {{ .Values.replicaCount }} replicas: {{ $.Values.replicaCount }}
selector: selector:
matchLabels: matchLabels:
app: {{ include "chart.name" . }} app: {{ include "chart.name" . }}
@@ -458,28 +478,28 @@ spec:
spec: spec:
containers: containers:
- name: {{ .Chart.Name }} - name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" image: "{{ $.Values.image.repository }}:{{ $.Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }} imagePullPolicy: {{ $.Values.image.pullPolicy }}
ports: ports:
- name: http - name: main
containerPort: 3000 containerPort: {{ $.Values.service.port | default 3000 }}
protocol: TCP protocol: TCP
"#; "#;
fs::write(templates_dir.join("deployment.yaml"), deployment_yaml)?; fs::write(templates_dir.join("deployment.yaml"), deployment_yaml)?;
// Create templates/ingress.yaml // Create templates/ingress.yaml
let ingress_yaml = r#" let ingress_yaml = r#"
{{- if .Values.ingress.enabled -}} {{- if $.Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: {{ include "chart.fullname" . }} name: {{ include "chart.fullname" . }}
annotations: annotations:
{{- toYaml .Values.ingress.annotations | nindent 4 }} {{- toYaml $.Values.ingress.annotations | nindent 4 }}
spec: spec:
{{- if .Values.ingress.tls }} {{- if $.Values.ingress.tls }}
tls: tls:
{{- range .Values.ingress.tls }} {{- range $.Values.ingress.tls }}
- hosts: - hosts:
{{- range .hosts }} {{- range .hosts }}
- {{ . | quote }} - {{ . | quote }}
@@ -488,7 +508,7 @@ spec:
{{- end }} {{- end }}
{{- end }} {{- end }}
rules: rules:
{{- range .Values.ingress.hosts }} {{- range $.Values.ingress.hosts }}
- host: {{ .host | quote }} - host: {{ .host | quote }}
http: http:
paths: paths:
@@ -499,7 +519,7 @@ spec:
service: service:
name: {{ include "chart.fullname" $ }} name: {{ include "chart.fullname" $ }}
port: port:
number: 3000 number: {{ $.Values.service.port | default 3000 }}
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- end }} {{- end }}
@@ -515,14 +535,18 @@ spec:
chart_dir: &PathBuf, chart_dir: &PathBuf,
) -> Result<PathBuf, Box<dyn std::error::Error>> { ) -> Result<PathBuf, Box<dyn std::error::Error>> {
let chart_dirname = chart_dir.file_name().expect("Should find a chart dirname"); let chart_dirname = chart_dir.file_name().expect("Should find a chart dirname");
info!( debug!(
"Launching `helm package {}` cli with CWD {}", "Launching `helm package {}` cli with CWD {}",
chart_dirname.to_string_lossy(), chart_dirname.to_string_lossy(),
&self.project_root.join("helm").to_string_lossy() &self
.project_root
.join(".harmony_generated")
.join("helm")
.to_string_lossy()
); );
let output = process::Command::new("helm") let output = process::Command::new("helm")
.args(["package", chart_dirname.to_str().unwrap()]) .args(["package", chart_dirname.to_str().unwrap()])
.current_dir(&self.project_root.join("helm")) // Run package from the parent dir .current_dir(&self.project_root.join(".harmony_generated").join("helm")) // Run package from the parent dir
.output()?; .output()?;
self.check_output(&output, "Failed to package Helm chart")?; self.check_output(&output, "Failed to package Helm chart")?;
@@ -539,7 +563,11 @@ spec:
} }
// The output from helm is relative, so we join it with the execution directory. // The output from helm is relative, so we join it with the execution directory.
Ok(self.project_root.join("helm").join(tgz_name)) Ok(self
.project_root
.join(".harmony_generated")
.join("helm")
.join(tgz_name))
} }
/// Pushes a packaged Helm chart to an OCI registry. /// Pushes a packaged Helm chart to an OCI registry.
@@ -552,7 +580,7 @@ spec:
let oci_push_url = format!("oci://{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT); let oci_push_url = format!("oci://{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT);
let oci_pull_url = format!("{oci_push_url}/{}-chart", self.name); let oci_pull_url = format!("{oci_push_url}/{}-chart", self.name);
info!( debug!(
"Pushing Helm chart {} to {}", "Pushing Helm chart {} to {}",
packaged_chart_path.to_string_lossy(), packaged_chart_path.to_string_lossy(),
oci_push_url oci_push_url

View File

@@ -10,14 +10,25 @@ use crate::{
topology::{HttpServer, Topology, Url}, topology::{HttpServer, Topology, Url},
}; };
/// Configure an HTTP server that is provided by the Topology
///
/// This Score will let you easily specify a file path to be served by the HTTP server
///
/// For example, if you have a folder of assets at `/var/www/assets` simply do :
///
/// ```rust,ignore
/// StaticFilesHttpScore {
/// files_to_serve: url!("file:///var/www/assets"),
/// }
/// ```
#[derive(Debug, new, Clone, Serialize)] #[derive(Debug, new, Clone, Serialize)]
pub struct HttpScore { pub struct StaticFilesHttpScore {
files_to_serve: Url, files_to_serve: Url,
} }
impl<T: Topology + HttpServer> Score<T> for HttpScore { impl<T: Topology + HttpServer> Score<T> for StaticFilesHttpScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(HttpInterpret::new(self.clone())) Box::new(StaticFilesHttpInterpret::new(self.clone()))
} }
fn name(&self) -> String { fn name(&self) -> String {
@@ -26,12 +37,12 @@ impl<T: Topology + HttpServer> Score<T> for HttpScore {
} }
#[derive(Debug, new, Clone)] #[derive(Debug, new, Clone)]
pub struct HttpInterpret { pub struct StaticFilesHttpInterpret {
score: HttpScore, score: StaticFilesHttpScore,
} }
#[async_trait] #[async_trait]
impl<T: Topology + HttpServer> Interpret<T> for HttpInterpret { impl<T: Topology + HttpServer> Interpret<T> for StaticFilesHttpInterpret {
async fn execute( async fn execute(
&self, &self,
_inventory: &Inventory, _inventory: &Inventory,

View File

@@ -1,12 +1,13 @@
use std::path::PathBuf; use std::path::PathBuf;
use async_trait::async_trait; use async_trait::async_trait;
use log::info; use log::{debug, info};
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
config::HARMONY_DATA_DIR, config::HARMONY_DATA_DIR,
data::{Id, Version}, data::{Id, Version},
instrumentation::{self, HarmonyEvent},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory, inventory::Inventory,
score::Score, score::Score,
@@ -50,22 +51,38 @@ impl<T: Topology> Interpret<T> for K3dInstallationInterpret {
async fn execute( async fn execute(
&self, &self,
_inventory: &Inventory, _inventory: &Inventory,
_topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
instrumentation::instrument(HarmonyEvent::InterpretExecutionStarted {
topology: topology.name().into(),
interpret: "k3d-installation".into(),
message: "installing k3d...".into(),
})
.unwrap();
let k3d = k3d_rs::K3d::new( let k3d = k3d_rs::K3d::new(
self.score.installation_path.clone(), self.score.installation_path.clone(),
Some(self.score.cluster_name.clone()), Some(self.score.cluster_name.clone()),
); );
match k3d.ensure_installed().await { let outcome = match k3d.ensure_installed().await {
Ok(_client) => { Ok(_client) => {
let msg = format!("k3d cluster {} is installed ", self.score.cluster_name); let msg = format!("k3d cluster '{}' installed ", self.score.cluster_name);
info!("{msg}"); debug!("{msg}");
Ok(Outcome::success(msg)) Ok(Outcome::success(msg))
} }
Err(msg) => Err(InterpretError::new(format!( Err(msg) => Err(InterpretError::new(format!(
"K3dInstallationInterpret failed to ensure k3d is installed : {msg}" "failed to ensure k3d is installed : {msg}"
))), ))),
} };
instrumentation::instrument(HarmonyEvent::InterpretExecutionFinished {
topology: topology.name().into(),
interpret: "k3d-installation".into(),
outcome: outcome.clone(),
})
.unwrap();
outcome
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {
InterpretName::K3dInstallation InterpretName::K3dInstallation

View File

@@ -4,9 +4,12 @@ use serde_yaml::{Mapping, Value};
use crate::{ use crate::{
interpret::{InterpretError, Outcome}, interpret::{InterpretError, Outcome},
modules::monitoring::kube_prometheus::{ modules::monitoring::{
prometheus::{Prometheus, PrometheusReceiver}, kube_prometheus::{
types::{AlertChannelConfig, AlertManagerChannelConfig}, prometheus::{KubePrometheus, KubePrometheusReceiver},
types::{AlertChannelConfig, AlertManagerChannelConfig},
},
prometheus::prometheus::{Prometheus, PrometheusReceiver},
}, },
topology::{Url, oberservability::monitoring::AlertReceiver}, topology::{Url, oberservability::monitoring::AlertReceiver},
}; };
@@ -37,6 +40,26 @@ impl PrometheusReceiver for DiscordWebhook {
} }
} }
#[async_trait]
impl AlertReceiver<KubePrometheus> for DiscordWebhook {
async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> {
sender.install_receiver(self).await
}
fn clone_box(&self) -> Box<dyn AlertReceiver<KubePrometheus>> {
Box::new(self.clone())
}
}
#[async_trait]
impl KubePrometheusReceiver for DiscordWebhook {
fn name(&self) -> String {
self.name.clone()
}
async fn configure_receiver(&self) -> AlertManagerChannelConfig {
self.get_config().await
}
}
#[async_trait] #[async_trait]
impl AlertChannelConfig for DiscordWebhook { impl AlertChannelConfig for DiscordWebhook {
async fn get_config(&self) -> AlertManagerChannelConfig { async fn get_config(&self) -> AlertManagerChannelConfig {

View File

@@ -4,9 +4,12 @@ use serde_yaml::{Mapping, Value};
use crate::{ use crate::{
interpret::{InterpretError, Outcome}, interpret::{InterpretError, Outcome},
modules::monitoring::kube_prometheus::{ modules::monitoring::{
prometheus::{Prometheus, PrometheusReceiver}, kube_prometheus::{
types::{AlertChannelConfig, AlertManagerChannelConfig}, prometheus::{KubePrometheus, KubePrometheusReceiver},
types::{AlertChannelConfig, AlertManagerChannelConfig},
},
prometheus::prometheus::{Prometheus, PrometheusReceiver},
}, },
topology::{Url, oberservability::monitoring::AlertReceiver}, topology::{Url, oberservability::monitoring::AlertReceiver},
}; };
@@ -36,6 +39,25 @@ impl PrometheusReceiver for WebhookReceiver {
self.get_config().await self.get_config().await
} }
} }
#[async_trait]
impl AlertReceiver<KubePrometheus> for WebhookReceiver {
async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> {
sender.install_receiver(self).await
}
fn clone_box(&self) -> Box<dyn AlertReceiver<KubePrometheus>> {
Box::new(self.clone())
}
}
#[async_trait]
impl KubePrometheusReceiver for WebhookReceiver {
fn name(&self) -> String {
self.name.clone()
}
async fn configure_receiver(&self) -> AlertManagerChannelConfig {
self.get_config().await
}
}
#[async_trait] #[async_trait]
impl AlertChannelConfig for WebhookReceiver { impl AlertChannelConfig for WebhookReceiver {

View File

@@ -5,13 +5,26 @@ use serde::Serialize;
use crate::{ use crate::{
interpret::{InterpretError, Outcome}, interpret::{InterpretError, Outcome},
modules::monitoring::kube_prometheus::{ modules::monitoring::{
prometheus::{Prometheus, PrometheusRule}, kube_prometheus::{
types::{AlertGroup, AlertManagerAdditionalPromRules}, prometheus::{KubePrometheus, KubePrometheusRule},
types::{AlertGroup, AlertManagerAdditionalPromRules},
},
prometheus::prometheus::{Prometheus, PrometheusRule},
}, },
topology::oberservability::monitoring::AlertRule, topology::oberservability::monitoring::AlertRule,
}; };
#[async_trait]
impl AlertRule<KubePrometheus> for AlertManagerRuleGroup {
async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> {
sender.install_rule(&self).await
}
fn clone_box(&self) -> Box<dyn AlertRule<KubePrometheus>> {
Box::new(self.clone())
}
}
#[async_trait] #[async_trait]
impl AlertRule<Prometheus> for AlertManagerRuleGroup { impl AlertRule<Prometheus> for AlertManagerRuleGroup {
async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> { async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> {
@@ -41,6 +54,25 @@ impl PrometheusRule for AlertManagerRuleGroup {
} }
} }
} }
#[async_trait]
impl KubePrometheusRule for AlertManagerRuleGroup {
fn name(&self) -> String {
self.name.clone()
}
async fn configure_rule(&self) -> AlertManagerAdditionalPromRules {
let mut additional_prom_rules = BTreeMap::new();
additional_prom_rules.insert(
self.name.clone(),
AlertGroup {
groups: vec![self.clone()],
},
);
AlertManagerAdditionalPromRules {
rules: additional_prom_rules,
}
}
}
impl AlertManagerRuleGroup { impl AlertManagerRuleGroup {
pub fn new(name: &str, rules: Vec<PrometheusAlertRule>) -> AlertManagerRuleGroup { pub fn new(name: &str, rules: Vec<PrometheusAlertRule>) -> AlertManagerRuleGroup {

View File

@@ -0,0 +1,44 @@
use std::sync::{Arc, Mutex};
use serde::Serialize;
use crate::{
modules::monitoring::{
kube_prometheus::types::ServiceMonitor,
prometheus::{prometheus::Prometheus, prometheus_config::PrometheusConfig},
},
score::Score,
topology::{
HelmCommand, Topology,
oberservability::monitoring::{AlertReceiver, AlertRule, AlertingInterpret},
tenant::TenantManager,
},
};
#[derive(Clone, Debug, Serialize)]
pub struct ApplicationPrometheusMonitoringScore {
pub receivers: Vec<Box<dyn AlertReceiver<Prometheus>>>,
pub rules: Vec<Box<dyn AlertRule<Prometheus>>>,
pub service_monitors: Vec<ServiceMonitor>,
}
impl<T: Topology + HelmCommand + TenantManager> Score<T> for ApplicationPrometheusMonitoringScore {
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
let mut prom_config = PrometheusConfig::new();
prom_config.alert_manager = true;
let config = Arc::new(Mutex::new(prom_config));
config
.try_lock()
.expect("couldn't lock config")
.additional_service_monitors = self.service_monitors.clone();
Box::new(AlertingInterpret {
sender: Prometheus::new(),
receivers: self.receivers.clone(),
rules: self.rules.clone(),
})
}
fn name(&self) -> String {
"ApplicationPrometheusMonitoringScore".to_string()
}
}

View File

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

View File

@@ -0,0 +1,28 @@
use non_blank_string_rs::NonBlankString;
use std::str::FromStr;
use crate::modules::helm::chart::HelmChartScore;
pub fn grafana_helm_chart_score(ns: &str) -> HelmChartScore {
let values = format!(
r#"
rbac:
namespaced: true
sidecar:
dashboards:
enabled: true
"#
);
HelmChartScore {
namespace: Some(NonBlankString::from_str(ns).unwrap()),
release_name: NonBlankString::from_str("grafana").unwrap(),
chart_name: NonBlankString::from_str("oci://ghcr.io/grafana/helm-charts/grafana").unwrap(),
chart_version: None,
values_overrides: None,
values_yaml: Some(values.to_string()),
create_namespace: true,
install_only: true,
repository: None,
}
}

View File

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

View File

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

View File

@@ -38,15 +38,15 @@ impl KubePrometheusConfig {
node_exporter: false, node_exporter: false,
prometheus: true, prometheus: true,
kubernetes_service_monitors: true, kubernetes_service_monitors: true,
kubernetes_api_server: false, kubernetes_api_server: true,
kubelet: true, kubelet: true,
kube_controller_manager: false, kube_controller_manager: true,
kube_etcd: false, kube_etcd: true,
kube_proxy: false, kube_proxy: true,
kube_state_metrics: true, kube_state_metrics: true,
prometheus_operator: true, prometheus_operator: true,
core_dns: false, core_dns: true,
kube_scheduler: false, kube_scheduler: true,
alert_receiver_configs: vec![], alert_receiver_configs: vec![],
alert_rules: vec![], alert_rules: vec![],
additional_service_monitors: vec![], additional_service_monitors: vec![],

View File

@@ -68,11 +68,14 @@ pub fn kube_prometheus_helm_chart_score(
let mut values = format!( let mut values = format!(
r#" r#"
global:
rbac:
create: true
prometheus: prometheus:
enabled: {prometheus} enabled: {prometheus}
prometheusSpec: prometheusSpec:
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 500Mi memory: 500Mi
limits: limits:
@@ -118,7 +121,7 @@ defaultRules:
windowsMonitoring: windowsMonitoring:
enabled: {windows_monitoring} enabled: {windows_monitoring}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -127,13 +130,13 @@ windowsMonitoring:
grafana: grafana:
enabled: {grafana} enabled: {grafana}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
cpu: 200m cpu: 200m
memory: 250Mi memory: 250Mi
initChownData: initChownData:
resources: resources:
requests: requests:
cpu: 10m cpu: 10m
@@ -154,7 +157,7 @@ kubernetesServiceMonitors:
kubeApiServer: kubeApiServer:
enabled: {kubernetes_api_server} enabled: {kubernetes_api_server}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -163,7 +166,7 @@ kubeApiServer:
kubelet: kubelet:
enabled: {kubelet} enabled: {kubelet}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -172,7 +175,7 @@ kubelet:
kubeControllerManager: kubeControllerManager:
enabled: {kube_controller_manager} enabled: {kube_controller_manager}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -181,7 +184,7 @@ kubeControllerManager:
coreDns: coreDns:
enabled: {core_dns} enabled: {core_dns}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -190,7 +193,7 @@ coreDns:
kubeEtcd: kubeEtcd:
enabled: {kube_etcd} enabled: {kube_etcd}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -199,7 +202,7 @@ kubeEtcd:
kubeScheduler: kubeScheduler:
enabled: {kube_scheduler} enabled: {kube_scheduler}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -208,7 +211,7 @@ kubeScheduler:
kubeProxy: kubeProxy:
enabled: {kube_proxy} enabled: {kube_proxy}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -218,7 +221,7 @@ kubeStateMetrics:
enabled: {kube_state_metrics} enabled: {kube_state_metrics}
kube-state-metrics: kube-state-metrics:
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -227,7 +230,7 @@ kube-state-metrics:
nodeExporter: nodeExporter:
enabled: {node_exporter} enabled: {node_exporter}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -235,16 +238,16 @@ nodeExporter:
memory: 250Mi memory: 250Mi
prometheus-node-exporter: prometheus-node-exporter:
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
cpu: 200m cpu: 200m
memory: 250Mi memory: 250Mi
prometheusOperator: prometheusOperator:
enabled: {prometheus_operator} enabled: true
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -252,7 +255,7 @@ prometheusOperator:
memory: 200Mi memory: 200Mi
prometheusConfigReloader: prometheusConfigReloader:
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@@ -264,7 +267,7 @@ prometheusOperator:
limits: limits:
cpu: 10m cpu: 10m
memory: 100Mi memory: 100Mi
requests: requests:
cpu: 10m cpu: 10m
memory: 100Mi memory: 100Mi
patch: patch:
@@ -272,7 +275,7 @@ prometheusOperator:
limits: limits:
cpu: 10m cpu: 10m
memory: 100Mi memory: 100Mi
requests: requests:
cpu: 10m cpu: 10m
memory: 100Mi memory: 100Mi
"#, "#,

View File

@@ -2,7 +2,7 @@ use std::sync::{Arc, Mutex};
use serde::Serialize; use serde::Serialize;
use super::{helm::config::KubePrometheusConfig, prometheus::Prometheus}; use super::{helm::config::KubePrometheusConfig, prometheus::KubePrometheus};
use crate::{ use crate::{
modules::monitoring::kube_prometheus::types::ServiceMonitor, modules::monitoring::kube_prometheus::types::ServiceMonitor,
score::Score, score::Score,
@@ -15,8 +15,8 @@ use crate::{
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct HelmPrometheusAlertingScore { pub struct HelmPrometheusAlertingScore {
pub receivers: Vec<Box<dyn AlertReceiver<Prometheus>>>, pub receivers: Vec<Box<dyn AlertReceiver<KubePrometheus>>>,
pub rules: Vec<Box<dyn AlertRule<Prometheus>>>, pub rules: Vec<Box<dyn AlertRule<KubePrometheus>>>,
pub service_monitors: Vec<ServiceMonitor>, pub service_monitors: Vec<ServiceMonitor>,
} }
@@ -28,7 +28,7 @@ impl<T: Topology + HelmCommand + TenantManager> Score<T> for HelmPrometheusAlert
.expect("couldn't lock config") .expect("couldn't lock config")
.additional_service_monitors = self.service_monitors.clone(); .additional_service_monitors = self.service_monitors.clone();
Box::new(AlertingInterpret { Box::new(AlertingInterpret {
sender: Prometheus::new(), sender: KubePrometheus { config },
receivers: self.receivers.clone(), receivers: self.receivers.clone(),
rules: self.rules.clone(), rules: self.rules.clone(),
}) })

View File

@@ -10,7 +10,7 @@ use crate::{
modules::monitoring::alert_rule::prometheus_alert_rule::AlertManagerRuleGroup, modules::monitoring::alert_rule::prometheus_alert_rule::AlertManagerRuleGroup,
score, score,
topology::{ topology::{
HelmCommand, K8sAnywhereTopology, Topology, HelmCommand, Topology,
installable::Installable, installable::Installable,
oberservability::monitoring::{AlertReceiver, AlertRule, AlertSender}, oberservability::monitoring::{AlertReceiver, AlertRule, AlertSender},
tenant::TenantManager, tenant::TenantManager,
@@ -27,14 +27,14 @@ use super::{
}; };
#[async_trait] #[async_trait]
impl AlertSender for Prometheus { impl AlertSender for KubePrometheus {
fn name(&self) -> String { fn name(&self) -> String {
"HelmKubePrometheus".to_string() "HelmKubePrometheus".to_string()
} }
} }
#[async_trait] #[async_trait]
impl<T: Topology + HelmCommand + TenantManager> Installable<T> for Prometheus { impl<T: Topology + HelmCommand + TenantManager> Installable<T> for KubePrometheus {
async fn configure(&self, _inventory: &Inventory, topology: &T) -> Result<(), InterpretError> { async fn configure(&self, _inventory: &Inventory, topology: &T) -> Result<(), InterpretError> {
self.configure_with_topology(topology).await; self.configure_with_topology(topology).await;
Ok(()) Ok(())
@@ -51,11 +51,11 @@ impl<T: Topology + HelmCommand + TenantManager> Installable<T> for Prometheus {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Prometheus { pub struct KubePrometheus {
pub config: Arc<Mutex<KubePrometheusConfig>>, pub config: Arc<Mutex<KubePrometheusConfig>>,
} }
impl Prometheus { impl KubePrometheus {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
config: Arc::new(Mutex::new(KubePrometheusConfig::new())), config: Arc::new(Mutex::new(KubePrometheusConfig::new())),
@@ -75,7 +75,7 @@ impl Prometheus {
pub async fn install_receiver( pub async fn install_receiver(
&self, &self,
prometheus_receiver: &dyn PrometheusReceiver, prometheus_receiver: &dyn KubePrometheusReceiver,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
let prom_receiver = prometheus_receiver.configure_receiver().await; let prom_receiver = prometheus_receiver.configure_receiver().await;
debug!( debug!(
@@ -120,12 +120,12 @@ impl Prometheus {
} }
#[async_trait] #[async_trait]
pub trait PrometheusReceiver: Send + Sync + std::fmt::Debug { pub trait KubePrometheusReceiver: Send + Sync + std::fmt::Debug {
fn name(&self) -> String; fn name(&self) -> String;
async fn configure_receiver(&self) -> AlertManagerChannelConfig; async fn configure_receiver(&self) -> AlertManagerChannelConfig;
} }
impl Serialize for Box<dyn AlertReceiver<Prometheus>> { impl Serialize for Box<dyn AlertReceiver<KubePrometheus>> {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where where
S: serde::Serializer, S: serde::Serializer,
@@ -134,19 +134,19 @@ impl Serialize for Box<dyn AlertReceiver<Prometheus>> {
} }
} }
impl Clone for Box<dyn AlertReceiver<Prometheus>> { impl Clone for Box<dyn AlertReceiver<KubePrometheus>> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
self.clone_box() self.clone_box()
} }
} }
#[async_trait] #[async_trait]
pub trait PrometheusRule: Send + Sync + std::fmt::Debug { pub trait KubePrometheusRule: Send + Sync + std::fmt::Debug {
fn name(&self) -> String; fn name(&self) -> String;
async fn configure_rule(&self) -> AlertManagerAdditionalPromRules; async fn configure_rule(&self) -> AlertManagerAdditionalPromRules;
} }
impl Serialize for Box<dyn AlertRule<Prometheus>> { impl Serialize for Box<dyn AlertRule<KubePrometheus>> {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where where
S: serde::Serializer, S: serde::Serializer,
@@ -155,7 +155,7 @@ impl Serialize for Box<dyn AlertRule<Prometheus>> {
} }
} }
impl Clone for Box<dyn AlertRule<Prometheus>> { impl Clone for Box<dyn AlertRule<KubePrometheus>> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
self.clone_box() self.clone_box()
} }

View File

@@ -212,7 +212,7 @@ pub struct ServiceMonitor {
pub name: String, pub name: String,
// # Additional labels to set used for the ServiceMonitorSelector. Together with standard labels from the chart // # Additional labels to set used for the ServiceMonitorSelector. Together with standard labels from the chart
pub additional_labels: Option<Mapping>, pub additional_labels: Option<HashMap<String, String>>,
// # Service label for use in assembling a job name of the form <label value>-<port> // # Service label for use in assembling a job name of the form <label value>-<port>
// # If no label is specified, the service name is used. // # If no label is specified, the service name is used.
@@ -240,7 +240,7 @@ pub struct ServiceMonitor {
// any: bool, // any: bool,
// # Explicit list of namespace names to select // # Explicit list of namespace names to select
// matchNames: Vec, // matchNames: Vec,
pub namespace_selector: Option<Mapping>, pub namespace_selector: Option<NamespaceSelector>,
// # Endpoints of the selected service to be monitored // # Endpoints of the selected service to be monitored
pub endpoints: Vec<ServiceMonitorEndpoint>, pub endpoints: Vec<ServiceMonitorEndpoint>,
@@ -250,6 +250,13 @@ pub struct ServiceMonitor {
pub fallback_scrape_protocol: Option<String>, pub fallback_scrape_protocol: Option<String>,
} }
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct NamespaceSelector {
pub any: bool,
pub match_names: Vec<String>,
}
impl Default for ServiceMonitor { impl Default for ServiceMonitor {
fn default() -> Self { fn default() -> Self {
Self { Self {

View File

@@ -1,4 +1,7 @@
pub mod alert_channel; pub mod alert_channel;
pub mod alert_rule; pub mod alert_rule;
pub mod application_monitoring;
pub mod grafana;
pub mod kube_prometheus; pub mod kube_prometheus;
pub mod ntfy; pub mod ntfy;
pub mod prometheus;

View File

@@ -3,7 +3,7 @@ use std::str::FromStr;
use crate::modules::helm::chart::{HelmChartScore, HelmRepository}; use crate::modules::helm::chart::{HelmChartScore, HelmRepository};
pub fn ntfy_helm_chart_score(namespace: String) -> HelmChartScore { pub fn ntfy_helm_chart_score(namespace: String, host: String) -> HelmChartScore {
let values = format!( let values = format!(
r#" r#"
replicaCount: 1 replicaCount: 1
@@ -28,12 +28,12 @@ service:
port: 80 port: 80
ingress: ingress:
enabled: false enabled: true
# annotations: # annotations:
# kubernetes.io/ingress.class: nginx # kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true" # kubernetes.io/tls-acme: "true"
hosts: hosts:
- host: ntfy.host.com - host: {host}
paths: paths:
- path: / - path: /
pathType: ImplementationSpecific pathType: ImplementationSpecific

View File

@@ -17,6 +17,7 @@ use crate::{
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct NtfyScore { pub struct NtfyScore {
pub namespace: String, pub namespace: String,
pub host: String,
} }
impl<T: Topology + HelmCommand + K8sclient> Score<T> for NtfyScore { impl<T: Topology + HelmCommand + K8sclient> Score<T> for NtfyScore {
@@ -126,7 +127,7 @@ impl<T: Topology + HelmCommand + K8sclient> Interpret<T> for NtfyInterpret {
inventory: &Inventory, inventory: &Inventory,
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
ntfy_helm_chart_score(self.score.namespace.clone()) ntfy_helm_chart_score(self.score.namespace.clone(), self.score.host.clone())
.create_interpret() .create_interpret()
.execute(inventory, topology) .execute(inventory, topology)
.await?; .await?;

View File

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

View File

@@ -0,0 +1,47 @@
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use non_blank_string_rs::NonBlankString;
use crate::modules::{
helm::chart::HelmChartScore, monitoring::prometheus::prometheus_config::PrometheusConfig,
};
pub fn prometheus_helm_chart_score(config: Arc<Mutex<PrometheusConfig>>) -> HelmChartScore {
let config = config.lock().unwrap();
let ns = config.namespace.clone().unwrap();
let values = format!(
r#"
rbac:
create: true
kube-state-metrics:
enabled: false
nodeExporter:
enabled: false
alertmanager:
enabled: false
pushgateway:
enabled: false
server:
serviceAccount:
create: false
rbac:
create: true
fullnameOverride: prometheus-{ns}
"#
);
HelmChartScore {
namespace: Some(NonBlankString::from_str(&config.namespace.clone().unwrap()).unwrap()),
release_name: NonBlankString::from_str("prometheus").unwrap(),
chart_name: NonBlankString::from_str(
"oci://ghcr.io/prometheus-community/charts/prometheus",
)
.unwrap(),
chart_version: None,
values_overrides: None,
values_yaml: Some(values.to_string()),
create_namespace: true,
install_only: true,
repository: None,
}
}

View File

@@ -0,0 +1,3 @@
pub mod helm;
pub mod prometheus;
pub mod prometheus_config;

View File

@@ -0,0 +1,190 @@
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use log::{debug, error};
use serde::Serialize;
use crate::{
interpret::{InterpretError, Outcome},
inventory::Inventory,
modules::monitoring::{
alert_rule::prometheus_alert_rule::AlertManagerRuleGroup,
grafana::helm::helm_grafana::grafana_helm_chart_score,
kube_prometheus::types::{AlertManagerAdditionalPromRules, AlertManagerChannelConfig},
},
score::Score,
topology::{
HelmCommand, Topology,
installable::Installable,
oberservability::monitoring::{AlertReceiver, AlertRule, AlertSender},
tenant::TenantManager,
},
};
use super::{
helm::prometheus_helm::prometheus_helm_chart_score, prometheus_config::PrometheusConfig,
};
#[derive(Debug)]
pub struct Prometheus {
pub config: Arc<Mutex<PrometheusConfig>>,
}
#[async_trait]
impl AlertSender for Prometheus {
fn name(&self) -> String {
"Prometheus".to_string()
}
}
impl Prometheus {
pub fn new() -> Self {
Self {
config: Arc::new(Mutex::new(PrometheusConfig::new())),
}
}
pub async fn configure_with_topology<T: TenantManager>(&self, topology: &T) {
let ns = topology
.get_tenant_config()
.await
.map(|cfg| cfg.name.clone())
.unwrap_or_else(|| "monitoring".to_string());
error!("This must be refactored, see comments in pr #74");
debug!("NS: {}", ns);
self.config.lock().unwrap().namespace = Some(ns);
}
pub async fn install_receiver(
&self,
prometheus_receiver: &dyn PrometheusReceiver,
) -> Result<Outcome, InterpretError> {
let prom_receiver = prometheus_receiver.configure_receiver().await;
debug!(
"adding alert receiver to prometheus config: {:#?}",
&prom_receiver
);
let mut config = self.config.lock().unwrap();
config.alert_receiver_configs.push(prom_receiver);
let prom_receiver_name = prometheus_receiver.name();
debug!("installed alert receiver {}", &prom_receiver_name);
Ok(Outcome::success(format!(
"Sucessfully installed receiver {}",
prom_receiver_name
)))
}
pub async fn install_rule(
&self,
prometheus_rule: &AlertManagerRuleGroup,
) -> Result<Outcome, InterpretError> {
let prometheus_rule = prometheus_rule.configure_rule().await;
let mut config = self.config.lock().unwrap();
config.alert_rules.push(prometheus_rule.clone());
Ok(Outcome::success(format!(
"Successfully installed alert rule: {:#?},",
prometheus_rule
)))
}
pub async fn install_prometheus<T: Topology + HelmCommand + Send + Sync>(
&self,
inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
prometheus_helm_chart_score(self.config.clone())
.create_interpret()
.execute(inventory, topology)
.await
}
pub async fn install_grafana<T: Topology + HelmCommand + Send + Sync>(
&self,
inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
let namespace = {
let config = self.config.lock().unwrap();
config.namespace.clone()
};
if let Some(ns) = namespace.as_deref() {
grafana_helm_chart_score(ns)
.create_interpret()
.execute(inventory, topology)
.await
} else {
Err(InterpretError::new(format!(
"could not install grafana, missing namespace",
)))
}
}
}
#[async_trait]
impl<T: Topology + HelmCommand + TenantManager> Installable<T> for Prometheus {
async fn configure(&self, _inventory: &Inventory, topology: &T) -> Result<(), InterpretError> {
self.configure_with_topology(topology).await;
Ok(())
}
async fn ensure_installed(
&self,
inventory: &Inventory,
topology: &T,
) -> Result<(), InterpretError> {
self.install_prometheus(inventory, topology).await?;
let install_grafana = {
let config = self.config.lock().unwrap();
config.grafana
};
if install_grafana {
self.install_grafana(inventory, topology).await?;
}
Ok(())
}
}
#[async_trait]
pub trait PrometheusReceiver: Send + Sync + std::fmt::Debug {
fn name(&self) -> String;
async fn configure_receiver(&self) -> AlertManagerChannelConfig;
}
impl Serialize for Box<dyn AlertReceiver<Prometheus>> {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
todo!()
}
}
impl Clone for Box<dyn AlertReceiver<Prometheus>> {
fn clone(&self) -> Self {
self.clone_box()
}
}
#[async_trait]
pub trait PrometheusRule: Send + Sync + std::fmt::Debug {
fn name(&self) -> String;
async fn configure_rule(&self) -> AlertManagerAdditionalPromRules;
}
impl Serialize for Box<dyn AlertRule<Prometheus>> {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
todo!()
}
}
impl Clone for Box<dyn AlertRule<Prometheus>> {
fn clone(&self) -> Self {
self.clone_box()
}
}

View File

@@ -0,0 +1,34 @@
use crate::modules::monitoring::kube_prometheus::types::{
AlertManagerAdditionalPromRules, AlertManagerChannelConfig, ServiceMonitor,
};
#[derive(Debug)]
pub struct PrometheusConfig {
pub namespace: Option<String>,
pub default_rules: bool,
pub alert_manager: bool,
pub node_exporter: bool,
pub kube_state_metrics: bool,
pub grafana: bool,
pub prometheus_pushgateway: bool,
pub alert_receiver_configs: Vec<AlertManagerChannelConfig>,
pub alert_rules: Vec<AlertManagerAdditionalPromRules>,
pub additional_service_monitors: Vec<ServiceMonitor>,
}
impl PrometheusConfig {
pub fn new() -> Self {
Self {
namespace: None,
default_rules: true,
alert_manager: true,
node_exporter: false,
kube_state_metrics: false,
grafana: true,
prometheus_pushgateway: false,
alert_receiver_configs: vec![],
alert_rules: vec![],
additional_service_monitors: vec![],
}
}
}

View File

@@ -13,6 +13,11 @@ harmony_tui = { path = "../harmony_tui", optional = true }
inquire.workspace = true inquire.workspace = true
tokio.workspace = true tokio.workspace = true
env_logger.workspace = true env_logger.workspace = true
console = "0.16.0"
indicatif = "0.18.0"
lazy_static = "1.5.0"
log.workspace = true
indicatif-log-bridge = "0.2.3"
[features] [features]

View File

@@ -0,0 +1,116 @@
use harmony::instrumentation::{self, HarmonyEvent};
use indicatif::{MultiProgress, ProgressBar};
use indicatif_log_bridge::LogWrapper;
use std::{
collections::{HashMap, hash_map},
sync::{Arc, Mutex},
};
use crate::progress;
pub fn init() -> tokio::task::JoinHandle<()> {
configure_logger();
let handle = tokio::spawn(handle_events());
loop {
if instrumentation::instrument(HarmonyEvent::HarmonyStarted).is_ok() {
break;
}
}
handle
}
fn configure_logger() {
let logger =
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).build();
let level = logger.filter();
let multi = MultiProgress::new();
LogWrapper::new(multi.clone(), logger).try_init().unwrap();
log::set_max_level(level);
}
async fn handle_events() {
instrumentation::subscribe("Harmony CLI Logger", {
let sections: Arc<Mutex<HashMap<String, MultiProgress>>> =
Arc::new(Mutex::new(HashMap::new()));
let progress_bars: Arc<Mutex<HashMap<String, ProgressBar>>> =
Arc::new(Mutex::new(HashMap::new()));
move |event| {
let sections_clone = Arc::clone(&sections);
let progress_bars_clone = Arc::clone(&progress_bars);
async move {
let mut sections = sections_clone.lock().unwrap();
let mut progress_bars = progress_bars_clone.lock().unwrap();
match event {
HarmonyEvent::HarmonyStarted => {}
HarmonyEvent::PrepareTopologyStarted { topology: name } => {
let section = progress::new_section(format!(
"{} Preparing environment: {name}...",
crate::theme::EMOJI_TOPOLOGY,
));
(*sections).insert(name, section);
}
HarmonyEvent::TopologyPrepared {
topology: name,
outcome,
} => {
let section = (*sections).get(&name).unwrap();
let progress = progress::add_spinner(section, "".into());
match outcome.status {
harmony::interpret::InterpretStatus::SUCCESS => {
progress::success(section, Some(progress), outcome.message);
}
harmony::interpret::InterpretStatus::FAILURE => {
progress::error(section, Some(progress), outcome.message);
}
harmony::interpret::InterpretStatus::RUNNING => todo!(),
harmony::interpret::InterpretStatus::QUEUED => todo!(),
harmony::interpret::InterpretStatus::BLOCKED => todo!(),
harmony::interpret::InterpretStatus::NOOP => {
progress::skip(section, Some(progress), outcome.message);
}
}
}
HarmonyEvent::InterpretExecutionStarted {
interpret: name,
topology,
message,
} => {
let section = (*sections).get(&topology).unwrap();
let progress_bar = progress::add_spinner(section, message);
(*progress_bars).insert(name, progress_bar);
}
HarmonyEvent::InterpretExecutionFinished {
topology,
interpret: name,
outcome,
} => {
let section = (*sections).get(&topology).unwrap();
let progress_bar = (*progress_bars).get(&name).cloned();
let _ = section.clear();
match outcome {
Ok(outcome) => {
progress::success(section, progress_bar, outcome.message);
}
Err(err) => {
progress::error(section, progress_bar, err.to_string());
}
}
(*progress_bars).remove(&name);
}
}
true
}
}
})
.await;
}

View File

@@ -4,8 +4,13 @@ use harmony;
use harmony::{score::Score, topology::Topology}; use harmony::{score::Score, topology::Topology};
use inquire::Confirm; use inquire::Confirm;
pub mod cli_logger; // FIXME: Don't make me pub
pub mod progress;
pub mod theme;
#[cfg(feature = "tui")] #[cfg(feature = "tui")]
use harmony_tui; use harmony_tui;
use log::debug;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
@@ -134,7 +139,7 @@ pub async fn init<T: Topology + Send + Sync + 'static>(
// Run filtered scores // Run filtered scores
for s in scores_vec { for s in scores_vec {
println!("Running: {}", s.name()); debug!("Running: {}", s.name());
maestro.interpret(s).await?; maestro.interpret(s).await?;
} }

View File

@@ -0,0 +1,50 @@
use std::time::Duration;
use indicatif::{MultiProgress, ProgressBar};
pub fn new_section(title: String) -> MultiProgress {
let multi_progress = MultiProgress::new();
let _ = multi_progress.println(title);
multi_progress
}
pub fn add_spinner(multi_progress: &MultiProgress, message: String) -> ProgressBar {
let progress = multi_progress.add(ProgressBar::new_spinner());
progress.set_style(crate::theme::SPINNER_STYLE.clone());
progress.set_message(message);
progress.enable_steady_tick(Duration::from_millis(100));
progress
}
pub fn success(multi_progress: &MultiProgress, progress: Option<ProgressBar>, message: String) {
if let Some(progress) = progress {
multi_progress.remove(&progress)
}
let progress = multi_progress.add(ProgressBar::new_spinner());
progress.set_style(crate::theme::SUCCESS_SPINNER_STYLE.clone());
progress.finish_with_message(message);
}
pub fn error(multi_progress: &MultiProgress, progress: Option<ProgressBar>, message: String) {
if let Some(progress) = progress {
multi_progress.remove(&progress)
}
let progress = multi_progress.add(ProgressBar::new_spinner());
progress.set_style(crate::theme::ERROR_SPINNER_STYLE.clone());
progress.finish_with_message(message);
}
pub fn skip(multi_progress: &MultiProgress, progress: Option<ProgressBar>, message: String) {
if let Some(progress) = progress {
multi_progress.remove(&progress)
}
let progress = multi_progress.add(ProgressBar::new_spinner());
progress.set_style(crate::theme::SKIP_SPINNER_STYLE.clone());
progress.finish_with_message(message);
}

26
harmony_cli/src/theme.rs Normal file
View File

@@ -0,0 +1,26 @@
use console::Emoji;
use indicatif::ProgressStyle;
use lazy_static::lazy_static;
pub static EMOJI_HARMONY: Emoji<'_, '_> = Emoji("🎼", "");
pub static EMOJI_SUCCESS: Emoji<'_, '_> = Emoji("", "");
pub static EMOJI_SKIP: Emoji<'_, '_> = Emoji("⏭️", "");
pub static EMOJI_ERROR: Emoji<'_, '_> = Emoji("⚠️", "");
pub static EMOJI_DEPLOY: Emoji<'_, '_> = Emoji("🚀", "");
pub static EMOJI_TOPOLOGY: Emoji<'_, '_> = Emoji("📦", "");
lazy_static! {
pub static ref SPINNER_STYLE: ProgressStyle = ProgressStyle::default_spinner()
.template(" {spinner:.green} {msg}")
.unwrap()
.tick_strings(&["", "", "", "", "", "", "", "", "", ""]);
pub static ref SUCCESS_SPINNER_STYLE: ProgressStyle = SPINNER_STYLE
.clone()
.tick_strings(&[format!("{}", EMOJI_SUCCESS).as_str()]);
pub static ref SKIP_SPINNER_STYLE: ProgressStyle = SPINNER_STYLE
.clone()
.tick_strings(&[format!("{}", EMOJI_SKIP).as_str()]);
pub static ref ERROR_SPINNER_STYLE: ProgressStyle = SPINNER_STYLE
.clone()
.tick_strings(&[format!("{}", EMOJI_ERROR).as_str()]);
}

View File

@@ -15,3 +15,9 @@ current_platform = "0.2.0"
futures-util = "0.3.31" futures-util = "0.3.31"
serde_json = "1.0.140" serde_json = "1.0.140"
cargo_metadata = "0.20.0" cargo_metadata = "0.20.0"
indicatif = "0.18.0"
console = "0.16.0"
lazy_static = "1.5.0"
once_cell = "1.21.3"
harmony_cli = { path = "../harmony_cli" }
indicatif-log-bridge = "0.2.3"

View File

@@ -0,0 +1,112 @@
use indicatif::{MultiProgress, ProgressBar};
use indicatif_log_bridge::LogWrapper;
use log::error;
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use crate::instrumentation::{self, HarmonyComposerEvent};
pub fn init() -> tokio::task::JoinHandle<()> {
configure_logger();
let handle = tokio::spawn(handle_events());
loop {
if instrumentation::instrument(HarmonyComposerEvent::HarmonyComposerStarted).is_ok() {
break;
}
}
handle
}
fn configure_logger() {
let logger =
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).build();
let level = logger.filter();
let multi = MultiProgress::new();
LogWrapper::new(multi.clone(), logger).try_init().unwrap();
log::set_max_level(level);
}
pub async fn handle_events() {
const PROGRESS_SETUP: &str = "project-initialization";
const PROGRESS_DEPLOYMENT: &str = "deployment";
instrumentation::subscribe("Harmony Composer Logger", {
let progresses: Arc<Mutex<HashMap<String, MultiProgress>>> =
Arc::new(Mutex::new(HashMap::new()));
let compilation_progress = Arc::new(Mutex::new(None::<ProgressBar>));
move |event| {
let progresses_clone = Arc::clone(&progresses);
let compilation_progress_clone = Arc::clone(&compilation_progress);
async move {
let mut progresses_guard = progresses_clone.lock().unwrap();
let mut compilation_progress_guard = compilation_progress_clone.lock().unwrap();
match event {
HarmonyComposerEvent::HarmonyComposerStarted => {}
HarmonyComposerEvent::ProjectInitializationStarted => {
let multi_progress = harmony_cli::progress::new_section(format!(
"{} Initializing Harmony project...",
harmony_cli::theme::EMOJI_HARMONY,
));
(*progresses_guard).insert(PROGRESS_SETUP.to_string(), multi_progress);
}
HarmonyComposerEvent::ProjectInitialized => println!("\n"),
HarmonyComposerEvent::ProjectCompilationStarted { details } => {
let initialization_progress =
(*progresses_guard).get(PROGRESS_SETUP).unwrap();
let _ = initialization_progress.clear();
let progress =
harmony_cli::progress::add_spinner(initialization_progress, details);
*compilation_progress_guard = Some(progress);
}
HarmonyComposerEvent::ProjectCompiled => {
let initialization_progress =
(*progresses_guard).get(PROGRESS_SETUP).unwrap();
harmony_cli::progress::success(
initialization_progress,
(*compilation_progress_guard).take(),
"project compiled".to_string(),
);
}
HarmonyComposerEvent::ProjectCompilationFailed { details } => {
let initialization_progress =
(*progresses_guard).get(PROGRESS_SETUP).unwrap();
harmony_cli::progress::error(
initialization_progress,
(*compilation_progress_guard).take(),
"failed to compile project".to_string(),
);
error!("{details}");
}
HarmonyComposerEvent::DeploymentStarted { target } => {
let multi_progress = harmony_cli::progress::new_section(format!(
"{} Starting deployment to {target}...\n\n",
harmony_cli::theme::EMOJI_DEPLOY
));
(*progresses_guard).insert(PROGRESS_DEPLOYMENT.to_string(), multi_progress);
}
HarmonyComposerEvent::DeploymentCompleted { details } => println!("\n"),
HarmonyComposerEvent::Shutdown => {
for (_, progresses) in (*progresses_guard).iter() {
progresses.clear().unwrap();
}
return false;
}
}
true
}
}
})
.await
}

View File

@@ -0,0 +1,53 @@
use log::debug;
use once_cell::sync::Lazy;
use tokio::sync::broadcast;
#[derive(Debug, Clone)]
pub enum HarmonyComposerEvent {
HarmonyComposerStarted,
ProjectInitializationStarted,
ProjectInitialized,
ProjectCompilationStarted { details: String },
ProjectCompiled,
ProjectCompilationFailed { details: String },
DeploymentStarted { target: String },
DeploymentCompleted { details: String },
Shutdown,
}
static HARMONY_COMPOSER_EVENT_BUS: Lazy<broadcast::Sender<HarmonyComposerEvent>> =
Lazy::new(|| {
// TODO: Adjust channel capacity
let (tx, _rx) = broadcast::channel(16);
tx
});
pub fn instrument(event: HarmonyComposerEvent) -> Result<(), &'static str> {
match HARMONY_COMPOSER_EVENT_BUS.send(event) {
Ok(_) => Ok(()),
Err(_) => Err("send error: no subscribers"),
}
}
pub async fn subscribe<F, Fut>(name: &str, mut handler: F)
where
F: FnMut(HarmonyComposerEvent) -> Fut + Send + 'static,
Fut: Future<Output = bool> + Send,
{
let mut rx = HARMONY_COMPOSER_EVENT_BUS.subscribe();
debug!("[{name}] Service started. Listening for events...");
loop {
match rx.recv().await {
Ok(event) => {
if !handler(event).await {
debug!("[{name}] Handler requested exit.");
break;
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
debug!("[{name}] Lagged behind by {n} messages.");
}
Err(_) => break,
}
}
}

View File

@@ -7,12 +7,16 @@ use bollard::secret::HostConfig;
use cargo_metadata::{Artifact, Message, MetadataCommand}; use cargo_metadata::{Artifact, Message, MetadataCommand};
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use futures_util::StreamExt; use futures_util::StreamExt;
use log::info; use instrumentation::HarmonyComposerEvent;
use log::{debug, info, log_enabled};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use tokio::fs; use tokio::fs;
mod harmony_composer_logger;
mod instrumentation;
#[derive(Parser)] #[derive(Parser)]
#[command(version, about, long_about = None, flatten_help = true, propagate_version = true)] #[command(version, about, long_about = None, flatten_help = true, propagate_version = true)]
struct GlobalArgs { struct GlobalArgs {
@@ -66,13 +70,15 @@ struct AllArgs {
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
env_logger::init(); let hc_logger_handle = harmony_composer_logger::init();
let cli_args = GlobalArgs::parse(); let cli_args = GlobalArgs::parse();
let harmony_path = Path::new(&cli_args.harmony_path) let harmony_path = Path::new(&cli_args.harmony_path)
.try_exists() .try_exists()
.expect("couldn't check if path exists"); .expect("couldn't check if path exists");
instrumentation::instrument(HarmonyComposerEvent::ProjectInitializationStarted).unwrap();
let harmony_bin_path: PathBuf = match harmony_path { let harmony_bin_path: PathBuf = match harmony_path {
true => { true => {
compile_harmony( compile_harmony(
@@ -85,6 +91,8 @@ async fn main() {
false => todo!("implement autodetect code"), false => todo!("implement autodetect code"),
}; };
instrumentation::instrument(HarmonyComposerEvent::ProjectInitialized).unwrap();
match cli_args.command { match cli_args.command {
Some(command) => match command { Some(command) => match command {
Commands::Check(args) => { Commands::Check(args) => {
@@ -98,7 +106,9 @@ async fn main() {
.expect("couldn't check if path exists") .expect("couldn't check if path exists")
{ {
true => (), true => (),
false => todo!("implement couldn't find path logic"), false => todo!(
"path {check_script_str} not found. Other paths currently unsupported."
),
}; };
let check_output = Command::new(check_script) let check_output = Command::new(check_script)
@@ -114,19 +124,31 @@ async fn main() {
} }
Commands::Deploy(args) => { Commands::Deploy(args) => {
let deploy = if args.staging { let deploy = if args.staging {
instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted {
target: "staging".to_string(),
})
.unwrap();
todo!("implement staging deployment") todo!("implement staging deployment")
} else if args.prod { } else if args.prod {
instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted {
target: "prod".to_string(),
})
.unwrap();
todo!("implement prod deployment") todo!("implement prod deployment")
} else { } else {
instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted {
target: "dev".to_string(),
})
.unwrap();
Command::new(harmony_bin_path).arg("-y").arg("-a").spawn() Command::new(harmony_bin_path).arg("-y").arg("-a").spawn()
} }
.expect("failed to run harmony deploy"); .expect("failed to run harmony deploy");
let deploy_output = deploy.wait_with_output().unwrap(); let deploy_output = deploy.wait_with_output().unwrap();
println!( instrumentation::instrument(HarmonyComposerEvent::DeploymentCompleted {
"deploy output: {}", details: String::from_utf8(deploy_output.stdout).unwrap(),
String::from_utf8(deploy_output.stdout).expect("couldn't parse from utf8") })
); .unwrap();
} }
Commands::All(_args) => todo!( Commands::All(_args) => todo!(
"take all previous match arms and turn them into separate functions, and call them all one after the other" "take all previous match arms and turn them into separate functions, and call them all one after the other"
@@ -135,6 +157,10 @@ async fn main() {
}, },
None => todo!("run interactively, ask for info on CLI"), None => todo!("run interactively, ask for info on CLI"),
} }
instrumentation::instrument(HarmonyComposerEvent::Shutdown).unwrap();
let _ = tokio::try_join!(hc_logger_handle);
} }
#[derive(Clone, Debug, clap::ValueEnum)] #[derive(Clone, Debug, clap::ValueEnum)]
@@ -155,6 +181,7 @@ async fn compile_harmony(
let cargo_exists = Command::new("which") let cargo_exists = Command::new("which")
.arg("cargo") .arg("cargo")
.stdout(Stdio::null())
.status() .status()
.expect("couldn't get `which cargo` status") .expect("couldn't get `which cargo` status")
.success(); .success();
@@ -163,17 +190,32 @@ async fn compile_harmony(
Some(m) => m, Some(m) => m,
None => { None => {
if cargo_exists { if cargo_exists {
return compile_cargo(platform, harmony_location).await; CompileMethod::LocalCargo
} else { } else {
return compile_docker(platform, harmony_location).await; CompileMethod::Docker
} }
} }
}; };
match method { let path = match method {
CompileMethod::LocalCargo => return compile_cargo(platform, harmony_location).await, CompileMethod::LocalCargo => {
CompileMethod::Docker => return compile_docker(platform, harmony_location).await, instrumentation::instrument(HarmonyComposerEvent::ProjectCompilationStarted {
details: "compiling project with cargo".to_string(),
})
.unwrap();
compile_cargo(platform, harmony_location).await
}
CompileMethod::Docker => {
instrumentation::instrument(HarmonyComposerEvent::ProjectCompilationStarted {
details: "compiling project with docker".to_string(),
})
.unwrap();
compile_docker(platform, harmony_location).await
}
}; };
instrumentation::instrument(HarmonyComposerEvent::ProjectCompiled).unwrap();
path
} }
// TODO: make sure this works with cargo workspaces // TODO: make sure this works with cargo workspaces
@@ -183,6 +225,12 @@ async fn compile_cargo(platform: String, harmony_location: String) -> PathBuf {
.exec() .exec()
.unwrap(); .unwrap();
let stderr = if log_enabled!(log::Level::Debug) {
Stdio::inherit()
} else {
Stdio::piped()
};
let mut cargo_build = Command::new("cargo") let mut cargo_build = Command::new("cargo")
.current_dir(&harmony_location) .current_dir(&harmony_location)
.args(vec![ .args(vec![
@@ -192,6 +240,7 @@ async fn compile_cargo(platform: String, harmony_location: String) -> PathBuf {
"--message-format=json-render-diagnostics", "--message-format=json-render-diagnostics",
]) ])
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(stderr)
.spawn() .spawn()
.expect("run cargo command failed"); .expect("run cargo command failed");
@@ -207,18 +256,20 @@ async fn compile_cargo(platform: String, harmony_location: String) -> PathBuf {
.expect("failed to get root package") .expect("failed to get root package")
.manifest_path .manifest_path
{ {
println!("{:?}", artifact); debug!("{:?}", artifact);
artifacts.push(artifact); artifacts.push(artifact);
} }
} }
Message::BuildScriptExecuted(_script) => (), Message::BuildScriptExecuted(_script) => (),
Message::BuildFinished(finished) => { Message::BuildFinished(finished) => {
println!("{:?}", finished); debug!("{:?}", finished);
} }
_ => (), // Unknown message _ => (), // Unknown message
} }
} }
cargo_build.wait().expect("run cargo command failed");
let bin = artifacts let bin = artifacts
.last() .last()
.expect("no binaries built") .expect("no binaries built")
@@ -234,7 +285,8 @@ async fn compile_cargo(platform: String, harmony_location: String) -> PathBuf {
bin_out = PathBuf::from(format!("{}/harmony", harmony_location)); bin_out = PathBuf::from(format!("{}/harmony", harmony_location));
let _copy_res = fs::copy(&bin, &bin_out).await; let _copy_res = fs::copy(&bin, &bin_out).await;
} }
return bin_out;
bin_out
} }
async fn compile_docker(platform: String, harmony_location: String) -> PathBuf { async fn compile_docker(platform: String, harmony_location: String) -> PathBuf {

View File

@@ -2,7 +2,7 @@ mod downloadable_asset;
use downloadable_asset::*; use downloadable_asset::*;
use kube::Client; use kube::Client;
use log::{debug, info, warn}; use log::{debug, warn};
use std::path::PathBuf; use std::path::PathBuf;
const K3D_BIN_FILE_NAME: &str = "k3d"; const K3D_BIN_FILE_NAME: &str = "k3d";
@@ -90,7 +90,7 @@ impl K3d {
let latest_release = self.get_latest_release_tag().await.unwrap(); let latest_release = self.get_latest_release_tag().await.unwrap();
let release_binary = self.get_binary_for_current_platform(latest_release).await; let release_binary = self.get_binary_for_current_platform(latest_release).await;
info!("Foudn K3d binary to install : {release_binary:#?}"); debug!("Foudn K3d binary to install : {release_binary:#?}");
release_binary.download_to_path(self.base_dir.clone()).await release_binary.download_to_path(self.base_dir.clone()).await
} }
@@ -175,7 +175,7 @@ impl K3d {
Err(_) => return Err("Could not get cluster_name, cannot initialize".to_string()), Err(_) => return Err("Could not get cluster_name, cannot initialize".to_string()),
}; };
info!("Initializing k3d cluster '{}'", cluster_name); debug!("Initializing k3d cluster '{}'", cluster_name);
self.create_cluster(cluster_name)?; self.create_cluster(cluster_name)?;
self.create_kubernetes_client().await self.create_kubernetes_client().await
@@ -205,7 +205,7 @@ impl K3d {
/// - `Err(String)` - Error message if any step failed /// - `Err(String)` - Error message if any step failed
pub async fn ensure_installed(&self) -> Result<Client, String> { pub async fn ensure_installed(&self) -> Result<Client, String> {
if !self.is_installed() { if !self.is_installed() {
info!("K3d is not installed, downloading latest release"); debug!("K3d is not installed, downloading latest release");
self.download_latest_release() self.download_latest_release()
.await .await
.map_err(|e| format!("Failed to download k3d: {}", e))?; .map_err(|e| format!("Failed to download k3d: {}", e))?;
@@ -216,13 +216,13 @@ impl K3d {
} }
if !self.is_cluster_initialized() { if !self.is_cluster_initialized() {
info!("Cluster is not initialized, initializing now"); debug!("Cluster is not initialized, initializing now");
return self.initialize_cluster().await; return self.initialize_cluster().await;
} }
self.start_cluster().await?; self.start_cluster().await?;
info!("K3d and cluster are already properly set up"); debug!("K3d and cluster are already properly set up");
self.create_kubernetes_client().await self.create_kubernetes_client().await
} }
@@ -325,12 +325,12 @@ impl K3d {
return Err(format!("Failed to create cluster: {}", stderr)); return Err(format!("Failed to create cluster: {}", stderr));
} }
info!("Successfully created k3d cluster '{}'", cluster_name); debug!("Successfully created k3d cluster '{}'", cluster_name);
Ok(()) Ok(())
} }
async fn create_kubernetes_client(&self) -> Result<Client, String> { async fn create_kubernetes_client(&self) -> Result<Client, String> {
warn!("TODO this method is way too dumb, it should make sure that the client is connected to the k3d cluster actually represented by this instance, not just any default client"); // TODO: Connect the client to the right k3d cluster (see https://git.nationtech.io/NationTech/harmony/issues/92)
Client::try_default() Client::try_default()
.await .await
.map_err(|e| format!("Failed to create Kubernetes client: {}", e)) .map_err(|e| format!("Failed to create Kubernetes client: {}", e))
@@ -352,7 +352,7 @@ impl K3d {
return Err(format!("Failed to start cluster: {}", stderr)); return Err(format!("Failed to start cluster: {}", stderr));
} }
info!("Successfully started k3d cluster '{}'", cluster_name); debug!("Successfully started k3d cluster '{}'", cluster_name);
Ok(()) Ok(())
} }
} }

View File

@@ -22,4 +22,4 @@ tokio-util = { version = "0.7.13", features = [ "codec" ] }
tokio-stream = "0.1.17" tokio-stream = "0.1.17"
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.4.1" pretty_assertions.workspace = true