Compare commits

..

3 Commits

Author SHA1 Message Date
tahahawa
670b701f6a use figment and try to make an "upgradeable firewall" 2025-07-16 02:12:58 -04:00
tahahawa
1eaae2016a WIP: Create Upgradeable trait 2025-07-15 00:16:57 -04:00
tahahawa
c4f4a58dcf Add new fields for OPNSense 25.1 2025-07-12 01:05:21 -04:00
140 changed files with 1253 additions and 5070 deletions

View File

@@ -9,7 +9,7 @@ jobs:
check: check:
runs-on: docker runs-on: docker
container: container:
image: hub.nationtech.io/harmony/harmony_composer:latest image: hub.nationtech.io/harmony/harmony_composer:latest@sha256:eb0406fcb95c63df9b7c4b19bc50ad7914dd8232ce98e9c9abef628e07c69386
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@@ -7,7 +7,7 @@ on:
jobs: jobs:
package_harmony_composer: package_harmony_composer:
container: container:
image: hub.nationtech.io/harmony/harmony_composer:latest image: hub.nationtech.io/harmony/harmony_composer:latest@sha256:eb0406fcb95c63df9b7c4b19bc50ad7914dd8232ce98e9c9abef628e07c69386
runs-on: dind runs-on: dind
steps: steps:
- name: Checkout code - name: Checkout code
@@ -45,14 +45,14 @@ jobs:
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \ -H "Authorization: token ${{ secrets.GITEATOKEN }}" \
"https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases/tags/snapshot-latest" \ "https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases/tags/snapshot-latest" \
| jq -r '.id // empty') | jq -r '.id // empty')
if [ -n "$RELEASE_ID" ]; then if [ -n "$RELEASE_ID" ]; then
# Delete existing release # Delete existing release
curl -X DELETE \ curl -X DELETE \
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \ -H "Authorization: token ${{ secrets.GITEATOKEN }}" \
"https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases/$RELEASE_ID" "https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases/$RELEASE_ID"
fi fi
# Create new release # Create new release
RESPONSE=$(curl -X POST \ RESPONSE=$(curl -X POST \
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \ -H "Authorization: token ${{ secrets.GITEATOKEN }}" \
@@ -65,7 +65,7 @@ jobs:
"prerelease": true "prerelease": true
}' \ }' \
"https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases") "https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases")
echo "RELEASE_ID=$(echo $RESPONSE | jq -r '.id')" >> $GITHUB_ENV echo "RELEASE_ID=$(echo $RESPONSE | jq -r '.id')" >> $GITHUB_ENV
- name: Upload Linux binary - name: Upload Linux binary

30
.gitignore vendored
View File

@@ -1,25 +1,5 @@
### General ### target
private_repos/ private_repos
log/
### Harmony ### *.tgz
harmony.log .gitignore
### 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

458
Cargo.lock generated
View File

@@ -96,12 +96,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "ansi_term"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b3568b48b7cefa6b8ce125f9bb4989e52fbcc29ebea88df04cc7c5f12f70455"
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.19" version = "0.6.19"
@@ -225,6 +219,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "atomic"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@@ -378,7 +381,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_repr", "serde_repr",
"serde_urlencoded", "serde_urlencoded",
"thiserror 2.0.14", "thiserror 2.0.12",
"tokio", "tokio",
"tokio-util", "tokio-util",
"tower-service", "tower-service",
@@ -415,6 +418,12 @@ version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "bytemuck"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
@@ -473,7 +482,7 @@ dependencies = [
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.14", "thiserror 2.0.12",
] ]
[[package]] [[package]]
@@ -515,12 +524,6 @@ 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 = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "chacha20" name = "chacha20"
version = "0.9.1" version = "0.9.1"
@@ -662,19 +665,6 @@ 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"
@@ -1162,12 +1152,6 @@ 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"
@@ -1271,18 +1255,6 @@ dependencies = [
name = "example" name = "example"
version = "0.0.0" version = "0.0.0"
[[package]]
name = "example-application-monitoring-with-tenant"
version = "0.1.0"
dependencies = [
"env_logger",
"harmony",
"harmony_cli",
"logging",
"tokio",
"url",
]
[[package]] [[package]]
name = "example-cli" name = "example-cli"
version = "0.1.0" version = "0.1.0"
@@ -1386,6 +1358,7 @@ dependencies = [
"cidr", "cidr",
"env_logger", "env_logger",
"harmony", "harmony",
"harmony_cli",
"harmony_macros", "harmony_macros",
"harmony_tui", "harmony_tui",
"harmony_types", "harmony_types",
@@ -1471,6 +1444,19 @@ 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 = "figment"
version = "0.10.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
dependencies = [
"atomic",
"pear",
"serde",
"uncased",
"version_check",
]
[[package]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.25" version = "0.2.25"
@@ -1695,11 +1681,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"r-efi", "r-efi",
"wasi 0.14.2+wasi-0.2.4", "wasi 0.14.2+wasi-0.2.4",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -1795,23 +1779,22 @@ dependencies = [
"dyn-clone", "dyn-clone",
"email_address", "email_address",
"env_logger", "env_logger",
"figment",
"fqdn", "fqdn",
"futures-util", "futures-util",
"harmony-secret-derive",
"harmony_macros", "harmony_macros",
"harmony_types", "harmony_types",
"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",
"kube-derive",
"lazy_static", "lazy_static",
"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", "pretty_assertions",
@@ -1819,7 +1802,6 @@ dependencies = [
"reqwest 0.11.27", "reqwest 0.11.27",
"russh", "russh",
"rust-ipmi", "rust-ipmi",
"schemars 0.8.22",
"semver", "semver",
"serde", "serde",
"serde-value", "serde-value",
@@ -1838,50 +1820,16 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "harmony-secret"
version = "0.1.0"
dependencies = [
"async-trait",
"directories",
"harmony-secret-derive",
"http 1.3.1",
"infisical",
"lazy_static",
"log",
"pretty_assertions",
"serde",
"serde_json",
"tempfile",
"thiserror 2.0.14",
"tokio",
]
[[package]]
name = "harmony-secret-derive"
version = "0.1.0"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "harmony_cli" name = "harmony_cli"
version = "0.1.0" 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",
] ]
@@ -1892,16 +1840,10 @@ 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",
] ]
@@ -2000,7 +1942,7 @@ dependencies = [
"non-blank-string-rs", "non-blank-string-rs",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.14", "thiserror 2.0.12",
] ]
[[package]] [[package]]
@@ -2168,7 +2110,7 @@ dependencies = [
"httpdate", "httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"socket2 0.5.10", "socket2",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -2247,7 +2189,6 @@ dependencies = [
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tower-service", "tower-service",
"webpki-roots",
] ]
[[package]] [[package]]
@@ -2310,7 +2251,7 @@ dependencies = [
"libc", "libc",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2 0.5.10", "socket2",
"system-configuration 0.6.1", "system-configuration 0.6.1",
"tokio", "tokio",
"tower-service", "tower-service",
@@ -2498,29 +2439,6 @@ 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"
@@ -2528,19 +2446,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]] [[package]]
name = "infisical" name = "inlinable_string"
version = "0.0.2" version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d97c33b08e22b2f7b9f87a8fc06a7d247442db7bf216ffc6661a74ed8aea658" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
dependencies = [
"base64 0.22.1",
"reqwest 0.12.20",
"serde",
"serde_json",
"thiserror 1.0.69",
"tokio",
"url",
]
[[package]] [[package]]
name = "inout" name = "inout"
@@ -2582,17 +2491,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "io-uring"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
dependencies = [
"bitflags 2.9.1",
"cfg-if",
"libc",
]
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.11.0" version = "2.11.0"
@@ -2686,7 +2584,7 @@ dependencies = [
"pest_derive", "pest_derive",
"regex", "regex",
"serde_json", "serde_json",
"thiserror 2.0.14", "thiserror 2.0.12",
] ]
[[package]] [[package]]
@@ -2754,7 +2652,6 @@ dependencies = [
"k8s-openapi", "k8s-openapi",
"kube-client", "kube-client",
"kube-core", "kube-core",
"kube-derive",
"kube-runtime", "kube-runtime",
] ]
@@ -2787,7 +2684,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"thiserror 2.0.14", "thiserror 2.0.12",
"tokio", "tokio",
"tokio-tungstenite", "tokio-tungstenite",
"tokio-util", "tokio-util",
@@ -2808,25 +2705,10 @@ dependencies = [
"http 1.3.1", "http 1.3.1",
"json-patch", "json-patch",
"k8s-openapi", "k8s-openapi",
"schemars 0.8.22",
"serde", "serde",
"serde-value", "serde-value",
"serde_json", "serde_json",
"thiserror 2.0.14", "thiserror 2.0.12",
]
[[package]]
name = "kube-derive"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "079fc8c1c397538628309cfdee20696ebdcc26745f9fb17f89b78782205bd995"
dependencies = [
"darling",
"proc-macro2",
"quote",
"serde",
"serde_json",
"syn",
] ]
[[package]] [[package]]
@@ -2850,7 +2732,7 @@ dependencies = [
"pin-project", "pin-project",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.14", "thiserror 2.0.12",
"tokio", "tokio",
"tokio-util", "tokio-util",
"tracing", "tracing",
@@ -2944,15 +2826,6 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "logging"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "461a8beca676e8ab1bd468c92e9b4436d6368e11e96ae038209e520cfe665e46"
dependencies = [
"ansi_term",
]
[[package]] [[package]]
name = "lru" name = "lru"
version = "0.12.5" version = "0.12.5"
@@ -2962,12 +2835,6 @@ dependencies = [
"hashbrown 0.15.4", "hashbrown 0.15.4",
] ]
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "md5" name = "md5"
version = "0.7.0" version = "0.7.0"
@@ -3270,7 +3137,7 @@ dependencies = [
"pretty_assertions", "pretty_assertions",
"rand 0.8.5", "rand 0.8.5",
"serde", "serde",
"thiserror 2.0.14", "thiserror 1.0.69",
"tokio", "tokio",
"uuid", "uuid",
"xml-rs", "xml-rs",
@@ -3405,6 +3272,29 @@ dependencies = [
"hmac", "hmac",
] ]
[[package]]
name = "pear"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
dependencies = [
"inlinable_string",
"pear_codegen",
"yansi",
]
[[package]]
name = "pear_codegen"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147"
dependencies = [
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn",
]
[[package]] [[package]]
name = "pem" name = "pem"
version = "3.0.5" version = "3.0.5"
@@ -3437,7 +3327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
dependencies = [ dependencies = [
"memchr", "memchr",
"thiserror 2.0.14", "thiserror 2.0.12",
"ucd-trie", "ucd-trie",
] ]
@@ -3658,15 +3548,6 @@ dependencies = [
"elliptic-curve", "elliptic-curve",
] ]
[[package]]
name = "proc-macro-crate"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
dependencies = [
"toml_edit",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.95" version = "1.0.95"
@@ -3676,67 +3557,25 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "proc-macro2-diagnostics"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
"syn",
"version_check",
"yansi",
]
[[package]] [[package]]
name = "punycode" name = "punycode"
version = "0.4.1" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9e1dcb320d6839f6edb64f7a4a59d39b30480d4d1765b56873f7c858538a5fe" checksum = "e9e1dcb320d6839f6edb64f7a4a59d39b30480d4d1765b56873f7c858538a5fe"
[[package]]
name = "quinn"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.5.10",
"thiserror 2.0.14",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
dependencies = [
"bytes",
"getrandom 0.3.3",
"lru-slab",
"rand 0.9.1",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.14",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.5.10",
"tracing",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.40" version = "1.0.40"
@@ -3855,7 +3694,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [ dependencies = [
"getrandom 0.2.16", "getrandom 0.2.16",
"libredox", "libredox",
"thiserror 2.0.14", "thiserror 2.0.12",
] ]
[[package]] [[package]]
@@ -3956,7 +3795,6 @@ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-channel",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2 0.4.10", "h2 0.4.10",
@@ -3973,8 +3811,6 @@ dependencies = [
"native-tls", "native-tls",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types", "rustls-pki-types",
"serde", "serde",
"serde_json", "serde_json",
@@ -3982,7 +3818,6 @@ dependencies = [
"sync_wrapper 1.0.2", "sync_wrapper 1.0.2",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-rustls",
"tokio-util", "tokio-util",
"tower", "tower",
"tower-http", "tower-http",
@@ -3992,7 +3827,6 @@ dependencies = [
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams", "wasm-streams",
"web-sys", "web-sys",
"webpki-roots",
] ]
[[package]] [[package]]
@@ -4156,7 +3990,7 @@ dependencies = [
"flurry", "flurry",
"log", "log",
"serde", "serde",
"thiserror 2.0.14", "thiserror 2.0.12",
"tokio", "tokio",
"tokio-util", "tokio-util",
] ]
@@ -4182,12 +4016,6 @@ version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@@ -4287,7 +4115,6 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
dependencies = [ dependencies = [
"web-time",
"zeroize", "zeroize",
] ]
@@ -4332,18 +4159,6 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "schemars"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
dependencies = [
"dyn-clone",
"schemars_derive",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "0.9.0" version = "0.9.0"
@@ -4358,9 +4173,9 @@ dependencies = [
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "1.0.4" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" checksum = "1375ba8ef45a6f15d83fa8748f1079428295d403d6ea991d09ab100155fbc06d"
dependencies = [ dependencies = [
"dyn-clone", "dyn-clone",
"ref-cast", "ref-cast",
@@ -4368,18 +4183,6 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "schemars_derive"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@@ -4512,17 +4315,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "serde_derive_internals"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.140" version = "1.0.140"
@@ -4601,7 +4393,7 @@ dependencies = [
"indexmap 1.9.3", "indexmap 1.9.3",
"indexmap 2.10.0", "indexmap 2.10.0",
"schemars 0.9.0", "schemars 0.9.0",
"schemars 1.0.4", "schemars 1.0.3",
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",
@@ -4726,7 +4518,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"num-traits", "num-traits",
"thiserror 2.0.14", "thiserror 2.0.12",
"time", "time",
] ]
@@ -4773,16 +4565,6 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "socket2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.9.8" version = "0.9.8"
@@ -4920,9 +4702,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.105" version = "2.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -5056,11 +4838,11 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.14" version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [ dependencies = [
"thiserror-impl 2.0.14", "thiserror-impl 2.0.12",
] ]
[[package]] [[package]]
@@ -5076,9 +4858,9 @@ dependencies = [
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "2.0.14" version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -5144,39 +4926,21 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "tinyvec"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.47.1" version = "1.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
"io-uring",
"libc", "libc",
"mio 1.0.4", "mio 1.0.4",
"parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"slab", "socket2",
"socket2 0.6.0",
"tokio-macros", "tokio-macros",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -5426,7 +5190,7 @@ dependencies = [
"log", "log",
"rand 0.9.1", "rand 0.9.1",
"sha1", "sha1",
"thiserror 2.0.14", "thiserror 2.0.12",
"utf-8", "utf-8",
] ]
@@ -5448,6 +5212,15 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "uncased"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.18" version = "1.0.18"
@@ -5489,12 +5262,6 @@ 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"
@@ -5727,15 +5494,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "webpki-roots"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View File

@@ -12,8 +12,6 @@ members = [
"harmony_cli", "harmony_cli",
"k3d", "k3d",
"harmony_composer", "harmony_composer",
"harmony_secret_derive",
"harmony_secret",
] ]
[workspace.package] [workspace.package]
@@ -55,10 +53,7 @@ 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" pretty_assertions = "1.4.1"
tempfile = "3.20.0"
bollard = "0.19.1" bollard = "0.19.1"
base64 = "0.22.1" base64 = "0.22.1"
tar = "0.4.44" tar = "0.4.44"
lazy_static = "1.5.0" figment = { version = "0.10.19", features = ["env"] }
directories = "6.0.0"
thiserror = "2.0.14"

View File

@@ -13,7 +13,6 @@ WORKDIR /app
RUN rustup target add x86_64-pc-windows-gnu RUN rustup target add x86_64-pc-windows-gnu
RUN rustup target add x86_64-unknown-linux-gnu RUN rustup target add x86_64-unknown-linux-gnu
RUN rustup component add rustfmt RUN rustup component add rustfmt
RUN rustup component add clippy
RUN apt update RUN apt update
@@ -23,4 +22,4 @@ RUN apt install -y nodejs docker.io mingw-w64
COPY --from=build /app/target/release/harmony_composer . COPY --from=build /app/target/release/harmony_composer .
ENTRYPOINT ["/app/harmony_composer"] ENTRYPOINT ["/app/harmony_composer"]

View File

@@ -1,6 +1,5 @@
# Harmony : Open-source infrastructure orchestration that treats your platform like first-class code # Harmony : Open-source infrastructure orchestration that treats your platform like first-class code.
*By [NationTech](https://nationtech.io)*
_By [NationTech](https://nationtech.io)_
[![Build](https://git.nationtech.io/NationTech/harmony/actions/workflows/check.yml/badge.svg)](https://git.nationtech.io/nationtech/harmony) [![Build](https://git.nationtech.io/NationTech/harmony/actions/workflows/check.yml/badge.svg)](https://git.nationtech.io/nationtech/harmony)
[![License](https://img.shields.io/badge/license-AGPLv3-blue?style=flat-square)](LICENSE) [![License](https://img.shields.io/badge/license-AGPLv3-blue?style=flat-square)](LICENSE)
@@ -24,11 +23,11 @@ From a **developer laptop** to a **global production cluster**, a single **sourc
Infrastructure is essential, but it shouldnt be your core business. Harmony is built on three guiding principles that make modern platforms reliable, repeatable, and easy to reason about. Infrastructure is essential, but it shouldnt be your core business. Harmony is built on three guiding principles that make modern platforms reliable, repeatable, and easy to reason about.
| Principle | What it means for you | | Principle | What it means for you |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | |-----------|-----------------------|
| **Infrastructure as Resilient Code** | Replace sprawling YAML and bash scripts with type-safe Rust. Test, refactor, and version your platform just like application code. | | **Infrastructure as Resilient Code** | Replace sprawling YAML and bash scripts with type-safe Rust. Test, refactor, and version your platform just like application code. |
| **Prove It Works — Before You Deploy** | Harmony uses the compiler to verify that your applications needs match the target environments capabilities at **compile-time**, eliminating an entire class of runtime outages. | | **Prove It Works — Before You Deploy** | Harmony uses the compiler to verify that your applications needs match the target environments capabilities at **compile-time**, eliminating an entire class of runtime outages. |
| **One Unified Model** | Software and infrastructure are a single system. Harmony models them together, enabling deep automation—from bare-metal servers to Kubernetes workloads—with zero context switching. | | **One Unified Model** | Software and infrastructure are a single system. Harmony models them together, enabling deep automation—from bare-metal servers to Kubernetes workloads—with zero context switching. |
These principles surface as simple, ergonomic Rust APIs that let teams focus on their product while trusting the platform underneath. These principles surface as simple, ergonomic Rust APIs that let teams focus on their product while trusting the platform underneath.
@@ -64,20 +63,22 @@ async fn main() {
}, },
}; };
// 2. Enhance with extra scores (monitoring, CI/CD, …) // 2. Pick where it should run
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
Inventory::autoload(), // auto-detect hardware / kube-config
K8sAnywhereTopology::from_env(), // local k3d, CI, staging, prod…
)
.await
.unwrap();
// 3. Enhance with extra scores (monitoring, CI/CD, …)
let mut monitoring = MonitoringAlertingStackScore::new(); let mut monitoring = MonitoringAlertingStackScore::new();
monitoring.namespace = Some(lamp_stack.config.namespace.clone()); monitoring.namespace = Some(lamp_stack.config.namespace.clone());
// 3. Run your scores on the desired topology & inventory maestro.register_all(vec![Box::new(lamp_stack), Box::new(monitoring)]);
harmony_cli::run(
Inventory::autoload(), // auto-detect hardware / kube-config // 4. Launch an interactive CLI / TUI
K8sAnywhereTopology::from_env(), // local k3d, CI, staging, prod… harmony_cli::init(maestro, None).await.unwrap();
vec![
Box::new(lamp_stack),
Box::new(monitoring)
],
None
).await.unwrap();
} }
``` ```
@@ -93,13 +94,13 @@ Harmony analyses the code, shows an execution plan in a TUI, and applies it once
## 3 · Core Concepts ## 3 · Core Concepts
| Term | One-liner | | Term | One-liner |
| ---------------- | ---------------------------------------------------------------------------------------------------- | |------|-----------|
| **Score<T>** | Declarative description of the desired state (e.g., `LAMPScore`). | | **Score<T>** | Declarative description of the desired state (e.g., `LAMPScore`). |
| **Interpret<T>** | Imperative logic that realises a `Score` on a specific environment. | | **Interpret<T>** | Imperative logic that realises a `Score` on a specific environment. |
| **Topology** | An environment (local k3d, AWS, bare-metal) exposing verified _Capabilities_ (Kubernetes, DNS, …). | | **Topology** | An environment (local k3d, AWS, bare-metal) exposing verified *Capabilities* (Kubernetes, DNS, …). |
| **Maestro** | Orchestrator that compiles Scores + Topology, ensuring all capabilities line up **at compile-time**. | | **Maestro** | Orchestrator that compiles Scores + Topology, ensuring all capabilities line up **at compile-time**. |
| **Inventory** | Optional catalogue of physical assets for bare-metal and edge deployments. | | **Inventory** | Optional catalogue of physical assets for bare-metal and edge deployments. |
A visual overview is in the diagram below. A visual overview is in the diagram below.
@@ -111,9 +112,9 @@ A visual overview is in the diagram below.
Prerequisites: Prerequisites:
- Rust * Rust
- Docker (if you deploy locally) * Docker (if you deploy locally)
- `kubectl` / `helm` for Kubernetes-based topologies * `kubectl` / `helm` for Kubernetes-based topologies
```bash ```bash
git clone https://git.nationtech.io/nationtech/harmony git clone https://git.nationtech.io/nationtech/harmony
@@ -125,15 +126,15 @@ cargo build --release # builds the CLI, TUI and libraries
## 5 · Learning More ## 5 · Learning More
- **Architectural Decision Records** dive into the rationale * **Architectural Decision Records** dive into the rationale
- [ADR-001 · Why Rust](adr/001-rust.md) - [ADR-001 · Why Rust](adr/001-rust.md)
- [ADR-003 · Infrastructure Abstractions](adr/003-infrastructure-abstractions.md) - [ADR-003 · Infrastructure Abstractions](adr/003-infrastructure-abstractions.md)
- [ADR-006 · Secret Management](adr/006-secret-management.md) - [ADR-006 · Secret Management](adr/006-secret-management.md)
- [ADR-011 · Multi-Tenant Cluster](adr/011-multi-tenant-cluster.md) - [ADR-011 · Multi-Tenant Cluster](adr/011-multi-tenant-cluster.md)
- **Extending Harmony** write new Scores / Interprets, add hardware like OPNsense firewalls, or embed Harmony in your own tooling (`/docs`). * **Extending Harmony** write new Scores / Interprets, add hardware like OPNsense firewalls, or embed Harmony in your own tooling (`/docs`).
- **Community** discussions and roadmap live in [GitLab issues](https://git.nationtech.io/nationtech/harmony/-/issues). PRs, ideas, and feedback are welcome! * **Community** discussions and roadmap live in [GitLab issues](https://git.nationtech.io/nationtech/harmony/-/issues). PRs, ideas, and feedback are welcome!
--- ---
@@ -147,4 +148,4 @@ See [LICENSE](LICENSE) for the full text.
--- ---
_Made with ❤️ & 🦀 by the NationTech and the Harmony community_ *Made with ❤️ & 🦀 by the NationTech and the Harmony community*

View File

@@ -1,7 +1,5 @@
#!/bin/sh #!/bin/sh
set -e set -e
cargo check --all-targets --all-features --keep-going cargo check --all-targets --all-features --keep-going
cargo fmt --check cargo fmt --check
cargo clippy
cargo test cargo test

View File

@@ -1,14 +0,0 @@
[package]
name = "example-application-monitoring-with-tenant"
edition = "2024"
version.workspace = true
readme.workspace = true
license.workspace = true
[dependencies]
env_logger.workspace = true
harmony = { version = "0.1.0", path = "../../harmony" }
harmony_cli = { version = "0.1.0", path = "../../harmony_cli" }
logging = "0.1.0"
tokio.workspace = true
url.workspace = true

View File

@@ -1,55 +0,0 @@
use std::{path::PathBuf, str::FromStr, sync::Arc};
use harmony::{
data::Id,
inventory::Inventory,
modules::{
application::{ApplicationScore, RustWebFramework, RustWebapp, features::Monitoring},
monitoring::alert_channel::webhook_receiver::WebhookReceiver,
tenant::TenantScore,
},
topology::{K8sAnywhereTopology, Url, tenant::TenantConfig},
};
#[tokio::main]
async fn main() {
//TODO there is a bug where the application is deployed into the namespace matching the
//application name and the tenant is created in the namesapce matching the tenant name
//in order for the application to be deployed in the tenant namespace the application.name and
//the TenantConfig.name must match
let tenant = TenantScore {
config: TenantConfig {
id: Id::from_str("test-tenant-id").unwrap(),
name: "example-monitoring".to_string(),
..Default::default()
},
};
let application = Arc::new(RustWebapp {
name: "example-monitoring".to_string(),
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
project_root: PathBuf::from("./examples/rust/webapp"),
framework: Some(RustWebFramework::Leptos),
});
let webhook_receiver = WebhookReceiver {
name: "sample-webhook-receiver".to_string(),
url: Url::Url(url::Url::parse("https://webhook-doesnt-exist.com").unwrap()),
};
let app = ApplicationScore {
features: vec![Box::new(Monitoring {
alert_receiver: vec![Box::new(webhook_receiver)],
application: application.clone(),
})],
application,
};
harmony_cli::run(
Inventory::autoload(),
K8sAnywhereTopology::from_env(),
vec![Box::new(tenant), Box::new(app)],
None,
)
.await
.unwrap();
}

View File

@@ -1,21 +1,20 @@
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,
maestro::Maestro,
modules::dummy::{ErrorScore, PanicScore, SuccessScore}, modules::dummy::{ErrorScore, PanicScore, SuccessScore},
topology::LocalhostTopology, topology::LocalhostTopology,
}; };
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
harmony_cli::run( let inventory = Inventory::autoload();
Inventory::autoload(), let topology = LocalhostTopology::new();
LocalhostTopology::new(), let mut maestro = Maestro::initialize(inventory, topology).await.unwrap();
vec![
Box::new(SuccessScore {}), maestro.register_all(vec![
Box::new(ErrorScore {}), Box::new(SuccessScore {}),
Box::new(PanicScore {}), Box::new(ErrorScore {}),
], Box::new(PanicScore {}),
None, ]);
) harmony_cli::init(maestro, None).await.unwrap();
.await
.unwrap();
} }

View File

@@ -125,47 +125,40 @@ spec:
name: nginx"#, name: nginx"#,
) )
.unwrap(); .unwrap();
deployment return deployment;
} }
fn nginx_deployment_2() -> Deployment { fn nginx_deployment_2() -> Deployment {
let pod_template = PodTemplateSpec { let mut pod_template = PodTemplateSpec::default();
metadata: Some(ObjectMeta { pod_template.metadata = Some(ObjectMeta {
labels: Some(BTreeMap::from([( labels: Some(BTreeMap::from([(
"app".to_string(), "app".to_string(),
"nginx-test".to_string(), "nginx-test".to_string(),
)])), )])),
..Default::default()
});
pod_template.spec = Some(PodSpec {
containers: vec![Container {
name: "nginx".to_string(),
image: Some("nginx".to_string()),
..Default::default() ..Default::default()
}), }],
spec: Some(PodSpec { ..Default::default()
containers: vec![Container { });
name: "nginx".to_string(), let mut spec = DeploymentSpec::default();
image: Some("nginx".to_string()), spec.template = pod_template;
..Default::default() spec.selector = LabelSelector {
}], match_expressions: None,
..Default::default() match_labels: Some(BTreeMap::from([(
}), "app".to_string(),
"nginx-test".to_string(),
)])),
}; };
let spec = DeploymentSpec { let mut deployment = Deployment::default();
template: pod_template, deployment.spec = Some(spec);
selector: LabelSelector { deployment.metadata.name = Some("nginx-test".to_string());
match_expressions: None,
match_labels: Some(BTreeMap::from([(
"app".to_string(),
"nginx-test".to_string(),
)])),
},
..Default::default()
};
Deployment { deployment
spec: Some(spec),
metadata: ObjectMeta {
name: Some("nginx-test".to_string()),
..Default::default()
},
..Default::default()
}
} }
fn nginx_deployment() -> Deployment { fn nginx_deployment() -> Deployment {

View File

@@ -1,6 +1,7 @@
use harmony::{ use harmony::{
data::Version, data::Version,
inventory::Inventory, inventory::Inventory,
maestro::Maestro,
modules::lamp::{LAMPConfig, LAMPScore}, modules::lamp::{LAMPConfig, LAMPScore},
topology::{K8sAnywhereTopology, Url}, topology::{K8sAnywhereTopology, Url},
}; };
@@ -23,7 +24,7 @@ async fn main() {
// This config can be extended as needed for more complicated configurations // This config can be extended as needed for more complicated configurations
config: LAMPConfig { config: LAMPConfig {
project_root: "./php".into(), project_root: "./php".into(),
database_size: "4Gi".to_string().into(), database_size: format!("4Gi").into(),
..Default::default() ..Default::default()
}, },
}; };
@@ -42,13 +43,15 @@ async fn main() {
// K8sAnywhereTopology as it is the most automatic one that enables you to easily deploy // K8sAnywhereTopology as it is the most automatic one that enables you to easily deploy
// locally, to development environment from a CI, to staging, and to production with settings // locally, to development environment from a CI, to staging, and to production with settings
// that automatically adapt to each environment grade. // that automatically adapt to each environment grade.
harmony_cli::run( let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
Inventory::autoload(), Inventory::autoload(),
K8sAnywhereTopology::from_env(), K8sAnywhereTopology::from_env(),
vec![Box::new(lamp_stack)],
None,
) )
.await .await
.unwrap(); .unwrap();
maestro.register_all(vec![Box::new(lamp_stack)]);
// Here we bootstrap the CLI, this gives some nice features if you need them
harmony_cli::init(maestro, None).await.unwrap();
} }
// That's it, end of the infra as code. // That's it, end of the infra as code.

View File

@@ -2,6 +2,7 @@ use std::collections::HashMap;
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,
maestro::Maestro,
modules::{ modules::{
monitoring::{ monitoring::{
alert_channel::discord_alert_channel::DiscordWebhook, alert_channel::discord_alert_channel::DiscordWebhook,
@@ -50,8 +51,8 @@ async fn main() {
let service_monitor_endpoint = ServiceMonitorEndpoint { let service_monitor_endpoint = ServiceMonitorEndpoint {
port: Some("80".to_string()), port: Some("80".to_string()),
path: Some("/metrics".to_string()), path: "/metrics".to_string(),
scheme: Some(HTTPScheme::HTTP), scheme: HTTPScheme::HTTP,
..Default::default() ..Default::default()
}; };
@@ -73,13 +74,13 @@ async fn main() {
rules: vec![Box::new(additional_rules), Box::new(additional_rules2)], rules: vec![Box::new(additional_rules), Box::new(additional_rules2)],
service_monitors: vec![service_monitor], service_monitors: vec![service_monitor],
}; };
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
harmony_cli::run(
Inventory::autoload(), Inventory::autoload(),
K8sAnywhereTopology::from_env(), K8sAnywhereTopology::from_env(),
vec![Box::new(alerting_score)],
None,
) )
.await .await
.unwrap(); .unwrap();
maestro.register_all(vec![Box::new(alerting_score)]);
harmony_cli::init(maestro, None).await.unwrap();
} }

View File

@@ -1,8 +1,9 @@
use std::{collections::HashMap, str::FromStr}; use std::collections::HashMap;
use harmony::{ use harmony::{
data::Id, data::Id,
inventory::Inventory, inventory::Inventory,
maestro::Maestro,
modules::{ modules::{
monitoring::{ monitoring::{
alert_channel::discord_alert_channel::DiscordWebhook, alert_channel::discord_alert_channel::DiscordWebhook,
@@ -28,7 +29,7 @@ use harmony::{
async fn main() { async fn main() {
let tenant = TenantScore { let tenant = TenantScore {
config: TenantConfig { config: TenantConfig {
id: Id::from_str("1234").unwrap(), id: Id::from_string("1234".to_string()),
name: "test-tenant".to_string(), name: "test-tenant".to_string(),
resource_limits: ResourceLimits { resource_limits: ResourceLimits {
cpu_request_cores: 6.0, cpu_request_cores: 6.0,
@@ -53,8 +54,8 @@ async fn main() {
let service_monitor_endpoint = ServiceMonitorEndpoint { let service_monitor_endpoint = ServiceMonitorEndpoint {
port: Some("80".to_string()), port: Some("80".to_string()),
path: Some("/metrics".to_string()), path: "/metrics".to_string(),
scheme: Some(HTTPScheme::HTTP), scheme: HTTPScheme::HTTP,
..Default::default() ..Default::default()
}; };
@@ -77,13 +78,13 @@ async fn main() {
rules: vec![Box::new(additional_rules)], rules: vec![Box::new(additional_rules)],
service_monitors: vec![service_monitor], service_monitors: vec![service_monitor],
}; };
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
harmony_cli::run(
Inventory::autoload(), Inventory::autoload(),
K8sAnywhereTopology::from_env(), K8sAnywhereTopology::from_env(),
vec![Box::new(tenant), Box::new(alerting_score)],
None,
) )
.await .await
.unwrap(); .unwrap();
maestro.register_all(vec![Box::new(tenant), Box::new(alerting_score)]);
harmony_cli::init(maestro, None).await.unwrap();
} }

View File

@@ -1,18 +1,20 @@
use harmony::{ use harmony::{
inventory::Inventory, modules::monitoring::ntfy::ntfy::NtfyScore, topology::K8sAnywhereTopology, inventory::Inventory, maestro::Maestro, modules::monitoring::ntfy::ntfy::NtfyScore,
topology::K8sAnywhereTopology,
}; };
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
harmony_cli::run( let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
Inventory::autoload(), Inventory::autoload(),
K8sAnywhereTopology::from_env(), K8sAnywhereTopology::from_env(),
vec![Box::new(NtfyScore {
namespace: "monitoring".to_string(),
host: "localhost".to_string(),
})],
None,
) )
.await .await
.unwrap(); .unwrap();
maestro.register_all(vec![Box::new(NtfyScore {
namespace: "monitoring".to_string(),
host: "localhost".to_string(),
})]);
harmony_cli::init(maestro, None).await.unwrap();
} }

View File

@@ -16,3 +16,4 @@ harmony_macros = { path = "../../harmony_macros" }
log = { workspace = true } log = { workspace = true }
env_logger = { workspace = true } env_logger = { workspace = true }
url = { workspace = true } url = { workspace = true }
harmony_cli = { version = "0.1.0", path = "../../harmony_cli" }

View File

@@ -13,7 +13,7 @@ use harmony::{
dummy::{ErrorScore, PanicScore, SuccessScore}, dummy::{ErrorScore, PanicScore, SuccessScore},
http::StaticFilesHttpScore, http::StaticFilesHttpScore,
okd::{dhcp::OKDDhcpScore, dns::OKDDnsScore, load_balancer::OKDLoadBalancerScore}, okd::{dhcp::OKDDhcpScore, dns::OKDDnsScore, load_balancer::OKDLoadBalancerScore},
opnsense::OPNsenseShellCommandScore, opnsense::{OPNSenseLaunchUpgrade, OPNsenseShellCommandScore},
tftp::TftpScore, tftp::TftpScore,
}, },
topology::{LogicalHost, UnmanagedRouter, Url}, topology::{LogicalHost, UnmanagedRouter, Url},
@@ -22,8 +22,10 @@ use harmony_macros::{ip, mac_address};
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
env_logger::init();
let firewall = harmony::topology::LogicalHost { let firewall = harmony::topology::LogicalHost {
ip: ip!("192.168.5.229"), ip: ip!("192.168.122.106"),
name: String::from("opnsense-1"), name: String::from("opnsense-1"),
}; };
@@ -95,9 +97,12 @@ async fn main() {
opnsense: opnsense.get_opnsense_config(), opnsense: opnsense.get_opnsense_config(),
command: "touch /tmp/helloharmonytouching".to_string(), command: "touch /tmp/helloharmonytouching".to_string(),
}), }),
// Box::new(OPNSenseLaunchUpgrade {
// opnsense: opnsense.get_opnsense_config(),
// }),
Box::new(SuccessScore {}), Box::new(SuccessScore {}),
Box::new(ErrorScore {}), Box::new(ErrorScore {}),
Box::new(PanicScore {}), Box::new(PanicScore {}),
]); ]);
harmony_tui::init(maestro).await.unwrap(); harmony_cli::init(maestro, None).await.unwrap();
} }

View File

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

View File

@@ -2,57 +2,42 @@ use std::{path::PathBuf, sync::Arc};
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,
modules::{ maestro::Maestro,
application::{ modules::application::{
ApplicationScore, RustWebFramework, RustWebapp, ApplicationScore, RustWebFramework, RustWebapp,
features::{ContinuousDelivery, Monitoring}, features::{ContinuousDelivery, Monitoring},
},
monitoring::alert_channel::{
discord_alert_channel::DiscordWebhook, webhook_receiver::WebhookReceiver,
},
}, },
topology::{K8sAnywhereTopology, Url}, topology::{K8sAnywhereTopology, Url},
}; };
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
env_logger::init();
let topology = K8sAnywhereTopology::from_env();
let mut maestro = Maestro::initialize(Inventory::autoload(), topology)
.await
.unwrap();
let application = Arc::new(RustWebapp { let application = Arc::new(RustWebapp {
name: "harmony-example-rust-webapp".to_string(), name: "harmony-example-rust-webapp".to_string(),
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()), domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
project_root: PathBuf::from("./webapp"), // Relative from 'harmony-path' param project_root: PathBuf::from("./examples/rust/webapp"),
framework: Some(RustWebFramework::Leptos), framework: Some(RustWebFramework::Leptos),
}); });
let discord_receiver = DiscordWebhook {
name: "test-discord".to_string(),
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
};
let webhook_receiver = WebhookReceiver {
name: "sample-webhook-receiver".to_string(),
url: Url::Url(url::Url::parse("https://webhook-doesnt-exist.com").unwrap()),
};
let app = ApplicationScore { let app = ApplicationScore {
features: vec![ features: vec![
Box::new(ContinuousDelivery { Box::new(ContinuousDelivery {
application: application.clone(), application: application.clone(),
}), }), // TODO add monitoring, backups, multisite ha, etc
Box::new(Monitoring { Box::new(Monitoring {
application: application.clone(), application: application.clone(),
alert_receiver: vec![Box::new(discord_receiver), Box::new(webhook_receiver)],
}), }),
// TODO add backups, multisite ha, etc
], ],
application, application,
}; };
harmony_cli::run( maestro.register_all(vec![Box::new(app)]);
Inventory::autoload(), harmony_cli::init(maestro, None).await.unwrap();
K8sAnywhereTopology::from_env(),
vec![Box::new(app)],
None,
)
.await
.unwrap();
} }

View File

@@ -0,0 +1,16 @@
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

@@ -1,8 +1,7 @@
use std::str::FromStr;
use harmony::{ use harmony::{
data::Id, data::Id,
inventory::Inventory, inventory::Inventory,
maestro::Maestro,
modules::tenant::TenantScore, modules::tenant::TenantScore,
topology::{K8sAnywhereTopology, tenant::TenantConfig}, topology::{K8sAnywhereTopology, tenant::TenantConfig},
}; };
@@ -11,20 +10,21 @@ use harmony::{
async fn main() { async fn main() {
let tenant = TenantScore { let tenant = TenantScore {
config: TenantConfig { config: TenantConfig {
id: Id::from_str("test-tenant-id").unwrap(), id: Id::from_str("test-tenant-id"),
name: "testtenant".to_string(), name: "testtenant".to_string(),
..Default::default() ..Default::default()
}, },
}; };
harmony_cli::run( let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
Inventory::autoload(), Inventory::autoload(),
K8sAnywhereTopology::from_env(), K8sAnywhereTopology::from_env(),
vec![Box::new(tenant)],
None,
) )
.await .await
.unwrap(); .unwrap();
maestro.register_all(vec![Box::new(tenant)]);
harmony_cli::init(maestro, None).await.unwrap();
} }
// TODO write tests // TODO write tests

View File

@@ -5,9 +5,6 @@ version.workspace = true
readme.workspace = true readme.workspace = true
license.workspace = true license.workspace = true
[features]
testing = []
[dependencies] [dependencies]
rand = "0.9" rand = "0.9"
hex = "0.4" hex = "0.4"
@@ -30,28 +27,29 @@ harmony_macros = { path = "../harmony_macros" }
harmony_types = { path = "../harmony_types" } harmony_types = { path = "../harmony_types" }
uuid.workspace = true uuid.workspace = true
url.workspace = true url.workspace = true
kube = { workspace = true, features = ["derive"] } kube.workspace = true
k8s-openapi.workspace = true 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" }
directories.workspace = true directories = "6.0.0"
lazy_static.workspace = true lazy_static = "1.5.0"
dockerfile_builder = "0.1.5" dockerfile_builder = "0.1.5"
temp-file = "0.1.9" temp-file = "0.1.9"
convert_case.workspace = true 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"
@@ -59,15 +57,12 @@ similar.workspace = true
futures-util = "0.3.31" 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.workspace = true tempfile = "3.20.0"
serde_with = "3.14.0" serde_with = "3.14.0"
schemars = "0.8.22"
kube-derive = "1.1.0"
bollard.workspace = true bollard.workspace = true
tar.workspace = true tar.workspace = true
base64.workspace = true base64.workspace = true
once_cell = "1.21.3" figment.workspace = true
harmony-secret-derive = { version = "0.1.0", path = "../harmony_secret_derive" }
[dev-dependencies] [dev-dependencies]
pretty_assertions.workspace = true pretty_assertions.workspace = true

Binary file not shown.

View File

@@ -1,15 +1,66 @@
use figment::{
Error, Figment, Metadata, Profile, Provider,
providers::{Env, Format},
value::{Dict, Map},
};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
lazy_static! { #[derive(Debug, Deserialize, Serialize)]
pub static ref HARMONY_DATA_DIR: PathBuf = directories::BaseDirs::new() pub struct Config {
.unwrap() pub data_dir: PathBuf,
.data_dir() pub registry_url: String,
.join("harmony"); pub registry_project: String,
pub static ref REGISTRY_URL: String = pub dry_run: bool,
std::env::var("HARMONY_REGISTRY_URL").unwrap_or_else(|_| "hub.nationtech.io".to_string()); pub run_upgrades: bool,
pub static ref REGISTRY_PROJECT: String = }
std::env::var("HARMONY_REGISTRY_PROJECT").unwrap_or_else(|_| "harmony".to_string());
pub static ref DRY_RUN: bool = impl Default for Config {
std::env::var("HARMONY_DRY_RUN").is_ok_and(|value| value.parse().unwrap_or(false)); fn default() -> Self {
Config {
data_dir: directories::BaseDirs::new()
.unwrap()
.data_dir()
.join("harmony"),
registry_url: "hub.nationtech.io".to_string(),
registry_project: "harmony".to_string(),
dry_run: true,
run_upgrades: false,
}
}
}
impl Config {
pub fn load() -> Result<Self, figment::Error> {
Figment::from(Config::default())
.merge(Env::prefixed("HARMONY_"))
.extract()
}
fn from<T: Provider>(provider: T) -> Result<Config, Error> {
Figment::from(provider).extract()
}
fn figment() -> Figment {
use figment::providers::Env;
// In reality, whatever the library desires.
Figment::from(Config::default()).merge(Env::prefixed("HARMONY_"))
}
}
impl Provider for Config {
fn metadata(&self) -> Metadata {
Metadata::named("Harmony Config")
}
fn data(&self) -> Result<Map<Profile, Dict>, Error> {
figment::providers::Serialized::defaults(Config::default()).data()
}
fn profile(&self) -> Option<Profile> {
// Optionally, a profile that's selected by default.
Some(Profile::Default)
}
} }

View File

@@ -1,6 +1,5 @@
use rand::distr::Alphanumeric; use rand::distr::Alphanumeric;
use rand::distr::SampleString; use rand::distr::SampleString;
use std::str::FromStr;
use std::time::SystemTime; use std::time::SystemTime;
use std::time::UNIX_EPOCH; use std::time::UNIX_EPOCH;
@@ -24,13 +23,13 @@ pub struct Id {
value: String, value: String,
} }
impl FromStr for Id { impl Id {
type Err = (); pub fn from_string(value: String) -> Self {
Self { value }
}
fn from_str(s: &str) -> Result<Self, Self::Err> { pub fn from_str(value: &str) -> Self {
Ok(Id { Self::from_string(value.to_string())
value: s.to_string(),
})
} }
} }

View File

@@ -47,7 +47,7 @@ impl serde::Serialize for Version {
impl std::fmt::Display for Version { impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.value.fmt(f) return self.value.fmt(f);
} }
} }

View File

@@ -35,9 +35,10 @@ impl PhysicalHost {
pub fn cluster_mac(&self) -> MacAddress { pub fn cluster_mac(&self) -> MacAddress {
self.network self.network
.first() .get(0)
.expect("Cluster physical host should have a network interface") .expect("Cluster physical host should have a network interface")
.mac_address .mac_address
.clone()
} }
pub fn cpu(mut self, cpu_count: Option<u64>) -> Self { pub fn cpu(mut self, cpu_count: Option<u64>) -> Self {

View File

@@ -1,82 +0,0 @@
use log::debug;
use once_cell::sync::Lazy;
use tokio::sync::broadcast;
use crate::modules::application::ApplicationFeatureStatus;
use super::{
interpret::{InterpretError, Outcome},
topology::TopologyStatus,
};
#[derive(Debug, Clone)]
pub enum HarmonyEvent {
HarmonyStarted,
HarmonyFinished,
InterpretExecutionStarted {
execution_id: String,
topology: String,
interpret: String,
score: String,
message: String,
},
InterpretExecutionFinished {
execution_id: String,
topology: String,
interpret: String,
score: String,
outcome: Result<Outcome, InterpretError>,
},
TopologyStateChanged {
topology: String,
status: TopologyStatus,
message: Option<String>,
},
ApplicationFeatureStateChanged {
topology: String,
application: String,
feature: String,
status: ApplicationFeatureStatus,
},
}
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> {
if cfg!(any(test, feature = "testing")) {
let _ = event; // Suppress the "unused variable" warning for `event`
Ok(())
} else {
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

@@ -7,7 +7,6 @@ use super::{
data::{Id, Version}, data::{Id, Version},
executors::ExecutorError, executors::ExecutorError,
inventory::Inventory, inventory::Inventory,
topology::PreparationError,
}; };
pub enum InterpretName { pub enum InterpretName {
@@ -24,14 +23,6 @@ pub enum InterpretName {
TenantInterpret, TenantInterpret,
Application, Application,
ArgoCD, ArgoCD,
Alerting,
Ntfy,
HelmChart,
HelmCommand,
K8sResource,
Lamp,
ApplicationMonitoring,
K8sPrometheusCrdAlerting,
} }
impl std::fmt::Display for InterpretName { impl std::fmt::Display for InterpretName {
@@ -50,14 +41,6 @@ impl std::fmt::Display for InterpretName {
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"), InterpretName::ArgoCD => f.write_str("ArgoCD"),
InterpretName::Alerting => f.write_str("Alerting"),
InterpretName::Ntfy => f.write_str("Ntfy"),
InterpretName::HelmChart => f.write_str("HelmChart"),
InterpretName::HelmCommand => f.write_str("HelmCommand"),
InterpretName::K8sResource => f.write_str("K8sResource"),
InterpretName::Lamp => f.write_str("LAMP"),
InterpretName::ApplicationMonitoring => f.write_str("ApplicationMonitoring"),
InterpretName::K8sPrometheusCrdAlerting => f.write_str("K8sPrometheusCrdAlerting"),
} }
} }
} }
@@ -130,14 +113,6 @@ impl std::fmt::Display for InterpretError {
} }
impl Error for InterpretError {} impl Error for InterpretError {}
impl From<PreparationError> for InterpretError {
fn from(value: PreparationError) -> Self {
Self {
msg: format!("InterpretError : {value}"),
}
}
}
impl From<ExecutorError> for InterpretError { impl From<ExecutorError> for InterpretError {
fn from(value: ExecutorError) -> Self { fn from(value: ExecutorError) -> Self {
Self { Self {

View File

@@ -1,14 +1,12 @@
use std::sync::{Arc, RwLock}; use std::sync::{Arc, Mutex, RwLock};
use log::{debug, warn}; use log::{info, warn};
use crate::topology::TopologyStatus;
use super::{ use super::{
interpret::{InterpretError, Outcome}, interpret::{InterpretError, InterpretStatus, Outcome},
inventory::Inventory, inventory::Inventory,
score::Score, score::Score,
topology::{PreparationError, PreparationOutcome, Topology, TopologyState}, topology::Topology,
}; };
type ScoreVec<T> = Vec<Box<dyn Score<T>>>; type ScoreVec<T> = Vec<Box<dyn Score<T>>>;
@@ -17,7 +15,7 @@ pub struct Maestro<T: Topology> {
inventory: Inventory, inventory: Inventory,
topology: T, topology: T,
scores: Arc<RwLock<ScoreVec<T>>>, scores: Arc<RwLock<ScoreVec<T>>>,
topology_state: TopologyState, topology_preparation_result: Mutex<Option<Outcome>>,
} }
impl<T: Topology> Maestro<T> { impl<T: Topology> Maestro<T> {
@@ -25,46 +23,36 @@ impl<T: Topology> Maestro<T> {
/// ///
/// This should rarely be used. Most of the time Maestro::initialize should be used instead. /// This should rarely be used. Most of the time Maestro::initialize should be used instead.
pub fn new_without_initialization(inventory: Inventory, topology: T) -> Self { pub fn new_without_initialization(inventory: Inventory, topology: T) -> Self {
let topology_name = topology.name().to_string();
Self { Self {
inventory, inventory,
topology, topology,
scores: Arc::new(RwLock::new(Vec::new())), scores: Arc::new(RwLock::new(Vec::new())),
topology_state: TopologyState::new(topology_name), topology_preparation_result: None.into(),
} }
} }
pub async fn initialize(inventory: Inventory, topology: T) -> Result<Self, PreparationError> { pub async fn initialize(inventory: Inventory, topology: T) -> Result<Self, InterpretError> {
let mut instance = Self::new_without_initialization(inventory, topology); let instance = Self::new_without_initialization(inventory, topology);
instance.prepare_topology().await?; instance.prepare_topology().await?;
Ok(instance) Ok(instance)
} }
/// 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.
async fn prepare_topology(&mut self) -> Result<PreparationOutcome, PreparationError> { pub async fn prepare_topology(&self) -> Result<Outcome, InterpretError> {
self.topology_state.prepare(); info!("Ensuring topology '{}' is ready...", self.topology.name());
let outcome = self.topology.ensure_ready().await?;
info!(
"Topology '{}' readiness check complete: {}",
self.topology.name(),
outcome.status
);
let result = self.topology.ensure_ready().await; self.topology_preparation_result
.lock()
match result { .unwrap()
Ok(outcome) => { .replace(outcome.clone());
match outcome.clone() { Ok(outcome)
PreparationOutcome::Success { details } => {
self.topology_state.success(details);
}
PreparationOutcome::Noop => {
self.topology_state.noop();
}
};
Ok(outcome)
}
Err(err) => {
self.topology_state.error(err.to_string());
Err(err)
}
}
} }
pub fn register_all(&mut self, mut scores: ScoreVec<T>) { pub fn register_all(&mut self, mut scores: ScoreVec<T>) {
@@ -73,7 +61,15 @@ impl<T: Topology> Maestro<T> {
} }
fn is_topology_initialized(&self) -> bool { fn is_topology_initialized(&self) -> bool {
self.topology_state.status == TopologyStatus::Success let result = self.topology_preparation_result.lock().unwrap();
if let Some(outcome) = result.as_ref() {
match outcome.status {
InterpretStatus::SUCCESS => return true,
_ => return false,
}
} else {
false
}
} }
pub async fn interpret(&self, score: Box<dyn Score<T>>) -> Result<Outcome, InterpretError> { pub async fn interpret(&self, score: Box<dyn Score<T>>) -> Result<Outcome, InterpretError> {
@@ -84,9 +80,11 @@ impl<T: Topology> Maestro<T> {
self.topology.name(), self.topology.name(),
); );
} }
debug!("Interpreting score {score:?}"); info!("Running score {score:?}");
let result = score.interpret(&self.inventory, &self.topology).await; let interpret = score.create_interpret();
debug!("Got result {result:?}"); info!("Launching interpret {interpret:?}");
let result = interpret.execute(&self.inventory, &self.topology).await;
info!("Got result {result:?}");
result result
} }

View File

@@ -3,7 +3,6 @@ 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

@@ -1,62 +1,22 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use async_trait::async_trait;
use serde::Serialize; use serde::Serialize;
use serde_value::Value; use serde_value::Value;
use super::{ use super::{interpret::Interpret, topology::Topology};
data::Id,
instrumentation::{self, HarmonyEvent},
interpret::{Interpret, InterpretError, Outcome},
inventory::Inventory,
topology::Topology,
};
#[async_trait]
pub trait Score<T: Topology>: pub trait Score<T: Topology>:
std::fmt::Debug + ScoreToString<T> + Send + Sync + CloneBoxScore<T> + SerializeScore<T> std::fmt::Debug + ScoreToString<T> + Send + Sync + CloneBoxScore<T> + SerializeScore<T>
{ {
async fn interpret(
&self,
inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
let id = Id::default();
let interpret = self.create_interpret();
instrumentation::instrument(HarmonyEvent::InterpretExecutionStarted {
execution_id: id.clone().to_string(),
topology: topology.name().into(),
interpret: interpret.get_name().to_string(),
score: self.name(),
message: format!("{} running...", interpret.get_name()),
})
.unwrap();
let result = interpret.execute(inventory, topology).await;
instrumentation::instrument(HarmonyEvent::InterpretExecutionFinished {
execution_id: id.clone().to_string(),
topology: topology.name().into(),
interpret: interpret.get_name().to_string(),
score: self.name(),
outcome: result.clone(),
})
.unwrap();
result
}
fn name(&self) -> String;
#[doc(hidden)]
fn create_interpret(&self) -> Box<dyn Interpret<T>>; fn create_interpret(&self) -> Box<dyn Interpret<T>>;
fn name(&self) -> String;
} }
pub trait SerializeScore<T: Topology> { pub trait SerializeScore<T: Topology> {
fn serialize(&self) -> Value; fn serialize(&self) -> Value;
} }
impl<S, T> SerializeScore<T> for S impl<'de, S, T> SerializeScore<T> for S
where where
T: Topology, T: Topology,
S: Score<T> + Serialize, S: Score<T> + Serialize,
@@ -64,7 +24,7 @@ where
fn serialize(&self) -> Value { fn serialize(&self) -> Value {
// TODO not sure if this is the right place to handle the error or it should bubble // TODO not sure if this is the right place to handle the error or it should bubble
// up? // up?
serde_value::to_value(self).expect("Score should serialize successfully") serde_value::to_value(&self).expect("Score should serialize successfully")
} }
} }

View File

@@ -1,9 +1,15 @@
use async_trait::async_trait; use async_trait::async_trait;
use harmony_macros::ip; use harmony_macros::ip;
use harmony_types::net::MacAddress; use harmony_types::net::MacAddress;
use log::error;
use log::info; use log::info;
use crate::config::Config;
use crate::executors::ExecutorError; use crate::executors::ExecutorError;
use crate::interpret::InterpretError;
use crate::interpret::Outcome;
use crate::inventory::Inventory;
use crate::topology::upgradeable::Upgradeable;
use super::DHCPStaticEntry; use super::DHCPStaticEntry;
use super::DhcpServer; use super::DhcpServer;
@@ -17,17 +23,18 @@ use super::K8sclient;
use super::LoadBalancer; use super::LoadBalancer;
use super::LoadBalancerService; use super::LoadBalancerService;
use super::LogicalHost; use super::LogicalHost;
use super::PreparationError;
use super::PreparationOutcome;
use super::Router; use super::Router;
use super::TftpServer; use super::TftpServer;
use super::Topology; use super::Topology;
use super::Url; use super::Url;
use super::k8s::K8sClient; use super::k8s::K8sClient;
use std::fmt::Debug;
use std::net::IpAddr;
use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
#[derive(Debug, Clone)] #[derive(Clone, Debug)]
pub struct HAClusterTopology { pub struct HAClusterTopology {
pub domain_name: String, pub domain_name: String,
pub router: Arc<dyn Router>, pub router: Arc<dyn Router>,
@@ -48,10 +55,16 @@ impl Topology for HAClusterTopology {
fn name(&self) -> &str { fn name(&self) -> &str {
"HAClusterTopology" "HAClusterTopology"
} }
async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> { async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {
todo!( error!(
"ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready." "ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready."
) );
let config = Config::load().expect("couldn't load config");
if config.run_upgrades {
self.upgrade(&Inventory::empty(), self).await?;
}
Ok(Outcome::success("for now do nothing".to_string()))
} }
} }
@@ -244,12 +257,17 @@ impl Topology for DummyInfra {
todo!() todo!()
} }
async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> { async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {
let dummy_msg = "This is a dummy infrastructure that does nothing"; let dummy_msg = "This is a dummy infrastructure that does nothing";
info!("{dummy_msg}"); info!("{dummy_msg}");
Ok(PreparationOutcome::Success { Ok(Outcome::success(dummy_msg.to_string()))
details: dummy_msg.into(), }
}) }
#[async_trait]
impl<T: Topology> Upgradeable<T> for DummyInfra {
async fn upgrade(&self, _inventory: &Inventory, _topology: &T) -> Result<(), InterpretError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
} }
} }
@@ -419,3 +437,12 @@ impl DnsServer for DummyInfra {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
} }
} }
#[async_trait]
impl<T: Topology> Upgradeable<T> for HAClusterTopology {
async fn upgrade(&self, inventory: &Inventory, topology: &T) -> Result<(), InterpretError> {
error!("TODO implement upgrades for all parts of the cluster");
self.firewall.upgrade(inventory, topology).await?;
Ok(())
}
}

View File

@@ -1,4 +1,5 @@
use derive_new::new; use derive_new::new;
use futures_util::StreamExt;
use k8s_openapi::{ use k8s_openapi::{
ClusterResourceScope, NamespaceResourceScope, ClusterResourceScope, NamespaceResourceScope,
api::{apps::v1::Deployment, core::v1::Pod}, api::{apps::v1::Deployment, core::v1::Pod},
@@ -16,23 +17,16 @@ use kube::{
runtime::wait::await_condition, runtime::wait::await_condition,
}; };
use log::{debug, error, trace}; use log::{debug, error, trace};
use serde::{Serialize, de::DeserializeOwned}; use serde::de::DeserializeOwned;
use similar::TextDiff; use similar::{DiffableStr, TextDiff};
use crate::config::Config as HarmonyConfig;
#[derive(new, Clone)] #[derive(new, Clone)]
pub struct K8sClient { pub struct K8sClient {
client: Client, client: Client,
} }
impl Serialize for K8sClient {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
todo!()
}
}
impl std::fmt::Debug for K8sClient { impl std::fmt::Debug for K8sClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// This is a poor man's debug implementation for now as kube::Client does not provide much // This is a poor man's debug implementation for now as kube::Client does not provide much
@@ -66,13 +60,13 @@ impl K8sClient {
} }
let establish = await_condition(api, name.as_str(), conditions::is_deployment_completed()); let establish = await_condition(api, name.as_str(), conditions::is_deployment_completed());
let t = timeout.unwrap_or(300); let t = if let Some(t) = timeout { t } else { 300 };
let res = tokio::time::timeout(std::time::Duration::from_secs(t), establish).await; let res = tokio::time::timeout(std::time::Duration::from_secs(t), establish).await;
if res.is_ok() { if let Ok(r) = res {
Ok(()) return Ok(());
} else { } else {
Err("timed out while waiting for deployment".to_string()) return Err("timed out while waiting for deployment".to_string());
} }
} }
@@ -111,7 +105,7 @@ impl K8sClient {
.await; .await;
match res { match res {
Err(e) => Err(e.to_string()), Err(e) => return Err(e.to_string()),
Ok(mut process) => { Ok(mut process) => {
let status = process let status = process
.take_status() .take_status()
@@ -120,10 +114,14 @@ impl K8sClient {
.expect("Couldn't unwrap status"); .expect("Couldn't unwrap status");
if let Some(s) = status.status { if let Some(s) = status.status {
debug!("Status: {} - {:?}", s, status.details); debug!("Status: {}", s);
if s == "Success" { Ok(()) } else { Err(s) } if s == "Success" {
return Ok(());
} else {
return Err(s);
}
} else { } else {
Err("Couldn't get inner status of pod exec".to_string()) return Err("Couldn't get inner status of pod exec".to_string());
} }
} }
} }
@@ -158,15 +156,16 @@ impl K8sClient {
.as_ref() .as_ref()
.expect("K8s Resource should have a name"); .expect("K8s Resource should have a name");
if *crate::config::DRY_RUN { let config = HarmonyConfig::load().expect("couldn't load config");
if config.dry_run {
match api.get(name).await { match api.get(name).await {
Ok(current) => { Ok(current) => {
trace!("Received current value {current:#?}"); trace!("Received current value {current:#?}");
// The resource exists, so we calculate and display a diff. // The resource exists, so we calculate and display a diff.
println!("\nPerforming dry-run for resource: '{}'", name); println!("\nPerforming dry-run for resource: '{}'", name);
let mut current_yaml = serde_yaml::to_value(&current).unwrap_or_else(|_| { let mut current_yaml = serde_yaml::to_value(&current)
panic!("Could not serialize current value : {current:#?}") .expect(&format!("Could not serialize current value : {current:#?}"));
});
if current_yaml.is_mapping() && current_yaml.get("status").is_some() { if current_yaml.is_mapping() && current_yaml.get("status").is_some() {
let map = current_yaml.as_mapping_mut().unwrap(); let map = current_yaml.as_mapping_mut().unwrap();
let removed = map.remove_entry("status"); let removed = map.remove_entry("status");
@@ -233,7 +232,7 @@ impl K8sClient {
} }
} }
pub async fn apply_many<K>(&self, resource: &[K], ns: Option<&str>) -> Result<Vec<K>, Error> pub async fn apply_many<K>(&self, resource: &Vec<K>, ns: Option<&str>) -> Result<Vec<K>, Error>
where where
K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize, K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize,
<K as Resource>::Scope: ApplyStrategy<K>, <K as Resource>::Scope: ApplyStrategy<K>,
@@ -249,7 +248,7 @@ impl K8sClient {
pub async fn apply_yaml_many( pub async fn apply_yaml_many(
&self, &self,
#[allow(clippy::ptr_arg)] yaml: &Vec<serde_yaml::Value>, yaml: &Vec<serde_yaml::Value>,
ns: Option<&str>, ns: Option<&str>,
) -> Result<(), Error> { ) -> Result<(), Error> {
for y in yaml.iter() { for y in yaml.iter() {

View File

@@ -1,46 +1,31 @@
use std::{process::Command, sync::Arc}; use std::{process::Command, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use figment::{Figment, providers::Env};
use inquire::Confirm;
use log::{debug, info, warn}; use log::{debug, info, warn};
use serde::Serialize; use serde::{Deserialize, Serialize};
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
use crate::{ use crate::{
executors::ExecutorError, executors::ExecutorError,
interpret::InterpretStatus, interpret::{InterpretError, Outcome},
inventory::Inventory, inventory::Inventory,
modules::{ maestro::Maestro,
k3d::K3DInstallationScore, modules::k3d::K3DInstallationScore,
monitoring::kube_prometheus::crd::{ topology::LocalhostTopology,
crd_alertmanager_config::CRDPrometheus,
prometheus_operator::prometheus_operator_helm_chart_score,
},
prometheus::{
k8s_prometheus_alerting_score::K8sPrometheusCRDAlertingScore,
prometheus::PrometheusApplicationMonitoring,
},
},
score::Score,
}; };
use super::{ use super::{
DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, PreparationError, DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology,
PreparationOutcome, Topology,
k8s::K8sClient, k8s::K8sClient,
oberservability::monitoring::AlertReceiver, tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager},
tenant::{
TenantConfig, TenantManager,
k8s::K8sTenantManager,
network_policy::{
K3dNetworkPolicyStrategy, NetworkPolicyStrategy, NoopNetworkPolicyStrategy,
},
},
}; };
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct K8sState { struct K8sState {
client: Arc<K8sClient>, client: Arc<K8sClient>,
source: K8sSource, _source: K8sSource,
message: String, message: String,
} }
@@ -74,42 +59,8 @@ impl K8sclient for K8sAnywhereTopology {
} }
} }
#[async_trait]
impl PrometheusApplicationMonitoring<CRDPrometheus> for K8sAnywhereTopology {
async fn install_prometheus(
&self,
sender: &CRDPrometheus,
inventory: &Inventory,
receivers: Option<Vec<Box<dyn AlertReceiver<CRDPrometheus>>>>,
) -> Result<PreparationOutcome, PreparationError> {
let po_result = self.ensure_prometheus_operator(sender).await?;
if po_result == PreparationOutcome::Noop {
debug!("Skipping Prometheus CR installation due to missing operator.");
return Ok(po_result);
}
let result = self
.get_k8s_prometheus_application_score(sender.clone(), receivers)
.await
.interpret(inventory, self)
.await;
match result {
Ok(outcome) => match outcome.status {
InterpretStatus::SUCCESS => Ok(PreparationOutcome::Success {
details: outcome.message,
}),
InterpretStatus::NOOP => Ok(PreparationOutcome::Noop),
_ => Err(PreparationError::new(outcome.message)),
},
Err(err) => Err(PreparationError::new(err.to_string())),
}
}
}
impl Serialize for K8sAnywhereTopology { impl Serialize for K8sAnywhereTopology {
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 +85,6 @@ impl K8sAnywhereTopology {
} }
} }
async fn get_k8s_prometheus_application_score(
&self,
sender: CRDPrometheus,
receivers: Option<Vec<Box<dyn AlertReceiver<CRDPrometheus>>>>,
) -> K8sPrometheusCRDAlertingScore {
K8sPrometheusCRDAlertingScore {
sender,
receivers: receivers.unwrap_or_default(),
service_monitors: vec![],
prometheus_rules: vec![],
}
}
fn is_helm_available(&self) -> Result<(), String> { fn is_helm_available(&self) -> Result<(), String> {
let version_result = Command::new("helm") let version_result = Command::new("helm")
.arg("version") .arg("version")
@@ -157,8 +95,9 @@ 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);
debug!("Helm version: {}", version_output.trim()); println!("Helm version: {}", version_output.trim());
Ok(()) Ok(())
} }
@@ -175,42 +114,33 @@ impl K8sAnywhereTopology {
K3DInstallationScore::default() K3DInstallationScore::default()
} }
async fn try_install_k3d(&self) -> Result<(), PreparationError> { async fn try_install_k3d(&self) -> Result<(), InterpretError> {
let result = self let maestro = Maestro::initialize(Inventory::autoload(), LocalhostTopology::new()).await?;
.get_k3d_installation_score() let k3d_score = self.get_k3d_installation_score();
.interpret(&Inventory::empty(), self) maestro.interpret(Box::new(k3d_score)).await?;
.await; Ok(())
match result {
Ok(outcome) => match outcome.status {
InterpretStatus::SUCCESS => Ok(()),
InterpretStatus::NOOP => Ok(()),
_ => Err(PreparationError::new(outcome.message)),
},
Err(err) => Err(PreparationError::new(err.to_string())),
}
} }
async fn try_get_or_install_k8s_client(&self) -> Result<Option<K8sState>, PreparationError> { async fn try_get_or_install_k8s_client(&self) -> Result<Option<K8sState>, InterpretError> {
let k8s_anywhere_config = &self.config; let k8s_anywhere_config = &self.config;
// 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 {
debug!("Using local k3d cluster because of use_local_k3d set to true"); info!("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}");
match self.try_load_kubeconfig(kubeconfig).await { match self.try_load_kubeconfig(&kubeconfig).await {
Some(client) => { Some(client) => {
return Ok(Some(K8sState { return Ok(Some(K8sState {
client: Arc::new(client), client: Arc::new(client),
source: K8sSource::Kubeconfig, _source: K8sSource::Kubeconfig,
message: format!("Loaded k8s client from kubeconfig {kubeconfig}"), message: format!("Loaded k8s client from kubeconfig {kubeconfig}"),
})); }));
} }
None => { None => {
return Err(PreparationError::new(format!( return Err(InterpretError::new(format!(
"Failed to load kubeconfig from {kubeconfig}" "Failed to load kubeconfig from {kubeconfig}"
))); )));
} }
@@ -229,13 +159,22 @@ impl K8sAnywhereTopology {
} }
if !k8s_anywhere_config.autoinstall { if !k8s_anywhere_config.autoinstall {
warn!( debug!("Autoinstall confirmation prompt");
"Installation cancelled, K8sAnywhere could not initialize a valid Kubernetes client" let confirmation = Confirm::new( "Harmony autoinstallation is not activated, do you wish to launch autoinstallation? : ")
); .with_default(false)
return Ok(None); .prompt()
.expect("Unexpected prompt error");
debug!("Autoinstall confirmation {confirmation}");
if !confirmation {
warn!(
"Installation cancelled, K8sAnywhere could not initialize a valid Kubernetes client"
);
return Ok(None);
}
} }
debug!("Starting K8sAnywhere installation"); info!("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
@@ -247,8 +186,8 @@ impl K8sAnywhereTopology {
let state = match k3d.get_client().await { let state = match k3d.get_client().await {
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: "K8s client ready".to_string(), message: "Successfully installed K3D cluster and acquired client".to_string(),
}, },
Err(_) => todo!(), Err(_) => todo!(),
}; };
@@ -256,21 +195,15 @@ impl K8sAnywhereTopology {
Ok(Some(state)) Ok(Some(state))
} }
async fn ensure_k8s_tenant_manager(&self, k8s_state: &K8sState) -> Result<(), String> { async fn ensure_k8s_tenant_manager(&self) -> Result<(), String> {
if self.tenant_manager.get().is_some() { if let Some(_) = self.tenant_manager.get() {
return Ok(()); return Ok(());
} }
self.tenant_manager self.tenant_manager
.get_or_try_init(async || -> Result<K8sTenantManager, String> { .get_or_try_init(async || -> Result<K8sTenantManager, String> {
let k8s_client = self.k8s_client().await?; let k8s_client = self.k8s_client().await?;
let network_policy_strategy: Box<dyn NetworkPolicyStrategy> = match k8s_state.source Ok(K8sTenantManager::new(k8s_client))
{
K8sSource::LocalK3d => Box::new(K3dNetworkPolicyStrategy::new()),
K8sSource::Kubeconfig => Box::new(NoopNetworkPolicyStrategy::new()),
};
Ok(K8sTenantManager::new(k8s_client, network_policy_strategy))
}) })
.await?; .await?;
@@ -285,58 +218,9 @@ impl K8sAnywhereTopology {
)), )),
} }
} }
async fn ensure_prometheus_operator(
&self,
sender: &CRDPrometheus,
) -> Result<PreparationOutcome, PreparationError> {
let status = Command::new("sh")
.args(["-c", "kubectl get crd -A | grep -i prometheuses"])
.status()
.map_err(|e| PreparationError::new(format!("could not connect to cluster: {}", e)))?;
if !status.success() {
if let Some(Some(k8s_state)) = self.k8s_state.get() {
match k8s_state.source {
K8sSource::LocalK3d => {
debug!("installing prometheus operator");
let op_score =
prometheus_operator_helm_chart_score(sender.namespace.clone());
let result = op_score.interpret(&Inventory::empty(), self).await;
return match result {
Ok(outcome) => match outcome.status {
InterpretStatus::SUCCESS => Ok(PreparationOutcome::Success {
details: "installed prometheus operator".into(),
}),
InterpretStatus::NOOP => Ok(PreparationOutcome::Noop),
_ => Err(PreparationError::new(
"failed to install prometheus operator (unknown error)".into(),
)),
},
Err(err) => Err(PreparationError::new(err.to_string())),
};
}
K8sSource::Kubeconfig => {
debug!("unable to install prometheus operator, contact cluster admin");
return Ok(PreparationOutcome::Noop);
}
}
} else {
warn!("Unable to detect k8s_state. Skipping Prometheus Operator install.");
return Ok(PreparationOutcome::Noop);
}
}
debug!("Prometheus operator is already present, skipping install");
Ok(PreparationOutcome::Success {
details: "prometheus operator present in cluster".into(),
})
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Deserialize)]
pub struct K8sAnywhereConfig { pub struct K8sAnywhereConfig {
/// The path of the KUBECONFIG file that Harmony should use to interact with the Kubernetes /// The path of the KUBECONFIG file that Harmony should use to interact with the Kubernetes
/// cluster /// cluster
@@ -354,7 +238,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: true /// Default: false
pub autoinstall: bool, pub autoinstall: bool,
/// Whether to use local k3d cluster. /// Whether to use local k3d cluster.
@@ -363,25 +247,29 @@ pub struct K8sAnywhereConfig {
/// ///
/// default: true /// default: true
pub use_local_k3d: bool, pub use_local_k3d: bool,
pub harmony_profile: String, pub profile: String,
}
impl Default for K8sAnywhereConfig {
fn default() -> Self {
Self {
kubeconfig: None,
use_system_kubeconfig: false,
autoinstall: false,
// TODO harmony_profile should be managed at a more core level than this
profile: "dev".to_string(),
use_local_k3d: true,
}
}
} }
impl K8sAnywhereConfig { impl K8sAnywhereConfig {
fn from_env() -> Self { fn from_env() -> Self {
Self { Figment::new()
kubeconfig: std::env::var("KUBECONFIG").ok().map(|v| v.to_string()), .merge(Env::prefixed("HARMONY_"))
use_system_kubeconfig: std::env::var("HARMONY_USE_SYSTEM_KUBECONFIG") .merge(Env::raw().only(&["KUBECONFIG"]))
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)), .extract()
autoinstall: std::env::var("HARMONY_AUTOINSTALL") .expect("couldn't load config from env")
.map_or_else(|_| true, |v| v.parse().ok().unwrap_or(false)),
// TODO harmony_profile should be managed at a more core level than this
harmony_profile: std::env::var("HARMONY_PROFILE").map_or_else(
|_| "dev".to_string(),
|v| v.parse().ok().unwrap_or("dev".to_string()),
),
use_local_k3d: std::env::var("HARMONY_USE_LOCAL_K3D")
.map_or_else(|_| true, |v| v.parse().ok().unwrap_or(true)),
}
} }
} }
@@ -391,25 +279,26 @@ impl Topology for K8sAnywhereTopology {
"K8sAnywhereTopology" "K8sAnywhereTopology"
} }
async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> { async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {
let k8s_state = self let k8s_state = self
.k8s_state .k8s_state
.get_or_try_init(|| self.try_get_or_install_k8s_client()) .get_or_try_init(|| self.try_get_or_install_k8s_client())
.await?; .await?;
let k8s_state: &K8sState = k8s_state.as_ref().ok_or(PreparationError::new( let k8s_state: &K8sState = k8s_state.as_ref().ok_or(InterpretError::new(
"no K8s client could be found or installed".to_string(), "No K8s client could be found or installed".to_string(),
))?; ))?;
self.ensure_k8s_tenant_manager(k8s_state) self.ensure_k8s_tenant_manager()
.await .await
.map_err(PreparationError::new)?; .map_err(|e| InterpretError::new(e))?;
match self.is_helm_available() { match self.is_helm_available() {
Ok(()) => Ok(PreparationOutcome::Success { Ok(()) => Ok(Outcome::success(format!(
details: format!("{} + helm available", k8s_state.message.clone()), "{} + helm available",
}), k8s_state.message.clone()
Err(e) => Err(PreparationError::new(format!("helm unavailable: {}", e))), ))),
Err(e) => Err(InterpretError::new(format!("helm unavailable: {}", e))),
} }
} }
} }
@@ -420,7 +309,7 @@ impl MultiTargetTopology for K8sAnywhereTopology {
return DeploymentTarget::LocalDev; return DeploymentTarget::LocalDev;
} }
match self.config.harmony_profile.to_lowercase().as_str() { match self.config.profile.to_lowercase().as_str() {
"staging" => DeploymentTarget::Staging, "staging" => DeploymentTarget::Staging,
"production" => DeploymentTarget::Production, "production" => DeploymentTarget::Production,
_ => todo!("HARMONY_PROFILE must be set when use_local_k3d is not set"), _ => todo!("HARMONY_PROFILE must be set when use_local_k3d is not set"),

View File

@@ -1,7 +1,9 @@
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new; use derive_new::new;
use super::{HelmCommand, PreparationError, PreparationOutcome, Topology}; use crate::interpret::{InterpretError, Outcome};
use super::{HelmCommand, Topology};
#[derive(new)] #[derive(new)]
pub struct LocalhostTopology; pub struct LocalhostTopology;
@@ -12,10 +14,10 @@ impl Topology for LocalhostTopology {
"LocalHostTopology" "LocalHostTopology"
} }
async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> { async fn ensure_ready(&self) -> Result<Outcome, InterpretError> {
Ok(PreparationOutcome::Success { Ok(Outcome::success(
details: "Localhost is Chuck Norris, always ready.".into(), "Localhost is Chuck Norris, always ready.".to_string(),
}) ))
} }
} }

View File

@@ -6,7 +6,7 @@ mod k8s_anywhere;
mod localhost; mod localhost;
pub mod oberservability; pub mod oberservability;
pub mod tenant; pub mod tenant;
use derive_new::new; pub mod upgradeable;
pub use k8s_anywhere::*; pub use k8s_anywhere::*;
pub use localhost::*; pub use localhost::*;
pub mod k8s; pub mod k8s;
@@ -27,13 +27,10 @@ pub use tftp::*;
mod helm_command; mod helm_command;
pub use helm_command::*; pub use helm_command::*;
use super::{
executors::ExecutorError,
instrumentation::{self, HarmonyEvent},
};
use std::error::Error;
use std::net::IpAddr; use std::net::IpAddr;
use super::interpret::{InterpretError, Outcome};
/// Represents a logical view of an infrastructure environment providing specific capabilities. /// Represents a logical view of an infrastructure environment providing specific capabilities.
/// ///
/// A Topology acts as a self-contained "package" responsible for managing access /// A Topology acts as a self-contained "package" responsible for managing access
@@ -61,128 +58,9 @@ pub trait Topology: Send + Sync {
/// * **Internal Orchestration:** For complex topologies, this method might manage dependencies on other sub-topologies, ensuring *their* `ensure_ready` is called first. Using nested `Maestros` to run setup `Scores` against these sub-topologies is the recommended pattern for non-trivial bootstrapping, allowing reuse of Harmony's core orchestration logic. /// * **Internal Orchestration:** For complex topologies, this method might manage dependencies on other sub-topologies, ensuring *their* `ensure_ready` is called first. Using nested `Maestros` to run setup `Scores` against these sub-topologies is the recommended pattern for non-trivial bootstrapping, allowing reuse of Harmony's core orchestration logic.
/// ///
/// # Returns /// # Returns
/// - `Ok(PreparationOutcome)`: Indicates the topology is now ready. The `Outcome` status might be `SUCCESS` if actions were taken, or `NOOP` if it was already ready. The message should provide context. /// - `Ok(Outcome)`: Indicates the topology is now ready. The `Outcome` status might be `SUCCESS` if actions were taken, or `NOOP` if it was already ready. The message should provide context.
/// - `Err(PreparationError)`: Indicates the topology could not reach a ready state due to configuration issues, discovery failures, bootstrap errors, or unsupported environments. /// - `Err(TopologyError)`: Indicates the topology could not reach a ready state due to configuration issues, discovery failures, bootstrap errors, or unsupported environments.
async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError>; async fn ensure_ready(&self) -> Result<Outcome, InterpretError>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PreparationOutcome {
Success { details: String },
Noop,
}
#[derive(Debug, Clone, new)]
pub struct PreparationError {
msg: String,
}
impl std::fmt::Display for PreparationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.msg)
}
}
impl Error for PreparationError {}
impl From<ExecutorError> for PreparationError {
fn from(value: ExecutorError) -> Self {
Self {
msg: format!("InterpretError : {value}"),
}
}
}
impl From<kube::Error> for PreparationError {
fn from(value: kube::Error) -> Self {
Self {
msg: format!("PreparationError : {value}"),
}
}
}
impl From<String> for PreparationError {
fn from(value: String) -> Self {
Self {
msg: format!("PreparationError : {value}"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum TopologyStatus {
Queued,
Preparing,
Success,
Noop,
Error,
}
pub struct TopologyState {
pub topology: String,
pub status: TopologyStatus,
}
impl TopologyState {
pub fn new(topology: String) -> Self {
let instance = Self {
topology,
status: TopologyStatus::Queued,
};
instrumentation::instrument(HarmonyEvent::TopologyStateChanged {
topology: instance.topology.clone(),
status: instance.status.clone(),
message: None,
})
.unwrap();
instance
}
pub fn prepare(&mut self) {
self.status = TopologyStatus::Preparing;
instrumentation::instrument(HarmonyEvent::TopologyStateChanged {
topology: self.topology.clone(),
status: self.status.clone(),
message: None,
})
.unwrap();
}
pub fn success(&mut self, message: String) {
self.status = TopologyStatus::Success;
instrumentation::instrument(HarmonyEvent::TopologyStateChanged {
topology: self.topology.clone(),
status: self.status.clone(),
message: Some(message),
})
.unwrap();
}
pub fn noop(&mut self) {
self.status = TopologyStatus::Noop;
instrumentation::instrument(HarmonyEvent::TopologyStateChanged {
topology: self.topology.clone(),
status: self.status.clone(),
message: None,
})
.unwrap();
}
pub fn error(&mut self, message: String) {
self.status = TopologyStatus::Error;
instrumentation::instrument(HarmonyEvent::TopologyStateChanged {
topology: self.topology.clone(),
status: self.status.clone(),
message: Some(message),
})
.unwrap();
}
} }
#[derive(Debug)] #[derive(Debug)]
@@ -211,7 +89,7 @@ impl Serialize for Url {
{ {
match self { match self {
Url::LocalFolder(path) => serializer.serialize_str(path), Url::LocalFolder(path) => serializer.serialize_str(path),
Url::Url(url) => serializer.serialize_str(url.as_str()), Url::Url(url) => serializer.serialize_str(&url.as_str()),
} }
} }
} }

View File

@@ -2,9 +2,15 @@ use std::{net::Ipv4Addr, str::FromStr, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use harmony_types::net::MacAddress; use harmony_types::net::MacAddress;
use log::debug;
use serde::Serialize; use serde::Serialize;
use crate::executors::ExecutorError; use crate::{
executors::ExecutorError,
interpret::InterpretError,
inventory::Inventory,
topology::{Topology, upgradeable::Upgradeable},
};
use super::{IpAddress, LogicalHost, k8s::K8sClient}; use super::{IpAddress, LogicalHost, k8s::K8sClient};
@@ -38,6 +44,15 @@ impl std::fmt::Debug for dyn Firewall {
} }
} }
// #[async_trait]
// impl<T: Topology> Upgradeable<T> for dyn Firewall {
// async fn upgrade(&self, inventory: &Inventory, topology: &T) -> Result<(), InterpretError> {
// debug!("upgrading");
// self.upgrade(inventory, topology).await?;
// Ok(())
// }
// }
pub struct NetworkDomain { pub struct NetworkDomain {
pub name: String, pub name: String,
} }

View File

@@ -1,5 +1,3 @@
use std::any::Any;
use async_trait::async_trait; use async_trait::async_trait;
use log::debug; use log::debug;
@@ -45,7 +43,7 @@ impl<S: AlertSender + Installable<T>, T: Topology> Interpret<T> for AlertingInte
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {
InterpretName::Alerting todo!()
} }
fn get_version(&self) -> Version { fn get_version(&self) -> Version {
@@ -64,9 +62,7 @@ impl<S: AlertSender + Installable<T>, T: Topology> Interpret<T> for AlertingInte
#[async_trait] #[async_trait]
pub trait AlertReceiver<S: AlertSender>: std::fmt::Debug + Send + Sync { pub trait AlertReceiver<S: AlertSender>: std::fmt::Debug + Send + Sync {
async fn install(&self, sender: &S) -> Result<Outcome, InterpretError>; async fn install(&self, sender: &S) -> Result<Outcome, InterpretError>;
fn name(&self) -> String;
fn clone_box(&self) -> Box<dyn AlertReceiver<S>>; fn clone_box(&self) -> Box<dyn AlertReceiver<S>>;
fn as_any(&self) -> &dyn Any;
} }
#[async_trait] #[async_trait]
@@ -76,6 +72,6 @@ pub trait AlertRule<S: AlertSender>: std::fmt::Debug + Send + Sync {
} }
#[async_trait] #[async_trait]
pub trait ScrapeTarget<S: AlertSender> { pub trait ScrapeTarger<S: AlertSender> {
async fn install(&self, sender: &S) -> Result<(), InterpretError>; async fn install(&self, sender: &S) -> Result<(), InterpretError>;
} }

View File

@@ -27,11 +27,11 @@ pub struct UnmanagedRouter {
impl Router for UnmanagedRouter { impl Router for UnmanagedRouter {
fn get_gateway(&self) -> IpAddress { fn get_gateway(&self) -> IpAddress {
self.gateway self.gateway.clone()
} }
fn get_cidr(&self) -> Ipv4Cidr { fn get_cidr(&self) -> Ipv4Cidr {
self.cidr self.cidr.clone()
} }
fn get_host(&self) -> LogicalHost { fn get_host(&self) -> LogicalHost {

View File

@@ -15,38 +15,36 @@ use k8s_openapi::{
apimachinery::pkg::util::intstr::IntOrString, apimachinery::pkg::util::intstr::IntOrString,
}; };
use kube::Resource; use kube::Resource;
use log::debug; use log::{debug, info, warn};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde_json::json; use serde_json::json;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
use super::{TenantConfig, TenantManager, network_policy::NetworkPolicyStrategy}; use super::{TenantConfig, TenantManager};
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct K8sTenantManager { pub struct K8sTenantManager {
k8s_client: Arc<K8sClient>, k8s_client: Arc<K8sClient>,
k8s_tenant_config: Arc<OnceCell<TenantConfig>>, k8s_tenant_config: Arc<OnceCell<TenantConfig>>,
network_policy_strategy: Box<dyn NetworkPolicyStrategy>,
} }
impl K8sTenantManager { impl K8sTenantManager {
pub fn new( pub fn new(client: Arc<K8sClient>) -> Self {
client: Arc<K8sClient>,
network_policy_strategy: Box<dyn NetworkPolicyStrategy>,
) -> Self {
Self { Self {
k8s_client: client, k8s_client: client,
k8s_tenant_config: Arc::new(OnceCell::new()), k8s_tenant_config: Arc::new(OnceCell::new()),
network_policy_strategy,
} }
} }
}
impl K8sTenantManager {
fn get_namespace_name(&self, config: &TenantConfig) -> String { fn get_namespace_name(&self, config: &TenantConfig) -> String {
config.name.clone() config.name.clone()
} }
fn ensure_constraints(&self, _namespace: &Namespace) -> Result<(), ExecutorError> { fn ensure_constraints(&self, _namespace: &Namespace) -> Result<(), ExecutorError> {
// TODO: Ensure constraints are applied to namespace (https://git.nationtech.io/NationTech/harmony/issues/98) warn!("Validate that when tenant already exists (by id) that name has not changed");
warn!("Make sure other Tenant constraints are respected by this k8s implementation");
Ok(()) Ok(())
} }
@@ -221,6 +219,24 @@ impl K8sTenantManager {
} }
] ]
}, },
{
"to": [
{
"ipBlock": {
"cidr": "10.43.0.1/32",
}
}
]
},
{
"to": [
{
"ipBlock": {
"cidr": "172.23.0.0/16",
}
}
]
},
{ {
"to": [ "to": [
{ {
@@ -288,19 +304,19 @@ impl K8sTenantManager {
let ports: Option<Vec<NetworkPolicyPort>> = let ports: Option<Vec<NetworkPolicyPort>> =
c.1.as_ref().map(|spec| match &spec.data { c.1.as_ref().map(|spec| match &spec.data {
super::PortSpecData::SinglePort(port) => vec![NetworkPolicyPort { super::PortSpecData::SinglePort(port) => vec![NetworkPolicyPort {
port: Some(IntOrString::Int((*port).into())), port: Some(IntOrString::Int(port.clone().into())),
..Default::default() ..Default::default()
}], }],
super::PortSpecData::PortRange(start, end) => vec![NetworkPolicyPort { super::PortSpecData::PortRange(start, end) => vec![NetworkPolicyPort {
port: Some(IntOrString::Int((*start).into())), port: Some(IntOrString::Int(start.clone().into())),
end_port: Some((*end).into()), end_port: Some(end.clone().into()),
protocol: None, // Not currently supported by Harmony protocol: None, // Not currently supported by Harmony
}], }],
super::PortSpecData::ListOfPorts(items) => items super::PortSpecData::ListOfPorts(items) => items
.iter() .iter()
.map(|i| NetworkPolicyPort { .map(|i| NetworkPolicyPort {
port: Some(IntOrString::Int((*i).into())), port: Some(IntOrString::Int(i.clone().into())),
..Default::default() ..Default::default()
}) })
.collect(), .collect(),
@@ -345,19 +361,19 @@ impl K8sTenantManager {
let ports: Option<Vec<NetworkPolicyPort>> = let ports: Option<Vec<NetworkPolicyPort>> =
c.1.as_ref().map(|spec| match &spec.data { c.1.as_ref().map(|spec| match &spec.data {
super::PortSpecData::SinglePort(port) => vec![NetworkPolicyPort { super::PortSpecData::SinglePort(port) => vec![NetworkPolicyPort {
port: Some(IntOrString::Int((*port).into())), port: Some(IntOrString::Int(port.clone().into())),
..Default::default() ..Default::default()
}], }],
super::PortSpecData::PortRange(start, end) => vec![NetworkPolicyPort { super::PortSpecData::PortRange(start, end) => vec![NetworkPolicyPort {
port: Some(IntOrString::Int((*start).into())), port: Some(IntOrString::Int(start.clone().into())),
end_port: Some((*end).into()), end_port: Some(end.clone().into()),
protocol: None, // Not currently supported by Harmony protocol: None, // Not currently supported by Harmony
}], }],
super::PortSpecData::ListOfPorts(items) => items super::PortSpecData::ListOfPorts(items) => items
.iter() .iter()
.map(|i| NetworkPolicyPort { .map(|i| NetworkPolicyPort {
port: Some(IntOrString::Int((*i).into())), port: Some(IntOrString::Int(i.clone().into())),
..Default::default() ..Default::default()
}) })
.collect(), .collect(),
@@ -390,27 +406,12 @@ impl K8sTenantManager {
} }
} }
impl Clone for K8sTenantManager {
fn clone(&self) -> Self {
Self {
k8s_client: self.k8s_client.clone(),
k8s_tenant_config: self.k8s_tenant_config.clone(),
network_policy_strategy: self.network_policy_strategy.clone_box(),
}
}
}
#[async_trait] #[async_trait]
impl TenantManager for K8sTenantManager { impl TenantManager for K8sTenantManager {
async fn provision_tenant(&self, config: &TenantConfig) -> Result<(), ExecutorError> { async fn provision_tenant(&self, config: &TenantConfig) -> Result<(), ExecutorError> {
let namespace = self.build_namespace(config)?; let namespace = self.build_namespace(config)?;
let resource_quota = self.build_resource_quota(config)?; let resource_quota = self.build_resource_quota(config)?;
let network_policy = self.build_network_policy(config)?; let network_policy = self.build_network_policy(config)?;
let network_policy = self
.network_policy_strategy
.adjust_policy(network_policy, config);
let resource_limit_range = self.build_limit_range(config)?; let resource_limit_range = self.build_limit_range(config)?;
self.ensure_constraints(&namespace)?; self.ensure_constraints(&namespace)?;
@@ -427,14 +428,13 @@ impl TenantManager for K8sTenantManager {
debug!("Creating network_policy for tenant {}", config.name); debug!("Creating network_policy for tenant {}", config.name);
self.apply_resource(network_policy, config).await?; self.apply_resource(network_policy, config).await?;
debug!( info!(
"Success provisionning K8s tenant id {} name {}", "Success provisionning K8s tenant id {} name {}",
config.id, config.name config.id, config.name
); );
self.store_config(config); self.store_config(config);
Ok(()) Ok(())
} }
async fn get_tenant_config(&self) -> Option<TenantConfig> { async fn get_tenant_config(&self) -> Option<TenantConfig> {
self.k8s_tenant_config.get().cloned() self.k8s_tenant_config.get().cloned()
} }

View File

@@ -1,11 +1,11 @@
pub mod k8s; pub mod k8s;
mod manager; mod manager;
pub mod network_policy; use std::str::FromStr;
use crate::data::Id;
pub use manager::*; pub use manager::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr;
use crate::data::Id;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] // Assuming serde for Scores #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] // Assuming serde for Scores
pub struct TenantConfig { pub struct TenantConfig {

View File

@@ -1,120 +0,0 @@
use k8s_openapi::api::networking::v1::{
IPBlock, NetworkPolicy, NetworkPolicyEgressRule, NetworkPolicyPeer, NetworkPolicySpec,
};
use super::TenantConfig;
pub trait NetworkPolicyStrategy: Send + Sync + std::fmt::Debug {
fn clone_box(&self) -> Box<dyn NetworkPolicyStrategy>;
fn adjust_policy(&self, policy: NetworkPolicy, config: &TenantConfig) -> NetworkPolicy;
}
#[derive(Clone, Debug)]
pub struct NoopNetworkPolicyStrategy {}
impl NoopNetworkPolicyStrategy {
pub fn new() -> Self {
Self {}
}
}
impl Default for NoopNetworkPolicyStrategy {
fn default() -> Self {
Self::new()
}
}
impl NetworkPolicyStrategy for NoopNetworkPolicyStrategy {
fn clone_box(&self) -> Box<dyn NetworkPolicyStrategy> {
Box::new(self.clone())
}
fn adjust_policy(&self, policy: NetworkPolicy, _config: &TenantConfig) -> NetworkPolicy {
policy
}
}
#[derive(Clone, Debug)]
pub struct K3dNetworkPolicyStrategy {}
impl K3dNetworkPolicyStrategy {
pub fn new() -> Self {
Self {}
}
}
impl Default for K3dNetworkPolicyStrategy {
fn default() -> Self {
Self::new()
}
}
impl NetworkPolicyStrategy for K3dNetworkPolicyStrategy {
fn clone_box(&self) -> Box<dyn NetworkPolicyStrategy> {
Box::new(self.clone())
}
fn adjust_policy(&self, policy: NetworkPolicy, _config: &TenantConfig) -> NetworkPolicy {
let mut egress = policy
.spec
.clone()
.unwrap_or_default()
.egress
.clone()
.unwrap_or_default();
egress.push(NetworkPolicyEgressRule {
to: Some(vec![NetworkPolicyPeer {
ip_block: Some(IPBlock {
cidr: "172.18.0.0/16".into(), // TODO: query the IP range https://git.nationtech.io/NationTech/harmony/issues/108
..Default::default()
}),
..Default::default()
}]),
..Default::default()
});
NetworkPolicy {
spec: Some(NetworkPolicySpec {
egress: Some(egress),
..policy.spec.unwrap_or_default()
}),
..policy
}
}
}
#[cfg(test)]
mod tests {
use k8s_openapi::api::networking::v1::{
IPBlock, NetworkPolicy, NetworkPolicyEgressRule, NetworkPolicyPeer, NetworkPolicySpec,
};
use super::{K3dNetworkPolicyStrategy, NetworkPolicyStrategy};
#[test]
pub fn should_add_ip_block_for_k3d_harmony_server() {
let strategy = K3dNetworkPolicyStrategy::new();
let policy =
strategy.adjust_policy(NetworkPolicy::default(), &super::TenantConfig::default());
let expected_policy = NetworkPolicy {
spec: Some(NetworkPolicySpec {
egress: Some(vec![NetworkPolicyEgressRule {
to: Some(vec![NetworkPolicyPeer {
ip_block: Some(IPBlock {
cidr: "172.18.0.0/16".into(),
..Default::default()
}),
..Default::default()
}]),
..Default::default()
}]),
..Default::default()
}),
..Default::default()
};
assert_eq!(expected_policy, policy);
}
}

View File

@@ -0,0 +1,8 @@
use async_trait::async_trait;
use crate::{interpret::InterpretError, inventory::Inventory};
#[async_trait]
pub trait Upgradeable<T>: Send + Sync {
async fn upgrade(&self, inventory: &Inventory, topology: &T) -> Result<(), InterpretError>;
}

View File

@@ -60,7 +60,7 @@ impl DnsServer for OPNSenseFirewall {
} }
fn get_ip(&self) -> IpAddress { fn get_ip(&self) -> IpAddress {
OPNSenseFirewall::get_ip(self) OPNSenseFirewall::get_ip(&self)
} }
fn get_host(&self) -> LogicalHost { fn get_host(&self) -> LogicalHost {

View File

@@ -48,7 +48,7 @@ impl HttpServer for OPNSenseFirewall {
async fn ensure_initialized(&self) -> Result<(), ExecutorError> { async fn ensure_initialized(&self) -> Result<(), ExecutorError> {
let mut config = self.opnsense_config.write().await; let mut config = self.opnsense_config.write().await;
let caddy = config.caddy(); let caddy = config.caddy();
if caddy.get_full_config().is_none() { if let None = caddy.get_full_config() {
info!("Http config not available in opnsense config, installing package"); info!("Http config not available in opnsense config, installing package");
config.install_package("os-caddy").await.map_err(|e| { config.install_package("os-caddy").await.map_err(|e| {
ExecutorError::UnexpectedError(format!( ExecutorError::UnexpectedError(format!(

View File

@@ -121,12 +121,10 @@ pub(crate) fn haproxy_xml_config_to_harmony_loadbalancer(
LoadBalancerService { LoadBalancerService {
backend_servers, backend_servers,
listening_port: frontend.bind.parse().unwrap_or_else(|_| { listening_port: frontend.bind.parse().expect(&format!(
panic!( "HAProxy frontend address should be a valid SocketAddr, got {}",
"HAProxy frontend address should be a valid SocketAddr, got {}", frontend.bind
frontend.bind )),
)
}),
health_check, health_check,
} }
}) })
@@ -169,28 +167,28 @@ pub(crate) fn get_health_check_for_backend(
None => return None, None => return None,
}; };
let haproxy_health_check = haproxy let haproxy_health_check = match haproxy
.healthchecks .healthchecks
.healthchecks .healthchecks
.iter() .iter()
.find(|h| &h.uuid == health_check_uuid)?; .find(|h| &h.uuid == health_check_uuid)
{
Some(health_check) => health_check,
None => return None,
};
let binding = haproxy_health_check.health_check_type.to_uppercase(); let binding = haproxy_health_check.health_check_type.to_uppercase();
let uppercase = binding.as_str(); let uppercase = binding.as_str();
match uppercase { match uppercase {
"TCP" => { "TCP" => {
if let Some(checkport) = haproxy_health_check.checkport.content.as_ref() { if let Some(checkport) = haproxy_health_check.checkport.content.as_ref() {
if !checkport.is_empty() { if checkport.len() > 0 {
return Some(HealthCheck::TCP(Some(checkport.parse().unwrap_or_else( return Some(HealthCheck::TCP(Some(checkport.parse().expect(&format!(
|_| { "HAProxy check port should be a valid port number, got {checkport}"
panic!( )))));
"HAProxy check port should be a valid port number, got {checkport}"
)
},
))));
} }
} }
Some(HealthCheck::TCP(None)) return Some(HealthCheck::TCP(None));
} }
"HTTP" => { "HTTP" => {
let path: String = haproxy_health_check let path: String = haproxy_health_check
@@ -357,13 +355,16 @@ mod tests {
// Create an HAProxy instance with servers // Create an HAProxy instance with servers
let mut haproxy = HAProxy::default(); let mut haproxy = HAProxy::default();
let server = HAProxyServer { let mut server = HAProxyServer::default();
uuid: "server1".to_string(), server.uuid = "server1".to_string();
address: "192.168.1.1".to_string(), server.address = "192.168.1.1".to_string();
port: 80, server.port = 80;
..Default::default()
};
haproxy.servers.servers.push(server); haproxy.servers.servers.push(server);
let mut server = HAProxyServer::default();
server.uuid = "server3".to_string();
server.address = "192.168.1.3".to_string();
server.port = 8080;
// Call the function // Call the function
let result = get_servers_for_backend(&backend, &haproxy); let result = get_servers_for_backend(&backend, &haproxy);
@@ -383,12 +384,10 @@ mod tests {
let backend = HAProxyBackend::default(); let backend = HAProxyBackend::default();
// Create an HAProxy instance with servers // Create an HAProxy instance with servers
let mut haproxy = HAProxy::default(); let mut haproxy = HAProxy::default();
let server = HAProxyServer { let mut server = HAProxyServer::default();
uuid: "server1".to_string(), server.uuid = "server1".to_string();
address: "192.168.1.1".to_string(), server.address = "192.168.1.1".to_string();
port: 80, server.port = 80;
..Default::default()
};
haproxy.servers.servers.push(server); haproxy.servers.servers.push(server);
// Call the function // Call the function
let result = get_servers_for_backend(&backend, &haproxy); let result = get_servers_for_backend(&backend, &haproxy);
@@ -403,12 +402,10 @@ mod tests {
backend.linked_servers.content = Some("server4,server5".to_string()); backend.linked_servers.content = Some("server4,server5".to_string());
// Create an HAProxy instance with servers // Create an HAProxy instance with servers
let mut haproxy = HAProxy::default(); let mut haproxy = HAProxy::default();
let server = HAProxyServer { let mut server = HAProxyServer::default();
uuid: "server1".to_string(), server.uuid = "server1".to_string();
address: "192.168.1.1".to_string(), server.address = "192.168.1.1".to_string();
port: 80, server.port = 80;
..Default::default()
};
haproxy.servers.servers.push(server); haproxy.servers.servers.push(server);
// Call the function // Call the function
let result = get_servers_for_backend(&backend, &haproxy); let result = get_servers_for_backend(&backend, &haproxy);
@@ -419,28 +416,20 @@ mod tests {
#[test] #[test]
fn test_get_servers_for_backend_multiple_linked_servers() { fn test_get_servers_for_backend_multiple_linked_servers() {
// Create a backend with multiple linked servers // Create a backend with multiple linked servers
#[allow(clippy::field_reassign_with_default)]
let mut backend = HAProxyBackend::default(); let mut backend = HAProxyBackend::default();
backend.linked_servers.content = Some("server1,server2".to_string()); backend.linked_servers.content = Some("server1,server2".to_string());
//
// Create an HAProxy instance with matching servers // Create an HAProxy instance with matching servers
let mut haproxy = HAProxy::default(); let mut haproxy = HAProxy::default();
let server = HAProxyServer { let mut server = HAProxyServer::default();
uuid: "server1".to_string(), server.uuid = "server1".to_string();
address: "some-hostname.test.mcd".to_string(), server.address = "some-hostname.test.mcd".to_string();
port: 80, server.port = 80;
..Default::default()
};
haproxy.servers.servers.push(server); haproxy.servers.servers.push(server);
let mut server = HAProxyServer::default();
let server = HAProxyServer { server.uuid = "server2".to_string();
uuid: "server2".to_string(), server.address = "192.168.1.2".to_string();
address: "192.168.1.2".to_string(), server.port = 8080;
port: 8080,
..Default::default()
};
haproxy.servers.servers.push(server); haproxy.servers.servers.push(server);
// Call the function // Call the function
let result = get_servers_for_backend(&backend, &haproxy); let result = get_servers_for_backend(&backend, &haproxy);
// Check the result // Check the result

View File

@@ -7,13 +7,18 @@ mod management;
mod tftp; mod tftp;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait;
pub use management::*; pub use management::*;
use opnsense_config_xml::Host; use opnsense_config_xml::Host;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::{ use crate::{
executors::ExecutorError, executors::ExecutorError,
topology::{IpAddress, LogicalHost}, interpret::InterpretError,
inventory::Inventory,
modules::opnsense::OPNSenseLaunchUpgrade,
score::Score,
topology::{IpAddress, LogicalHost, Topology, upgradeable::Upgradeable},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -22,18 +27,12 @@ pub struct OPNSenseFirewall {
host: LogicalHost, host: LogicalHost,
} }
// TODO figure out a design to have a unique identifiere for this firewall
// I think a project identifier would be good enough, then the secrets module configuration will
// point to the project's vault and this opnsense modules doesn't need to know anything about it
const OPNSENSE_CREDENTIALS: &str = "OPNSENSE_CREDENTIALS";
impl OPNSenseFirewall { impl OPNSenseFirewall {
pub fn get_ip(&self) -> IpAddress { pub fn get_ip(&self) -> IpAddress {
self.host.ip self.host.ip
} }
pub async fn new(host: LogicalHost, port: Option<u16>, username: &str, password: &str) -> Self { pub async fn new(host: LogicalHost, port: Option<u16>, username: &str, password: &str) -> Self {
// let credentials = Secrets::get_by_name(OPNSENSE_CREDENTIALS)
Self { Self {
opnsense_config: Arc::new(RwLock::new( opnsense_config: Arc::new(RwLock::new(
opnsense_config::Config::from_credentials(host.ip, port, username, password).await, opnsense_config::Config::from_credentials(host.ip, port, username, password).await,
@@ -55,3 +54,17 @@ impl OPNSenseFirewall {
.map_err(|e| ExecutorError::UnexpectedError(e.to_string())) .map_err(|e| ExecutorError::UnexpectedError(e.to_string()))
} }
} }
#[async_trait]
impl<T: Topology> Upgradeable<T> for OPNSenseFirewall {
async fn upgrade(&self, inventory: &Inventory, topology: &T) -> Result<(), InterpretError> {
OPNSenseLaunchUpgrade {
opnsense: self.get_opnsense_config(),
}
.create_interpret()
.execute(inventory, topology)
.await?;
Ok(())
}
}

View File

@@ -58,7 +58,7 @@ impl TftpServer for OPNSenseFirewall {
async fn ensure_initialized(&self) -> Result<(), ExecutorError> { async fn ensure_initialized(&self) -> Result<(), ExecutorError> {
let mut config = self.opnsense_config.write().await; let mut config = self.opnsense_config.write().await;
let tftp = config.tftp(); let tftp = config.tftp();
if tftp.get_full_config().is_none() { if let None = tftp.get_full_config() {
info!("Tftp config not available in opnsense config, installing package"); info!("Tftp config not available in opnsense config, installing package");
config.install_package("os-tftp").await.map_err(|e| { config.install_package("os-tftp").await.map_err(|e| {
ExecutorError::UnexpectedError(format!( ExecutorError::UnexpectedError(format!(

View File

@@ -13,7 +13,7 @@ pub trait ApplicationFeature<T: Topology>:
fn name(&self) -> String; fn name(&self) -> String;
} }
pub trait ApplicationFeatureClone<T: Topology> { trait ApplicationFeatureClone<T: Topology> {
fn clone_box(&self) -> Box<dyn ApplicationFeature<T>>; fn clone_box(&self) -> Box<dyn ApplicationFeature<T>>;
} }
@@ -27,7 +27,7 @@ where
} }
impl<T: Topology> Serialize for Box<dyn ApplicationFeature<T>> { impl<T: Topology> Serialize for Box<dyn ApplicationFeature<T>> {
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,
{ {

View File

@@ -184,11 +184,12 @@ impl ArgoApplication {
pub fn to_yaml(&self) -> serde_yaml::Value { pub fn to_yaml(&self) -> serde_yaml::Value {
let name = &self.name; let name = &self.name;
let namespace = if let Some(ns) = self.namespace.as_ref() { let namespace = if let Some(ns) = self.namespace.as_ref() {
ns &ns
} else { } else {
"argocd" "argocd"
}; };
let project = &self.project; let project = &self.project;
let source = &self.source;
let yaml_str = format!( let yaml_str = format!(
r#" r#"
@@ -227,7 +228,7 @@ spec:
serde_yaml::to_value(&self.source).expect("couldn't serialize source to value"); serde_yaml::to_value(&self.source).expect("couldn't serialize source to value");
let sync_policy = serde_yaml::to_value(&self.sync_policy) let sync_policy = serde_yaml::to_value(&self.sync_policy)
.expect("couldn't serialize sync_policy to value"); .expect("couldn't serialize sync_policy to value");
let revision_history_limit = serde_yaml::to_value(self.revision_history_limit) let revision_history_limit = serde_yaml::to_value(&self.revision_history_limit)
.expect("couldn't serialize revision_history_limit to value"); .expect("couldn't serialize revision_history_limit to value");
spec.insert( spec.insert(

View File

@@ -1,16 +1,16 @@
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::info; use log::{error, info};
use serde_yaml::Value; use serde_yaml::Value;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use crate::{ use crate::{
config::HARMONY_DATA_DIR, config::Config,
data::Version, data::Version,
inventory::Inventory, inventory::Inventory,
modules::application::{ modules::application::{
ApplicationFeature, HelmPackage, OCICompliant, Application, ApplicationFeature, HelmPackage, OCICompliant,
features::{ArgoApplication, ArgoHelmScore}, features::{ArgoApplication, ArgoHelmScore},
}, },
score::Score, score::Score,
@@ -56,9 +56,14 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
chart_url: String, chart_url: String,
image_name: String, image_name: String,
) -> Result<(), String> { ) -> Result<(), String> {
// TODO: This works only with local k3d installations, which is fine only for current demo purposes. We assume usage of K8sAnywhereTopology" let config = Config::load().expect("couldn't load config");
// https://git.nationtech.io/NationTech/harmony/issues/106
let k3d_bin_path = (*HARMONY_DATA_DIR).join("k3d").join("k3d"); error!(
"FIXME This works only with local k3d installations, which is fine only for current demo purposes. We assume usage of K8sAnywhereTopology"
);
error!("TODO hardcoded k3d bin path is wrong");
let k3d_bin_path = config.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!( info!(
"Importing image '{}' into k3d cluster 'harmony'", "Importing image '{}' into k3d cluster 'harmony'",
@@ -142,18 +147,25 @@ impl<
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
error!(
"TODO reverse helm chart packaging and docker image build. I put helm package first for faster iterations"
);
// TODO Write CI/CD workflow files // TODO Write CI/CD workflow files
// we can autotedect the CI type using the remote url (default to github action for github // we can autotedect the CI type using the remote url (default to github action for github
// url, etc..) // url, etc..)
// Or ask for it when unknown // Or ask for it when unknown
let helm_chart = self.application.build_push_helm_package(&image).await?; let helm_chart = self.application.build_push_helm_package(&image).await?;
info!("Pushed new helm chart {helm_chart}");
// TODO: Make building image configurable/skippable if image already exists (prompt)") error!("TODO Make building image configurable/skippable");
// https://git.nationtech.io/NationTech/harmony/issues/104
let image = self.application.build_push_oci_image().await?; let image = self.application.build_push_oci_image().await?;
info!("Pushed new docker image {image}");
// TODO: this is a temporary hack for demo purposes, the deployment target should be driven info!("Installing ContinuousDelivery feature");
// 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.
// //
@@ -166,20 +178,17 @@ impl<
// access it. This forces every Topology to understand the concept of targets though... So // access it. This forces every Topology to understand the concept of targets though... So
// instead I'll create a new Capability which is MultiTargetTopology and we'll see how it // instead I'll create a new Capability which is MultiTargetTopology and we'll see how it
// goes. It still does not feel right though. // goes. It still does not feel right though.
//
// https://git.nationtech.io/NationTech/harmony/issues/106
match topology.current_target() { match topology.current_target() {
DeploymentTarget::LocalDev => { DeploymentTarget::LocalDev => {
info!("Deploying {} locally...", self.application.name());
self.deploy_to_local_k3d(self.application.name(), helm_chart, image) self.deploy_to_local_k3d(self.application.name(), helm_chart, image)
.await?; .await?;
} }
target => { target => {
info!("Deploying {} to target {target:?}", self.application.name()); info!("Deploying to target {target:?}");
let score = ArgoHelmScore { let score = ArgoHelmScore {
namespace: "harmony-example-rust-webapp".to_string(), namespace: "harmonydemo-staging".to_string(),
openshift: true, openshift: false,
domain: "argo.harmonydemo.apps.ncd0.harmony.mcd".to_string(), domain: "argo.harmonydemo.apps.st.mcd".to_string(),
argo_apps: vec![ArgoApplication::from(CDApplicationConfig { argo_apps: vec![ArgoApplication::from(CDApplicationConfig {
// helm pull oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart --version 0.1.0 // helm pull oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart --version 0.1.0
version: Version::from("0.1.0").unwrap(), version: Version::from("0.1.0").unwrap(),
@@ -187,11 +196,12 @@ impl<
helm_chart_name: "harmony-example-rust-webapp-chart".to_string(), helm_chart_name: "harmony-example-rust-webapp-chart".to_string(),
values_overrides: None, values_overrides: None,
name: "harmony-demo-rust-webapp".to_string(), name: "harmony-demo-rust-webapp".to_string(),
namespace: "harmony-example-rust-webapp".to_string(), namespace: "harmonydemo-staging".to_string(),
})], })],
}; };
score score
.interpret(&Inventory::empty(), topology) .create_interpret()
.execute(&Inventory::empty(), topology)
.await .await
.unwrap(); .unwrap();
} }

View File

@@ -1,4 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use log::error;
use non_blank_string_rs::NonBlankString; use non_blank_string_rs::NonBlankString;
use serde::Serialize; use serde::Serialize;
use std::str::FromStr; use std::str::FromStr;
@@ -49,21 +50,20 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for ArgoInterpret {
inventory: &Inventory, inventory: &Inventory,
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
self.score.interpret(inventory, topology).await?; error!("Uncomment below, only disabled for debugging");
self.score
.create_interpret()
.execute(inventory, topology)
.await?;
let k8s_client = topology.k8s_client().await?; let k8s_client = topology.k8s_client().await?;
k8s_client k8s_client
.apply_yaml_many(&self.argo_apps.iter().map(|a| a.to_yaml()).collect(), None) .apply_yaml_many(&self.argo_apps.iter().map(|a| a.to_yaml()).collect(), None)
.await .await
.unwrap(); .unwrap();
Ok(Outcome::success(format!( Ok(Outcome::success(format!(
"ArgoCD installed with {} {}", "Successfully installed ArgoCD and {} Applications",
self.argo_apps.len(), self.argo_apps.len()
match self.argo_apps.len() {
1 => "application",
_ => "applications",
}
))) )))
} }
@@ -986,7 +986,7 @@ commitServer:
); );
HelmChartScore { HelmChartScore {
namespace: Some(NonBlankString::from_str(namespace).unwrap()), namespace: Some(NonBlankString::from_str(&namespace).unwrap()),
release_name: NonBlankString::from_str("argo-cd").unwrap(), release_name: NonBlankString::from_str("argo-cd").unwrap(),
chart_name: NonBlankString::from_str("argo/argo-cd").unwrap(), chart_name: NonBlankString::from_str("argo/argo-cd").unwrap(),
chart_version: Some(NonBlankString::from_str("8.1.2").unwrap()), chart_version: Some(NonBlankString::from_str("8.1.2").unwrap()),

View File

@@ -1,67 +1,51 @@
use std::sync::Arc; use std::sync::Arc;
use crate::modules::application::{Application, ApplicationFeature};
use crate::modules::monitoring::application_monitoring::application_monitoring_score::ApplicationMonitoringScore;
use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus;
use crate::topology::MultiTargetTopology;
use crate::{
inventory::Inventory,
modules::monitoring::{
alert_channel::webhook_receiver::WebhookReceiver, ntfy::ntfy::NtfyScore,
},
score::Score,
topology::{HelmCommand, K8sclient, Topology, Url, tenant::TenantManager},
};
use crate::{
modules::prometheus::prometheus::PrometheusApplicationMonitoring,
topology::oberservability::monitoring::AlertReceiver,
};
use async_trait::async_trait; use async_trait::async_trait;
use base64::{Engine as _, engine::general_purpose}; use base64::{Engine as _, engine::general_purpose};
use log::{debug, info}; use log::{debug, info};
use crate::{
inventory::Inventory,
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, Clone)] #[derive(Debug, Clone)]
pub struct Monitoring { pub struct Monitoring {
pub application: Arc<dyn Application>, pub application: Arc<dyn OCICompliant>,
pub alert_receiver: Vec<Box<dyn AlertReceiver<CRDPrometheus>>>,
} }
#[async_trait] #[async_trait]
impl< impl<T: Topology + HelmCommand + K8sclient + 'static + TenantManager> ApplicationFeature<T>
T: Topology for Monitoring
+ HelmCommand
+ 'static
+ TenantManager
+ K8sclient
+ MultiTargetTopology
+ std::fmt::Debug
+ PrometheusApplicationMonitoring<CRDPrometheus>,
> ApplicationFeature<T> for Monitoring
{ {
async fn ensure_installed(&self, topology: &T) -> Result<(), String> { async fn ensure_installed(&self, topology: &T) -> Result<(), String> {
info!("Ensuring monitoring is available for application"); info!("Ensuring monitoring is available for application");
let namespace = topology
.get_tenant_config()
.await
.map(|ns| ns.name.clone())
.unwrap_or_else(|| self.application.name());
let mut alerting_score = ApplicationMonitoringScore {
sender: CRDPrometheus {
namespace: namespace.clone(),
client: topology.k8s_client().await.unwrap(),
},
application: self.application.clone(),
receivers: self.alert_receiver.clone(),
};
let ntfy = NtfyScore { let ntfy = NtfyScore {
namespace: namespace.clone(), // namespace: topology
host: "ntfy.harmonydemo.apps.ncd0.harmony.mcd".to_string(), // .get_tenant_config()
// .await
// .expect("couldn't get tenant config")
// .name,
namespace: self.application.name(),
host: "localhost".to_string(),
}; };
ntfy.interpret(&Inventory::empty(), topology) ntfy.create_interpret()
.execute(&Inventory::empty(), topology)
.await .await
.map_err(|e| e.to_string())?; .expect("couldn't create interpret for ntfy");
let ntfy_default_auth_username = "harmony"; let ntfy_default_auth_username = "harmony";
let ntfy_default_auth_password = "harmony"; let ntfy_default_auth_password = "harmony";
@@ -86,7 +70,7 @@ impl<
url::Url::parse( url::Url::parse(
format!( format!(
"http://ntfy.{}.svc.cluster.local/rust-web-app?auth={ntfy_default_auth_param}", "http://ntfy.{}.svc.cluster.local/rust-web-app?auth={ntfy_default_auth_param}",
namespace.clone() self.application.name()
) )
.as_str(), .as_str(),
) )
@@ -94,11 +78,31 @@ impl<
), ),
}; };
alerting_score.receivers.push(Box::new(ntfy_receiver)); 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 alerting_score
.interpret(&Inventory::empty(), topology) .create_interpret()
.execute(&Inventory::empty(), topology)
.await .await
.map_err(|e| e.to_string())?; .unwrap();
Ok(()) Ok(())
} }
fn name(&self) -> String { fn name(&self) -> String {

View File

@@ -5,28 +5,19 @@ mod rust;
use std::sync::Arc; use std::sync::Arc;
pub use feature::*; pub use feature::*;
use log::debug; use log::info;
pub use oci::*; pub use oci::*;
pub use rust::*; pub use rust::*;
use async_trait::async_trait; use async_trait::async_trait;
use serde::Serialize;
use crate::{ use crate::{
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,
topology::Topology, topology::Topology,
}; };
#[derive(Clone, Debug)]
pub enum ApplicationFeatureStatus {
Installing,
Installed,
Failed { details: String },
}
pub trait Application: std::fmt::Debug + Send + Sync { pub trait Application: std::fmt::Debug + Send + Sync {
fn name(&self) -> String; fn name(&self) -> String;
} }
@@ -45,7 +36,7 @@ impl<A: Application, T: Topology + std::fmt::Debug> Interpret<T> for Application
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
let app_name = self.application.name(); let app_name = self.application.name();
debug!( info!(
"Preparing {} features [{}] for application {app_name}", "Preparing {} features [{}] for application {app_name}",
self.features.len(), self.features.len(),
self.features self.features
@@ -55,41 +46,20 @@ impl<A: Application, T: Topology + std::fmt::Debug> Interpret<T> for Application
.join(", ") .join(", ")
); );
for feature in self.features.iter() { for feature in self.features.iter() {
instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged { info!(
topology: topology.name().into(), "Installing feature {} for application {app_name}",
application: self.application.name(), feature.name()
feature: feature.name(), );
status: ApplicationFeatureStatus::Installing,
})
.unwrap();
let _ = match feature.ensure_installed(topology).await { let _ = match feature.ensure_installed(topology).await {
Ok(()) => { Ok(()) => (),
instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged {
topology: topology.name().into(),
application: self.application.name(),
feature: feature.name(),
status: ApplicationFeatureStatus::Installed,
})
.unwrap();
}
Err(msg) => { Err(msg) => {
instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged {
topology: topology.name().into(),
application: self.application.name(),
feature: feature.name(),
status: ApplicationFeatureStatus::Failed {
details: msg.clone(),
},
})
.unwrap();
return Err(InterpretError::new(format!( return Err(InterpretError::new(format!(
"Application Interpret failed to install feature : {msg}" "Application Interpret failed to install feature : {msg}"
))); )));
} }
}; };
} }
Ok(Outcome::success("Application created".to_string())) Ok(Outcome::success("successfully created app".to_string()))
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {
@@ -108,12 +78,3 @@ impl<A: Application, T: Topology + std::fmt::Debug> Interpret<T> for Application
todo!() todo!()
} }
} }
impl Serialize for dyn Application {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
todo!()
}
}

View File

@@ -1,5 +1,5 @@
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use std::process; use std::process;
use std::sync::Arc; use std::sync::Arc;
@@ -10,11 +10,11 @@ use dockerfile_builder::Dockerfile;
use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR}; use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR};
use dockerfile_builder::instruction_builder::CopyBuilder; use dockerfile_builder::instruction_builder::CopyBuilder;
use futures_util::StreamExt; use futures_util::StreamExt;
use log::{debug, info, log_enabled}; use log::{debug, error, info};
use serde::Serialize; use serde::Serialize;
use tar::Archive; use tar::Archive;
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL}; use crate::config::Config;
use crate::{ use crate::{
score::Score, score::Score,
topology::{Topology, Url}, topology::{Topology, Url},
@@ -46,7 +46,7 @@ where
} }
fn name(&self) -> String { fn name(&self) -> String {
format!("{} [ApplicationScore]", self.application.name()) format!("Application: {}", self.application.name())
} }
} }
@@ -109,20 +109,24 @@ impl OCICompliant for RustWebapp {
// 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); info!("Starting OCI image build and push for '{}'", self.name);
// 1. Build the image by calling the synchronous helper function. // 1. Build the local image by calling the synchronous helper function.
let image_tag = self.image_name(); let local_image_name = self.local_image_name();
self.build_docker_image(&image_tag) self.build_docker_image(&local_image_name)
.await .await
.map_err(|e| format!("Failed to build Docker image: {}", e))?; .map_err(|e| format!("Failed to build Docker image: {}", e))?;
info!("Successfully built Docker image: {}", image_tag); info!(
"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(&image_tag) self.push_docker_image(&local_image_name, &remote_image_name)
.await .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: {}", image_tag); info!("Successfully pushed Docker image to: {}", remote_image_name);
Ok(image_tag) Ok(remote_image_name)
} }
fn local_image_name(&self) -> String { fn local_image_name(&self) -> String {
@@ -130,10 +134,12 @@ impl OCICompliant for RustWebapp {
} }
fn image_name(&self) -> String { fn image_name(&self) -> String {
let config = Config::load().expect("couldn't load config");
format!( format!(
"{}/{}/{}", "{}/{}/{}",
*REGISTRY_URL, config.registry_url,
*REGISTRY_PROJECT, config.registry_project,
&self.local_image_name() &self.local_image_name()
) )
} }
@@ -159,22 +165,20 @@ impl RustWebapp {
&self, &self,
image_name: &str, image_name: &str,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
debug!("Generating Dockerfile for '{}'", self.name); info!("Generating Dockerfile for '{}'", self.name);
let _dockerfile_path = self.build_dockerfile()?; let _dockerfile_path = self.build_dockerfile()?;
let docker = Docker::connect_with_socket_defaults().unwrap(); let docker = Docker::connect_with_socket_defaults().unwrap();
let quiet = !log_enabled!(log::Level::Debug);
let build_image_options = bollard::query_parameters::BuildImageOptionsBuilder::default() let build_image_options = bollard::query_parameters::BuildImageOptionsBuilder::default()
.dockerfile("Dockerfile.harmony") .dockerfile("Dockerfile.harmony")
.t(image_name) .t(image_name)
.q(quiet) .q(false)
.version(bollard::query_parameters::BuilderVersion::BuilderV1) .version(bollard::query_parameters::BuilderVersion::BuilderV1)
.platform("linux/x86_64"); .platform("linux/x86_64");
let mut temp_tar_builder = tar::Builder::new(Vec::new()); let mut temp_tar_builder = tar::Builder::new(Vec::new());
temp_tar_builder let _ = temp_tar_builder
.append_dir_all("", self.project_root.clone()) .append_dir_all("", self.project_root.clone())
.unwrap(); .unwrap();
let archive = temp_tar_builder let archive = temp_tar_builder
@@ -195,7 +199,7 @@ impl RustWebapp {
); );
while let Some(msg) = image_build_stream.next().await { while let Some(msg) = image_build_stream.next().await {
debug!("Message: {msg:?}"); println!("Message: {msg:?}");
} }
Ok(image_name.to_string()) Ok(image_name.to_string())
@@ -204,25 +208,23 @@ impl RustWebapp {
/// Tags and pushes a Docker image to the configured remote registry. /// Tags and pushes a Docker image to the configured remote registry.
async fn push_docker_image( async fn push_docker_image(
&self, &self,
image_tag: &str, image_name: &str,
full_tag: &str,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
debug!("Pushing docker image {image_tag}"); info!("Pushing docker image {full_tag}");
let docker = Docker::connect_with_socket_defaults().unwrap(); let docker = Docker::connect_with_socket_defaults().unwrap();
// let push_options = PushImageOptionsBuilder::new().tag(tag); // let push_options = PushImageOptionsBuilder::new().tag(tag);
let mut push_image_stream = docker.push_image( let mut push_image_stream =
image_tag, docker.push_image(full_tag, Some(PushImageOptionsBuilder::new().build()), None);
Some(PushImageOptionsBuilder::new().build()),
None,
);
while let Some(msg) = push_image_stream.next().await { while let Some(msg) = push_image_stream.next().await {
debug!("Message: {msg:?}"); println!("Message: {msg:?}");
} }
Ok(image_tag.to_string()) Ok(full_tag.to_string())
} }
/// Checks the output of a process command for success. /// Checks the output of a process command for success.
@@ -288,8 +290,9 @@ impl RustWebapp {
.unwrap(), .unwrap(),
); );
// Copy the compiled binary from the builder stage. // Copy the compiled binary from the builder stage.
// TODO: Should not be using score name here, instead should use name from Cargo.toml error!(
// https://git.nationtech.io/NationTech/harmony/issues/105 "FIXME Should not be using score name here, instead should use name from Cargo.toml"
);
let binary_path_in_builder = format!("/app/target/release/{}", self.name); let binary_path_in_builder = format!("/app/target/release/{}", self.name);
let binary_path_in_final = format!("/home/appuser/{}", self.name); let binary_path_in_final = format!("/home/appuser/{}", self.name);
dockerfile.push( dockerfile.push(
@@ -327,8 +330,9 @@ impl RustWebapp {
)); ));
// Copy only the compiled binary from the builder stage. // Copy only the compiled binary from the builder stage.
// TODO: Should not be using score name here, instead should use name from Cargo.toml error!(
// https://git.nationtech.io/NationTech/harmony/issues/105 "FIXME Should not be using score name here, instead should use name from Cargo.toml"
);
let binary_path_in_builder = format!("/app/target/release/{}", self.name); let binary_path_in_builder = format!("/app/target/release/{}", self.name);
let binary_path_in_final = format!("/usr/local/bin/{}", self.name); let binary_path_in_final = format!("/usr/local/bin/{}", self.name);
dockerfile.push( dockerfile.push(
@@ -528,9 +532,12 @@ spec:
} }
/// Packages a Helm chart directory into a .tgz file. /// Packages a Helm chart directory into a .tgz file.
fn package_helm_chart(&self, chart_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> { fn package_helm_chart(
&self,
chart_dir: &PathBuf,
) -> 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");
debug!( info!(
"Launching `helm package {}` cli with CWD {}", "Launching `helm package {}` cli with CWD {}",
chart_dirname.to_string_lossy(), chart_dirname.to_string_lossy(),
&self &self
@@ -541,13 +548,14 @@ spec:
); );
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(".harmony_generated").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")?;
// Helm prints the path of the created chart to stdout. // Helm prints the path of the created chart to stdout.
let tgz_name = String::from_utf8(output.stdout)? let tgz_name = String::from_utf8(output.stdout)?
.trim()
.split_whitespace() .split_whitespace()
.last() .last()
.unwrap_or_default() .unwrap_or_default()
@@ -567,14 +575,16 @@ spec:
/// Pushes a packaged Helm chart to an OCI registry. /// Pushes a packaged Helm chart to an OCI registry.
fn push_helm_chart( fn push_helm_chart(
&self, &self,
packaged_chart_path: &Path, packaged_chart_path: &PathBuf,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
let config = Config::load().expect("couldn't load config");
// The chart name is the file stem of the .tgz file // The chart name is the file stem of the .tgz file
let chart_file_name = packaged_chart_path.file_stem().unwrap().to_str().unwrap(); let chart_file_name = packaged_chart_path.file_stem().unwrap().to_str().unwrap();
let oci_push_url = format!("oci://{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT); let oci_push_url = format!("oci://{}/{}", config.registry_url, config.registry_project);
let oci_pull_url = format!("{oci_push_url}/{}-chart", self.name); let oci_pull_url = format!("{oci_push_url}/{}-chart", self.name);
debug!( info!(
"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

@@ -41,6 +41,6 @@ impl<T: Topology + HelmCommand> Score<T> for CertManagerHelmScore {
} }
fn name(&self) -> String { fn name(&self) -> String {
"CertManagerHelmScore".to_string() format!("CertManagerHelmScore")
} }
} }

View File

@@ -111,7 +111,7 @@ impl DhcpInterpret {
let boot_filename_outcome = match &self.score.boot_filename { let boot_filename_outcome = match &self.score.boot_filename {
Some(boot_filename) => { Some(boot_filename) => {
dhcp_server.set_boot_filename(boot_filename).await?; dhcp_server.set_boot_filename(&boot_filename).await?;
Outcome::new( Outcome::new(
InterpretStatus::SUCCESS, InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set boot filename to {boot_filename}"), format!("Dhcp Interpret Set boot filename to {boot_filename}"),
@@ -122,7 +122,7 @@ impl DhcpInterpret {
let filename_outcome = match &self.score.filename { let filename_outcome = match &self.score.filename {
Some(filename) => { Some(filename) => {
dhcp_server.set_filename(filename).await?; dhcp_server.set_filename(&filename).await?;
Outcome::new( Outcome::new(
InterpretStatus::SUCCESS, InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set filename to {filename}"), format!("Dhcp Interpret Set filename to {filename}"),
@@ -133,7 +133,7 @@ impl DhcpInterpret {
let filename64_outcome = match &self.score.filename64 { let filename64_outcome = match &self.score.filename64 {
Some(filename64) => { Some(filename64) => {
dhcp_server.set_filename64(filename64).await?; dhcp_server.set_filename64(&filename64).await?;
Outcome::new( Outcome::new(
InterpretStatus::SUCCESS, InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set filename64 to {filename64}"), format!("Dhcp Interpret Set filename64 to {filename64}"),
@@ -144,7 +144,7 @@ impl DhcpInterpret {
let filenameipxe_outcome = match &self.score.filenameipxe { let filenameipxe_outcome = match &self.score.filenameipxe {
Some(filenameipxe) => { Some(filenameipxe) => {
dhcp_server.set_filenameipxe(filenameipxe).await?; dhcp_server.set_filenameipxe(&filenameipxe).await?;
Outcome::new( Outcome::new(
InterpretStatus::SUCCESS, InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set filenameipxe to {filenameipxe}"), format!("Dhcp Interpret Set filenameipxe to {filenameipxe}"),
@@ -209,7 +209,7 @@ impl<T: DhcpServer> Interpret<T> for DhcpInterpret {
Ok(Outcome::new( Ok(Outcome::new(
InterpretStatus::SUCCESS, InterpretStatus::SUCCESS,
"Dhcp Interpret execution successful".to_string(), format!("Dhcp Interpret execution successful"),
)) ))
} }
} }

View File

@@ -112,7 +112,7 @@ impl<T: Topology + DnsServer> Interpret<T> for DnsInterpret {
Ok(Outcome::new( Ok(Outcome::new(
InterpretStatus::SUCCESS, InterpretStatus::SUCCESS,
"Dns Interpret execution successful".to_string(), format!("Dns Interpret execution successful"),
)) ))
} }
} }

View File

@@ -55,7 +55,7 @@ impl<T: Topology + HelmCommand> Score<T> for HelmChartScore {
} }
fn name(&self) -> String { fn name(&self) -> String {
format!("{} [HelmChartScore]", self.release_name) format!("{} {} HelmChartScore", self.release_name, self.chart_name)
} }
} }
@@ -90,10 +90,14 @@ impl HelmChartInterpret {
); );
match add_output.status.success() { match add_output.status.success() {
true => Ok(()), true => {
false => Err(InterpretError::new(format!( return Ok(());
"Failed to add helm repository!\n{full_output}" }
))), false => {
return Err(InterpretError::new(format!(
"Failed to add helm repository!\n{full_output}"
)));
}
} }
} }
} }
@@ -208,7 +212,7 @@ impl<T: Topology + HelmCommand> Interpret<T> for HelmChartInterpret {
} }
let res = helm_executor.install_or_upgrade( let res = helm_executor.install_or_upgrade(
ns, &ns,
&self.score.release_name, &self.score.release_name,
&self.score.chart_name, &self.score.chart_name,
self.score.chart_version.as_ref(), self.score.chart_version.as_ref(),
@@ -225,27 +229,24 @@ impl<T: Topology + HelmCommand> Interpret<T> for HelmChartInterpret {
match status { match status {
helm_wrapper_rs::HelmDeployStatus::Deployed => Ok(Outcome::new( helm_wrapper_rs::HelmDeployStatus::Deployed => Ok(Outcome::new(
InterpretStatus::SUCCESS, InterpretStatus::SUCCESS,
format!("Helm Chart {} deployed", self.score.release_name), "Helm Chart deployed".to_string(),
)), )),
helm_wrapper_rs::HelmDeployStatus::PendingInstall => Ok(Outcome::new( helm_wrapper_rs::HelmDeployStatus::PendingInstall => Ok(Outcome::new(
InterpretStatus::RUNNING, InterpretStatus::RUNNING,
format!("Helm Chart {} pending install...", self.score.release_name), "Helm Chart Pending install".to_string(),
)), )),
helm_wrapper_rs::HelmDeployStatus::PendingUpgrade => Ok(Outcome::new( helm_wrapper_rs::HelmDeployStatus::PendingUpgrade => Ok(Outcome::new(
InterpretStatus::RUNNING, InterpretStatus::RUNNING,
format!("Helm Chart {} pending upgrade...", self.score.release_name), "Helm Chart pending upgrade".to_string(),
)),
helm_wrapper_rs::HelmDeployStatus::Failed => Err(InterpretError::new(
"Failed to install helm chart".to_string(),
)), )),
helm_wrapper_rs::HelmDeployStatus::Failed => Err(InterpretError::new(format!(
"Helm Chart {} installation failed",
self.score.release_name
))),
} }
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {
InterpretName::HelmChart todo!()
} }
fn get_version(&self) -> Version { fn get_version(&self) -> Version {
todo!() todo!()
} }

View File

@@ -77,11 +77,14 @@ impl HelmCommandExecutor {
)?; )?;
} }
let out = self.clone().run_command( let out = match self.clone().run_command(
self.chart self.chart
.clone() .clone()
.helm_args(self.globals.chart_home.clone().unwrap()), .helm_args(self.globals.chart_home.clone().unwrap()),
)?; ) {
Ok(out) => out,
Err(e) => return Err(e),
};
// TODO: don't use unwrap here // TODO: don't use unwrap here
let s = String::from_utf8(out.stdout).unwrap(); let s = String::from_utf8(out.stdout).unwrap();
@@ -95,11 +98,14 @@ impl HelmCommandExecutor {
} }
pub fn version(self) -> Result<String, std::io::Error> { pub fn version(self) -> Result<String, std::io::Error> {
let out = self.run_command(vec![ let out = match self.run_command(vec![
"version".to_string(), "version".to_string(),
"-c".to_string(), "-c".to_string(),
"--short".to_string(), "--short".to_string(),
])?; ]) {
Ok(out) => out,
Err(e) => return Err(e),
};
// TODO: don't use unwrap // TODO: don't use unwrap
Ok(String::from_utf8(out.stdout).unwrap()) Ok(String::from_utf8(out.stdout).unwrap())
@@ -123,11 +129,15 @@ impl HelmCommandExecutor {
None => PathBuf::from(TempDir::new()?.path()), None => PathBuf::from(TempDir::new()?.path()),
}; };
if let Some(yaml_str) = self.chart.values_inline { match self.chart.values_inline {
let tf: TempFile = temp_file::with_contents(yaml_str.as_bytes()); Some(yaml_str) => {
self.chart let tf: TempFile;
.additional_values_files tf = temp_file::with_contents(yaml_str.as_bytes());
.push(PathBuf::from(tf.path())); self.chart
.additional_values_files
.push(PathBuf::from(tf.path()));
}
None => (),
}; };
self.env.insert( self.env.insert(
@@ -170,9 +180,9 @@ impl HelmChart {
match self.repo { match self.repo {
Some(r) => { Some(r) => {
if r.starts_with("oci://") { if r.starts_with("oci://") {
args.push( args.push(String::from(
r.trim_end_matches("/").to_string() + "/" + self.name.clone().as_str(), r.trim_end_matches("/").to_string() + "/" + self.name.clone().as_str(),
); ));
} else { } else {
args.push("--repo".to_string()); args.push("--repo".to_string());
args.push(r.to_string()); args.push(r.to_string());
@@ -183,9 +193,12 @@ impl HelmChart {
None => args.push(self.name), None => args.push(self.name),
}; };
if let Some(v) = self.version { match self.version {
args.push("--version".to_string()); Some(v) => {
args.push(v.to_string()); args.push("--version".to_string());
args.push(v.to_string());
}
None => (),
} }
args args
@@ -349,7 +362,7 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for HelmChartInterpretV
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {
InterpretName::HelmCommand todo!()
} }
fn get_version(&self) -> Version { fn get_version(&self) -> Version {
todo!() todo!()

View File

@@ -1,11 +1,11 @@
use std::path::PathBuf; use std::path::PathBuf;
use async_trait::async_trait; use async_trait::async_trait;
use log::debug; use log::info;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
config::HARMONY_DATA_DIR, config::Config,
data::{Id, Version}, data::{Id, Version},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory, inventory::Inventory,
@@ -21,22 +21,24 @@ pub struct K3DInstallationScore {
impl Default for K3DInstallationScore { impl Default for K3DInstallationScore {
fn default() -> Self { fn default() -> Self {
let config = Config::load().expect("couldn't load config");
Self { Self {
installation_path: HARMONY_DATA_DIR.join("k3d"), installation_path: config.data_dir.join("k3d"),
cluster_name: "harmony".to_string(), cluster_name: "harmony".to_string(),
} }
} }
} }
impl<T: Topology> Score<T> for K3DInstallationScore { impl<T: Topology> Score<T> for K3DInstallationScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
Box::new(K3dInstallationInterpret { Box::new(K3dInstallationInterpret {
score: self.clone(), score: self.clone(),
}) })
} }
fn name(&self) -> String { fn name(&self) -> String {
"K3dInstallationScore".into() todo!()
} }
} }
@@ -56,15 +58,14 @@ impl<T: Topology> Interpret<T> for K3dInstallationInterpret {
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 { match k3d.ensure_installed().await {
Ok(_client) => { Ok(_client) => {
let msg = format!("k3d cluster '{}' installed ", self.score.cluster_name); let msg = format!("k3d cluster {} is installed ", self.score.cluster_name);
debug!("{msg}"); info!("{msg}");
Ok(Outcome::success(msg)) Ok(Outcome::success(msg))
} }
Err(msg) => Err(InterpretError::new(format!( Err(msg) => Err(InterpretError::new(format!(
"failed to ensure k3d is installed : {msg}" "K3dInstallationInterpret failed to ensure k3d is installed : {msg}"
))), ))),
} }
} }

View File

@@ -89,7 +89,7 @@ where
)) ))
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {
InterpretName::K8sResource todo!()
} }
fn get_version(&self) -> Version { fn get_version(&self) -> Version {
todo!() todo!()

View File

@@ -14,7 +14,7 @@ use async_trait::async_trait;
use log::{debug, info}; use log::{debug, info};
use serde::Serialize; use serde::Serialize;
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL}; use crate::config::Config as HarmonyConfig;
use crate::modules::k8s::ingress::K8sIngressScore; use crate::modules::k8s::ingress::K8sIngressScore;
use crate::topology::HelmCommand; use crate::topology::HelmCommand;
use crate::{ use crate::{
@@ -128,12 +128,13 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for LAMPInterpret {
info!("Deploying score {deployment_score:#?}"); info!("Deploying score {deployment_score:#?}");
deployment_score.interpret(inventory, topology).await?; deployment_score
.create_interpret()
.execute(inventory, topology)
.await?;
info!("LAMP deployment_score {deployment_score:?}"); info!("LAMP deployment_score {deployment_score:?}");
let ingress_path = ingress_path!("/");
let lamp_ingress = K8sIngressScore { let lamp_ingress = K8sIngressScore {
name: fqdn!("lamp-ingress"), name: fqdn!("lamp-ingress"),
host: fqdn!("test"), host: fqdn!("test"),
@@ -143,14 +144,17 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for LAMPInterpret {
.as_str() .as_str()
), ),
port: 8080, port: 8080,
path: Some(ingress_path), path: Some(ingress_path!("/")),
path_type: None, path_type: None,
namespace: self namespace: self
.get_namespace() .get_namespace()
.map(|nbs| fqdn!(nbs.to_string().as_str())), .map(|nbs| fqdn!(nbs.to_string().as_str())),
}; };
lamp_ingress.interpret(inventory, topology).await?; lamp_ingress
.create_interpret()
.execute(inventory, topology)
.await?;
info!("LAMP lamp_ingress {lamp_ingress:?}"); info!("LAMP lamp_ingress {lamp_ingress:?}");
@@ -160,7 +164,7 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for LAMPInterpret {
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {
InterpretName::Lamp todo!()
} }
fn get_version(&self) -> Version { fn get_version(&self) -> Version {
@@ -209,7 +213,7 @@ impl LAMPInterpret {
repository: None, repository: None,
}; };
score.interpret(inventory, topology).await score.create_interpret().execute(inventory, topology).await
} }
fn build_dockerfile(&self, score: &LAMPScore) -> Result<PathBuf, Box<dyn std::error::Error>> { fn build_dockerfile(&self, score: &LAMPScore) -> Result<PathBuf, Box<dyn std::error::Error>> {
let mut dockerfile = Dockerfile::new(); let mut dockerfile = Dockerfile::new();
@@ -351,7 +355,12 @@ opcache.fast_shutdown=1
} }
fn push_docker_image(&self, image_name: &str) -> Result<String, Box<dyn std::error::Error>> { fn push_docker_image(&self, image_name: &str) -> Result<String, Box<dyn std::error::Error>> {
let full_tag = format!("{}/{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT, &image_name); let config = HarmonyConfig::load().expect("couldn't load config");
let full_tag = format!(
"{}/{}/{}",
config.registry_url, config.registry_project, &image_name
);
let output = std::process::Command::new("docker") let output = std::process::Command::new("docker")
.args(["tag", image_name, &full_tag]) .args(["tag", image_name, &full_tag])
.output()?; .output()?;

View File

@@ -1,16 +1,7 @@
use std::any::Any;
use std::collections::BTreeMap;
use async_trait::async_trait; use async_trait::async_trait;
use k8s_openapi::api::core::v1::Secret;
use kube::api::ObjectMeta;
use serde::Serialize; use serde::Serialize;
use serde_json::json;
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::{
AlertmanagerConfig, AlertmanagerConfigSpec, CRDPrometheus,
};
use crate::{ use crate::{
interpret::{InterpretError, Outcome}, interpret::{InterpretError, Outcome},
modules::monitoring::{ modules::monitoring::{
@@ -29,98 +20,14 @@ pub struct DiscordWebhook {
pub url: Url, pub url: Url,
} }
#[async_trait]
impl AlertReceiver<CRDPrometheus> for DiscordWebhook {
async fn install(&self, sender: &CRDPrometheus) -> Result<Outcome, InterpretError> {
let ns = sender.namespace.clone();
let secret_name = format!("{}-secret", self.name.clone());
let webhook_key = format!("{}", self.url.clone());
let mut string_data = BTreeMap::new();
string_data.insert("webhook-url".to_string(), webhook_key.clone());
let secret = Secret {
metadata: kube::core::ObjectMeta {
name: Some(secret_name.clone()),
..Default::default()
},
string_data: Some(string_data),
type_: Some("Opaque".to_string()),
..Default::default()
};
let _ = sender.client.apply(&secret, Some(&ns)).await;
let spec = AlertmanagerConfigSpec {
data: json!({
"route": {
"receiver": self.name,
},
"receivers": [
{
"name": self.name,
"discordConfigs": [
{
"apiURL": {
"name": secret_name,
"key": "webhook-url",
},
"title": "{{ template \"discord.default.title\" . }}",
"message": "{{ template \"discord.default.message\" . }}"
}
]
}
]
}),
};
let alertmanager_configs = AlertmanagerConfig {
metadata: ObjectMeta {
name: Some(self.name.clone()),
labels: Some(std::collections::BTreeMap::from([(
"alertmanagerConfig".to_string(),
"enabled".to_string(),
)])),
namespace: Some(ns),
..Default::default()
},
spec,
};
sender
.client
.apply(&alertmanager_configs, Some(&sender.namespace))
.await?;
Ok(Outcome::success(format!(
"installed crd-alertmanagerconfigs for {}",
self.name
)))
}
fn name(&self) -> String {
"discord-webhook".to_string()
}
fn clone_box(&self) -> Box<dyn AlertReceiver<CRDPrometheus>> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[async_trait] #[async_trait]
impl AlertReceiver<Prometheus> for DiscordWebhook { impl AlertReceiver<Prometheus> for DiscordWebhook {
async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> { async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> {
sender.install_receiver(self).await sender.install_receiver(self).await
} }
fn name(&self) -> String {
"discord-webhook".to_string()
}
fn clone_box(&self) -> Box<dyn AlertReceiver<Prometheus>> { fn clone_box(&self) -> Box<dyn AlertReceiver<Prometheus>> {
Box::new(self.clone()) Box::new(self.clone())
} }
fn as_any(&self) -> &dyn Any {
self
}
} }
#[async_trait] #[async_trait]
@@ -141,12 +48,6 @@ impl AlertReceiver<KubePrometheus> for DiscordWebhook {
fn clone_box(&self) -> Box<dyn AlertReceiver<KubePrometheus>> { fn clone_box(&self) -> Box<dyn AlertReceiver<KubePrometheus>> {
Box::new(self.clone()) Box::new(self.clone())
} }
fn name(&self) -> String {
"discord-webhook".to_string()
}
fn as_any(&self) -> &dyn Any {
self
}
} }
#[async_trait] #[async_trait]

View File

@@ -1,19 +1,11 @@
use std::any::Any;
use async_trait::async_trait; use async_trait::async_trait;
use kube::api::ObjectMeta;
use log::debug;
use serde::Serialize; use serde::Serialize;
use serde_json::json;
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
use crate::{ use crate::{
interpret::{InterpretError, Outcome}, interpret::{InterpretError, Outcome},
modules::monitoring::{ modules::monitoring::{
kube_prometheus::{ kube_prometheus::{
crd::crd_alertmanager_config::{
AlertmanagerConfig, AlertmanagerConfigSpec, CRDPrometheus,
},
prometheus::{KubePrometheus, KubePrometheusReceiver}, prometheus::{KubePrometheus, KubePrometheusReceiver},
types::{AlertChannelConfig, AlertManagerChannelConfig}, types::{AlertChannelConfig, AlertManagerChannelConfig},
}, },
@@ -28,81 +20,14 @@ pub struct WebhookReceiver {
pub url: Url, pub url: Url,
} }
#[async_trait]
impl AlertReceiver<CRDPrometheus> for WebhookReceiver {
async fn install(&self, sender: &CRDPrometheus) -> Result<Outcome, InterpretError> {
let spec = AlertmanagerConfigSpec {
data: json!({
"route": {
"receiver": self.name,
},
"receivers": [
{
"name": self.name,
"webhookConfigs": [
{
"url": self.url,
}
]
}
]
}),
};
let alertmanager_configs = AlertmanagerConfig {
metadata: ObjectMeta {
name: Some(self.name.clone()),
labels: Some(std::collections::BTreeMap::from([(
"alertmanagerConfig".to_string(),
"enabled".to_string(),
)])),
namespace: Some(sender.namespace.clone()),
..Default::default()
},
spec,
};
debug!(
"alert manager configs: \n{:#?}",
alertmanager_configs.clone()
);
sender
.client
.apply(&alertmanager_configs, Some(&sender.namespace))
.await?;
Ok(Outcome::success(format!(
"installed crd-alertmanagerconfigs for {}",
self.name
)))
}
fn name(&self) -> String {
"webhook-receiver".to_string()
}
fn clone_box(&self) -> Box<dyn AlertReceiver<CRDPrometheus>> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[async_trait] #[async_trait]
impl AlertReceiver<Prometheus> for WebhookReceiver { impl AlertReceiver<Prometheus> for WebhookReceiver {
async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> { async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> {
sender.install_receiver(self).await sender.install_receiver(self).await
} }
fn name(&self) -> String {
"webhook-receiver".to_string()
}
fn clone_box(&self) -> Box<dyn AlertReceiver<Prometheus>> { fn clone_box(&self) -> Box<dyn AlertReceiver<Prometheus>> {
Box::new(self.clone()) Box::new(self.clone())
} }
fn as_any(&self) -> &dyn Any {
self
}
} }
#[async_trait] #[async_trait]
@@ -119,15 +44,9 @@ impl AlertReceiver<KubePrometheus> for WebhookReceiver {
async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> { async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> {
sender.install_receiver(self).await sender.install_receiver(self).await
} }
fn name(&self) -> String {
"webhook-receiver".to_string()
}
fn clone_box(&self) -> Box<dyn AlertReceiver<KubePrometheus>> { fn clone_box(&self) -> Box<dyn AlertReceiver<KubePrometheus>> {
Box::new(self.clone()) Box::new(self.clone())
} }
fn as_any(&self) -> &dyn Any {
self
}
} }
#[async_trait] #[async_trait]

View File

@@ -18,7 +18,7 @@ use crate::{
#[async_trait] #[async_trait]
impl AlertRule<KubePrometheus> for AlertManagerRuleGroup { impl AlertRule<KubePrometheus> for AlertManagerRuleGroup {
async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> { async fn install(&self, sender: &KubePrometheus) -> Result<Outcome, InterpretError> {
sender.install_rule(self).await sender.install_rule(&self).await
} }
fn clone_box(&self) -> Box<dyn AlertRule<KubePrometheus>> { fn clone_box(&self) -> Box<dyn AlertRule<KubePrometheus>> {
Box::new(self.clone()) Box::new(self.clone())
@@ -28,7 +28,7 @@ impl AlertRule<KubePrometheus> for AlertManagerRuleGroup {
#[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> {
sender.install_rule(self).await sender.install_rule(&self).await
} }
fn clone_box(&self) -> Box<dyn AlertRule<Prometheus>> { fn clone_box(&self) -> Box<dyn AlertRule<Prometheus>> {
Box::new(self.clone()) Box::new(self.clone())

View File

@@ -1,91 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use serde::Serialize;
use crate::{
data::{Id, Version},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory,
modules::{
application::Application,
monitoring::kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus,
prometheus::prometheus::PrometheusApplicationMonitoring,
},
score::Score,
topology::{PreparationOutcome, Topology, oberservability::monitoring::AlertReceiver},
};
#[derive(Debug, Clone, Serialize)]
pub struct ApplicationMonitoringScore {
pub sender: CRDPrometheus,
pub application: Arc<dyn Application>,
pub receivers: Vec<Box<dyn AlertReceiver<CRDPrometheus>>>,
}
impl<T: Topology + PrometheusApplicationMonitoring<CRDPrometheus>> Score<T>
for ApplicationMonitoringScore
{
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(ApplicationMonitoringInterpret {
score: self.clone(),
})
}
fn name(&self) -> String {
format!(
"{} monitoring [ApplicationMonitoringScore]",
self.application.name()
)
}
}
#[derive(Debug)]
pub struct ApplicationMonitoringInterpret {
score: ApplicationMonitoringScore,
}
#[async_trait]
impl<T: Topology + PrometheusApplicationMonitoring<CRDPrometheus>> Interpret<T>
for ApplicationMonitoringInterpret
{
async fn execute(
&self,
inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
let result = topology
.install_prometheus(
&self.score.sender,
inventory,
Some(self.score.receivers.clone()),
)
.await;
match result {
Ok(outcome) => match outcome {
PreparationOutcome::Success { details: _ } => {
Ok(Outcome::success("Prometheus installed".into()))
}
PreparationOutcome::Noop => Ok(Outcome::noop()),
},
Err(err) => Err(InterpretError::from(err)),
}
}
fn get_name(&self) -> InterpretName {
InterpretName::ApplicationMonitoring
}
fn get_version(&self) -> Version {
todo!()
}
fn get_status(&self) -> InterpretStatus {
todo!()
}
fn get_children(&self) -> Vec<Id> {
todo!()
}
}

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

@@ -1 +1 @@
pub mod application_monitoring_score; pub mod k8s_application_monitoring_score;

View File

@@ -4,14 +4,15 @@ use std::str::FromStr;
use crate::modules::helm::chart::HelmChartScore; use crate::modules::helm::chart::HelmChartScore;
pub fn grafana_helm_chart_score(ns: &str) -> HelmChartScore { pub fn grafana_helm_chart_score(ns: &str) -> HelmChartScore {
let values = r#" let values = format!(
r#"
rbac: rbac:
namespaced: true namespaced: true
sidecar: sidecar:
dashboards: dashboards:
enabled: true enabled: true
"# "#
.to_string(); );
HelmChartScore { HelmChartScore {
namespace: Some(NonBlankString::from_str(ns).unwrap()), namespace: Some(NonBlankString::from_str(ns).unwrap()),

View File

@@ -1,50 +0,0 @@
use std::sync::Arc;
use kube::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::topology::{
k8s::K8sClient,
oberservability::monitoring::{AlertReceiver, AlertSender},
};
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[kube(
group = "monitoring.coreos.com",
version = "v1alpha1",
kind = "AlertmanagerConfig",
plural = "alertmanagerconfigs",
namespaced
)]
pub struct AlertmanagerConfigSpec {
#[serde(flatten)]
pub data: serde_json::Value,
}
#[derive(Debug, Clone, Serialize)]
pub struct CRDPrometheus {
pub namespace: String,
pub client: Arc<K8sClient>,
}
impl AlertSender for CRDPrometheus {
fn name(&self) -> String {
"CRDAlertManager".to_string()
}
}
impl Clone for Box<dyn AlertReceiver<CRDPrometheus>> {
fn clone(&self) -> Self {
self.clone_box()
}
}
impl Serialize for Box<dyn AlertReceiver<CRDPrometheus>> {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
todo!()
}
}

View File

@@ -1,52 +0,0 @@
use kube::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::crd_prometheuses::LabelSelector;
/// Rust CRD for `Alertmanager` from Prometheus Operator
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[kube(
group = "monitoring.coreos.com",
version = "v1",
kind = "Alertmanager",
plural = "alertmanagers",
namespaced
)]
#[serde(rename_all = "camelCase")]
pub struct AlertmanagerSpec {
/// Number of replicas for HA
pub replicas: i32,
/// Selectors for AlertmanagerConfig CRDs
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alertmanager_config_selector: Option<LabelSelector>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alertmanager_config_namespace_selector: Option<LabelSelector>,
/// Optional pod template metadata (annotations, labels)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pod_metadata: Option<LabelSelector>,
/// Optional topology spread settings
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
impl Default for AlertmanagerSpec {
fn default() -> Self {
AlertmanagerSpec {
replicas: 1,
// Match all AlertmanagerConfigs in the same namespace
alertmanager_config_namespace_selector: None,
// Empty selector matches all AlertmanagerConfigs in that namespace
alertmanager_config_selector: Some(LabelSelector::default()),
pod_metadata: None,
version: None,
}
}
}

View File

@@ -1,25 +0,0 @@
use crate::modules::prometheus::alerts::k8s::{
deployment::alert_deployment_unavailable,
pod::{alert_container_restarting, alert_pod_not_ready, pod_failed},
pvc::high_pvc_fill_rate_over_two_days,
service::alert_service_down,
};
use super::crd_prometheus_rules::Rule;
pub fn build_default_application_rules() -> Vec<Rule> {
let pod_failed: Rule = pod_failed().into();
let container_restarting: Rule = alert_container_restarting().into();
let pod_not_ready: Rule = alert_pod_not_ready().into();
let service_down: Rule = alert_service_down().into();
let deployment_unavailable: Rule = alert_deployment_unavailable().into();
let high_pvc_fill_rate: Rule = high_pvc_fill_rate_over_two_days().into();
vec![
pod_failed,
container_restarting,
pod_not_ready,
service_down,
deployment_unavailable,
high_pvc_fill_rate,
]
}

View File

@@ -1,153 +0,0 @@
use std::collections::BTreeMap;
use kube::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::crd_prometheuses::LabelSelector;
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[kube(
group = "grafana.integreatly.org",
version = "v1beta1",
kind = "Grafana",
plural = "grafanas",
namespaced
)]
#[serde(rename_all = "camelCase")]
pub struct GrafanaSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<GrafanaConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub admin_user: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub admin_password: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ingress: Option<GrafanaIngress>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub persistence: Option<GrafanaPersistence>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resources: Option<ResourceRequirements>,
}
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct GrafanaConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub log: Option<GrafanaLogConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub security: Option<GrafanaSecurityConfig>,
}
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct GrafanaLogConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub level: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct GrafanaSecurityConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub admin_user: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub admin_password: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct GrafanaIngress {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hosts: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct GrafanaPersistence {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub storage_class_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<String>,
}
// ------------------------------------------------------------------------------------------------
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[kube(
group = "grafana.integreatly.org",
version = "v1beta1",
kind = "GrafanaDashboard",
plural = "grafanadashboards",
namespaced
)]
#[serde(rename_all = "camelCase")]
pub struct GrafanaDashboardSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resync_period: Option<String>,
pub instance_selector: LabelSelector,
pub json: String,
}
// ------------------------------------------------------------------------------------------------
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[kube(
group = "grafana.integreatly.org",
version = "v1beta1",
kind = "GrafanaDatasource",
plural = "grafanadatasources",
namespaced
)]
#[serde(rename_all = "camelCase")]
pub struct GrafanaDatasourceSpec {
pub instance_selector: LabelSelector,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_cross_namespace_import: Option<bool>,
pub datasource: GrafanaDatasourceConfig,
}
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct GrafanaDatasourceConfig {
pub access: String,
pub database: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub json_data: Option<BTreeMap<String, String>>,
pub name: String,
pub r#type: String,
pub url: String,
}
// ------------------------------------------------------------------------------------------------
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct ResourceRequirements {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub limits: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub requests: BTreeMap<String, String>,
}

View File

@@ -1,57 +0,0 @@
use std::collections::BTreeMap;
use kube::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::modules::monitoring::alert_rule::prometheus_alert_rule::PrometheusAlertRule;
#[derive(CustomResource, Debug, Serialize, Deserialize, Clone, JsonSchema)]
#[kube(
group = "monitoring.coreos.com",
version = "v1",
kind = "PrometheusRule",
plural = "prometheusrules",
namespaced
)]
#[serde(rename_all = "camelCase")]
pub struct PrometheusRuleSpec {
pub groups: Vec<RuleGroup>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct RuleGroup {
pub name: String,
pub rules: Vec<Rule>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Rule {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alert: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expr: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub for_: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub labels: Option<std::collections::BTreeMap<String, String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<std::collections::BTreeMap<String, String>>,
}
impl From<PrometheusAlertRule> for Rule {
fn from(value: PrometheusAlertRule) -> Self {
Rule {
alert: Some(value.alert),
expr: Some(value.expr),
for_: value.r#for,
labels: Some(value.labels.into_iter().collect::<BTreeMap<_, _>>()),
annotations: Some(value.annotations.into_iter().collect::<BTreeMap<_, _>>()),
}
}
}

View File

@@ -1,118 +0,0 @@
use std::collections::BTreeMap;
use kube::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::modules::monitoring::kube_prometheus::types::Operator;
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[kube(
group = "monitoring.coreos.com",
version = "v1",
kind = "Prometheus",
plural = "prometheuses",
namespaced
)]
#[serde(rename_all = "camelCase")]
pub struct PrometheusSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alerting: Option<PrometheusSpecAlerting>,
pub service_account_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub service_monitor_namespace_selector: Option<LabelSelector>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub service_monitor_selector: Option<LabelSelector>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub service_discovery_role: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pod_monitor_selector: Option<LabelSelector>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rule_selector: Option<LabelSelector>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rule_namespace_selector: Option<LabelSelector>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct NamespaceSelector {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub match_names: Vec<String>,
}
/// Contains alerting configuration, specifically Alertmanager endpoints.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
pub struct PrometheusSpecAlerting {
#[serde(skip_serializing_if = "Option::is_none")]
pub alertmanagers: Option<Vec<AlertmanagerEndpoints>>,
}
/// Represents an Alertmanager endpoint configuration used by Prometheus.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
pub struct AlertmanagerEndpoints {
/// Name of the Alertmanager Service.
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// Namespace of the Alertmanager Service.
#[serde(skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
/// Port to access on the Alertmanager Service (e.g. "web").
#[serde(skip_serializing_if = "Option::is_none")]
pub port: Option<String>,
/// Scheme to use for connecting (e.g. "http").
#[serde(skip_serializing_if = "Option::is_none")]
pub scheme: Option<String>,
// Other fields like `tls_config`, `path_prefix`, etc., can be added if needed.
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "camelCase")]
pub struct LabelSelector {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub match_labels: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub match_expressions: Vec<LabelSelectorRequirement>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct LabelSelectorRequirement {
pub key: String,
pub operator: Operator,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub values: Vec<String>,
}
impl Default for PrometheusSpec {
fn default() -> Self {
PrometheusSpec {
alerting: None,
service_account_name: "prometheus".into(),
// null means "only my namespace"
service_monitor_namespace_selector: None,
// empty selector means match all ServiceMonitors in that namespace
service_monitor_selector: Some(LabelSelector::default()),
service_discovery_role: Some("Endpoints".into()),
pod_monitor_selector: None,
rule_selector: None,
rule_namespace_selector: Some(LabelSelector::default()),
}
}
}

View File

@@ -1,203 +0,0 @@
pub fn build_default_dashboard(namespace: &str) -> String {
let dashboard = format!(
r#"{{
"annotations": {{
"list": []
}},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": null,
"iteration": 171105,
"panels": [
{{
"datasource": "$datasource",
"fieldConfig": {{
"defaults": {{
"unit": "short"
}},
"overrides": []
}},
"gridPos": {{
"h": 6,
"w": 6,
"x": 0,
"y": 0
}},
"id": 1,
"options": {{
"reduceOptions": {{
"calcs": ["lastNotNull"],
"fields": "",
"values": false
}}
}},
"pluginVersion": "9.0.0",
"targets": [
{{
"expr": "sum(kube_pod_status_phase{{namespace=\"{namespace}\", phase=\"Running\"}})",
"legendFormat": "",
"refId": "A"
}}
],
"title": "Pods in Namespace",
"type": "stat"
}},
{{
"datasource": "$datasource",
"fieldConfig": {{
"defaults": {{
"unit": "short"
}},
"overrides": []
}},
"gridPos": {{
"h": 6,
"w": 6,
"x": 6,
"y": 0
}},
"id": 2,
"options": {{
"reduceOptions": {{
"calcs": ["lastNotNull"],
"fields": "",
"values": false
}}
}},
"pluginVersion": "9.0.0",
"targets": [
{{
"expr": "sum(kube_pod_status_phase{{phase=\"Failed\", namespace=\"{namespace}\"}})",
"legendFormat": "",
"refId": "A"
}}
],
"title": "Pods in Failed State",
"type": "stat"
}},
{{
"datasource": "$datasource",
"fieldConfig": {{
"defaults": {{
"unit": "percentunit"
}},
"overrides": []
}},
"gridPos": {{
"h": 6,
"w": 12,
"x": 0,
"y": 6
}},
"id": 3,
"options": {{
"reduceOptions": {{
"calcs": ["lastNotNull"],
"fields": "",
"values": false
}}
}},
"pluginVersion": "9.0.0",
"targets": [
{{
"expr": "sum(kube_deployment_status_replicas_available{{namespace=\"{namespace}\"}}) / sum(kube_deployment_spec_replicas{{namespace=\"{namespace}\"}})",
"legendFormat": "",
"refId": "A"
}}
],
"title": "Deployment Health (Available / Desired)",
"type": "stat"
}},
{{
"datasource": "$datasource",
"fieldConfig": {{
"defaults": {{
"unit": "short"
}},
"overrides": []
}},
"gridPos": {{
"h": 6,
"w": 12,
"x": 0,
"y": 12
}},
"id": 4,
"options": {{
"reduceOptions": {{
"calcs": ["lastNotNull"],
"fields": "",
"values": false
}}
}},
"pluginVersion": "9.0.0",
"targets": [
{{
"expr": "sum by(pod) (rate(kube_pod_container_status_restarts_total{{namespace=\"{namespace}\"}}[5m]))",
"legendFormat": "{{{{pod}}}}",
"refId": "A"
}}
],
"title": "Container Restarts (per pod)",
"type": "timeseries"
}},
{{
"datasource": "$datasource",
"fieldConfig": {{
"defaults": {{
"unit": "short"
}},
"overrides": []
}},
"gridPos": {{
"h": 6,
"w": 12,
"x": 0,
"y": 18
}},
"id": 5,
"options": {{
"reduceOptions": {{
"calcs": ["lastNotNull"],
"fields": "",
"values": false
}}
}},
"pluginVersion": "9.0.0",
"targets": [
{{
"expr": "sum(ALERTS{{alertstate=\"firing\", namespace=\"{namespace}\"}}) or vector(0)",
"legendFormat": "",
"refId": "A"
}}
],
"title": "Firing Alerts in Namespace",
"type": "stat"
}}
],
"schemaVersion": 36,
"templating": {{
"list": [
{{
"name": "datasource",
"type": "datasource",
"pluginId": "prometheus",
"label": "Prometheus",
"query": "prometheus",
"refresh": 1,
"hide": 0,
"current": {{
"selected": true,
"text": "Prometheus",
"value": "Prometheus"
}}
}}
]
}},
"title": "Tenant Namespace Overview",
"version": 1
}}"#
);
dashboard
}

View File

@@ -1,20 +0,0 @@
use std::str::FromStr;
use non_blank_string_rs::NonBlankString;
use crate::modules::helm::chart::HelmChartScore;
pub fn grafana_operator_helm_chart_score(ns: String) -> HelmChartScore {
HelmChartScore {
namespace: Some(NonBlankString::from_str(&ns).unwrap()),
release_name: NonBlankString::from_str("grafana_operator").unwrap(),
chart_name: NonBlankString::from_str("oci://ghcr.io/grafana/helm-charts/grafana-operator")
.unwrap(),
chart_version: None,
values_overrides: None,
values_yaml: None,
create_namespace: true,
install_only: true,
repository: None,
}
}

View File

@@ -1,11 +0,0 @@
pub mod crd_alertmanager_config;
pub mod crd_alertmanagers;
pub mod crd_default_rules;
pub mod crd_grafana;
pub mod crd_prometheus_rules;
pub mod crd_prometheuses;
pub mod grafana_default_dashboard;
pub mod grafana_operator;
pub mod prometheus_operator;
pub mod role;
pub mod service_monitor;

View File

@@ -1,22 +0,0 @@
use std::str::FromStr;
use non_blank_string_rs::NonBlankString;
use crate::modules::helm::chart::HelmChartScore;
pub fn prometheus_operator_helm_chart_score(ns: String) -> HelmChartScore {
HelmChartScore {
namespace: Some(NonBlankString::from_str(&ns).unwrap()),
release_name: NonBlankString::from_str("prometheus-operator").unwrap(),
chart_name: NonBlankString::from_str(
"oci://hub.nationtech.io/harmony/nt-prometheus-operator",
)
.unwrap(),
chart_version: None,
values_overrides: None,
values_yaml: None,
create_namespace: true,
install_only: true,
repository: None,
}
}

View File

@@ -1,62 +0,0 @@
use k8s_openapi::api::{
core::v1::ServiceAccount,
rbac::v1::{PolicyRule, Role, RoleBinding, RoleRef, Subject},
};
use kube::api::ObjectMeta;
pub fn build_prom_role(role_name: String, namespace: String) -> Role {
Role {
metadata: ObjectMeta {
name: Some(role_name),
namespace: Some(namespace),
..Default::default()
},
rules: Some(vec![PolicyRule {
api_groups: Some(vec!["".into()]), // core API group
resources: Some(vec!["services".into(), "endpoints".into(), "pods".into()]),
verbs: vec!["get".into(), "list".into(), "watch".into()],
..Default::default()
}]),
}
}
pub fn build_prom_rolebinding(
role_name: String,
namespace: String,
service_account_name: String,
) -> RoleBinding {
RoleBinding {
metadata: ObjectMeta {
name: Some(format!("{}-rolebinding", role_name)),
namespace: Some(namespace.clone()),
..Default::default()
},
role_ref: RoleRef {
api_group: "rbac.authorization.k8s.io".into(),
kind: "Role".into(),
name: role_name,
},
subjects: Some(vec![Subject {
kind: "ServiceAccount".into(),
name: service_account_name,
namespace: Some(namespace.clone()),
..Default::default()
}]),
}
}
pub fn build_prom_service_account(
service_account_name: String,
namespace: String,
) -> ServiceAccount {
ServiceAccount {
automount_service_account_token: None,
image_pull_secrets: None,
metadata: ObjectMeta {
name: Some(service_account_name),
namespace: Some(namespace),
..Default::default()
},
secrets: None,
}
}

View File

@@ -1,87 +0,0 @@
use std::collections::HashMap;
use kube::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::modules::monitoring::kube_prometheus::types::{
HTTPScheme, MatchExpression, NamespaceSelector, Operator, Selector,
ServiceMonitor as KubeServiceMonitor, ServiceMonitorEndpoint,
};
/// This is the top-level struct for the ServiceMonitor Custom Resource.
/// The `#[derive(CustomResource)]` macro handles all the boilerplate for you,
/// including the `impl Resource`.
#[derive(CustomResource, Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[kube(
group = "monitoring.coreos.com",
version = "v1",
kind = "ServiceMonitor",
plural = "servicemonitors",
namespaced
)]
#[serde(rename_all = "camelCase")]
pub struct ServiceMonitorSpec {
/// A label selector to select services to monitor.
pub selector: Selector,
/// A list of endpoints on the selected services to be monitored.
pub endpoints: Vec<ServiceMonitorEndpoint>,
/// Selector to select which namespaces the Kubernetes Endpoints objects
/// are discovered from.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub namespace_selector: Option<NamespaceSelector>,
/// The label to use to retrieve the job name from.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub job_label: Option<String>,
/// Pod-based target labels to transfer from the Kubernetes Pod onto the target.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pod_target_labels: Vec<String>,
/// TargetLabels transfers labels on the Kubernetes Service object to the target.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub target_labels: Vec<String>,
}
impl Default for ServiceMonitorSpec {
fn default() -> Self {
let labels = HashMap::new();
Self {
selector: Selector {
match_labels: { labels },
match_expressions: vec![MatchExpression {
key: "app.kubernetes.io/name".into(),
operator: Operator::Exists,
values: vec![],
}],
},
endpoints: vec![ServiceMonitorEndpoint {
port: Some("http".to_string()),
path: Some("/metrics".into()),
interval: Some("30s".into()),
scheme: Some(HTTPScheme::HTTP),
..Default::default()
}],
namespace_selector: None, // only the same namespace
job_label: Some("app".into()),
pod_target_labels: vec![],
target_labels: vec![],
}
}
}
impl From<KubeServiceMonitor> for ServiceMonitorSpec {
fn from(value: KubeServiceMonitor) -> Self {
Self {
selector: value.selector,
endpoints: value.endpoints,
namespace_selector: value.namespace_selector,
job_label: value.job_label,
pod_target_labels: value.pod_target_labels,
target_labels: value.target_labels,
}
}
}

View File

@@ -27,12 +27,6 @@ pub struct KubePrometheusConfig {
pub alert_rules: Vec<AlertManagerAdditionalPromRules>, pub alert_rules: Vec<AlertManagerAdditionalPromRules>,
pub additional_service_monitors: Vec<ServiceMonitor>, pub additional_service_monitors: Vec<ServiceMonitor>,
} }
impl Default for KubePrometheusConfig {
fn default() -> Self {
Self::new()
}
}
impl KubePrometheusConfig { impl KubePrometheusConfig {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -41,7 +35,7 @@ impl KubePrometheusConfig {
windows_monitoring: false, windows_monitoring: false,
alert_manager: true, alert_manager: true,
grafana: true, grafana: true,
node_exporter: true, node_exporter: false,
prometheus: true, prometheus: true,
kubernetes_service_monitors: true, kubernetes_service_monitors: true,
kubernetes_api_server: true, kubernetes_api_server: true,

View File

@@ -12,8 +12,8 @@ use crate::modules::{
helm::chart::HelmChartScore, helm::chart::HelmChartScore,
monitoring::kube_prometheus::types::{ monitoring::kube_prometheus::types::{
AlertGroup, AlertManager, AlertManagerAdditionalPromRules, AlertManagerConfig, AlertGroup, AlertManager, AlertManagerAdditionalPromRules, AlertManagerConfig,
AlertManagerConfigSelector, AlertManagerRoute, AlertManagerSpec, AlertManagerValues, AlertManagerRoute, AlertManagerSpec, AlertManagerValues, ConfigReloader, Limits,
ConfigReloader, Limits, PrometheusConfig, Requests, Resources, PrometheusConfig, Requests, Resources,
}, },
}; };
@@ -35,7 +35,7 @@ pub fn kube_prometheus_helm_chart_score(
let kube_proxy = config.kube_proxy.to_string(); let kube_proxy = config.kube_proxy.to_string();
let kube_state_metrics = config.kube_state_metrics.to_string(); let kube_state_metrics = config.kube_state_metrics.to_string();
let node_exporter = config.node_exporter.to_string(); let node_exporter = config.node_exporter.to_string();
let _prometheus_operator = config.prometheus_operator.to_string(); let prometheus_operator = config.prometheus_operator.to_string();
let prometheus = config.prometheus.to_string(); let prometheus = config.prometheus.to_string();
let resource_limit = Resources { let resource_limit = Resources {
limits: Limits { limits: Limits {
@@ -64,7 +64,7 @@ pub fn kube_prometheus_helm_chart_score(
indent_lines(&yaml, indent_level + 2) indent_lines(&yaml, indent_level + 2)
) )
} }
let _resource_section = resource_block(&resource_limit, 2); let resource_section = resource_block(&resource_limit, 2);
let mut values = format!( let mut values = format!(
r#" r#"
@@ -332,11 +332,6 @@ prometheusOperator:
.push(receiver.channel_receiver.clone()); .push(receiver.channel_receiver.clone());
} }
let mut labels = BTreeMap::new();
labels.insert("alertmanagerConfig".to_string(), "enabled".to_string());
let alert_manager_config_selector = AlertManagerConfigSelector {
match_labels: labels,
};
let alert_manager_values = AlertManagerValues { let alert_manager_values = AlertManagerValues {
alertmanager: AlertManager { alertmanager: AlertManager {
enabled: config.alert_manager, enabled: config.alert_manager,
@@ -352,8 +347,6 @@ prometheusOperator:
cpu: "100m".to_string(), cpu: "100m".to_string(),
}, },
}, },
alert_manager_config_selector,
replicas: 2,
}, },
init_config_reloader: ConfigReloader { init_config_reloader: ConfigReloader {
resources: Resources { resources: Resources {

View File

@@ -1,4 +1,3 @@
pub mod crd;
pub mod helm; pub mod helm;
pub mod helm_prometheus_alert_score; pub mod helm_prometheus_alert_score;
pub mod prometheus; pub mod prometheus;

View File

@@ -55,12 +55,6 @@ pub struct KubePrometheus {
pub config: Arc<Mutex<KubePrometheusConfig>>, pub config: Arc<Mutex<KubePrometheusConfig>>,
} }
impl Default for KubePrometheus {
fn default() -> Self {
Self::new()
}
}
impl KubePrometheus { impl KubePrometheus {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -119,7 +113,8 @@ impl KubePrometheus {
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
kube_prometheus_helm_chart_score(self.config.clone()) kube_prometheus_helm_chart_score(self.config.clone())
.interpret(inventory, topology) .create_interpret()
.execute(inventory, topology)
.await .await
} }
} }

View File

@@ -1,8 +1,7 @@
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use async_trait::async_trait; use async_trait::async_trait;
use schemars::JsonSchema; use serde::Serialize;
use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Sequence, Value}; use serde_yaml::{Mapping, Sequence, Value};
use crate::modules::monitoring::alert_rule::prometheus_alert_rule::AlertManagerRuleGroup; use crate::modules::monitoring::alert_rule::prometheus_alert_rule::AlertManagerRuleGroup;
@@ -56,14 +55,6 @@ pub struct AlertManagerChannelConfig {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AlertManagerSpec { pub struct AlertManagerSpec {
pub(crate) resources: Resources, pub(crate) resources: Resources,
pub replicas: u32,
pub alert_manager_config_selector: AlertManagerConfigSelector,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AlertManagerConfigSelector {
pub match_labels: BTreeMap<String, String>,
} }
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
@@ -95,7 +86,7 @@ pub struct AlertGroup {
pub groups: Vec<AlertManagerRuleGroup>, pub groups: Vec<AlertManagerRuleGroup>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Clone, Serialize)]
pub enum HTTPScheme { pub enum HTTPScheme {
#[serde(rename = "http")] #[serde(rename = "http")]
HTTP, HTTP,
@@ -103,7 +94,7 @@ pub enum HTTPScheme {
HTTPS, HTTPS,
} }
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Clone, Serialize)]
pub enum Operator { pub enum Operator {
In, In,
NotIn, NotIn,
@@ -148,83 +139,74 @@ pub struct ServiceMonitorTLSConfig {
pub server_name: Option<String>, pub server_name: Option<String>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, Default)] #[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ServiceMonitorEndpoint { pub struct ServiceMonitorEndpoint {
/// Name of the service port this endpoint refers to. // ## Name of the endpoint's service port
// ## Mutually exclusive with targetPort
pub port: Option<String>, pub port: Option<String>,
/// Interval at which metrics should be scraped. // ## Name or number of the endpoint's target port
#[serde(default, skip_serializing_if = "Option::is_none")] // ## Mutually exclusive with port
pub target_port: Option<String>,
// ## File containing bearer token to be used when scraping targets
// ##
pub bearer_token_file: Option<String>,
// ## Interval at which metrics should be scraped
// ##
pub interval: Option<String>, pub interval: Option<String>,
/// The HTTP path to scrape for metrics. // ## HTTP path to scrape for metrics
#[serde(default, skip_serializing_if = "Option::is_none")] // ##
pub path: Option<String>, pub path: String,
/// HTTP scheme to use for scraping. // ## HTTP scheme to use for scraping
#[serde(default, skip_serializing_if = "Option::is_none")] // ##
pub scheme: Option<HTTPScheme>, pub scheme: HTTPScheme,
/// Relabelings to apply to samples before scraping. // ## TLS configuration to use when scraping the endpoint
#[serde(default, skip_serializing_if = "Vec::is_empty")] // ##
pub relabelings: Vec<RelabelConfig>, pub tls_config: Option<ServiceMonitorTLSConfig>,
/// MetricRelabelings to apply to samples after scraping, but before ingestion. // ## MetricRelabelConfigs to apply to samples after scraping, but before ingestion.
#[serde(default, skip_serializing_if = "Vec::is_empty")] // ## ref: https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api-reference/api.md#relabelconfig
pub metric_relabelings: Vec<RelabelConfig>, // ##
// # - action: keep
// # regex: 'kube_(daemonset|deployment|pod|namespace|node|statefulset).+'
// # sourceLabels: [__name__]
pub metric_relabelings: Vec<Mapping>,
// ## RelabelConfigs to apply to samples before scraping
// ## ref: https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api-reference/api.md#relabelconfig
// ##
// # - sourceLabels: [__meta_kubernetes_pod_node_name]
// # separator: ;
// # regex: ^(.*)$
// # targetLabel: nodename
// # replacement: $1
// # action: replace
pub relabelings: Vec<Mapping>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RelabelConfig {
/// The action to perform based on the regex matching.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action: Option<String>,
/// A list of labels from which to extract values.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub source_labels: Vec<String>,
/// Separator to be used when concatenating source_labels.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub separator: Option<String>,
/// The label to which the resulting value is written.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_label: Option<String>,
/// A regular expression to match against the concatenated source label values.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub regex: Option<String>,
/// The replacement value to use.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub replacement: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct MatchExpression { pub struct MatchExpression {
pub key: String, pub key: String,
pub operator: Operator, // "In", "NotIn", "Exists", "DoesNotExist" pub operator: Operator,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub values: Vec<String>, pub values: Vec<String>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, Default)] #[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Selector { pub struct Selector {
/// A map of key-value pairs to match. // # label selector for services
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub match_labels: HashMap<String, String>, pub match_labels: HashMap<String, String>,
/// A list of label selector requirements.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub match_expressions: Vec<MatchExpression>, pub match_expressions: Vec<MatchExpression>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ServiceMonitor { pub struct ServiceMonitor {
pub name: String, pub name: String,
@@ -268,15 +250,10 @@ pub struct ServiceMonitor {
pub fallback_scrape_protocol: Option<String>, pub fallback_scrape_protocol: Option<String>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, Default)] #[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct NamespaceSelector { pub struct NamespaceSelector {
/// Select all namespaces.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub any: bool, pub any: bool,
/// List of namespace names to select from.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub match_names: Vec<String>, pub match_names: Vec<String>,
} }
@@ -298,3 +275,19 @@ impl Default for ServiceMonitor {
} }
} }
} }
impl Default for ServiceMonitorEndpoint {
fn default() -> Self {
Self {
port: Some("80".to_string()),
target_port: Default::default(),
bearer_token_file: Default::default(),
interval: Default::default(),
path: "/metrics".to_string(),
scheme: HTTPScheme::HTTP,
tls_config: Default::default(),
metric_relabelings: Default::default(),
relabelings: Default::default(),
}
}
}

View File

@@ -1,25 +1,9 @@
use non_blank_string_rs::NonBlankString; use non_blank_string_rs::NonBlankString;
use std::str::FromStr; use std::str::FromStr;
use crate::{modules::helm::chart::HelmChartScore, topology::DeploymentTarget}; use crate::modules::helm::chart::{HelmChartScore, HelmRepository};
pub fn ntfy_helm_chart_score(
namespace: String,
host: String,
target: DeploymentTarget,
) -> HelmChartScore {
// TODO not actually the correct logic, this should be fixed by using an ingresss which is the
// correct k8s standard.
//
// Another option is to delegate to the topology the ingress technology it wants to use Route,
// Ingress or other
let route_enabled = match target {
DeploymentTarget::LocalDev => false,
DeploymentTarget::Staging => true,
DeploymentTarget::Production => true,
};
let ingress_enabled = !route_enabled;
pub fn ntfy_helm_chart_score(namespace: String, host: String) -> HelmChartScore {
let values = format!( let values = format!(
r#" r#"
replicaCount: 1 replicaCount: 1
@@ -41,14 +25,23 @@ serviceAccount:
service: service:
type: ClusterIP type: ClusterIP
port: 8080 port: 80
ingress: ingress:
enabled: {ingress_enabled} enabled: true
# annotations:
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: {host}
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
route:
enabled: {route_enabled}
host: {host}
autoscaling: autoscaling:
enabled: false enabled: false
@@ -56,7 +49,7 @@ autoscaling:
config: config:
enabled: true enabled: true
data: data:
base-url: "https://{host}" # base-url: "https://ntfy.something.com"
auth-file: "/var/cache/ntfy/user.db" auth-file: "/var/cache/ntfy/user.db"
auth-default-access: "deny-all" auth-default-access: "deny-all"
cache-file: "/var/cache/ntfy/cache.db" cache-file: "/var/cache/ntfy/cache.db"
@@ -65,8 +58,6 @@ config:
# web-root: "disable" # web-root: "disable"
enable-signup: false enable-signup: false
enable-login: "true" enable-login: "true"
enable-metrics: "true"
listen-http: ":8080"
persistence: persistence:
enabled: true enabled: true
@@ -77,12 +68,16 @@ persistence:
HelmChartScore { HelmChartScore {
namespace: Some(NonBlankString::from_str(&namespace).unwrap()), namespace: Some(NonBlankString::from_str(&namespace).unwrap()),
release_name: NonBlankString::from_str("ntfy").unwrap(), release_name: NonBlankString::from_str("ntfy").unwrap(),
chart_name: NonBlankString::from_str("oci://hub.nationtech.io/harmony/ntfy").unwrap(), chart_name: NonBlankString::from_str("sarab97/ntfy").unwrap(),
chart_version: Some(NonBlankString::from_str("0.1.7-nationtech.1").unwrap()), chart_version: Some(NonBlankString::from_str("0.1.7").unwrap()),
values_overrides: None, values_overrides: None,
values_yaml: Some(values.to_string()), values_yaml: Some(values.to_string()),
create_namespace: true, create_namespace: true,
install_only: false, install_only: false,
repository: None, repository: Some(HelmRepository::new(
"sarab97".to_string(),
url::Url::parse("https://charts.sarabsingh.com").unwrap(),
true,
)),
} }
} }

View File

@@ -1,3 +1,2 @@
pub mod helm; pub mod helm;
#[allow(clippy::module_inception)]
pub mod ntfy; pub mod ntfy;

View File

@@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use log::info; use log::debug;
use serde::Serialize; use serde::Serialize;
use strum::{Display, EnumString}; use strum::{Display, EnumString};
@@ -11,7 +11,7 @@ use crate::{
inventory::Inventory, inventory::Inventory,
modules::monitoring::ntfy::helm::ntfy_helm_chart::ntfy_helm_chart_score, modules::monitoring::ntfy::helm::ntfy_helm_chart::ntfy_helm_chart_score,
score::Score, score::Score,
topology::{HelmCommand, K8sclient, MultiTargetTopology, Topology, k8s::K8sClient}, topology::{HelmCommand, K8sclient, Topology, k8s::K8sClient},
}; };
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
@@ -20,7 +20,7 @@ pub struct NtfyScore {
pub host: String, pub host: String,
} }
impl<T: Topology + HelmCommand + K8sclient + MultiTargetTopology> Score<T> for NtfyScore { impl<T: Topology + HelmCommand + K8sclient> Score<T> for NtfyScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(NtfyInterpret { Box::new(NtfyInterpret {
score: self.clone(), score: self.clone(),
@@ -28,7 +28,7 @@ impl<T: Topology + HelmCommand + K8sclient + MultiTargetTopology> Score<T> for N
} }
fn name(&self) -> String { fn name(&self) -> String {
"alert receiver [NtfyScore]".into() format!("Ntfy")
} }
} }
@@ -39,21 +39,31 @@ pub struct NtfyInterpret {
#[derive(Debug, EnumString, Display)] #[derive(Debug, EnumString, Display)]
enum NtfyAccessMode { enum NtfyAccessMode {
#[strum(serialize = "read-write", serialize = "rw")] #[strum(serialize = "read-write", serialize = "rw", to_string = "read-write")]
ReadWrite, ReadWrite,
#[strum(serialize = "read-only", serialize = "ro", serialize = "read")] #[strum(
serialize = "read-only",
serialize = "ro",
serialize = "read",
to_string = "read-only"
)]
ReadOnly, ReadOnly,
#[strum(serialize = "write-only", serialize = "wo", serialize = "write")] #[strum(
serialize = "write-only",
serialize = "wo",
serialize = "write",
to_string = "write-only"
)]
WriteOnly, WriteOnly,
#[strum(serialize = "deny", serialize = "none")] #[strum(serialize = "none", to_string = "deny")]
Deny, Deny,
} }
#[derive(Debug, EnumString, Display)] #[derive(Debug, EnumString, Display)]
enum NtfyRole { enum NtfyRole {
#[strum(serialize = "user")] #[strum(serialize = "user", to_string = "user")]
User, User,
#[strum(serialize = "admin")] #[strum(serialize = "admin", to_string = "admin")]
Admin, Admin,
} }
@@ -77,7 +87,7 @@ impl NtfyInterpret {
vec![ vec![
"sh", "sh",
"-c", "-c",
format!("NTFY_PASSWORD={password} ntfy user add --role={role} --ignore-exists {username}") format!("NTFY_PASSWORD={password} ntfy user add --role={role} {username}")
.as_str(), .as_str(),
], ],
) )
@@ -85,52 +95,69 @@ impl NtfyInterpret {
Ok(()) Ok(())
} }
async fn set_access(
&self,
k8s_client: Arc<K8sClient>,
username: &str,
topic: &str,
mode: NtfyAccessMode,
) -> Result<(), String> {
k8s_client
.exec_app(
"ntfy".to_string(),
Some(&self.score.namespace),
vec![
"sh",
"-c",
format!("ntfy access {username} {topic} {mode}").as_str(),
],
)
.await?;
Ok(())
}
} }
/// We need a ntfy interpret to wrap the HelmChartScore in order to run the score, and then bootstrap the config inside ntfy /// We need a ntfy interpret to wrap the HelmChartScore in order to run the score, and then bootstrap the config inside ntfy
#[async_trait] #[async_trait]
impl<T: Topology + HelmCommand + K8sclient + MultiTargetTopology> Interpret<T> for NtfyInterpret { impl<T: Topology + HelmCommand + K8sclient> Interpret<T> for NtfyInterpret {
async fn execute( async fn execute(
&self, &self,
inventory: &Inventory, inventory: &Inventory,
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
ntfy_helm_chart_score( ntfy_helm_chart_score(self.score.namespace.clone(), self.score.host.clone())
self.score.namespace.clone(), .create_interpret()
self.score.host.clone(), .execute(inventory, topology)
topology.current_target(), .await?;
)
.interpret(inventory, topology)
.await?;
info!("installed ntfy helm chart"); debug!("installed ntfy helm chart");
let client = topology let client = topology
.k8s_client() .k8s_client()
.await .await
.expect("couldn't get k8s client"); .expect("couldn't get k8s client");
info!("deploying ntfy...");
client client
.wait_until_deployment_ready( .wait_until_deployment_ready(
"ntfy".to_string(), "ntfy".to_string(),
Some(self.score.namespace.as_str()), Some(&self.score.namespace.as_str()),
None, None,
) )
.await?; .await?;
info!("ntfy deployed"); debug!("created k8s client");
info!("adding user harmony");
self.add_user(client, "harmony", "harmony", Some(NtfyRole::Admin)) self.add_user(client, "harmony", "harmony", Some(NtfyRole::Admin))
.await?; .await?;
info!("user added");
Ok(Outcome::success("Ntfy installed".to_string())) debug!("exec into pod done");
Ok(Outcome::success("installed ntfy".to_string()))
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {
InterpretName::Ntfy todo!()
} }
fn get_version(&self) -> Version { fn get_version(&self) -> Version {
todo!() todo!()
} }

View File

@@ -1,4 +1,3 @@
pub mod helm; pub mod helm;
#[allow(clippy::module_inception)]
pub mod prometheus; pub mod prometheus;
pub mod prometheus_config; pub mod prometheus_config;

View File

@@ -37,12 +37,6 @@ impl AlertSender for Prometheus {
} }
} }
impl Default for Prometheus {
fn default() -> Self {
Self::new()
}
}
impl Prometheus { impl Prometheus {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -100,7 +94,8 @@ impl Prometheus {
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
prometheus_helm_chart_score(self.config.clone()) prometheus_helm_chart_score(self.config.clone())
.interpret(inventory, topology) .create_interpret()
.execute(inventory, topology)
.await .await
} }
pub async fn install_grafana<T: Topology + HelmCommand + Send + Sync>( pub async fn install_grafana<T: Topology + HelmCommand + Send + Sync>(
@@ -115,12 +110,13 @@ impl Prometheus {
if let Some(ns) = namespace.as_deref() { if let Some(ns) = namespace.as_deref() {
grafana_helm_chart_score(ns) grafana_helm_chart_score(ns)
.interpret(inventory, topology) .create_interpret()
.execute(inventory, topology)
.await .await
} else { } else {
Err(InterpretError::new( Err(InterpretError::new(format!(
"could not install grafana, missing namespace".to_string(), "could not install grafana, missing namespace",
)) )))
} }
} }
} }

View File

@@ -16,12 +16,6 @@ pub struct PrometheusConfig {
pub additional_service_monitors: Vec<ServiceMonitor>, pub additional_service_monitors: Vec<ServiceMonitor>,
} }
impl Default for PrometheusConfig {
fn default() -> Self {
Self::new()
}
}
impl PrometheusConfig { impl PrometheusConfig {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {

View File

@@ -32,7 +32,7 @@ impl OKDBootstrapDhcpScore {
logical_host: topology.bootstrap_host.clone(), logical_host: topology.bootstrap_host.clone(),
physical_host: inventory physical_host: inventory
.worker_host .worker_host
.first() .get(0)
.expect("Should have at least one worker to be used as bootstrap node") .expect("Should have at least one worker to be used as bootstrap node")
.clone(), .clone(),
}); });

View File

@@ -6,12 +6,6 @@ pub struct OKDUpgradeScore {
_target_version: Version, _target_version: Version,
} }
impl Default for OKDUpgradeScore {
fn default() -> Self {
Self::new()
}
}
impl OKDUpgradeScore { impl OKDUpgradeScore {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {

View File

@@ -1,23 +0,0 @@
use std::collections::HashMap;
use crate::modules::monitoring::alert_rule::prometheus_alert_rule::PrometheusAlertRule;
pub fn alert_deployment_unavailable() -> PrometheusAlertRule {
PrometheusAlertRule {
alert: "DeploymentUnavailable".into(),
expr: "kube_deployment_status_replicas_unavailable > 0".into(),
r#for: Some("2m".into()),
labels: HashMap::from([("severity".into(), "warning".into())]),
annotations: HashMap::from([
(
"summary".into(),
"Deployment has unavailable replicas".into(),
),
(
"description".into(),
"A deployment in this namespace has unavailable replicas for over 2 minutes."
.into(),
),
]),
}
}

View File

@@ -1,37 +0,0 @@
use std::collections::HashMap;
use crate::modules::monitoring::alert_rule::prometheus_alert_rule::PrometheusAlertRule;
pub fn alert_high_memory_usage() -> PrometheusAlertRule {
PrometheusAlertRule {
alert: "HighMemoryUsage".into(),
expr: "container_memory_working_set_bytes{container!=\"\",namespace!=\"\"} > 500000000"
.into(),
r#for: Some("2m".into()),
labels: HashMap::from([("severity".into(), "warning".into())]),
annotations: HashMap::from([
("summary".into(), "Pod is using high memory".into()),
(
"description".into(),
"A pod is consuming more than 500Mi of memory.".into(),
),
]),
}
}
pub fn alert_high_cpu_usage() -> PrometheusAlertRule {
PrometheusAlertRule {
alert: "HighCPUUsage".into(),
expr: "rate(container_cpu_usage_seconds_total{container!=\"\",namespace!=\"\"}[1m]) > 0.9"
.into(),
r#for: Some("1m".into()),
labels: HashMap::from([("severity".into(), "warning".into())]),
annotations: HashMap::from([
("summary".into(), "Pod is using high CPU".into()),
(
"description".into(),
"A pod is using more than 90% of a core over 1 minute.".into(),
),
]),
}
}

View File

@@ -1,5 +1 @@
pub mod deployment;
pub mod memory_usage;
pub mod pod;
pub mod pvc; pub mod pvc;
pub mod service;

Some files were not shown because too many files have changed in this diff Show More