Compare commits
4 Commits
feat/webho
...
feat/kube-
| Author | SHA1 | Date | |
|---|---|---|---|
| de3e7869f7 | |||
| 57eabc9834 | |||
| cd40660350 | |||
| 2ca732cecd |
@@ -1,2 +0,0 @@
|
|||||||
target/
|
|
||||||
Dockerfile
|
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
name: Run Check Script
|
name: Run Check Script
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
runs-on: docker
|
runs-on: rust-cargo
|
||||||
container:
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
name: Compile and package harmony_composer
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
package_harmony_composer:
|
|
||||||
container:
|
|
||||||
image: hub.nationtech.io/harmony/harmony_composer:latest@sha256:eb0406fcb95c63df9b7c4b19bc50ad7914dd8232ce98e9c9abef628e07c69386
|
|
||||||
runs-on: dind
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Build for Linux x86_64
|
|
||||||
run: cargo build --release --bin harmony_composer --target x86_64-unknown-linux-gnu
|
|
||||||
|
|
||||||
- name: Build for Windows x86_64 GNU
|
|
||||||
run: cargo build --release --bin harmony_composer --target x86_64-pc-windows-gnu
|
|
||||||
|
|
||||||
- name: Setup log into hub.nationtech.io
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: hub.nationtech.io
|
|
||||||
username: ${{ secrets.HUB_BOT_USER }}
|
|
||||||
password: ${{ secrets.HUB_BOT_PASSWORD }}
|
|
||||||
|
|
||||||
# TODO: build ARM images and MacOS binaries (or other targets) too
|
|
||||||
|
|
||||||
- name: Update snapshot-latest tag
|
|
||||||
run: |
|
|
||||||
git config user.name "Gitea CI"
|
|
||||||
git config user.email "ci@nationtech.io"
|
|
||||||
git tag -f snapshot-latest
|
|
||||||
git push origin snapshot-latest --force
|
|
||||||
|
|
||||||
- name: Install jq
|
|
||||||
run: apt install -y jq # The current image includes apt lists so we don't have to apt update and rm /var/lib/apt... every time. But if the image is optimized it won't work anymore
|
|
||||||
|
|
||||||
- name: Create or update release
|
|
||||||
run: |
|
|
||||||
# First, check if release exists and delete it if it does
|
|
||||||
RELEASE_ID=$(curl -s -X GET \
|
|
||||||
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \
|
|
||||||
"https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases/tags/snapshot-latest" \
|
|
||||||
| jq -r '.id // empty')
|
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
|
||||||
# Delete existing release
|
|
||||||
curl -X DELETE \
|
|
||||||
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \
|
|
||||||
"https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases/$RELEASE_ID"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create new release
|
|
||||||
RESPONSE=$(curl -X POST \
|
|
||||||
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"tag_name": "snapshot-latest",
|
|
||||||
"name": "Latest Snapshot",
|
|
||||||
"body": "Automated snapshot build from master branch",
|
|
||||||
"draft": false,
|
|
||||||
"prerelease": true
|
|
||||||
}' \
|
|
||||||
"https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases")
|
|
||||||
|
|
||||||
echo "RELEASE_ID=$(echo $RESPONSE | jq -r '.id')" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Upload Linux binary
|
|
||||||
run: |
|
|
||||||
curl -X POST \
|
|
||||||
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
--data-binary "@target/x86_64-unknown-linux-gnu/release/harmony_composer" \
|
|
||||||
"https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases/${{ env.RELEASE_ID }}/assets?name=harmony_composer"
|
|
||||||
|
|
||||||
- name: Upload Windows binary
|
|
||||||
run: |
|
|
||||||
curl -X POST \
|
|
||||||
-H "Authorization: token ${{ secrets.GITEATOKEN }}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
--data-binary "@target/x86_64-pc-windows-gnu/release/harmony_composer.exe" \
|
|
||||||
"https://git.nationtech.io/api/v1/repos/nationtech/harmony/releases/${{ env.RELEASE_ID }}/assets?name=harmony_composer.exe"
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: hub.nationtech.io/harmony/harmony_composer:latest
|
|
||||||
336
Cargo.lock
generated
336
Cargo.lock
generated
@@ -295,51 +295,6 @@ dependencies = [
|
|||||||
"cipher",
|
"cipher",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bollard"
|
|
||||||
version = "0.19.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "af706e9dc793491dd382c99c22fde6e9934433d4cc0d6a4b34eb2cdc57a5c917"
|
|
||||||
dependencies = [
|
|
||||||
"base64 0.22.1",
|
|
||||||
"bollard-stubs",
|
|
||||||
"bytes",
|
|
||||||
"futures-core",
|
|
||||||
"futures-util",
|
|
||||||
"hex",
|
|
||||||
"http 1.3.1",
|
|
||||||
"http-body-util",
|
|
||||||
"hyper 1.6.0",
|
|
||||||
"hyper-named-pipe",
|
|
||||||
"hyper-util",
|
|
||||||
"hyperlocal",
|
|
||||||
"log",
|
|
||||||
"pin-project-lite",
|
|
||||||
"serde",
|
|
||||||
"serde_derive",
|
|
||||||
"serde_json",
|
|
||||||
"serde_repr",
|
|
||||||
"serde_urlencoded",
|
|
||||||
"thiserror 2.0.12",
|
|
||||||
"tokio",
|
|
||||||
"tokio-util",
|
|
||||||
"tower-service",
|
|
||||||
"url",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bollard-stubs"
|
|
||||||
version = "1.48.2-rc.28.0.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "79cdf0fccd5341b38ae0be74b74410bdd5eceeea8876dc149a13edfe57e3b259"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"serde_repr",
|
|
||||||
"serde_with",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bstr"
|
name = "bstr"
|
||||||
version = "1.12.0"
|
version = "1.12.0"
|
||||||
@@ -369,55 +324,6 @@ version = "1.10.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "camino"
|
|
||||||
version = "1.1.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cargo-platform"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "84982c6c0ae343635a3a4ee6dedef965513735c8b183caa7289fa6e27399ebd4"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cargo-util-schemas"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e63d2780ac94487eb9f1fea7b0d56300abc9eb488800854ca217f102f5caccca"
|
|
||||||
dependencies = [
|
|
||||||
"semver",
|
|
||||||
"serde",
|
|
||||||
"serde-untagged",
|
|
||||||
"serde-value",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
"toml",
|
|
||||||
"unicode-xid",
|
|
||||||
"url",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cargo_metadata"
|
|
||||||
version = "0.20.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4f7835cfc6135093070e95eb2b53e5d9b5c403dc3a6be6040ee026270aa82502"
|
|
||||||
dependencies = [
|
|
||||||
"camino",
|
|
||||||
"cargo-platform",
|
|
||||||
"cargo-util-schemas",
|
|
||||||
"semver",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror 2.0.12",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cassowary"
|
name = "cassowary"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@@ -488,9 +394,6 @@ name = "cidr"
|
|||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6bdf600c45bd958cf2945c445264471cca8b6c8e67bc87b71affd6d7e5682621"
|
checksum = "6bdf600c45bd958cf2945c445264471cca8b6c8e67bc87b71affd6d7e5682621"
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cipher"
|
name = "cipher"
|
||||||
@@ -763,12 +666,6 @@ dependencies = [
|
|||||||
"cipher",
|
"cipher",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "current_platform"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a74858bcfe44b22016cb49337d7b6f04618c58e5dbfdef61b06b8c434324a0bc"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "curve25519-dalek"
|
name = "curve25519-dalek"
|
||||||
version = "4.1.3"
|
version = "4.1.3"
|
||||||
@@ -855,7 +752,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1081,16 +977,6 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "erased-serde"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
"typeid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
@@ -1154,16 +1040,6 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "example-monitoring"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"harmony",
|
|
||||||
"harmony_cli",
|
|
||||||
"tokio",
|
|
||||||
"url",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "example-nanodc"
|
name = "example-nanodc"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -1194,21 +1070,6 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "example-tenant"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"cidr",
|
|
||||||
"env_logger",
|
|
||||||
"harmony",
|
|
||||||
"harmony_cli",
|
|
||||||
"harmony_macros",
|
|
||||||
"harmony_types",
|
|
||||||
"log",
|
|
||||||
"tokio",
|
|
||||||
"url",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "example-tui"
|
name = "example-tui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -1512,7 +1373,7 @@ dependencies = [
|
|||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"indexmap 2.9.0",
|
"indexmap",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -1531,7 +1392,7 @@ dependencies = [
|
|||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"indexmap 2.9.0",
|
"indexmap",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -1543,20 +1404,17 @@ name = "harmony"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
|
||||||
"cidr",
|
"cidr",
|
||||||
"convert_case",
|
"convert_case",
|
||||||
"derive-new",
|
"derive-new",
|
||||||
"directories",
|
"directories",
|
||||||
"dockerfile_builder",
|
"dockerfile_builder",
|
||||||
"dyn-clone",
|
|
||||||
"email_address",
|
"email_address",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"fqdn",
|
"fqdn",
|
||||||
"harmony_macros",
|
"harmony_macros",
|
||||||
"harmony_types",
|
"harmony_types",
|
||||||
"helm-wrapper-rs",
|
"helm-wrapper-rs",
|
||||||
"hex",
|
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"inquire",
|
"inquire",
|
||||||
"k3d-rs",
|
"k3d-rs",
|
||||||
@@ -1568,7 +1426,6 @@ dependencies = [
|
|||||||
"non-blank-string-rs",
|
"non-blank-string-rs",
|
||||||
"opnsense-config",
|
"opnsense-config",
|
||||||
"opnsense-config-xml",
|
"opnsense-config-xml",
|
||||||
"rand 0.9.1",
|
|
||||||
"reqwest 0.11.27",
|
"reqwest 0.11.27",
|
||||||
"russh",
|
"russh",
|
||||||
"rust-ipmi",
|
"rust-ipmi",
|
||||||
@@ -1597,26 +1454,10 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "harmony_composer"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"bollard",
|
|
||||||
"cargo_metadata",
|
|
||||||
"clap",
|
|
||||||
"current_platform",
|
|
||||||
"env_logger",
|
|
||||||
"futures-util",
|
|
||||||
"log",
|
|
||||||
"serde_json",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "harmony_macros"
|
name = "harmony_macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cidr",
|
|
||||||
"harmony_types",
|
"harmony_types",
|
||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1649,12 +1490,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.12.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.3"
|
version = "0.15.3"
|
||||||
@@ -1715,12 +1550,6 @@ version = "0.3.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hex"
|
|
||||||
version = "0.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex-literal"
|
name = "hex-literal"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -1911,21 +1740,6 @@ dependencies = [
|
|||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hyper-named-pipe"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278"
|
|
||||||
dependencies = [
|
|
||||||
"hex",
|
|
||||||
"hyper 1.6.0",
|
|
||||||
"hyper-util",
|
|
||||||
"pin-project-lite",
|
|
||||||
"tokio",
|
|
||||||
"tower-service",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-rustls"
|
name = "hyper-rustls"
|
||||||
version = "0.27.5"
|
version = "0.27.5"
|
||||||
@@ -2007,21 +1821,6 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hyperlocal"
|
|
||||||
version = "0.9.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7"
|
|
||||||
dependencies = [
|
|
||||||
"hex",
|
|
||||||
"http-body-util",
|
|
||||||
"hyper 1.6.0",
|
|
||||||
"hyper-util",
|
|
||||||
"pin-project-lite",
|
|
||||||
"tokio",
|
|
||||||
"tower-service",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.63"
|
version = "0.1.63"
|
||||||
@@ -2165,17 +1964,6 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indexmap"
|
|
||||||
version = "1.9.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"hashbrown 0.12.3",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
@@ -2183,8 +1971,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.15.3",
|
"hashbrown",
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2522,7 +2309,7 @@ version = "0.12.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.15.3",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3872,9 +3659,6 @@ name = "semver"
|
|||||||
version = "1.0.26"
|
version = "1.0.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
|
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
@@ -3885,17 +3669,6 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde-untagged"
|
|
||||||
version = "0.1.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "299d9c19d7d466db4ab10addd5703e4c615dec2a5a16dbbafe191045e87ee66e"
|
|
||||||
dependencies = [
|
|
||||||
"erased-serde",
|
|
||||||
"serde",
|
|
||||||
"typeid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde-value"
|
name = "serde-value"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -3939,26 +3712,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_repr"
|
|
||||||
version = "0.1.20"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_spanned"
|
|
||||||
version = "0.6.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_tokenstream"
|
name = "serde_tokenstream"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -3983,30 +3736,13 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_with"
|
|
||||||
version = "3.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
|
|
||||||
dependencies = [
|
|
||||||
"base64 0.22.1",
|
|
||||||
"chrono",
|
|
||||||
"hex",
|
|
||||||
"indexmap 1.9.3",
|
|
||||||
"indexmap 2.9.0",
|
|
||||||
"serde",
|
|
||||||
"serde_derive",
|
|
||||||
"serde_json",
|
|
||||||
"time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_yaml"
|
name = "serde_yaml"
|
||||||
version = "0.9.34+deprecated"
|
version = "0.9.34+deprecated"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.9.0",
|
"indexmap",
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -4550,47 +4286,6 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml"
|
|
||||||
version = "0.8.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
"serde_spanned",
|
|
||||||
"toml_datetime",
|
|
||||||
"toml_edit",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml_datetime"
|
|
||||||
version = "0.6.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml_edit"
|
|
||||||
version = "0.22.27"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
|
||||||
dependencies = [
|
|
||||||
"indexmap 2.9.0",
|
|
||||||
"serde",
|
|
||||||
"serde_spanned",
|
|
||||||
"toml_datetime",
|
|
||||||
"toml_write",
|
|
||||||
"winnow",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml_write"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -4716,12 +4411,6 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typeid"
|
|
||||||
version = "1.0.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.18.0"
|
version = "1.18.0"
|
||||||
@@ -4769,12 +4458,6 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-xid"
|
|
||||||
version = "0.2.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "universal-hash"
|
name = "universal-hash"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -5312,15 +4995,6 @@ version = "0.53.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winnow"
|
|
||||||
version = "0.7.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winreg"
|
name = "winreg"
|
||||||
version = "0.50.0"
|
version = "0.50.0"
|
||||||
|
|||||||
13
Cargo.toml
13
Cargo.toml
@@ -11,7 +11,6 @@ members = [
|
|||||||
"opnsense-config-xml",
|
"opnsense-config-xml",
|
||||||
"harmony_cli",
|
"harmony_cli",
|
||||||
"k3d",
|
"k3d",
|
||||||
"harmony_composer",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
@@ -24,13 +23,8 @@ log = "0.4.22"
|
|||||||
env_logger = "0.11.5"
|
env_logger = "0.11.5"
|
||||||
derive-new = "0.7.0"
|
derive-new = "0.7.0"
|
||||||
async-trait = "0.1.82"
|
async-trait = "0.1.82"
|
||||||
tokio = { version = "1.40.0", features = [
|
tokio = { version = "1.40.0", features = ["io-std", "fs", "macros", "rt-multi-thread"] }
|
||||||
"io-std",
|
cidr = "0.2.3"
|
||||||
"fs",
|
|
||||||
"macros",
|
|
||||||
"rt-multi-thread",
|
|
||||||
] }
|
|
||||||
cidr = { features = ["serde"], version = "0.2" }
|
|
||||||
russh = "0.45.0"
|
russh = "0.45.0"
|
||||||
russh-keys = "0.45.0"
|
russh-keys = "0.45.0"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
@@ -41,8 +35,7 @@ serde_yaml = "0.9.34"
|
|||||||
serde-value = "0.7.0"
|
serde-value = "0.7.0"
|
||||||
http = "1.2.0"
|
http = "1.2.0"
|
||||||
inquire = "0.7.5"
|
inquire = "0.7.5"
|
||||||
convert_case = "0.8.0"
|
convert_case = "0.8.0"
|
||||||
chrono = "0.4"
|
|
||||||
|
|
||||||
[workspace.dependencies.uuid]
|
[workspace.dependencies.uuid]
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
|
|||||||
25
Dockerfile
25
Dockerfile
@@ -1,25 +0,0 @@
|
|||||||
FROM docker.io/rust:1.87.0 AS build
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN cargo build --release --bin harmony_composer
|
|
||||||
|
|
||||||
FROM docker.io/rust:1.87.0
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN rustup target add x86_64-pc-windows-gnu
|
|
||||||
RUN rustup target add x86_64-unknown-linux-gnu
|
|
||||||
RUN rustup component add rustfmt
|
|
||||||
|
|
||||||
RUN apt update
|
|
||||||
|
|
||||||
# TODO: Consider adding more supported targets
|
|
||||||
# nodejs for checkout action, docker for building containers, mingw for cross-compiling for windows
|
|
||||||
RUN apt install -y nodejs docker.io mingw-w64
|
|
||||||
|
|
||||||
COPY --from=build /app/target/release/harmony_composer .
|
|
||||||
|
|
||||||
ENTRYPOINT ["/app/harmony_composer"]
|
|
||||||
252
README.md
252
README.md
@@ -1,151 +1,171 @@
|
|||||||
# Harmony : Open-source infrastructure orchestration that treats your platform like first-class code.
|
# Harmony : Open Infrastructure Orchestration
|
||||||
*By [NationTech](https://nationtech.io)*
|
|
||||||
|
|
||||||
[](https://git.nationtech.io/nationtech/harmony)
|
## Quick demo
|
||||||
[](LICENSE)
|
|
||||||
|
|
||||||
### Unify
|
`cargo run -p example-tui`
|
||||||
|
|
||||||
- **Project Scaffolding**
|
This will launch Harmony's minimalist terminal ui which embeds a few demo scores.
|
||||||
- **Infrastructure Provisioning**
|
|
||||||
- **Application Deployment**
|
|
||||||
- **Day-2 operations**
|
|
||||||
|
|
||||||
All in **one strongly-typed Rust codebase**.
|
Usage instructions will be displayed at the bottom of the TUI.
|
||||||
|
|
||||||
### Deploy anywhere
|
`cargo run --bin example-cli -- --help`
|
||||||
|
|
||||||
From a **developer laptop** to a **global production cluster**, a single **source of truth** drives the **full software lifecycle.**
|
This is the harmony CLI, a minimal implementation
|
||||||
|
|
||||||
---
|
The current help text:
|
||||||
|
|
||||||
## 1 · The Harmony Philosophy
|
````
|
||||||
|
Usage: example-cli [OPTIONS]
|
||||||
|
|
||||||
Infrastructure is essential, but it shouldn’t be your core business. Harmony is built on three guiding principles that make modern platforms reliable, repeatable, and easy to reason about.
|
Options:
|
||||||
|
-y, --yes Run score(s) or not
|
||||||
|
-f, --filter <FILTER> Filter query
|
||||||
|
-i, --interactive Run interactive TUI or not
|
||||||
|
-a, --all Run all or nth, defaults to all
|
||||||
|
-n, --number <NUMBER> Run nth matching, zero indexed [default: 0]
|
||||||
|
-l, --list list scores, will also be affected by run filter
|
||||||
|
-h, --help Print help
|
||||||
|
-V, --version Print version```
|
||||||
|
|
||||||
| Principle | What it means for you |
|
## Core architecture
|
||||||
|-----------|-----------------------|
|
|
||||||
| **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 application’s needs match the target environment’s 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. |
|
|
||||||
|
|
||||||
These principles surface as simple, ergonomic Rust APIs that let teams focus on their product while trusting the platform underneath.
|

|
||||||
|
````
|
||||||
|
## Supporting a new field in OPNSense `config.xml`
|
||||||
|
|
||||||
---
|
Two steps:
|
||||||
|
- Supporting the field in `opnsense-config-xml`
|
||||||
|
- Enabling Harmony to control the field
|
||||||
|
|
||||||
## 2 · Quick Start
|
We'll use the `filename` field in the `dhcpcd` section of the file as an example.
|
||||||
|
|
||||||
The snippet below spins up a complete **production-grade LAMP stack** with monitoring. Swap it for your own scores to deploy anything from microservices to machine-learning pipelines.
|
### Supporting the field
|
||||||
|
|
||||||
```rust
|
As type checking if enforced, every field from `config.xml` must be known by the code. Each subsection of `config.xml` has its `.rs` file. For the `dhcpcd` section, we'll modify `opnsense-config-xml/src/data/dhcpd.rs`.
|
||||||
use harmony::{
|
|
||||||
data::Version,
|
|
||||||
inventory::Inventory,
|
|
||||||
maestro::Maestro,
|
|
||||||
modules::{
|
|
||||||
lamp::{LAMPConfig, LAMPScore},
|
|
||||||
monitoring::monitoring_alerting::MonitoringAlertingStackScore,
|
|
||||||
},
|
|
||||||
topology::{K8sAnywhereTopology, Url},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::main]
|
When a new field appears in the xml file, an error like this will be thrown and Harmony will panic :
|
||||||
async fn main() {
|
```
|
||||||
// 1. Describe what you want
|
Running `/home/stremblay/nt/dir/harmony/target/debug/example-nanodc`
|
||||||
let lamp_stack = LAMPScore {
|
Found unauthorized element filename
|
||||||
name: "harmony-lamp-demo".into(),
|
thread 'main' panicked at opnsense-config-xml/src/data/opnsense.rs:54:14:
|
||||||
domain: Url::Url(url::Url::parse("https://lampdemo.example.com").unwrap()),
|
OPNSense received invalid string, should be full XML: ()
|
||||||
php_version: Version::from("8.3.0").unwrap(),
|
|
||||||
config: LAMPConfig {
|
|
||||||
project_root: "./php".into(),
|
|
||||||
database_size: "4Gi".into(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
monitoring.namespace = Some(lamp_stack.config.namespace.clone());
|
|
||||||
|
|
||||||
maestro.register_all(vec![Box::new(lamp_stack), Box::new(monitoring)]);
|
|
||||||
|
|
||||||
// 4. Launch an interactive CLI / TUI
|
|
||||||
harmony_cli::init(maestro, None).await.unwrap();
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Run it:
|
Define the missing field (`filename`) in the `DhcpInterface` struct of `opnsense-config-xml/src/data/dhcpd.rs`:
|
||||||
|
```
|
||||||
```bash
|
pub struct DhcpInterface {
|
||||||
cargo run
|
...
|
||||||
|
pub filename: Option<String>,
|
||||||
```
|
```
|
||||||
|
|
||||||
Harmony analyses the code, shows an execution plan in a TUI, and applies it once you confirm. Same code, same binary—every environment.
|
Harmony should now be fixed, build and run.
|
||||||
|
|
||||||
---
|
### Controlling the field
|
||||||
|
|
||||||
## 3 · Core Concepts
|
Define the `xml field setter` in `opnsense-config/src/modules/dhcpd.rs`.
|
||||||
|
```
|
||||||
| Term | One-liner |
|
impl<'a> DhcpConfig<'a> {
|
||||||
|------|-----------|
|
...
|
||||||
| **Score<T>** | Declarative description of the desired state (e.g., `LAMPScore`). |
|
pub fn set_filename(&mut self, filename: &str) {
|
||||||
| **Interpret<T>** | Imperative logic that realises a `Score` on a specific environment. |
|
self.enable_netboot();
|
||||||
| **Topology** | An environment (local k3d, AWS, bare-metal) exposing verified *Capabilities* (Kubernetes, DNS, …). |
|
self.get_lan_dhcpd().filename = Some(filename.to_string());
|
||||||
| **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. |
|
...
|
||||||
|
|
||||||
A visual overview is in the diagram below.
|
|
||||||
|
|
||||||
[Harmony Core Architecture](docs/diagrams/Harmony_Core_Architecture.drawio.svg)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4 · Install
|
|
||||||
|
|
||||||
Prerequisites:
|
|
||||||
|
|
||||||
* Rust
|
|
||||||
* Docker (if you deploy locally)
|
|
||||||
* `kubectl` / `helm` for Kubernetes-based topologies
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://git.nationtech.io/nationtech/harmony
|
|
||||||
cd harmony
|
|
||||||
cargo build --release # builds the CLI, TUI and libraries
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
Define the `value setter` in the `DhcpServer trait` in `domain/topology/network.rs`
|
||||||
|
```
|
||||||
|
#[async_trait]
|
||||||
|
pub trait DhcpServer: Send + Sync {
|
||||||
|
...
|
||||||
|
async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError>;
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
## 5 · Learning More
|
Implement the `value setter` in each `DhcpServer` implementation.
|
||||||
|
`infra/opnsense/dhcp.rs`:
|
||||||
|
```
|
||||||
|
#[async_trait]
|
||||||
|
impl DhcpServer for OPNSenseFirewall {
|
||||||
|
...
|
||||||
|
async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError> {
|
||||||
|
{
|
||||||
|
let mut writable_opnsense = self.opnsense_config.write().await;
|
||||||
|
writable_opnsense.dhcp().set_filename(filename);
|
||||||
|
debug!("OPNsense dhcp server set filename {filename}");
|
||||||
|
}
|
||||||
|
|
||||||
* **Architectural Decision Records** – dive into the rationale
|
Ok(())
|
||||||
- [ADR-001 · Why Rust](adr/001-rust.md)
|
}
|
||||||
- [ADR-003 · Infrastructure Abstractions](adr/003-infrastructure-abstractions.md)
|
...
|
||||||
- [ADR-006 · Secret Management](adr/006-secret-management.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`).
|
`domain/topology/ha_cluster.rs`
|
||||||
|
```
|
||||||
|
#[async_trait]
|
||||||
|
impl DhcpServer for DummyInfra {
|
||||||
|
...
|
||||||
|
async fn set_filename(&self, _filename: &str) -> Result<(), ExecutorError> {
|
||||||
|
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
||||||
|
}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
* **Community** – discussions and roadmap live in [GitLab issues](https://git.nationtech.io/nationtech/harmony/-/issues). PRs, ideas, and feedback are welcome!
|
Add the new field to the DhcpScore in `modules/dhcp.rs`
|
||||||
|
```
|
||||||
|
pub struct DhcpScore {
|
||||||
|
...
|
||||||
|
pub filename: Option<String>,
|
||||||
|
```
|
||||||
|
|
||||||
---
|
Define it in its implementation in `modules/okd/dhcp.rs`
|
||||||
|
```
|
||||||
|
impl OKDDhcpScore {
|
||||||
|
...
|
||||||
|
Self {
|
||||||
|
dhcp_score: DhcpScore {
|
||||||
|
...
|
||||||
|
filename: Some("undionly.kpxe".to_string()),
|
||||||
|
```
|
||||||
|
|
||||||
## 6 · License
|
Define it in its implementation in `modules/okd/bootstrap_dhcp.rs`
|
||||||
|
```
|
||||||
|
impl OKDDhcpScore {
|
||||||
|
...
|
||||||
|
Self {
|
||||||
|
dhcp_score: DhcpScore::new(
|
||||||
|
...
|
||||||
|
Some("undionly.kpxe".to_string()),
|
||||||
|
```
|
||||||
|
|
||||||
Harmony is released under the **GNU AGPL v3**.
|
Update the interpret (function called by the `execute` fn of the interpret) so it now updates the `filename` field value in `modules/dhcp.rs`
|
||||||
|
```
|
||||||
|
impl DhcpInterpret {
|
||||||
|
...
|
||||||
|
let filename_outcome = match &self.score.filename {
|
||||||
|
Some(filename) => {
|
||||||
|
let dhcp_server = Arc::new(topology.dhcp_server.clone());
|
||||||
|
dhcp_server.set_filename(&filename).await?;
|
||||||
|
Outcome::new(
|
||||||
|
InterpretStatus::SUCCESS,
|
||||||
|
format!("Dhcp Interpret Set filename to {filename}"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
None => Outcome::noop(),
|
||||||
|
};
|
||||||
|
|
||||||
> We choose a strong copyleft license to ensure the project—and every improvement to it—remains open and benefits the entire community. Fork it, enhance it, even out-innovate us; just keep it open.
|
if next_server_outcome.status == InterpretStatus::NOOP
|
||||||
|
&& boot_filename_outcome.status == InterpretStatus::NOOP
|
||||||
|
&& filename_outcome.status == InterpretStatus::NOOP
|
||||||
|
|
||||||
See [LICENSE](LICENSE) for the full text.
|
...
|
||||||
|
|
||||||
---
|
Ok(Outcome::new(
|
||||||
|
InterpretStatus::SUCCESS,
|
||||||
*Made with ❤️ & 🦀 by the NationTech and the Harmony community*
|
format!(
|
||||||
|
"Dhcp Interpret Set next boot to [{:?}], boot_filename to [{:?}], filename to [{:?}]",
|
||||||
|
self.score.boot_filename, self.score.boot_filename, self.score.filename
|
||||||
|
)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
pub trait MonitoringSystem {}
|
|
||||||
|
|
||||||
// 1. Modified AlertReceiver trait:
|
|
||||||
// - Removed the problematic `clone` method.
|
|
||||||
// - Added `box_clone` which returns a Box<dyn AlertReceiver>.
|
|
||||||
pub trait AlertReceiver {
|
|
||||||
type M: MonitoringSystem;
|
|
||||||
fn install(&self, sender: &Self::M) -> Result<(), String>;
|
|
||||||
// This method allows concrete types to clone themselves into a Box<dyn AlertReceiver>
|
|
||||||
fn box_clone(&self) -> Box<dyn AlertReceiver<M = Self::M>>;
|
|
||||||
}
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct Prometheus{}
|
|
||||||
impl MonitoringSystem for Prometheus {}
|
|
||||||
|
|
||||||
#[derive(Clone)] // Keep derive(Clone) for DiscordWebhook itself
|
|
||||||
struct DiscordWebhook{}
|
|
||||||
|
|
||||||
impl AlertReceiver for DiscordWebhook {
|
|
||||||
type M = Prometheus;
|
|
||||||
fn install(&self, sender: &Self::M) -> Result<(), String> {
|
|
||||||
// Placeholder for actual installation logic
|
|
||||||
println!("DiscordWebhook installed for Prometheus monitoring.");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
// 2. Implement `box_clone` for DiscordWebhook:
|
|
||||||
// This uses the derived `Clone` for DiscordWebhook to create a new boxed instance.
|
|
||||||
fn box_clone(&self) -> Box<dyn AlertReceiver<M = Self::M>> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Implement `std::clone::Clone` for `Box<dyn AlertReceiver<M= M>>`:
|
|
||||||
// This allows `Box<dyn AlertReceiver>` to be cloned.
|
|
||||||
// The `+ 'static` lifetime bound is often necessary for trait objects stored in collections,
|
|
||||||
// ensuring they live long enough.
|
|
||||||
impl<M: MonitoringSystem + 'static> Clone for Box<dyn AlertReceiver<M= M>> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
self.box_clone() // Call the custom `box_clone` method
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MonitoringConfig can now derive Clone because its `receivers` field
|
|
||||||
// (Vec<Box<dyn AlertReceiver<M = M>>>) is now cloneable.
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct MonitoringConfig <M: MonitoringSystem + 'static>{
|
|
||||||
receivers: Vec<Box<dyn AlertReceiver<M = M>>>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example usage to demonstrate compilation and functionality
|
|
||||||
fn main() {
|
|
||||||
let prometheus_instance = Prometheus{};
|
|
||||||
let discord_webhook_instance = DiscordWebhook{};
|
|
||||||
|
|
||||||
let mut config = MonitoringConfig {
|
|
||||||
receivers: Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a boxed alert receiver
|
|
||||||
let boxed_receiver: Box<dyn AlertReceiver<M = Prometheus>> = Box::new(discord_webhook_instance);
|
|
||||||
config.receivers.push(boxed_receiver);
|
|
||||||
|
|
||||||
// Clone the config, which will now correctly clone the boxed receiver
|
|
||||||
let cloned_config = config.clone();
|
|
||||||
|
|
||||||
println!("Original config has {} receivers.", config.receivers.len());
|
|
||||||
println!("Cloned config has {} receivers.", cloned_config.receivers.len());
|
|
||||||
|
|
||||||
// Example of using the installed receiver
|
|
||||||
if let Some(receiver) = config.receivers.get(0) {
|
|
||||||
let _ = receiver.install(&prometheus_instance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -137,9 +137,8 @@ Our approach addresses both customer and team multi-tenancy requirements:
|
|||||||
### Implementation Roadmap
|
### Implementation Roadmap
|
||||||
1. **Phase 1**: Implement VPN access and manual tenant provisioning
|
1. **Phase 1**: Implement VPN access and manual tenant provisioning
|
||||||
2. **Phase 2**: Deploy TenantScore automation for namespace, RBAC, and NetworkPolicy management
|
2. **Phase 2**: Deploy TenantScore automation for namespace, RBAC, and NetworkPolicy management
|
||||||
4. **Phase 3**: Work on privilege escalation from pods, audit for weaknesses, enforce security policies on pod runtimes
|
3. **Phase 3**: Integrate Keycloak for centralized identity management
|
||||||
3. **Phase 4**: Integrate Keycloak for centralized identity management
|
4. **Phase 4**: Add advanced monitoring and per-tenant observability
|
||||||
4. **Phase 5**: Add advanced monitoring and per-tenant observability
|
|
||||||
|
|
||||||
### TenantScore Structure Preview
|
### TenantScore Structure Preview
|
||||||
```rust
|
```rust
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: NetworkPolicy
|
|
||||||
metadata:
|
|
||||||
name: tenant-isolation-policy
|
|
||||||
namespace: testtenant
|
|
||||||
spec:
|
|
||||||
podSelector: {} # Selects all pods in the namespace
|
|
||||||
policyTypes:
|
|
||||||
- Ingress
|
|
||||||
- Egress
|
|
||||||
ingress:
|
|
||||||
- from:
|
|
||||||
- podSelector: {} # Allow from all pods in the same namespace
|
|
||||||
egress:
|
|
||||||
- to:
|
|
||||||
- podSelector: {} # Allow to all pods in the same namespace
|
|
||||||
- to:
|
|
||||||
- podSelector: {}
|
|
||||||
namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: openshift-dns # Target the openshift-dns namespace
|
|
||||||
# Note, only opening port 53 is not enough, will have to dig deeper into this one eventually
|
|
||||||
# ports:
|
|
||||||
# - protocol: UDP
|
|
||||||
# port: 53
|
|
||||||
# - protocol: TCP
|
|
||||||
# port: 53
|
|
||||||
# Allow egress to public internet only
|
|
||||||
- to:
|
|
||||||
- ipBlock:
|
|
||||||
cidr: 0.0.0.0/0
|
|
||||||
except:
|
|
||||||
- 10.0.0.0/8 # RFC1918
|
|
||||||
- 172.16.0.0/12 # RFC1918
|
|
||||||
- 192.168.0.0/16 # RFC1918
|
|
||||||
- 169.254.0.0/16 # Link-local
|
|
||||||
- 127.0.0.0/8 # Loopback
|
|
||||||
- 224.0.0.0/4 # Multicast
|
|
||||||
- 240.0.0.0/4 # Reserved
|
|
||||||
- 100.64.0.0/10 # Carrier-grade NAT
|
|
||||||
- 0.0.0.0/8 # Reserved
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: testtenant
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: testtenant2
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: test-web
|
|
||||||
namespace: testtenant
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: test-web
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: test-web
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: nginx
|
|
||||||
image: nginxinc/nginx-unprivileged
|
|
||||||
ports:
|
|
||||||
- containerPort: 80
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: test-web
|
|
||||||
namespace: testtenant
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: test-web
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
targetPort: 8080
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: test-client
|
|
||||||
namespace: testtenant
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: test-client
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: test-client
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: curl
|
|
||||||
image: curlimages/curl:latest
|
|
||||||
command: ["/bin/sh", "-c", "sleep 3600"]
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: test-web
|
|
||||||
namespace: testtenant2
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: test-web
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: test-web
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: nginx
|
|
||||||
image: nginxinc/nginx-unprivileged
|
|
||||||
ports:
|
|
||||||
- containerPort: 80
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: test-web
|
|
||||||
namespace: testtenant2
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: test-web
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
targetPort: 8080
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# Architecture Decision Record: \<Title\>
|
|
||||||
|
|
||||||
Initial Author: Jean-Gabriel Gill-Couture
|
|
||||||
|
|
||||||
Initial Date: 2025-06-04
|
|
||||||
|
|
||||||
Last Updated Date: 2025-06-04
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Proposed
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
As Harmony's goal is to make software delivery easier, we must provide an easy way for developers to express their app's semantics and dependencies with great abstractions, in a similar fashion to what the score.dev project is doing.
|
|
||||||
|
|
||||||
Thus, we started working on ways to package common types of applications such as LAMP, which we started working on with `LAMPScore`.
|
|
||||||
|
|
||||||
Now is time for the next step : we want to pave the way towards complete lifecycle automation. To do this, we will start with a way to execute Harmony's modules easily from anywhere, starting with locally and in CI environments.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
To achieve easy, portable execution of Harmony, we will follow this architecture :
|
|
||||||
|
|
||||||
- Host a basic harmony release that is compiled with the CLI by our gitea/github server
|
|
||||||
- This binary will do the following : check if there is a `harmony` folder in the current path
|
|
||||||
- If yes
|
|
||||||
- Check if cargo is available locally and compile the harmony binary, or compile the harmony binary using a rust docker container, if neither cargo or a container runtime is available, output a message explaining the situation
|
|
||||||
- Run the newly compiled binary. (Ideally using pid handoff like exec does but some research around this should be done. I think handing off the process is to help with OS interaction such as terminal apps, signals, exit codes, process handling, etc but there might be some side effects)
|
|
||||||
- If not
|
|
||||||
- Suggest initializing a project by auto detecting what the project looks like
|
|
||||||
- When the project type cannot be auto detected, provide links to Harmony's documentation on how to set up a project, a link to the examples folder, and a ask the user if he wants to initialize an empty Harmony project in the current folder
|
|
||||||
- harmony/Cargo.toml with dependencies set
|
|
||||||
- harmony/src/main.rs with an example LAMPScore setup and ready to run
|
|
||||||
- This same binary can be used in a CI environment to run the target project's Harmony module. By default, we provide these opinionated steps :
|
|
||||||
1. **An empty check step.** The purpose of this step is to run all tests and checks against the codebase. For complex projects this could involve a very complex pipeline of test environments setup and execution but this is out of scope for now. This is not handled by harmony. For projects with automatic setup, we can fill this step with something like `cargo fmt --check; cargo test; cargo build` but Harmony is not directly involved in the execution of this step.
|
|
||||||
2. **Package and publish.** Once all checks have passed, the production ready container is built and pushed to a registry. This is done by Harmony.
|
|
||||||
3. **Deploy to staging automatically.**
|
|
||||||
4. **Run a sanity check on staging.** As Harmony is responsible for deploying, Harmony should have all the knowledge of how to perform a sanity check on the staging environment. This will, most of the time, be a simple verification of the kubernetes health of all deployed components, and a poke on the public endpoint when there is one.
|
|
||||||
5. **Deploy to production automatically.** Many projects will require manual approval here, this can be easily set up in the CI afterwards, but our opinion is that
|
|
||||||
6. **Run a sanity check on production.** Same check as staging, but on production.
|
|
||||||
|
|
||||||
*Note on providing a base pipeline :* Having a complete pipeline set up automatically will encourage development teams to build upon these by adding tests where they belong. The goal here is to provide an opiniated solution that works for most small and large projects. Of course, many orgnizations will need to add steps such as deploying to sandbox environments, requiring more advanced approvals, more complex publication and coordination with other projects. But this here encompasses the basics required to build and deploy software reliably at any scale.
|
|
||||||
|
|
||||||
### Environment setup
|
|
||||||
|
|
||||||
TBD : For now, environments (tenants) will be set up and configured manually. Harmony will rely on the kubeconfig provided in the environment where it is running to deploy in the namespace.
|
|
||||||
|
|
||||||
For the CD tool such as Argo or Flux they will be activated by default by Harmony when using application level Scores such as LAMPScore in a similar way that the container is automatically built. Then, CI deployment steps will be notifying the CD tool using its API of the new release to deploy.
|
|
||||||
|
|
||||||
## Rationale
|
|
||||||
|
|
||||||
Reasoning behind the decision
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
Pros/Cons of chosen solution
|
|
||||||
|
|
||||||
## Alternatives considered
|
|
||||||
|
|
||||||
Pros/Cons of various proposed solutions considered
|
|
||||||
|
|
||||||
## Additional Notes
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Not much here yet, see the `adr` folder for now. More to come in time!
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
## Conceptual metaphor : The Cyborg and the Central Nervous System
|
|
||||||
|
|
||||||
At the heart of Harmony lies a core belief: in modern, decentralized systems, **software and infrastructure are not separate entities.** They are a single, symbiotic organism—a cyborg.
|
|
||||||
|
|
||||||
The software is the electronics, the "mind"; the infrastructure is the biological host, the "body". They live or die, thrive or sink together.
|
|
||||||
|
|
||||||
Traditional approaches attempt to manage this complex organism with fragmented tools: static YAML for configuration, brittle scripts for automation, and separate Infrastructure as Code (IaC) for provisioning. This creates a disjointed system that struggles to scale or heal itself, making it inadequate for the demands of fully automated, enterprise-grade clusters.
|
|
||||||
|
|
||||||
Harmony's goal is to provide the **central nervous system for this cyborg**. We aim to achieve the full automation of complex, decentralized clouds by managing this integrated entity holistically.
|
|
||||||
|
|
||||||
To achieve this, a tool must be both robust and powerful. It must manage the entire lifecycle—deployment, upgrades, failure recovery, and decommissioning—with precision. This requires full control over application packaging and a deep, intrinsic integration between the software and the infrastructure it inhabits.
|
|
||||||
|
|
||||||
This is why Harmony uses a powerful, living language like Rust. It replaces static, lifeless configuration files with a dynamic, breathing codebase. It allows us to express the complex relationships and behaviors of a modern distributed system, enabling the creation of truly automated, resilient, and powerful platforms that can thrive.
|
|
||||||
@@ -2,7 +2,10 @@ use harmony::{
|
|||||||
data::Version,
|
data::Version,
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
maestro::Maestro,
|
maestro::Maestro,
|
||||||
modules::lamp::{LAMPConfig, LAMPScore},
|
modules::{
|
||||||
|
lamp::{LAMPConfig, LAMPScore},
|
||||||
|
monitoring::monitoring_alerting::{AlertChannel, MonitoringAlertingStackScore},
|
||||||
|
},
|
||||||
topology::{K8sAnywhereTopology, Url},
|
topology::{K8sAnywhereTopology, Url},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,28 +32,24 @@ async fn main() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
//let monitoring = MonitoringAlertingScore {
|
|
||||||
// alert_receivers: vec![Box::new(DiscordWebhook {
|
|
||||||
// url: Url::Url(url::Url::parse("https://discord.idonotexist.com").unwrap()),
|
|
||||||
// // TODO write url macro
|
|
||||||
// // url: url!("https://discord.idonotexist.com"),
|
|
||||||
// })],
|
|
||||||
// alert_rules: vec![],
|
|
||||||
// scrape_targets: vec![],
|
|
||||||
//};
|
|
||||||
|
|
||||||
// You can choose the type of Topology you want, we suggest starting with the
|
// You can choose the type of Topology you want, we suggest starting with the
|
||||||
// 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.
|
||||||
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
|
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
|
||||||
Inventory::autoload(),
|
Inventory::autoload(),
|
||||||
K8sAnywhereTopology::from_env(),
|
K8sAnywhereTopology::new(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
maestro.register_all(vec![Box::new(lamp_stack)]);
|
let url = url::Url::parse("https://discord.com/api/webhooks/dummy_channel/dummy_token")
|
||||||
|
.expect("invalid URL");
|
||||||
|
|
||||||
|
let mut monitoring_stack_score = MonitoringAlertingStackScore::new();
|
||||||
|
monitoring_stack_score.namespace = Some(lamp_stack.config.namespace.clone());
|
||||||
|
|
||||||
|
maestro.register_all(vec![Box::new(lamp_stack), Box::new(monitoring_stack_score)]);
|
||||||
// Here we bootstrap the CLI, this gives some nice features if you need them
|
// Here we bootstrap the CLI, this gives some nice features if you need them
|
||||||
harmony_cli::init(maestro, None).await.unwrap();
|
harmony_cli::init(maestro, None).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "example-monitoring"
|
|
||||||
edition = "2024"
|
|
||||||
version.workspace = true
|
|
||||||
readme.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
harmony = { version = "0.1.0", path = "../../harmony" }
|
|
||||||
harmony_cli = { version = "0.1.0", path = "../../harmony_cli" }
|
|
||||||
tokio.workspace = true
|
|
||||||
url.workspace = true
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
use harmony::{
|
|
||||||
inventory::Inventory,
|
|
||||||
maestro::Maestro,
|
|
||||||
modules::{
|
|
||||||
monitoring::{
|
|
||||||
alert_channel::discord_alert_channel::DiscordWebhook,
|
|
||||||
alert_rule::prometheus_alert_rule::AlertManagerRuleGroup,
|
|
||||||
kube_prometheus::helm_prometheus_alert_score::HelmPrometheusAlertingScore,
|
|
||||||
},
|
|
||||||
prometheus::alerts::{
|
|
||||||
infra::dell_server::{
|
|
||||||
alert_global_storage_status_critical, alert_global_storage_status_non_recoverable,
|
|
||||||
global_storage_status_degraded_non_critical,
|
|
||||||
},
|
|
||||||
k8s::pvc::high_pvc_fill_rate_over_two_days,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
topology::{K8sAnywhereTopology, Url},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let discord_receiver = DiscordWebhook {
|
|
||||||
name: "test-discord".to_string(),
|
|
||||||
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let high_pvc_fill_rate_over_two_days_alert = high_pvc_fill_rate_over_two_days();
|
|
||||||
let dell_system_storage_degraded = global_storage_status_degraded_non_critical();
|
|
||||||
let alert_global_storage_status_critical = alert_global_storage_status_critical();
|
|
||||||
let alert_global_storage_status_non_recoverable = alert_global_storage_status_non_recoverable();
|
|
||||||
|
|
||||||
let additional_rules =
|
|
||||||
AlertManagerRuleGroup::new("pvc-alerts", vec![high_pvc_fill_rate_over_two_days_alert]);
|
|
||||||
let additional_rules2 = AlertManagerRuleGroup::new(
|
|
||||||
"dell-server-alerts",
|
|
||||||
vec![
|
|
||||||
dell_system_storage_degraded,
|
|
||||||
alert_global_storage_status_critical,
|
|
||||||
alert_global_storage_status_non_recoverable,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
let alerting_score = HelmPrometheusAlertingScore {
|
|
||||||
receivers: vec![Box::new(discord_receiver)],
|
|
||||||
rules: vec![Box::new(additional_rules), Box::new(additional_rules2)],
|
|
||||||
};
|
|
||||||
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
|
|
||||||
Inventory::autoload(),
|
|
||||||
K8sAnywhereTopology::from_env(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
maestro.register_all(vec![Box::new(alerting_score)]);
|
|
||||||
harmony_cli::init(maestro, None).await.unwrap();
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "example-tenant"
|
|
||||||
edition = "2024"
|
|
||||||
version.workspace = true
|
|
||||||
readme.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
harmony = { path = "../../harmony" }
|
|
||||||
harmony_cli = { path = "../../harmony_cli" }
|
|
||||||
harmony_types = { path = "../../harmony_types" }
|
|
||||||
cidr = { workspace = true }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
harmony_macros = { path = "../../harmony_macros" }
|
|
||||||
log = { workspace = true }
|
|
||||||
env_logger = { workspace = true }
|
|
||||||
url = { workspace = true }
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
use harmony::{
|
|
||||||
data::Id,
|
|
||||||
inventory::Inventory,
|
|
||||||
maestro::Maestro,
|
|
||||||
modules::tenant::TenantScore,
|
|
||||||
topology::{K8sAnywhereTopology, tenant::TenantConfig},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let tenant = TenantScore {
|
|
||||||
config: TenantConfig {
|
|
||||||
id: Id::from_str("test-tenant-id"),
|
|
||||||
name: "testtenant".to_string(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
|
|
||||||
Inventory::autoload(),
|
|
||||||
K8sAnywhereTopology::from_env(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
maestro.register_all(vec![Box::new(tenant)]);
|
|
||||||
harmony_cli::init(maestro, None).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO write tests
|
|
||||||
// - Create Tenant with default config mostly, make sure namespace is created
|
|
||||||
// - deploy sample client/server app with nginx unprivileged and a service
|
|
||||||
// - exec in the client pod and validate the following
|
|
||||||
// - can reach internet
|
|
||||||
// - can reach server pod
|
|
||||||
// - can resolve dns queries to internet
|
|
||||||
// - can resolve dns queries to services
|
|
||||||
// - cannot reach services and pods in other namespaces
|
|
||||||
// - Create Tenant with specific cpu/ram/storage requests / limits and make sure they are enforced by trying to
|
|
||||||
// deploy a pod with lower requests/limits (accepted) and higher requests/limits (rejected)
|
|
||||||
// - Create TenantCredentials and make sure they give only access to the correct tenant
|
|
||||||
@@ -6,8 +6,6 @@ readme.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rand = "0.9"
|
|
||||||
hex = "0.4"
|
|
||||||
libredfish = "0.1.1"
|
libredfish = "0.1.1"
|
||||||
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
||||||
russh = "0.45.0"
|
russh = "0.45.0"
|
||||||
@@ -42,7 +40,6 @@ 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
|
|
||||||
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",
|
||||||
|
|||||||
@@ -1,23 +1,5 @@
|
|||||||
use rand::distr::Alphanumeric;
|
|
||||||
use rand::distr::SampleString;
|
|
||||||
use std::time::SystemTime;
|
|
||||||
use std::time::UNIX_EPOCH;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// A unique identifier designed for ease of use.
|
|
||||||
///
|
|
||||||
/// You can pass it any String to use and Id, or you can use the default format with `Id::default()`
|
|
||||||
///
|
|
||||||
/// The default format looks like this
|
|
||||||
///
|
|
||||||
/// `462d4c_g2COgai`
|
|
||||||
///
|
|
||||||
/// The first part is the unix timesamp in hexadecimal which makes Id easily sorted by creation time.
|
|
||||||
/// Second part is a serie of 7 random characters.
|
|
||||||
///
|
|
||||||
/// **It is not meant to be very secure or unique**, it is suitable to generate up to 10 000 items per
|
|
||||||
/// second with a reasonable collision rate of 0,000014 % as calculated by this calculator : https://kevingal.com/apps/collision.html
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Id {
|
pub struct Id {
|
||||||
value: String,
|
value: String,
|
||||||
@@ -27,31 +9,4 @@ impl Id {
|
|||||||
pub fn from_string(value: String) -> Self {
|
pub fn from_string(value: String) -> Self {
|
||||||
Self { value }
|
Self { value }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_str(value: &str) -> Self {
|
|
||||||
Self::from_string(value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Id {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(&self.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Id {
|
|
||||||
fn default() -> Self {
|
|
||||||
let start = SystemTime::now();
|
|
||||||
let since_the_epoch = start
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.expect("Time went backwards");
|
|
||||||
let timestamp = since_the_epoch.as_secs();
|
|
||||||
|
|
||||||
let hex_timestamp = format!("{:x}", timestamp & 0xffffff);
|
|
||||||
|
|
||||||
let random_part: String = Alphanumeric.sample_string(&mut rand::rng(), 7);
|
|
||||||
|
|
||||||
let value = format!("{}_{}", hex_timestamp, random_part);
|
|
||||||
Self { value }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ pub enum InterpretName {
|
|||||||
Panic,
|
Panic,
|
||||||
OPNSense,
|
OPNSense,
|
||||||
K3dInstallation,
|
K3dInstallation,
|
||||||
TenantInterpret,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for InterpretName {
|
impl std::fmt::Display for InterpretName {
|
||||||
@@ -36,7 +35,6 @@ impl std::fmt::Display for InterpretName {
|
|||||||
InterpretName::Panic => f.write_str("Panic"),
|
InterpretName::Panic => f.write_str("Panic"),
|
||||||
InterpretName::OPNSense => f.write_str("OPNSense"),
|
InterpretName::OPNSense => f.write_str("OPNSense"),
|
||||||
InterpretName::K3dInstallation => f.write_str("K3dInstallation"),
|
InterpretName::K3dInstallation => f.write_str("K3dInstallation"),
|
||||||
InterpretName::TenantInterpret => f.write_str("Tenant"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
use crate::{interpret::InterpretError, inventory::Inventory};
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait Installable<T>: Send + Sync {
|
|
||||||
async fn ensure_installed(
|
|
||||||
&self,
|
|
||||||
inventory: &Inventory,
|
|
||||||
topology: &T,
|
|
||||||
) -> Result<(), InterpretError>;
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
use k8s_openapi::{ClusterResourceScope, NamespaceResourceScope};
|
use k8s_openapi::NamespaceResourceScope;
|
||||||
use kube::{
|
use kube::{
|
||||||
Api, Client, Config, Error, Resource,
|
Api, Client, Config, Error, Resource,
|
||||||
api::{Patch, PatchParams},
|
api::PostParams,
|
||||||
config::{KubeConfigOptions, Kubeconfig},
|
config::{KubeConfigOptions, Kubeconfig},
|
||||||
};
|
};
|
||||||
use log::{debug, error, trace};
|
use log::error;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
#[derive(new)]
|
#[derive(new)]
|
||||||
@@ -20,50 +20,52 @@ impl K8sClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a resource in namespace
|
pub async fn apply_all<
|
||||||
///
|
K: Resource<Scope = NamespaceResourceScope>
|
||||||
/// See `kubectl apply` for more information on the expected behavior of this function
|
+ std::fmt::Debug
|
||||||
pub async fn apply<K>(&self, resource: &K, namespace: Option<&str>) -> Result<K, Error>
|
+ Sync
|
||||||
|
+ DeserializeOwned
|
||||||
|
+ Default
|
||||||
|
+ serde::Serialize
|
||||||
|
+ Clone,
|
||||||
|
>(
|
||||||
|
&self,
|
||||||
|
resource: &Vec<K>,
|
||||||
|
) -> Result<Vec<K>, kube::Error>
|
||||||
where
|
where
|
||||||
K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize,
|
|
||||||
<K as Resource>::Scope: ApplyStrategy<K>,
|
|
||||||
<K as kube::Resource>::DynamicType: Default,
|
<K as kube::Resource>::DynamicType: Default,
|
||||||
{
|
{
|
||||||
debug!(
|
let mut result = vec![];
|
||||||
"Applying resource {:?} with ns {:?}",
|
for r in resource.iter() {
|
||||||
resource.meta().name,
|
let api: Api<K> = Api::all(self.client.clone());
|
||||||
namespace
|
result.push(api.create(&PostParams::default(), &r).await?);
|
||||||
);
|
}
|
||||||
trace!(
|
Ok(result)
|
||||||
"{:#}",
|
|
||||||
serde_json::to_value(resource).unwrap_or(serde_json::Value::Null)
|
|
||||||
);
|
|
||||||
|
|
||||||
let api: Api<K> =
|
|
||||||
<<K as Resource>::Scope as ApplyStrategy<K>>::get_api(&self.client, namespace);
|
|
||||||
// api.create(&PostParams::default(), &resource).await
|
|
||||||
let patch_params = PatchParams::apply("harmony");
|
|
||||||
let name = resource
|
|
||||||
.meta()
|
|
||||||
.name
|
|
||||||
.as_ref()
|
|
||||||
.expect("K8s Resource should have a name");
|
|
||||||
api.patch(name, &patch_params, &Patch::Apply(resource))
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn apply_many<K>(&self, resource: &Vec<K>, ns: Option<&str>) -> Result<Vec<K>, Error>
|
pub async fn apply_namespaced<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<Scope = NamespaceResourceScope>
|
||||||
<K as Resource>::Scope: ApplyStrategy<K>,
|
+ Clone
|
||||||
|
+ std::fmt::Debug
|
||||||
|
+ DeserializeOwned
|
||||||
|
+ serde::Serialize
|
||||||
|
+ Default,
|
||||||
<K as kube::Resource>::DynamicType: Default,
|
<K as kube::Resource>::DynamicType: Default,
|
||||||
{
|
{
|
||||||
let mut result = Vec::new();
|
let mut resources = Vec::new();
|
||||||
for r in resource.iter() {
|
for r in resource.iter() {
|
||||||
result.push(self.apply(r, ns).await?);
|
let api: Api<K> = match ns {
|
||||||
|
Some(ns) => Api::namespaced(self.client.clone(), ns),
|
||||||
|
None => Api::default_namespaced(self.client.clone()),
|
||||||
|
};
|
||||||
|
resources.push(api.create(&PostParams::default(), &r).await?);
|
||||||
}
|
}
|
||||||
|
Ok(resources)
|
||||||
Ok(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn from_kubeconfig(path: &str) -> Option<K8sClient> {
|
pub(crate) async fn from_kubeconfig(path: &str) -> Option<K8sClient> {
|
||||||
@@ -84,35 +86,3 @@ impl K8sClient {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ApplyStrategy<K: Resource> {
|
|
||||||
fn get_api(client: &Client, ns: Option<&str>) -> Api<K>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implementation for all resources that are cluster-scoped.
|
|
||||||
/// It will always use `Api::all` and ignore the namespace parameter.
|
|
||||||
impl<K> ApplyStrategy<K> for ClusterResourceScope
|
|
||||||
where
|
|
||||||
K: Resource<Scope = ClusterResourceScope>,
|
|
||||||
<K as kube::Resource>::DynamicType: Default,
|
|
||||||
{
|
|
||||||
fn get_api(client: &Client, _ns: Option<&str>) -> Api<K> {
|
|
||||||
Api::all(client.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implementation for all resources that are namespace-scoped.
|
|
||||||
/// It will use `Api::namespaced` if a namespace is provided, otherwise
|
|
||||||
/// it falls back to the default namespace configured in your kubeconfig.
|
|
||||||
impl<K> ApplyStrategy<K> for NamespaceResourceScope
|
|
||||||
where
|
|
||||||
K: Resource<Scope = NamespaceResourceScope>,
|
|
||||||
<K as kube::Resource>::DynamicType: Default,
|
|
||||||
{
|
|
||||||
fn get_api(client: &Client, ns: Option<&str>) -> Api<K> {
|
|
||||||
match ns {
|
|
||||||
Some(ns) => Api::namespaced(client.clone(), ns),
|
|
||||||
None => Api::default_namespaced(client.clone()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use std::{process::Command, sync::Arc};
|
use std::{collections::HashMap, process::Command, sync::Arc};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use inquire::Confirm;
|
use inquire::Confirm;
|
||||||
use log::{debug, info, warn};
|
use log::{info, warn};
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::{Mutex, OnceCell};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
executors::ExecutorError,
|
executors::ExecutorError,
|
||||||
@@ -17,12 +17,15 @@ use crate::{
|
|||||||
use super::{
|
use super::{
|
||||||
HelmCommand, K8sclient, Topology,
|
HelmCommand, K8sclient, Topology,
|
||||||
k8s::K8sClient,
|
k8s::K8sClient,
|
||||||
tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager},
|
oberservability::monitoring::AlertReceiver,
|
||||||
|
tenant::{
|
||||||
|
ResourceLimits, TenantConfig, TenantManager, TenantNetworkPolicy, k8s::K8sTenantManager,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
struct K8sState {
|
struct K8sState {
|
||||||
client: Arc<K8sClient>,
|
client: Arc<K8sClient>,
|
||||||
_source: K8sSource,
|
source: K8sSource,
|
||||||
message: String,
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +38,7 @@ enum K8sSource {
|
|||||||
pub struct K8sAnywhereTopology {
|
pub struct K8sAnywhereTopology {
|
||||||
k8s_state: OnceCell<Option<K8sState>>,
|
k8s_state: OnceCell<Option<K8sState>>,
|
||||||
tenant_manager: OnceCell<K8sTenantManager>,
|
tenant_manager: OnceCell<K8sTenantManager>,
|
||||||
config: K8sAnywhereConfig,
|
pub alert_receivers: Mutex<HashMap<String, OnceCell<AlertReceiver>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -56,19 +59,11 @@ impl K8sclient for K8sAnywhereTopology {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl K8sAnywhereTopology {
|
impl K8sAnywhereTopology {
|
||||||
pub fn from_env() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
k8s_state: OnceCell::new(),
|
k8s_state: OnceCell::new(),
|
||||||
tenant_manager: OnceCell::new(),
|
tenant_manager: OnceCell::new(),
|
||||||
config: K8sAnywhereConfig::from_env(),
|
alert_receivers: Mutex::new(HashMap::new()),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_config(config: K8sAnywhereConfig) -> Self {
|
|
||||||
Self {
|
|
||||||
k8s_state: OnceCell::new(),
|
|
||||||
tenant_manager: OnceCell::new(),
|
|
||||||
config,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,15 +104,27 @@ impl K8sAnywhereTopology {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn try_get_or_install_k8s_client(&self) -> Result<Option<K8sState>, InterpretError> {
|
async fn try_get_or_install_k8s_client(&self) -> Result<Option<K8sState>, InterpretError> {
|
||||||
let k8s_anywhere_config = &self.config;
|
let k8s_anywhere_config = K8sAnywhereConfig {
|
||||||
|
kubeconfig: std::env::var("KUBECONFIG").ok().map(|v| v.to_string()),
|
||||||
|
use_system_kubeconfig: std::env::var("HARMONY_USE_SYSTEM_KUBECONFIG")
|
||||||
|
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
|
||||||
|
autoinstall: std::env::var("HARMONY_AUTOINSTALL")
|
||||||
|
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(kubeconfig) = &k8s_anywhere_config.kubeconfig {
|
if k8s_anywhere_config.use_system_kubeconfig {
|
||||||
debug!("Loading kubeconfig {kubeconfig}");
|
match self.try_load_system_kubeconfig().await {
|
||||||
|
Some(_client) => todo!(),
|
||||||
|
None => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(kubeconfig) = k8s_anywhere_config.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}"),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -129,14 +136,6 @@ impl K8sAnywhereTopology {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if k8s_anywhere_config.use_system_kubeconfig {
|
|
||||||
debug!("Loading system kubeconfig");
|
|
||||||
match self.try_load_system_kubeconfig().await {
|
|
||||||
Some(_client) => todo!(),
|
|
||||||
None => todo!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("No kubernetes configuration found");
|
info!("No kubernetes configuration found");
|
||||||
|
|
||||||
if !k8s_anywhere_config.autoinstall {
|
if !k8s_anywhere_config.autoinstall {
|
||||||
@@ -165,7 +164,7 @@ 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: "Successfully installed K3D cluster and acquired client".to_string(),
|
message: "Successfully installed K3D cluster and acquired client".to_string(),
|
||||||
},
|
},
|
||||||
Err(_) => todo!(),
|
Err(_) => todo!(),
|
||||||
@@ -174,22 +173,6 @@ impl K8sAnywhereTopology {
|
|||||||
Ok(Some(state))
|
Ok(Some(state))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ensure_k8s_tenant_manager(&self) -> Result<(), String> {
|
|
||||||
if let Some(_) = self.tenant_manager.get() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.tenant_manager
|
|
||||||
.get_or_try_init(async || -> Result<K8sTenantManager, String> {
|
|
||||||
let k8s_client = self.k8s_client().await?;
|
|
||||||
Ok(K8sTenantManager::new(k8s_client))
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_k8s_tenant_manager(&self) -> Result<&K8sTenantManager, ExecutorError> {
|
fn get_k8s_tenant_manager(&self) -> Result<&K8sTenantManager, ExecutorError> {
|
||||||
match self.tenant_manager.get() {
|
match self.tenant_manager.get() {
|
||||||
Some(t) => Ok(t),
|
Some(t) => Ok(t),
|
||||||
@@ -200,37 +183,25 @@ impl K8sAnywhereTopology {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct K8sAnywhereConfig {
|
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
|
||||||
///
|
///
|
||||||
/// Default : None
|
/// Default : None
|
||||||
pub kubeconfig: Option<String>,
|
kubeconfig: Option<String>,
|
||||||
|
|
||||||
/// Whether to use the system KUBECONFIG, either the environment variable or the file in the
|
/// Whether to use the system KUBECONFIG, either the environment variable or the file in the
|
||||||
/// default or configured location
|
/// default or configured location
|
||||||
///
|
///
|
||||||
/// Default : false
|
/// Default : false
|
||||||
pub use_system_kubeconfig: bool,
|
use_system_kubeconfig: bool,
|
||||||
|
|
||||||
/// Whether to install automatically a kubernetes cluster
|
/// Whether to install automatically a kubernetes cluster
|
||||||
///
|
///
|
||||||
/// 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: true
|
||||||
pub autoinstall: bool,
|
autoinstall: bool,
|
||||||
}
|
|
||||||
|
|
||||||
impl K8sAnywhereConfig {
|
|
||||||
fn from_env() -> Self {
|
|
||||||
Self {
|
|
||||||
kubeconfig: std::env::var("KUBECONFIG").ok().map(|v| v.to_string()),
|
|
||||||
use_system_kubeconfig: std::env::var("HARMONY_USE_SYSTEM_KUBECONFIG")
|
|
||||||
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
|
|
||||||
autoinstall: std::env::var("HARMONY_AUTOINSTALL")
|
|
||||||
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -249,10 +220,6 @@ impl Topology for K8sAnywhereTopology {
|
|||||||
"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()
|
|
||||||
.await
|
|
||||||
.map_err(|e| InterpretError::new(e))?;
|
|
||||||
|
|
||||||
match self.is_helm_available() {
|
match self.is_helm_available() {
|
||||||
Ok(()) => Ok(Outcome::success(format!(
|
Ok(()) => Ok(Outcome::success(format!(
|
||||||
"{} + helm available",
|
"{} + helm available",
|
||||||
@@ -272,4 +239,30 @@ impl TenantManager for K8sAnywhereTopology {
|
|||||||
.provision_tenant(config)
|
.provision_tenant(config)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_tenant_resource_limits(
|
||||||
|
&self,
|
||||||
|
tenant_name: &str,
|
||||||
|
new_limits: &ResourceLimits,
|
||||||
|
) -> Result<(), ExecutorError> {
|
||||||
|
self.get_k8s_tenant_manager()?
|
||||||
|
.update_tenant_resource_limits(tenant_name, new_limits)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_tenant_network_policy(
|
||||||
|
&self,
|
||||||
|
tenant_name: &str,
|
||||||
|
new_policy: &TenantNetworkPolicy,
|
||||||
|
) -> Result<(), ExecutorError> {
|
||||||
|
self.get_k8s_tenant_manager()?
|
||||||
|
.update_tenant_network_policy(tenant_name, new_policy)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn deprovision_tenant(&self, tenant_name: &str) -> Result<(), ExecutorError> {
|
||||||
|
self.get_k8s_tenant_manager()?
|
||||||
|
.deprovision_tenant(tenant_name)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
mod ha_cluster;
|
mod ha_cluster;
|
||||||
mod host_binding;
|
mod host_binding;
|
||||||
mod http;
|
mod http;
|
||||||
pub mod installable;
|
|
||||||
mod k8s_anywhere;
|
mod k8s_anywhere;
|
||||||
mod localhost;
|
mod localhost;
|
||||||
pub mod oberservability;
|
pub mod oberservability;
|
||||||
|
|||||||
@@ -1,76 +1,33 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::debug;
|
use dyn_clone::DynClone;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{
|
use std::fmt::Debug;
|
||||||
data::{Id, Version},
|
|
||||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
|
||||||
inventory::Inventory,
|
|
||||||
topology::{Topology, installable::Installable},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
use crate::interpret::InterpretError;
|
||||||
|
|
||||||
|
use crate::{interpret::Outcome, topology::Topology};
|
||||||
|
|
||||||
|
/// Represents an entity responsible for collecting and organizing observability data
|
||||||
|
/// from various telemetry sources
|
||||||
|
/// A `Monitor` abstracts the logic required to scrape, aggregate, and structure
|
||||||
|
/// monitoring data, enabling consistent processing regardless of the underlying data source.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait AlertSender: Send + Sync + std::fmt::Debug {
|
pub trait Monitor<T: Topology>: Debug + Send + Sync {
|
||||||
fn name(&self) -> String;
|
async fn deploy_monitor(&self, topology: &T) -> Result<Outcome, InterpretError>;
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
async fn delete_monitor(&self, topolgy: &T) -> Result<Outcome, InterpretError>;
|
||||||
pub struct AlertingInterpret<S: AlertSender> {
|
|
||||||
pub sender: S,
|
|
||||||
pub receivers: Vec<Box<dyn AlertReceiver<S>>>,
|
|
||||||
pub rules: Vec<Box<dyn AlertRule<S>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<S: AlertSender + Installable<T>, T: Topology> Interpret<T> for AlertingInterpret<S> {
|
pub trait AlertReceiverDeployment<T: Topology>: Debug + DynClone + Send + Sync {
|
||||||
async fn execute(
|
async fn deploy_alert_receiver(&self, topology: &T) -> Result<Outcome, InterpretError>;
|
||||||
&self,
|
|
||||||
inventory: &Inventory,
|
|
||||||
topology: &T,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
for receiver in self.receivers.iter() {
|
|
||||||
receiver.install(&self.sender).await?;
|
|
||||||
}
|
|
||||||
for rule in self.rules.iter() {
|
|
||||||
debug!("installing rule: {:#?}", rule);
|
|
||||||
rule.install(&self.sender).await?;
|
|
||||||
}
|
|
||||||
self.sender.ensure_installed(inventory, topology).await?;
|
|
||||||
Ok(Outcome::success(format!(
|
|
||||||
"successfully installed alert sender {}",
|
|
||||||
self.sender.name()
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_name(&self) -> InterpretName {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_version(&self) -> Version {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_status(&self) -> InterpretStatus {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_children(&self) -> Vec<Id> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
dyn_clone::clone_trait_object!(<T> AlertReceiverDeployment<T>);
|
||||||
pub trait AlertReceiver<S: AlertSender>: std::fmt::Debug + Send + Sync {
|
|
||||||
async fn install(&self, sender: &S) -> Result<Outcome, InterpretError>;
|
|
||||||
fn clone_box(&self) -> Box<dyn AlertReceiver<S>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub trait AlertRule<S: AlertSender>: std::fmt::Debug + Send + Sync {
|
pub struct AlertReceiver {
|
||||||
async fn install(&self, sender: &S) -> Result<Outcome, InterpretError>;
|
pub receiver_id: String,
|
||||||
fn clone_box(&self) -> Box<dyn AlertRule<S>>;
|
pub receiver_installed: bool,
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait ScrapeTarger<S: AlertSender> {
|
|
||||||
async fn install(&self, sender: &S) -> Result<(), InterpretError>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,327 +1,95 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{executors::ExecutorError, topology::k8s::K8sClient};
|
||||||
executors::ExecutorError,
|
|
||||||
topology::k8s::{ApplyStrategy, K8sClient},
|
|
||||||
};
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
use k8s_openapi::{
|
use k8s_openapi::api::core::v1::Namespace;
|
||||||
api::{
|
|
||||||
core::v1::{Namespace, ResourceQuota},
|
|
||||||
networking::v1::{
|
|
||||||
NetworkPolicy, NetworkPolicyEgressRule, NetworkPolicyIngressRule, NetworkPolicyPort,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
apimachinery::pkg::util::intstr::IntOrString,
|
|
||||||
};
|
|
||||||
use kube::Resource;
|
|
||||||
use log::{debug, info, warn};
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use super::{TenantConfig, TenantManager};
|
use super::{ResourceLimits, TenantConfig, TenantManager, TenantNetworkPolicy};
|
||||||
|
|
||||||
#[derive(new)]
|
#[derive(new)]
|
||||||
pub struct K8sTenantManager {
|
pub struct K8sTenantManager {
|
||||||
k8s_client: Arc<K8sClient>,
|
k8s_client: Arc<K8sClient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl K8sTenantManager {
|
#[async_trait]
|
||||||
fn get_namespace_name(&self, config: &TenantConfig) -> String {
|
impl TenantManager for K8sTenantManager {
|
||||||
config.name.clone()
|
async fn provision_tenant(&self, config: &TenantConfig) -> Result<(), ExecutorError> {
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_constraints(&self, _namespace: &Namespace) -> Result<(), ExecutorError> {
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn apply_resource<
|
|
||||||
K: Resource + std::fmt::Debug + Sync + DeserializeOwned + Default + serde::Serialize + Clone,
|
|
||||||
>(
|
|
||||||
&self,
|
|
||||||
mut resource: K,
|
|
||||||
config: &TenantConfig,
|
|
||||||
) -> Result<K, ExecutorError>
|
|
||||||
where
|
|
||||||
<K as kube::Resource>::DynamicType: Default,
|
|
||||||
<K as kube::Resource>::Scope: ApplyStrategy<K>,
|
|
||||||
{
|
|
||||||
self.apply_labels(&mut resource, config);
|
|
||||||
self.k8s_client
|
|
||||||
.apply(&resource, Some(&self.get_namespace_name(config)))
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
ExecutorError::UnexpectedError(format!("Could not create Tenant resource : {e}"))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_labels<K: Resource>(&self, resource: &mut K, config: &TenantConfig) {
|
|
||||||
let labels = resource.meta_mut().labels.get_or_insert_default();
|
|
||||||
labels.insert(
|
|
||||||
"app.kubernetes.io/managed-by".to_string(),
|
|
||||||
"harmony".to_string(),
|
|
||||||
);
|
|
||||||
labels.insert("harmony/tenant-id".to_string(), config.id.to_string());
|
|
||||||
labels.insert("harmony/tenant-name".to_string(), config.name.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_namespace(&self, config: &TenantConfig) -> Result<Namespace, ExecutorError> {
|
|
||||||
let namespace = json!(
|
let namespace = json!(
|
||||||
{
|
{
|
||||||
"apiVersion": "v1",
|
"apiVersion": "v1",
|
||||||
"kind": "Namespace",
|
"kind": "Namespace",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"labels": {
|
"labels": {
|
||||||
"harmony.nationtech.io/tenant.id": config.id.to_string(),
|
"harmony.nationtech.io/tenant.id": config.id,
|
||||||
"harmony.nationtech.io/tenant.name": config.name,
|
"harmony.nationtech.io/tenant.name": config.name,
|
||||||
},
|
},
|
||||||
"name": self.get_namespace_name(config),
|
"name": config.name,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
serde_json::from_value(namespace).map_err(|e| {
|
todo!("Validate that when tenant already exists (by id) that name has not changed");
|
||||||
ExecutorError::ConfigurationError(format!(
|
|
||||||
"Could not build TenantManager Namespace. {}",
|
let namespace: Namespace = serde_json::from_value(namespace).unwrap();
|
||||||
e
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_resource_quota(&self, config: &TenantConfig) -> Result<ResourceQuota, ExecutorError> {
|
|
||||||
let resource_quota = json!(
|
let resource_quota = json!(
|
||||||
{
|
{
|
||||||
"apiVersion": "v1",
|
"apiVersion": "v1",
|
||||||
"kind": "ResourceQuota",
|
"kind": "List",
|
||||||
"metadata": {
|
"items": [
|
||||||
"name": format!("{}-quota", config.name),
|
{
|
||||||
"labels": {
|
"apiVersion": "v1",
|
||||||
"harmony.nationtech.io/tenant.id": config.id.to_string(),
|
"kind": "ResourceQuota",
|
||||||
"harmony.nationtech.io/tenant.name": config.name,
|
"metadata": {
|
||||||
},
|
"name": config.name,
|
||||||
"namespace": self.get_namespace_name(config),
|
"labels": {
|
||||||
},
|
"harmony.nationtech.io/tenant.id": config.id,
|
||||||
"spec": {
|
"harmony.nationtech.io/tenant.name": config.name,
|
||||||
"hard": {
|
},
|
||||||
"limits.cpu": format!("{:.0}",config.resource_limits.cpu_limit_cores),
|
"namespace": config.name,
|
||||||
"limits.memory": format!("{:.3}Gi", config.resource_limits.memory_limit_gb),
|
},
|
||||||
"requests.cpu": format!("{:.0}",config.resource_limits.cpu_request_cores),
|
"spec": {
|
||||||
"requests.memory": format!("{:.3}Gi", config.resource_limits.memory_request_gb),
|
"hard": {
|
||||||
"requests.storage": format!("{:.3}Gi", config.resource_limits.storage_total_gb),
|
"limits.cpu": format!("{:.0}",config.resource_limits.cpu_limit_cores),
|
||||||
"pods": "20",
|
"limits.memory": format!("{:.3}Gi", config.resource_limits.memory_limit_gb),
|
||||||
"services": "10",
|
"requests.cpu": format!("{:.0}",config.resource_limits.cpu_request_cores),
|
||||||
"configmaps": "30",
|
"requests.memory": format!("{:.3}Gi", config.resource_limits.memory_request_gb),
|
||||||
"secrets": "30",
|
"requests.storage": format!("{:.3}", config.resource_limits.storage_total_gb),
|
||||||
"persistentvolumeclaims": "15",
|
"pods": "20",
|
||||||
"services.loadbalancers": "2",
|
"services": "10",
|
||||||
"services.nodeports": "5",
|
"configmaps": "30",
|
||||||
"limits.ephemeral-storage": "10Gi",
|
"secrets": "30",
|
||||||
|
"persistentvolumeclaims": "15",
|
||||||
|
"services.loadbalancers": "2",
|
||||||
|
"services.nodeports": "5",
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
);
|
);
|
||||||
serde_json::from_value(resource_quota).map_err(|e| {
|
|
||||||
ExecutorError::ConfigurationError(format!(
|
|
||||||
"Could not build TenantManager ResourceQuota. {}",
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_network_policy(&self, config: &TenantConfig) -> Result<NetworkPolicy, ExecutorError> {
|
async fn update_tenant_resource_limits(
|
||||||
let network_policy = json!({
|
&self,
|
||||||
"apiVersion": "networking.k8s.io/v1",
|
tenant_name: &str,
|
||||||
"kind": "NetworkPolicy",
|
new_limits: &ResourceLimits,
|
||||||
"metadata": {
|
) -> Result<(), ExecutorError> {
|
||||||
"name": format!("{}-network-policy", config.name),
|
todo!()
|
||||||
},
|
}
|
||||||
"spec": {
|
|
||||||
"podSelector": {},
|
|
||||||
"egress": [
|
|
||||||
{ "to": [ {"podSelector": {}}]},
|
|
||||||
{ "to":
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"podSelector": {},
|
|
||||||
"namespaceSelector": {
|
|
||||||
"matchLabels": {
|
|
||||||
"kubernetes.io/metadata.name":"openshift-dns"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{ "to": [
|
|
||||||
{
|
|
||||||
"ipBlock": {
|
|
||||||
|
|
||||||
"cidr": "0.0.0.0/0",
|
async fn update_tenant_network_policy(
|
||||||
// See https://en.wikipedia.org/wiki/Reserved_IP_addresses
|
&self,
|
||||||
"except": [
|
tenant_name: &str,
|
||||||
"10.0.0.0/8",
|
new_policy: &TenantNetworkPolicy,
|
||||||
"172.16.0.0/12",
|
) -> Result<(), ExecutorError> {
|
||||||
"192.168.0.0/16",
|
todo!()
|
||||||
"192.0.0.0/24",
|
}
|
||||||
"192.0.2.0/24",
|
|
||||||
"192.88.99.0/24",
|
|
||||||
"192.18.0.0/15",
|
|
||||||
"198.51.100.0/24",
|
|
||||||
"169.254.0.0/16",
|
|
||||||
"203.0.113.0/24",
|
|
||||||
"127.0.0.0/8",
|
|
||||||
|
|
||||||
// Not sure we should block this one as it is
|
async fn deprovision_tenant(&self, tenant_name: &str) -> Result<(), ExecutorError> {
|
||||||
// used for multicast. But better block more than less.
|
todo!()
|
||||||
"224.0.0.0/4",
|
|
||||||
"240.0.0.0/4",
|
|
||||||
"100.64.0.0/10",
|
|
||||||
"233.252.0.0/24",
|
|
||||||
"0.0.0.0/8",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"ingress": [
|
|
||||||
{ "from": [ {"podSelector": {}}]}
|
|
||||||
],
|
|
||||||
"policyTypes": [
|
|
||||||
"Ingress", "Egress",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut network_policy: NetworkPolicy =
|
|
||||||
serde_json::from_value(network_policy).map_err(|e| {
|
|
||||||
ExecutorError::ConfigurationError(format!(
|
|
||||||
"Could not build TenantManager NetworkPolicy. {}",
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
config
|
|
||||||
.network_policy
|
|
||||||
.additional_allowed_cidr_ingress
|
|
||||||
.iter()
|
|
||||||
.try_for_each(|c| -> Result<(), ExecutorError> {
|
|
||||||
let cidr_list: Vec<serde_json::Value> =
|
|
||||||
c.0.iter()
|
|
||||||
.map(|ci| {
|
|
||||||
json!({
|
|
||||||
"ipBlock": {
|
|
||||||
"cidr": ci.to_string(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let rule = serde_json::from_value::<NetworkPolicyIngressRule>(json!({
|
|
||||||
"from": cidr_list
|
|
||||||
}))
|
|
||||||
.map_err(|e| {
|
|
||||||
ExecutorError::ConfigurationError(format!(
|
|
||||||
"Could not build TenantManager NetworkPolicyIngressRule. {}",
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
network_policy
|
|
||||||
.spec
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.ingress
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.push(rule);
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
config
|
|
||||||
.network_policy
|
|
||||||
.additional_allowed_cidr_egress
|
|
||||||
.iter()
|
|
||||||
.try_for_each(|c| -> Result<(), ExecutorError> {
|
|
||||||
let cidr_list: Vec<serde_json::Value> =
|
|
||||||
c.0.iter()
|
|
||||||
.map(|ci| {
|
|
||||||
json!({
|
|
||||||
"ipBlock": {
|
|
||||||
"cidr": ci.to_string(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let ports: Option<Vec<NetworkPolicyPort>> =
|
|
||||||
c.1.as_ref().map(|spec| match &spec.data {
|
|
||||||
super::PortSpecData::SinglePort(port) => vec![NetworkPolicyPort {
|
|
||||||
port: Some(IntOrString::Int(port.clone().into())),
|
|
||||||
..Default::default()
|
|
||||||
}],
|
|
||||||
super::PortSpecData::PortRange(start, end) => vec![NetworkPolicyPort {
|
|
||||||
port: Some(IntOrString::Int(start.clone().into())),
|
|
||||||
end_port: Some(end.clone().into()),
|
|
||||||
protocol: None, // Not currently supported by Harmony
|
|
||||||
}],
|
|
||||||
|
|
||||||
super::PortSpecData::ListOfPorts(items) => items
|
|
||||||
.iter()
|
|
||||||
.map(|i| NetworkPolicyPort {
|
|
||||||
port: Some(IntOrString::Int(i.clone().into())),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
});
|
|
||||||
let rule = serde_json::from_value::<NetworkPolicyEgressRule>(json!({
|
|
||||||
"to": cidr_list,
|
|
||||||
"ports": ports,
|
|
||||||
}))
|
|
||||||
.map_err(|e| {
|
|
||||||
ExecutorError::ConfigurationError(format!(
|
|
||||||
"Could not build TenantManager NetworkPolicyEgressRule. {}",
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
network_policy
|
|
||||||
.spec
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.egress
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.push(rule);
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(network_policy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl TenantManager for K8sTenantManager {
|
|
||||||
async fn provision_tenant(&self, config: &TenantConfig) -> Result<(), ExecutorError> {
|
|
||||||
let namespace = self.build_namespace(config)?;
|
|
||||||
let resource_quota = self.build_resource_quota(config)?;
|
|
||||||
let network_policy = self.build_network_policy(config)?;
|
|
||||||
|
|
||||||
self.ensure_constraints(&namespace)?;
|
|
||||||
|
|
||||||
debug!("Creating namespace for tenant {}", config.name);
|
|
||||||
self.apply_resource(namespace, config).await?;
|
|
||||||
|
|
||||||
debug!("Creating resource_quota for tenant {}", config.name);
|
|
||||||
self.apply_resource(resource_quota, config).await?;
|
|
||||||
|
|
||||||
debug!("Creating network_policy for tenant {}", config.name);
|
|
||||||
self.apply_resource(network_policy, config).await?;
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Success provisionning K8s tenant id {} name {}",
|
|
||||||
config.id, config.name
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,42 @@ use crate::executors::ExecutorError;
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait TenantManager {
|
pub trait TenantManager {
|
||||||
/// Creates or update tenant based on the provided configuration.
|
/// Provisions a new tenant based on the provided configuration.
|
||||||
/// This operation should be idempotent; if a tenant with the same `config.id`
|
/// This operation should be idempotent; if a tenant with the same `config.name`
|
||||||
/// already exists and matches the config, it will succeed without changes.
|
/// already exists and matches the config, it will succeed without changes.
|
||||||
///
|
|
||||||
/// If it exists but differs, it will be updated, or return an error if the update
|
/// If it exists but differs, it will be updated, or return an error if the update
|
||||||
/// action is not supported
|
/// action is not supported
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `config`: The desired configuration for the new tenant.
|
/// * `config`: The desired configuration for the new tenant.
|
||||||
async fn provision_tenant(&self, config: &TenantConfig) -> Result<(), ExecutorError>;
|
async fn provision_tenant(&self, config: &TenantConfig) -> Result<(), ExecutorError>;
|
||||||
|
|
||||||
|
/// Updates the resource limits for an existing tenant.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `tenant_name`: The logical name of the tenant to update.
|
||||||
|
/// * `new_limits`: The new set of resource limits to apply.
|
||||||
|
async fn update_tenant_resource_limits(
|
||||||
|
&self,
|
||||||
|
tenant_name: &str,
|
||||||
|
new_limits: &ResourceLimits,
|
||||||
|
) -> Result<(), ExecutorError>;
|
||||||
|
|
||||||
|
/// Updates the high-level network isolation policy for an existing tenant.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `tenant_name`: The logical name of the tenant to update.
|
||||||
|
/// * `new_policy`: The new network policy to apply.
|
||||||
|
async fn update_tenant_network_policy(
|
||||||
|
&self,
|
||||||
|
tenant_name: &str,
|
||||||
|
new_policy: &TenantNetworkPolicy,
|
||||||
|
) -> Result<(), ExecutorError>;
|
||||||
|
|
||||||
|
/// Decommissions an existing tenant, removing its isolated context and associated resources.
|
||||||
|
/// This operation should be idempotent.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `tenant_name`: The logical name of the tenant to deprovision.
|
||||||
|
async fn deprovision_tenant(&self, tenant_name: &str) -> Result<(), ExecutorError>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
pub mod k8s;
|
pub mod k8s;
|
||||||
mod manager;
|
mod manager;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
pub use manager::*;
|
pub use manager::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::data::Id;
|
use crate::data::Id;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] // Assuming serde for Scores
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] // Assuming serde for Scores
|
||||||
@@ -21,26 +21,13 @@ pub struct TenantConfig {
|
|||||||
|
|
||||||
/// High-level network isolation policies for the tenant.
|
/// High-level network isolation policies for the tenant.
|
||||||
pub network_policy: TenantNetworkPolicy,
|
pub network_policy: TenantNetworkPolicy,
|
||||||
|
|
||||||
|
/// Key-value pairs for provider-specific tagging, labeling, or metadata.
|
||||||
|
/// Useful for billing, organization, or filtering within the provider's console.
|
||||||
|
pub labels_or_tags: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TenantConfig {
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||||
fn default() -> Self {
|
|
||||||
let id = Id::default();
|
|
||||||
Self {
|
|
||||||
name: format!("tenant_{id}"),
|
|
||||||
id,
|
|
||||||
resource_limits: ResourceLimits::default(),
|
|
||||||
network_policy: TenantNetworkPolicy {
|
|
||||||
default_inter_tenant_ingress: InterTenantIngressPolicy::DenyAll,
|
|
||||||
default_internet_egress: InternetEgressPolicy::AllowAll,
|
|
||||||
additional_allowed_cidr_ingress: vec![],
|
|
||||||
additional_allowed_cidr_egress: vec![],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub struct ResourceLimits {
|
pub struct ResourceLimits {
|
||||||
/// Requested/guaranteed CPU cores (e.g., 2.0).
|
/// Requested/guaranteed CPU cores (e.g., 2.0).
|
||||||
pub cpu_request_cores: f32,
|
pub cpu_request_cores: f32,
|
||||||
@@ -56,18 +43,6 @@ pub struct ResourceLimits {
|
|||||||
pub storage_total_gb: f32,
|
pub storage_total_gb: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ResourceLimits {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
cpu_request_cores: 4.0,
|
|
||||||
cpu_limit_cores: 4.0,
|
|
||||||
memory_request_gb: 4.0,
|
|
||||||
memory_limit_gb: 4.0,
|
|
||||||
storage_total_gb: 20.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct TenantNetworkPolicy {
|
pub struct TenantNetworkPolicy {
|
||||||
/// Policy for ingress traffic originating from other tenants within the same Harmony-managed environment.
|
/// Policy for ingress traffic originating from other tenants within the same Harmony-managed environment.
|
||||||
@@ -75,20 +50,6 @@ pub struct TenantNetworkPolicy {
|
|||||||
|
|
||||||
/// Policy for egress traffic destined for the public internet.
|
/// Policy for egress traffic destined for the public internet.
|
||||||
pub default_internet_egress: InternetEgressPolicy,
|
pub default_internet_egress: InternetEgressPolicy,
|
||||||
|
|
||||||
pub additional_allowed_cidr_ingress: Vec<(Vec<cidr::Ipv4Cidr>, Option<PortSpec>)>,
|
|
||||||
pub additional_allowed_cidr_egress: Vec<(Vec<cidr::Ipv4Cidr>, Option<PortSpec>)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TenantNetworkPolicy {
|
|
||||||
fn default() -> Self {
|
|
||||||
TenantNetworkPolicy {
|
|
||||||
default_inter_tenant_ingress: InterTenantIngressPolicy::DenyAll,
|
|
||||||
default_internet_egress: InternetEgressPolicy::DenyAll,
|
|
||||||
additional_allowed_cidr_ingress: vec![],
|
|
||||||
additional_allowed_cidr_egress: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
@@ -104,121 +65,3 @@ pub enum InternetEgressPolicy {
|
|||||||
/// Deny all outbound traffic to the internet by default.
|
/// Deny all outbound traffic to the internet by default.
|
||||||
DenyAll,
|
DenyAll,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a port specification that can be either a single port, a comma-separated list of ports,
|
|
||||||
/// or a range separated by a dash.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub struct PortSpec {
|
|
||||||
/// The actual representation of the ports as strings for serialization/deserialization purposes.
|
|
||||||
pub data: PortSpecData,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PortSpec {
|
|
||||||
/// TODO write short rust doc that shows what types of input are supported
|
|
||||||
fn parse_from_str(spec: &str) -> Result<PortSpec, String> {
|
|
||||||
// Check for single port
|
|
||||||
if let Ok(port) = spec.parse::<u16>() {
|
|
||||||
let spec = PortSpecData::SinglePort(port);
|
|
||||||
return Ok(Self { data: spec });
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(range) = spec.find('-') {
|
|
||||||
let start_str = &spec[..range];
|
|
||||||
let end_str = &spec[(range + 1)..];
|
|
||||||
|
|
||||||
if let (Ok(start), Ok(end)) = (start_str.parse::<u16>(), end_str.parse::<u16>()) {
|
|
||||||
let spec = PortSpecData::PortRange(start, end);
|
|
||||||
return Ok(Self { data: spec });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let ports: Vec<&str> = spec.split(',').collect();
|
|
||||||
if !ports.is_empty() && ports.iter().all(|p| p.parse::<u16>().is_ok()) {
|
|
||||||
let maybe_ports = ports.iter().try_fold(vec![], |mut list, &p| {
|
|
||||||
if let Ok(p) = p.parse::<u16>() {
|
|
||||||
list.push(p);
|
|
||||||
return Ok(list);
|
|
||||||
}
|
|
||||||
Err(())
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Ok(ports) = maybe_ports {
|
|
||||||
let spec = PortSpecData::ListOfPorts(ports);
|
|
||||||
return Ok(Self { data: spec });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(format!("Invalid port spec format {spec}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for PortSpec {
|
|
||||||
type Err = String;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Self::parse_from_str(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub enum PortSpecData {
|
|
||||||
SinglePort(u16),
|
|
||||||
PortRange(u16, u16),
|
|
||||||
ListOfPorts(Vec<u16>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_single_port() {
|
|
||||||
let port_spec = "2144".parse::<PortSpec>().unwrap();
|
|
||||||
match port_spec.data {
|
|
||||||
PortSpecData::SinglePort(port) => assert_eq!(port, 2144),
|
|
||||||
_ => panic!("Expected SinglePort"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_port_range() {
|
|
||||||
let port_spec = "80-90".parse::<PortSpec>().unwrap();
|
|
||||||
match port_spec.data {
|
|
||||||
PortSpecData::PortRange(start, end) => {
|
|
||||||
assert_eq!(start, 80);
|
|
||||||
assert_eq!(end, 90);
|
|
||||||
}
|
|
||||||
_ => panic!("Expected PortRange"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_list_of_ports() {
|
|
||||||
let port_spec = "2144,3424".parse::<PortSpec>().unwrap();
|
|
||||||
match port_spec.data {
|
|
||||||
PortSpecData::ListOfPorts(ports) => {
|
|
||||||
assert_eq!(ports[0], 2144);
|
|
||||||
assert_eq!(ports[1], 3424);
|
|
||||||
}
|
|
||||||
_ => panic!("Expected ListOfPorts"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_port_spec() {
|
|
||||||
let result = "invalid".parse::<PortSpec>();
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_empty_input() {
|
|
||||||
let result = "".parse::<PortSpec>();
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_only_coma() {
|
|
||||||
let result = ",".parse::<PortSpec>();
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for HelmChartInterpretV
|
|||||||
_inventory: &Inventory,
|
_inventory: &Inventory,
|
||||||
_topology: &T,
|
_topology: &T,
|
||||||
) -> Result<Outcome, InterpretError> {
|
) -> Result<Outcome, InterpretError> {
|
||||||
let _ns = self
|
let ns = self
|
||||||
.score
|
.score
|
||||||
.chart
|
.chart
|
||||||
.namespace
|
.namespace
|
||||||
@@ -339,7 +339,7 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for HelmChartInterpretV
|
|||||||
|
|
||||||
let res = helm_executor.generate();
|
let res = helm_executor.generate();
|
||||||
|
|
||||||
let _output = match res {
|
let output = match res {
|
||||||
Ok(output) => output,
|
Ok(output) => output,
|
||||||
Err(err) => return Err(InterpretError::new(err.to_string())),
|
Err(err) => return Err(InterpretError::new(err.to_string())),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use harmony_macros::ingress_path;
|
use harmony_macros::ingress_path;
|
||||||
use k8s_openapi::api::networking::v1::Ingress;
|
use k8s_openapi::api::networking::v1::Ingress;
|
||||||
use log::{debug, trace};
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
@@ -57,24 +56,22 @@ impl<T: Topology + K8sclient> Score<T> for K8sIngressScore {
|
|||||||
let ingress = json!(
|
let ingress = json!(
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": self.name.to_string(),
|
"name": self.name
|
||||||
},
|
},
|
||||||
"spec": {
|
"spec": {
|
||||||
"rules": [
|
"rules": [
|
||||||
{ "host": self.host.to_string(),
|
{ "host": self.host,
|
||||||
"http": {
|
"http": {
|
||||||
"paths": [
|
"paths": [
|
||||||
{
|
{
|
||||||
"path": path,
|
"path": path,
|
||||||
"pathType": path_type.as_str(),
|
"pathType": path_type.as_str(),
|
||||||
"backend": {
|
"backend": [
|
||||||
"service": {
|
{
|
||||||
"name": self.backend_service.to_string(),
|
"service": self.backend_service,
|
||||||
"port": {
|
"port": self.port
|
||||||
"number": self.port,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -84,16 +81,13 @@ impl<T: Topology + K8sclient> Score<T> for K8sIngressScore {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
trace!("Building ingresss object from Value {ingress:#}");
|
|
||||||
let ingress: Ingress = serde_json::from_value(ingress).unwrap();
|
let ingress: Ingress = serde_json::from_value(ingress).unwrap();
|
||||||
debug!(
|
|
||||||
"Successfully built Ingress for host {:?}",
|
|
||||||
ingress.metadata.name
|
|
||||||
);
|
|
||||||
Box::new(K8sResourceInterpret {
|
Box::new(K8sResourceInterpret {
|
||||||
score: K8sResourceScore::single(
|
score: K8sResourceScore::single(
|
||||||
ingress.clone(),
|
ingress.clone(),
|
||||||
self.namespace.clone().map(|f| f.to_string()),
|
self.namespace
|
||||||
|
.clone()
|
||||||
|
.map(|f| f.as_c_str().to_str().unwrap().to_string()),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use k8s_openapi::NamespaceResourceScope;
|
use k8s_openapi::NamespaceResourceScope;
|
||||||
use kube::Resource;
|
use kube::Resource;
|
||||||
use log::info;
|
|
||||||
use serde::{Serialize, de::DeserializeOwned};
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -76,12 +75,11 @@ where
|
|||||||
_inventory: &Inventory,
|
_inventory: &Inventory,
|
||||||
topology: &T,
|
topology: &T,
|
||||||
) -> Result<Outcome, InterpretError> {
|
) -> Result<Outcome, InterpretError> {
|
||||||
info!("Applying {} resources", self.score.resource.len());
|
|
||||||
topology
|
topology
|
||||||
.k8s_client()
|
.k8s_client()
|
||||||
.await
|
.await
|
||||||
.expect("Environment should provide enough information to instanciate a client")
|
.expect("Environment should provide enough information to instanciate a client")
|
||||||
.apply_many(&self.score.resource, self.score.namespace.as_deref())
|
.apply_namespaced(&self.score.resource, self.score.namespace.as_deref())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Outcome::success(
|
Ok(Outcome::success(
|
||||||
|
|||||||
@@ -12,6 +12,4 @@ pub mod load_balancer;
|
|||||||
pub mod monitoring;
|
pub mod monitoring;
|
||||||
pub mod okd;
|
pub mod okd;
|
||||||
pub mod opnsense;
|
pub mod opnsense;
|
||||||
pub mod prometheus;
|
|
||||||
pub mod tenant;
|
|
||||||
pub mod tftp;
|
pub mod tftp;
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
use serde::Serialize;
|
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
interpret::{InterpretError, Outcome},
|
|
||||||
modules::monitoring::kube_prometheus::{
|
|
||||||
prometheus::{Prometheus, PrometheusReceiver},
|
|
||||||
types::{AlertChannelConfig, AlertManagerChannelConfig},
|
|
||||||
},
|
|
||||||
topology::{Url, oberservability::monitoring::AlertReceiver},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct DiscordWebhook {
|
|
||||||
pub name: String,
|
|
||||||
pub url: Url,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AlertReceiver<Prometheus> for DiscordWebhook {
|
|
||||||
async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> {
|
|
||||||
sender.install_receiver(self).await
|
|
||||||
}
|
|
||||||
fn clone_box(&self) -> Box<dyn AlertReceiver<Prometheus>> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl PrometheusReceiver for DiscordWebhook {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
async fn configure_receiver(&self) -> AlertManagerChannelConfig {
|
|
||||||
self.get_config().await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AlertChannelConfig for DiscordWebhook {
|
|
||||||
async fn get_config(&self) -> AlertManagerChannelConfig {
|
|
||||||
let channel_global_config = None;
|
|
||||||
let channel_receiver = self.alert_channel_receiver().await;
|
|
||||||
let channel_route = self.alert_channel_route().await;
|
|
||||||
|
|
||||||
AlertManagerChannelConfig {
|
|
||||||
channel_global_config,
|
|
||||||
channel_receiver,
|
|
||||||
channel_route,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DiscordWebhook {
|
|
||||||
async fn alert_channel_route(&self) -> serde_yaml::Value {
|
|
||||||
let mut route = Mapping::new();
|
|
||||||
route.insert(
|
|
||||||
Value::String("receiver".to_string()),
|
|
||||||
Value::String(self.name.clone()),
|
|
||||||
);
|
|
||||||
route.insert(
|
|
||||||
Value::String("matchers".to_string()),
|
|
||||||
Value::Sequence(vec![Value::String("alertname!=Watchdog".to_string())]),
|
|
||||||
);
|
|
||||||
route.insert(Value::String("continue".to_string()), Value::Bool(true));
|
|
||||||
Value::Mapping(route)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn alert_channel_receiver(&self) -> serde_yaml::Value {
|
|
||||||
let mut receiver = Mapping::new();
|
|
||||||
receiver.insert(
|
|
||||||
Value::String("name".to_string()),
|
|
||||||
Value::String(self.name.clone()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut discord_config = Mapping::new();
|
|
||||||
discord_config.insert(
|
|
||||||
Value::String("webhook_url".to_string()),
|
|
||||||
Value::String(self.url.to_string()),
|
|
||||||
);
|
|
||||||
|
|
||||||
receiver.insert(
|
|
||||||
Value::String("discord_configs".to_string()),
|
|
||||||
Value::Sequence(vec![Value::Mapping(discord_config)]),
|
|
||||||
);
|
|
||||||
|
|
||||||
Value::Mapping(receiver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn discord_serialize_should_match() {
|
|
||||||
let discord_receiver = DiscordWebhook {
|
|
||||||
name: "test-discord".to_string(),
|
|
||||||
url: Url::Url(url::Url::parse("https://discord.i.dont.exist.com").unwrap()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let discord_receiver_receiver =
|
|
||||||
serde_yaml::to_string(&discord_receiver.alert_channel_receiver().await).unwrap();
|
|
||||||
println!("receiver \n{:#}", discord_receiver_receiver);
|
|
||||||
let discord_receiver_receiver_yaml = r#"name: test-discord
|
|
||||||
discord_configs:
|
|
||||||
- webhook_url: https://discord.i.dont.exist.com/
|
|
||||||
"#
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let discord_receiver_route =
|
|
||||||
serde_yaml::to_string(&discord_receiver.alert_channel_route().await).unwrap();
|
|
||||||
println!("route \n{:#}", discord_receiver_route);
|
|
||||||
let discord_receiver_route_yaml = r#"receiver: test-discord
|
|
||||||
matchers:
|
|
||||||
- alertname!=Watchdog
|
|
||||||
continue: true
|
|
||||||
"#
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
assert_eq!(discord_receiver_receiver, discord_receiver_receiver_yaml);
|
|
||||||
assert_eq!(discord_receiver_route, discord_receiver_route_yaml);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pub mod discord_alert_channel;
|
|
||||||
pub mod webhook_receiver;
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
use serde::Serialize;
|
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
interpret::{InterpretError, Outcome},
|
|
||||||
modules::monitoring::kube_prometheus::{
|
|
||||||
prometheus::{Prometheus, PrometheusReceiver},
|
|
||||||
types::{AlertChannelConfig, AlertManagerChannelConfig},
|
|
||||||
},
|
|
||||||
topology::{Url, oberservability::monitoring::AlertReceiver},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct WebhookReceiver {
|
|
||||||
pub name: String,
|
|
||||||
pub url: Url,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AlertReceiver<Prometheus> for WebhookReceiver {
|
|
||||||
async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> {
|
|
||||||
sender.install_receiver(self).await
|
|
||||||
}
|
|
||||||
fn clone_box(&self) -> Box<dyn AlertReceiver<Prometheus>> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl PrometheusReceiver for WebhookReceiver {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
async fn configure_receiver(&self) -> AlertManagerChannelConfig {
|
|
||||||
self.get_config().await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AlertChannelConfig for WebhookReceiver {
|
|
||||||
async fn get_config(&self) -> AlertManagerChannelConfig {
|
|
||||||
let channel_global_config = None;
|
|
||||||
let channel_receiver = self.alert_channel_receiver().await;
|
|
||||||
let channel_route = self.alert_channel_route().await;
|
|
||||||
|
|
||||||
AlertManagerChannelConfig {
|
|
||||||
channel_global_config,
|
|
||||||
channel_receiver,
|
|
||||||
channel_route,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WebhookReceiver {
|
|
||||||
async fn alert_channel_route(&self) -> serde_yaml::Value {
|
|
||||||
let mut route = Mapping::new();
|
|
||||||
route.insert(
|
|
||||||
Value::String("receiver".to_string()),
|
|
||||||
Value::String(self.name.clone()),
|
|
||||||
);
|
|
||||||
route.insert(
|
|
||||||
Value::String("matchers".to_string()),
|
|
||||||
Value::Sequence(vec![Value::String("alertname!=Watchdog".to_string())]),
|
|
||||||
);
|
|
||||||
route.insert(Value::String("continue".to_string()), Value::Bool(true));
|
|
||||||
Value::Mapping(route)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn alert_channel_receiver(&self) -> serde_yaml::Value {
|
|
||||||
let mut receiver = Mapping::new();
|
|
||||||
receiver.insert(
|
|
||||||
Value::String("name".to_string()),
|
|
||||||
Value::String(self.name.clone()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut webhook_config = Mapping::new();
|
|
||||||
webhook_config.insert(
|
|
||||||
Value::String("url".to_string()),
|
|
||||||
Value::String(self.url.to_string()),
|
|
||||||
);
|
|
||||||
|
|
||||||
receiver.insert(
|
|
||||||
Value::String("webhook_configs".to_string()),
|
|
||||||
Value::Sequence(vec![Value::Mapping(webhook_config)]),
|
|
||||||
);
|
|
||||||
|
|
||||||
Value::Mapping(receiver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
#[tokio::test]
|
|
||||||
async fn webhook_serialize_should_match() {
|
|
||||||
let webhook_receiver = WebhookReceiver {
|
|
||||||
name: "test-webhook".to_string(),
|
|
||||||
url: Url::Url(url::Url::parse("https://webhook.i.dont.exist.com").unwrap()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let webhook_receiver_receiver =
|
|
||||||
serde_yaml::to_string(&webhook_receiver.alert_channel_receiver().await).unwrap();
|
|
||||||
println!("receiver \n{:#}", webhook_receiver_receiver);
|
|
||||||
let webhook_receiver_receiver_yaml = r#"name: test-webhook
|
|
||||||
webhook_configs:
|
|
||||||
- url: https://webhook.i.dont.exist.com/
|
|
||||||
"#
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let webhook_receiver_route =
|
|
||||||
serde_yaml::to_string(&webhook_receiver.alert_channel_route().await).unwrap();
|
|
||||||
println!("route \n{:#}", webhook_receiver_route);
|
|
||||||
let webhook_receiver_route_yaml = r#"receiver: test-webhook
|
|
||||||
matchers:
|
|
||||||
- alertname!=Watchdog
|
|
||||||
continue: true
|
|
||||||
"#
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
assert_eq!(webhook_receiver_receiver, webhook_receiver_receiver_yaml);
|
|
||||||
assert_eq!(webhook_receiver_route, webhook_receiver_route_yaml);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub mod prometheus_alert_rule;
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
use std::collections::{BTreeMap, HashMap};
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
interpret::{InterpretError, Outcome},
|
|
||||||
modules::monitoring::kube_prometheus::{
|
|
||||||
prometheus::{Prometheus, PrometheusRule},
|
|
||||||
types::{AlertGroup, AlertManagerAdditionalPromRules},
|
|
||||||
},
|
|
||||||
topology::oberservability::monitoring::AlertRule,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AlertRule<Prometheus> for AlertManagerRuleGroup {
|
|
||||||
async fn install(&self, sender: &Prometheus) -> Result<Outcome, InterpretError> {
|
|
||||||
sender.install_rule(&self).await
|
|
||||||
}
|
|
||||||
fn clone_box(&self) -> Box<dyn AlertRule<Prometheus>> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl PrometheusRule for AlertManagerRuleGroup {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
async fn configure_rule(&self) -> AlertManagerAdditionalPromRules {
|
|
||||||
let mut additional_prom_rules = BTreeMap::new();
|
|
||||||
|
|
||||||
additional_prom_rules.insert(
|
|
||||||
self.name.clone(),
|
|
||||||
AlertGroup {
|
|
||||||
groups: vec![self.clone()],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
AlertManagerAdditionalPromRules {
|
|
||||||
rules: additional_prom_rules,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AlertManagerRuleGroup {
|
|
||||||
pub fn new(name: &str, rules: Vec<PrometheusAlertRule>) -> AlertManagerRuleGroup {
|
|
||||||
AlertManagerRuleGroup {
|
|
||||||
name: name.to_string().to_lowercase(),
|
|
||||||
rules,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
///logical group of alert rules
|
|
||||||
///evaluates to:
|
|
||||||
///name:
|
|
||||||
/// groups:
|
|
||||||
/// - name: name
|
|
||||||
/// rules: PrometheusAlertRule
|
|
||||||
pub struct AlertManagerRuleGroup {
|
|
||||||
pub name: String,
|
|
||||||
pub rules: Vec<PrometheusAlertRule>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct PrometheusAlertRule {
|
|
||||||
pub alert: String,
|
|
||||||
pub expr: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub r#for: Option<String>,
|
|
||||||
pub labels: HashMap<String, String>,
|
|
||||||
pub annotations: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrometheusAlertRule {
|
|
||||||
pub fn new(alert_name: &str, expr: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
alert: alert_name.into(),
|
|
||||||
expr: expr.into(),
|
|
||||||
r#for: Some("1m".into()),
|
|
||||||
labels: HashMap::new(),
|
|
||||||
annotations: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn for_duration(mut self, duration: &str) -> Self {
|
|
||||||
self.r#for = Some(duration.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub fn label(mut self, key: &str, value: &str) -> Self {
|
|
||||||
self.labels.insert(key.into(), value.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn annotation(mut self, key: &str, value: &str) -> Self {
|
|
||||||
self.annotations.insert(key.into(), value.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
102
harmony/src/modules/monitoring/alertmanager_types.rs
Normal file
102
harmony/src/modules/monitoring/alertmanager_types.rs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AlertManagerValues {
|
||||||
|
pub alertmanager: AlertManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AlertManager {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub config: AlertManagerConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AlertChannelConfig {
|
||||||
|
pub receiver: AlertChannelReceiver,
|
||||||
|
pub route: AlertChannelRoute,
|
||||||
|
pub global_config: Option<AlertChannelGlobalConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AlertChannelReceiver {
|
||||||
|
pub name: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub slack_configs: Option<Vec<SlackConfig>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub webhook_configs: Option<Vec<WebhookConfig>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AlertManagerRoute {
|
||||||
|
pub group_by: Vec<String>,
|
||||||
|
pub group_wait: String,
|
||||||
|
pub group_interval: String,
|
||||||
|
pub repeat_interval: String,
|
||||||
|
pub routes: Vec<AlertChannelRoute>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AlertChannelGlobalConfig {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub slack_api_url: Option<Url>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SlackConfig {
|
||||||
|
pub channel: String,
|
||||||
|
pub send_resolved: bool,
|
||||||
|
pub title: String,
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct WebhookConfig {
|
||||||
|
pub url: Url,
|
||||||
|
pub send_resolved: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AlertChannelRoute {
|
||||||
|
pub receiver: String,
|
||||||
|
pub matchers: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub r#continue: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AlertManagerConfig {
|
||||||
|
pub global: Option<AlertChannelGlobalConfig>,
|
||||||
|
pub route: AlertManagerRoute,
|
||||||
|
pub receivers: Vec<AlertChannelReceiver>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlertManagerValues {
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
alertmanager: AlertManager {
|
||||||
|
enabled: true,
|
||||||
|
config: AlertManagerConfig {
|
||||||
|
global: None,
|
||||||
|
route: AlertManagerRoute {
|
||||||
|
group_by: vec!["job".to_string()],
|
||||||
|
group_wait: "30s".to_string(),
|
||||||
|
group_interval: "5m".to_string(),
|
||||||
|
repeat_interval: "12h".to_string(),
|
||||||
|
routes: vec![AlertChannelRoute {
|
||||||
|
receiver: "null".to_string(),
|
||||||
|
matchers: vec!["alertname=Watchdog".to_string()],
|
||||||
|
r#continue: false,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
receivers: vec![AlertChannelReceiver {
|
||||||
|
name: "null".to_string(),
|
||||||
|
slack_configs: None,
|
||||||
|
webhook_configs: None,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::modules::monitoring::{
|
use super::monitoring_alerting::AlertChannel;
|
||||||
alert_rule::prometheus_alert_rule::AlertManagerRuleGroup,
|
|
||||||
kube_prometheus::types::{AlertManagerAdditionalPromRules, AlertManagerChannelConfig},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct KubePrometheusConfig {
|
pub struct KubePrometheusConfig {
|
||||||
@@ -24,8 +21,7 @@ pub struct KubePrometheusConfig {
|
|||||||
pub kube_proxy: bool,
|
pub kube_proxy: bool,
|
||||||
pub kube_state_metrics: bool,
|
pub kube_state_metrics: bool,
|
||||||
pub prometheus_operator: bool,
|
pub prometheus_operator: bool,
|
||||||
pub alert_receiver_configs: Vec<AlertManagerChannelConfig>,
|
pub alert_channel: Vec<AlertChannel>,
|
||||||
pub alert_rules: Vec<AlertManagerAdditionalPromRules>,
|
|
||||||
}
|
}
|
||||||
impl KubePrometheusConfig {
|
impl KubePrometheusConfig {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
@@ -34,6 +30,7 @@ impl KubePrometheusConfig {
|
|||||||
default_rules: true,
|
default_rules: true,
|
||||||
windows_monitoring: false,
|
windows_monitoring: false,
|
||||||
alert_manager: true,
|
alert_manager: true,
|
||||||
|
alert_channel: Vec::new(),
|
||||||
grafana: true,
|
grafana: true,
|
||||||
node_exporter: false,
|
node_exporter: false,
|
||||||
prometheus: true,
|
prometheus: true,
|
||||||
@@ -47,8 +44,6 @@ impl KubePrometheusConfig {
|
|||||||
prometheus_operator: true,
|
prometheus_operator: true,
|
||||||
core_dns: false,
|
core_dns: false,
|
||||||
kube_scheduler: false,
|
kube_scheduler: false,
|
||||||
alert_receiver_configs: vec![],
|
|
||||||
alert_rules: vec![],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
35
harmony/src/modules/monitoring/discord_alert_manager.rs
Normal file
35
harmony/src/modules/monitoring/discord_alert_manager.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use non_blank_string_rs::NonBlankString;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::modules::helm::chart::HelmChartScore;
|
||||||
|
|
||||||
|
pub fn discord_alert_manager_score(
|
||||||
|
webhook_url: Url,
|
||||||
|
namespace: String,
|
||||||
|
name: String,
|
||||||
|
) -> HelmChartScore {
|
||||||
|
let values = format!(
|
||||||
|
r#"
|
||||||
|
environment:
|
||||||
|
- name: "DISCORD_WEBHOOK"
|
||||||
|
value: "{webhook_url}"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
HelmChartScore {
|
||||||
|
namespace: Some(NonBlankString::from_str(&namespace).unwrap()),
|
||||||
|
release_name: NonBlankString::from_str(&name).unwrap(),
|
||||||
|
chart_name: NonBlankString::from_str(
|
||||||
|
"oci://hub.nationtech.io/library/alertmanager-discord",
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
chart_version: None,
|
||||||
|
values_overrides: None,
|
||||||
|
values_yaml: Some(values.to_string()),
|
||||||
|
create_namespace: true,
|
||||||
|
install_only: true,
|
||||||
|
repository: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
168
harmony/src/modules/monitoring/discord_webhook_sender.rs
Normal file
168
harmony/src/modules/monitoring/discord_webhook_sender.rs
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
use super::{
|
||||||
|
discord_alert_manager::discord_alert_manager_score, kube_prometheus_monitor::AlertManagerConfig,
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_yaml::Value;
|
||||||
|
use tokio::sync::OnceCell;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
data::{Id, Version},
|
||||||
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||||
|
inventory::Inventory,
|
||||||
|
score::Score,
|
||||||
|
topology::{
|
||||||
|
HelmCommand, K8sAnywhereTopology, Topology,
|
||||||
|
oberservability::monitoring::{AlertReceiver, AlertReceiverDeployment},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology + DiscordWebhookReceiver> AlertReceiverDeployment<T> for DiscordWebhookConfig {
|
||||||
|
async fn deploy_alert_receiver(&self, topology: &T) -> Result<Outcome, InterpretError> {
|
||||||
|
topology.deploy_discord_webhook_receiver(self.clone()).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct DiscordWebhookConfig {
|
||||||
|
pub webhook_url: Url,
|
||||||
|
pub name: String,
|
||||||
|
pub send_resolved_notifications: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait DiscordWebhookReceiver {
|
||||||
|
async fn deploy_discord_webhook_receiver(
|
||||||
|
&self,
|
||||||
|
config: DiscordWebhookConfig,
|
||||||
|
) -> Result<Outcome, InterpretError>;
|
||||||
|
fn delete_discord_webhook_receiver(
|
||||||
|
&self,
|
||||||
|
config: DiscordWebhookConfig,
|
||||||
|
) -> Result<Outcome, InterpretError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: DiscordWebhookReceiver> AlertManagerConfig<T> for DiscordWebhookConfig {
|
||||||
|
async fn get_alert_manager_config(&self) -> Result<Value, InterpretError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl DiscordWebhookReceiver for K8sAnywhereTopology {
|
||||||
|
async fn deploy_discord_webhook_receiver(
|
||||||
|
&self,
|
||||||
|
config: DiscordWebhookConfig,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
let receiver_key = config.name.clone();
|
||||||
|
let mut adapters_map_guard = self.alert_receivers.lock().await;
|
||||||
|
|
||||||
|
let cell = adapters_map_guard
|
||||||
|
.entry(receiver_key.clone())
|
||||||
|
.or_insert_with(OnceCell::new);
|
||||||
|
|
||||||
|
if let Some(initialized_receiver) = cell.get() {
|
||||||
|
return Ok(Outcome::success(format!(
|
||||||
|
"Discord Webhook adapter for '{}' already initialized.",
|
||||||
|
initialized_receiver.receiver_id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let final_state = cell
|
||||||
|
.get_or_try_init(|| async {
|
||||||
|
initialize_discord_webhook_receiver(config.clone(), self).await
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Outcome::success(format!(
|
||||||
|
"Discord Webhook Receiver for '{}' ensured/initialized.",
|
||||||
|
final_state.receiver_id
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_discord_webhook_receiver(
|
||||||
|
&self,
|
||||||
|
_config: DiscordWebhookConfig,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn initialize_discord_webhook_receiver(
|
||||||
|
conf: DiscordWebhookConfig,
|
||||||
|
topology: &K8sAnywhereTopology,
|
||||||
|
) -> Result<AlertReceiver, InterpretError> {
|
||||||
|
println!(
|
||||||
|
"Attempting to initialize Discord adapter for: {}",
|
||||||
|
conf.name
|
||||||
|
);
|
||||||
|
let score = DiscordWebhookReceiverScore {
|
||||||
|
config: conf.clone(),
|
||||||
|
};
|
||||||
|
let inventory = Inventory::autoload();
|
||||||
|
let interpret = score.create_interpret();
|
||||||
|
|
||||||
|
interpret.execute(&inventory, topology).await?;
|
||||||
|
|
||||||
|
Ok(AlertReceiver {
|
||||||
|
receiver_id: conf.name,
|
||||||
|
receiver_installed: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
struct DiscordWebhookReceiverScore {
|
||||||
|
config: DiscordWebhookConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology + HelmCommand> Score<T> for DiscordWebhookReceiverScore {
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(DiscordWebhookReceiverScoreInterpret {
|
||||||
|
config: self.config.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"DiscordWebhookReceiverScore".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct DiscordWebhookReceiverScoreInterpret {
|
||||||
|
config: DiscordWebhookConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology + HelmCommand> Interpret<T> for DiscordWebhookReceiverScoreInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
topology: &T,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
discord_alert_manager_score(
|
||||||
|
self.config.webhook_url.clone(),
|
||||||
|
self.config.name.clone(),
|
||||||
|
self.config.name.clone(),
|
||||||
|
)
|
||||||
|
.create_interpret()
|
||||||
|
.execute(inventory, topology)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_name(&self) -> InterpretName {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_version(&self) -> Version {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_status(&self) -> InterpretStatus {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_children(&self) -> Vec<Id> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
use super::config::KubePrometheusConfig;
|
|
||||||
use log::debug;
|
|
||||||
use non_blank_string_rs::NonBlankString;
|
|
||||||
use serde_yaml::{Mapping, Value};
|
|
||||||
use std::{
|
|
||||||
collections::BTreeMap,
|
|
||||||
str::FromStr,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::modules::{
|
|
||||||
helm::chart::HelmChartScore,
|
|
||||||
monitoring::kube_prometheus::types::{
|
|
||||||
AlertGroup, AlertManager, AlertManagerAdditionalPromRules, AlertManagerConfig,
|
|
||||||
AlertManagerRoute, AlertManagerValues,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn kube_prometheus_helm_chart_score(
|
|
||||||
config: Arc<Mutex<KubePrometheusConfig>>,
|
|
||||||
) -> HelmChartScore {
|
|
||||||
let config = config.lock().unwrap();
|
|
||||||
|
|
||||||
let default_rules = config.default_rules.to_string();
|
|
||||||
let windows_monitoring = config.windows_monitoring.to_string();
|
|
||||||
let grafana = config.grafana.to_string();
|
|
||||||
let kubernetes_service_monitors = config.kubernetes_service_monitors.to_string();
|
|
||||||
let kubernetes_api_server = config.kubernetes_api_server.to_string();
|
|
||||||
let kubelet = config.kubelet.to_string();
|
|
||||||
let kube_controller_manager = config.kube_controller_manager.to_string();
|
|
||||||
let core_dns = config.core_dns.to_string();
|
|
||||||
let kube_etcd = config.kube_etcd.to_string();
|
|
||||||
let kube_scheduler = config.kube_scheduler.to_string();
|
|
||||||
let kube_proxy = config.kube_proxy.to_string();
|
|
||||||
let kube_state_metrics = config.kube_state_metrics.to_string();
|
|
||||||
let node_exporter = config.node_exporter.to_string();
|
|
||||||
let prometheus_operator = config.prometheus_operator.to_string();
|
|
||||||
let prometheus = config.prometheus.to_string();
|
|
||||||
let mut values = format!(
|
|
||||||
r#"
|
|
||||||
defaultRules:
|
|
||||||
create: {default_rules}
|
|
||||||
rules:
|
|
||||||
alertmanager: true
|
|
||||||
etcd: true
|
|
||||||
configReloaders: true
|
|
||||||
general: true
|
|
||||||
k8sContainerCpuUsageSecondsTotal: true
|
|
||||||
k8sContainerMemoryCache: true
|
|
||||||
k8sContainerMemoryRss: true
|
|
||||||
k8sContainerMemorySwap: true
|
|
||||||
k8sContainerResource: true
|
|
||||||
k8sContainerMemoryWorkingSetBytes: true
|
|
||||||
k8sPodOwner: true
|
|
||||||
kubeApiserverAvailability: true
|
|
||||||
kubeApiserverBurnrate: true
|
|
||||||
kubeApiserverHistogram: true
|
|
||||||
kubeApiserverSlos: true
|
|
||||||
kubeControllerManager: true
|
|
||||||
kubelet: true
|
|
||||||
kubeProxy: true
|
|
||||||
kubePrometheusGeneral: true
|
|
||||||
kubePrometheusNodeRecording: true
|
|
||||||
kubernetesApps: true
|
|
||||||
kubernetesResources: true
|
|
||||||
kubernetesStorage: true
|
|
||||||
kubernetesSystem: true
|
|
||||||
kubeSchedulerAlerting: true
|
|
||||||
kubeSchedulerRecording: true
|
|
||||||
kubeStateMetrics: true
|
|
||||||
network: true
|
|
||||||
node: true
|
|
||||||
nodeExporterAlerting: true
|
|
||||||
nodeExporterRecording: true
|
|
||||||
prometheus: true
|
|
||||||
prometheusOperator: true
|
|
||||||
windows: true
|
|
||||||
windowsMonitoring:
|
|
||||||
enabled: {windows_monitoring}
|
|
||||||
grafana:
|
|
||||||
enabled: {grafana}
|
|
||||||
kubernetesServiceMonitors:
|
|
||||||
enabled: {kubernetes_service_monitors}
|
|
||||||
kubeApiServer:
|
|
||||||
enabled: {kubernetes_api_server}
|
|
||||||
kubelet:
|
|
||||||
enabled: {kubelet}
|
|
||||||
kubeControllerManager:
|
|
||||||
enabled: {kube_controller_manager}
|
|
||||||
coreDns:
|
|
||||||
enabled: {core_dns}
|
|
||||||
kubeEtcd:
|
|
||||||
enabled: {kube_etcd}
|
|
||||||
kubeScheduler:
|
|
||||||
enabled: {kube_scheduler}
|
|
||||||
kubeProxy:
|
|
||||||
enabled: {kube_proxy}
|
|
||||||
kubeStateMetrics:
|
|
||||||
enabled: {kube_state_metrics}
|
|
||||||
nodeExporter:
|
|
||||||
enabled: {node_exporter}
|
|
||||||
prometheusOperator:
|
|
||||||
enabled: {prometheus_operator}
|
|
||||||
prometheus:
|
|
||||||
enabled: {prometheus}
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
// add required null receiver for prometheus alert manager
|
|
||||||
let mut null_receiver = Mapping::new();
|
|
||||||
null_receiver.insert(
|
|
||||||
Value::String("receiver".to_string()),
|
|
||||||
Value::String("null".to_string()),
|
|
||||||
);
|
|
||||||
null_receiver.insert(
|
|
||||||
Value::String("matchers".to_string()),
|
|
||||||
Value::Sequence(vec![Value::String("alertname!=Watchdog".to_string())]),
|
|
||||||
);
|
|
||||||
null_receiver.insert(Value::String("continue".to_string()), Value::Bool(true));
|
|
||||||
|
|
||||||
//add alert channels
|
|
||||||
let mut alert_manager_channel_config = AlertManagerConfig {
|
|
||||||
global: Mapping::new(),
|
|
||||||
route: AlertManagerRoute {
|
|
||||||
routes: vec![Value::Mapping(null_receiver)],
|
|
||||||
},
|
|
||||||
receivers: vec![serde_yaml::from_str("name: 'null'").unwrap()],
|
|
||||||
};
|
|
||||||
for receiver in config.alert_receiver_configs.iter() {
|
|
||||||
if let Some(global) = receiver.channel_global_config.clone() {
|
|
||||||
alert_manager_channel_config
|
|
||||||
.global
|
|
||||||
.insert(global.0, global.1);
|
|
||||||
}
|
|
||||||
alert_manager_channel_config
|
|
||||||
.route
|
|
||||||
.routes
|
|
||||||
.push(receiver.channel_route.clone());
|
|
||||||
alert_manager_channel_config
|
|
||||||
.receivers
|
|
||||||
.push(receiver.channel_receiver.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let alert_manager_values = AlertManagerValues {
|
|
||||||
alertmanager: AlertManager {
|
|
||||||
enabled: config.alert_manager,
|
|
||||||
config: alert_manager_channel_config,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let alert_manager_yaml =
|
|
||||||
serde_yaml::to_string(&alert_manager_values).expect("Failed to serialize YAML");
|
|
||||||
debug!("serialized alert manager: \n {:#}", alert_manager_yaml);
|
|
||||||
values.push_str(&alert_manager_yaml);
|
|
||||||
|
|
||||||
//format alert manager additional rules for helm chart
|
|
||||||
let mut merged_rules: BTreeMap<String, AlertGroup> = BTreeMap::new();
|
|
||||||
|
|
||||||
for additional_rule in config.alert_rules.clone() {
|
|
||||||
for (key, group) in additional_rule.rules {
|
|
||||||
merged_rules.insert(key, group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let merged_rules = AlertManagerAdditionalPromRules {
|
|
||||||
rules: merged_rules,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut alert_manager_additional_rules = serde_yaml::Mapping::new();
|
|
||||||
let rules_value = serde_yaml::to_value(merged_rules).unwrap();
|
|
||||||
|
|
||||||
alert_manager_additional_rules.insert(
|
|
||||||
serde_yaml::Value::String("additionalPrometheusRulesMap".to_string()),
|
|
||||||
rules_value,
|
|
||||||
);
|
|
||||||
|
|
||||||
let alert_manager_additional_rules_yaml =
|
|
||||||
serde_yaml::to_string(&alert_manager_additional_rules).expect("Failed to serialize YAML");
|
|
||||||
debug!(
|
|
||||||
"alert_rules_yaml:\n{:#}",
|
|
||||||
alert_manager_additional_rules_yaml
|
|
||||||
);
|
|
||||||
|
|
||||||
values.push_str(&alert_manager_additional_rules_yaml);
|
|
||||||
debug!("full values.yaml: \n {:#}", values);
|
|
||||||
|
|
||||||
HelmChartScore {
|
|
||||||
namespace: Some(NonBlankString::from_str(&config.namespace).unwrap()),
|
|
||||||
release_name: NonBlankString::from_str("kube-prometheus").unwrap(),
|
|
||||||
chart_name: NonBlankString::from_str(
|
|
||||||
"oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack",
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
chart_version: None,
|
|
||||||
values_overrides: None,
|
|
||||||
values_yaml: Some(values.to_string()),
|
|
||||||
create_namespace: true,
|
|
||||||
install_only: true,
|
|
||||||
repository: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pub mod config;
|
|
||||||
pub mod kube_prometheus_helm_chart;
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use super::{helm::config::KubePrometheusConfig, prometheus::Prometheus};
|
|
||||||
use crate::{
|
|
||||||
score::Score,
|
|
||||||
topology::{
|
|
||||||
HelmCommand, Topology,
|
|
||||||
oberservability::monitoring::{AlertReceiver, AlertRule, AlertingInterpret},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
|
||||||
pub struct HelmPrometheusAlertingScore {
|
|
||||||
pub receivers: Vec<Box<dyn AlertReceiver<Prometheus>>>,
|
|
||||||
pub rules: Vec<Box<dyn AlertRule<Prometheus>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Topology + HelmCommand> Score<T> for HelmPrometheusAlertingScore {
|
|
||||||
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
|
|
||||||
Box::new(AlertingInterpret {
|
|
||||||
sender: Prometheus {
|
|
||||||
config: Arc::new(Mutex::new(KubePrometheusConfig::new())),
|
|
||||||
},
|
|
||||||
receivers: self.receivers.clone(),
|
|
||||||
rules: self.rules.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn name(&self) -> String {
|
|
||||||
"HelmPrometheusAlertingScore".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
pub mod helm;
|
|
||||||
pub mod helm_prometheus_alert_score;
|
|
||||||
pub mod prometheus;
|
|
||||||
pub mod types;
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use log::debug;
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
interpret::{InterpretError, Outcome},
|
|
||||||
inventory::Inventory,
|
|
||||||
modules::monitoring::alert_rule::prometheus_alert_rule::AlertManagerRuleGroup,
|
|
||||||
score,
|
|
||||||
topology::{
|
|
||||||
HelmCommand, Topology,
|
|
||||||
installable::Installable,
|
|
||||||
oberservability::monitoring::{AlertReceiver, AlertRule, AlertSender},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use score::Score;
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
helm::{
|
|
||||||
config::KubePrometheusConfig, kube_prometheus_helm_chart::kube_prometheus_helm_chart_score,
|
|
||||||
},
|
|
||||||
types::{AlertManagerAdditionalPromRules, AlertManagerChannelConfig},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AlertSender for Prometheus {
|
|
||||||
fn name(&self) -> String {
|
|
||||||
"HelmKubePrometheus".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<T: Topology + HelmCommand> Installable<T> for Prometheus {
|
|
||||||
async fn ensure_installed(
|
|
||||||
&self,
|
|
||||||
inventory: &Inventory,
|
|
||||||
topology: &T,
|
|
||||||
) -> Result<(), InterpretError> {
|
|
||||||
self.install_prometheus(inventory, topology).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Prometheus {
|
|
||||||
pub config: Arc<Mutex<KubePrometheusConfig>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Prometheus {
|
|
||||||
pub async fn install_receiver(
|
|
||||||
&self,
|
|
||||||
prometheus_receiver: &dyn PrometheusReceiver,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
let prom_receiver = prometheus_receiver.configure_receiver().await;
|
|
||||||
debug!(
|
|
||||||
"adding alert receiver to prometheus config: {:#?}",
|
|
||||||
&prom_receiver
|
|
||||||
);
|
|
||||||
let mut config = self.config.lock().unwrap();
|
|
||||||
|
|
||||||
config.alert_receiver_configs.push(prom_receiver);
|
|
||||||
let prom_receiver_name = prometheus_receiver.name();
|
|
||||||
debug!("installed alert receiver {}", &prom_receiver_name);
|
|
||||||
Ok(Outcome::success(format!(
|
|
||||||
"Sucessfully installed receiver {}",
|
|
||||||
prom_receiver_name
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn install_rule(
|
|
||||||
&self,
|
|
||||||
prometheus_rule: &AlertManagerRuleGroup,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
let prometheus_rule = prometheus_rule.configure_rule().await;
|
|
||||||
let mut config = self.config.lock().unwrap();
|
|
||||||
|
|
||||||
config.alert_rules.push(prometheus_rule.clone());
|
|
||||||
Ok(Outcome::success(format!(
|
|
||||||
"Successfully installed alert rule: {:#?},",
|
|
||||||
prometheus_rule
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn install_prometheus<T: Topology + HelmCommand + Send + Sync>(
|
|
||||||
&self,
|
|
||||||
inventory: &Inventory,
|
|
||||||
topology: &T,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
kube_prometheus_helm_chart_score(self.config.clone())
|
|
||||||
.create_interpret()
|
|
||||||
.execute(inventory, topology)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait PrometheusReceiver: Send + Sync + std::fmt::Debug {
|
|
||||||
fn name(&self) -> String;
|
|
||||||
async fn configure_receiver(&self) -> AlertManagerChannelConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for Box<dyn AlertReceiver<Prometheus>> {
|
|
||||||
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for Box<dyn AlertReceiver<Prometheus>> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
self.clone_box()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait PrometheusRule: Send + Sync + std::fmt::Debug {
|
|
||||||
fn name(&self) -> String;
|
|
||||||
async fn configure_rule(&self) -> AlertManagerAdditionalPromRules;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for Box<dyn AlertRule<Prometheus>> {
|
|
||||||
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for Box<dyn AlertRule<Prometheus>> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
self.clone_box()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use serde::Serialize;
|
|
||||||
use serde_yaml::{Mapping, Sequence, Value};
|
|
||||||
|
|
||||||
use crate::modules::monitoring::alert_rule::prometheus_alert_rule::AlertManagerRuleGroup;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait AlertChannelConfig {
|
|
||||||
async fn get_config(&self) -> AlertManagerChannelConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct AlertManagerValues {
|
|
||||||
pub alertmanager: AlertManager,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct AlertManager {
|
|
||||||
pub enabled: bool,
|
|
||||||
pub config: AlertManagerConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct AlertManagerConfig {
|
|
||||||
pub global: Mapping,
|
|
||||||
pub route: AlertManagerRoute,
|
|
||||||
pub receivers: Sequence,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct AlertManagerRoute {
|
|
||||||
pub routes: Sequence,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct AlertManagerChannelConfig {
|
|
||||||
///expecting an option that contains two values
|
|
||||||
///if necessary for the alertchannel
|
|
||||||
///[ jira_api_url: <string> ]
|
|
||||||
pub channel_global_config: Option<(Value, Value)>,
|
|
||||||
pub channel_route: Value,
|
|
||||||
pub channel_receiver: Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct AlertManagerAdditionalPromRules {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub rules: BTreeMap<String, AlertGroup>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct AlertGroup {
|
|
||||||
pub groups: Vec<AlertManagerRuleGroup>,
|
|
||||||
}
|
|
||||||
262
harmony/src/modules/monitoring/kube_prometheus_helm_chart.rs
Normal file
262
harmony/src/modules/monitoring/kube_prometheus_helm_chart.rs
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
use super::{config::KubePrometheusConfig, monitoring_alerting::AlertChannel};
|
||||||
|
use log::info;
|
||||||
|
use non_blank_string_rs::NonBlankString;
|
||||||
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::modules::helm::chart::HelmChartScore;
|
||||||
|
|
||||||
|
pub fn kube_prometheus_helm_chart_score(config: &KubePrometheusConfig) -> HelmChartScore {
|
||||||
|
//TODO this should be make into a rule with default formatting that can be easily passed as a vec
|
||||||
|
//to the overrides or something leaving the user to deal with formatting here seems bad
|
||||||
|
let default_rules = config.default_rules.to_string();
|
||||||
|
let windows_monitoring = config.windows_monitoring.to_string();
|
||||||
|
let alert_manager = config.alert_manager.to_string();
|
||||||
|
let grafana = config.grafana.to_string();
|
||||||
|
let kubernetes_service_monitors = config.kubernetes_service_monitors.to_string();
|
||||||
|
let kubernetes_api_server = config.kubernetes_api_server.to_string();
|
||||||
|
let kubelet = config.kubelet.to_string();
|
||||||
|
let kube_controller_manager = config.kube_controller_manager.to_string();
|
||||||
|
let core_dns = config.core_dns.to_string();
|
||||||
|
let kube_etcd = config.kube_etcd.to_string();
|
||||||
|
let kube_scheduler = config.kube_scheduler.to_string();
|
||||||
|
let kube_proxy = config.kube_proxy.to_string();
|
||||||
|
let kube_state_metrics = config.kube_state_metrics.to_string();
|
||||||
|
let node_exporter = config.node_exporter.to_string();
|
||||||
|
let prometheus_operator = config.prometheus_operator.to_string();
|
||||||
|
let prometheus = config.prometheus.to_string();
|
||||||
|
let mut values = format!(
|
||||||
|
r#"
|
||||||
|
additionalPrometheusRulesMap:
|
||||||
|
pods-status-alerts:
|
||||||
|
groups:
|
||||||
|
- name: pods
|
||||||
|
rules:
|
||||||
|
- alert: "[CRIT] POD not healthy"
|
||||||
|
expr: min_over_time(sum by (namespace, pod) (kube_pod_status_phase{{phase=~"Pending|Unknown|Failed"}})[15m:1m]) > 0
|
||||||
|
for: 0m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
title: "[CRIT] POD not healthy : {{{{ $labels.pod }}}}"
|
||||||
|
description: |
|
||||||
|
A POD is in a non-ready state!
|
||||||
|
- **Pod**: {{{{ $labels.pod }}}}
|
||||||
|
- **Namespace**: {{{{ $labels.namespace }}}}
|
||||||
|
- alert: "[CRIT] POD crash looping"
|
||||||
|
expr: increase(kube_pod_container_status_restarts_total[5m]) > 3
|
||||||
|
for: 0m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
title: "[CRIT] POD crash looping : {{{{ $labels.pod }}}}"
|
||||||
|
description: |
|
||||||
|
A POD is drowning in a crash loop!
|
||||||
|
- **Pod**: {{{{ $labels.pod }}}}
|
||||||
|
- **Namespace**: {{{{ $labels.namespace }}}}
|
||||||
|
- **Instance**: {{{{ $labels.instance }}}}
|
||||||
|
pvc-alerts:
|
||||||
|
groups:
|
||||||
|
- name: pvc-alerts
|
||||||
|
rules:
|
||||||
|
- alert: 'PVC Fill Over 95 Percent In 2 Days'
|
||||||
|
expr: |
|
||||||
|
(
|
||||||
|
kubelet_volume_stats_used_bytes
|
||||||
|
/
|
||||||
|
kubelet_volume_stats_capacity_bytes
|
||||||
|
) > 0.95
|
||||||
|
AND
|
||||||
|
predict_linear(kubelet_volume_stats_used_bytes[2d], 2 * 24 * 60 * 60)
|
||||||
|
/
|
||||||
|
kubelet_volume_stats_capacity_bytes
|
||||||
|
> 0.95
|
||||||
|
for: 1m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
description: The PVC {{{{ $labels.persistentvolumeclaim }}}} in namespace {{{{ $labels.namespace }}}} is predicted to fill over 95% in less than 2 days.
|
||||||
|
title: PVC {{{{ $labels.persistentvolumeclaim }}}} in namespace {{{{ $labels.namespace }}}} will fill over 95% in less than 2 days
|
||||||
|
defaultRules:
|
||||||
|
create: {default_rules}
|
||||||
|
rules:
|
||||||
|
alertmanager: true
|
||||||
|
etcd: true
|
||||||
|
configReloaders: true
|
||||||
|
general: true
|
||||||
|
k8sContainerCpuUsageSecondsTotal: true
|
||||||
|
k8sContainerMemoryCache: true
|
||||||
|
k8sContainerMemoryRss: true
|
||||||
|
k8sContainerMemorySwap: true
|
||||||
|
k8sContainerResource: true
|
||||||
|
k8sContainerMemoryWorkingSetBytes: true
|
||||||
|
k8sPodOwner: true
|
||||||
|
kubeApiserverAvailability: true
|
||||||
|
kubeApiserverBurnrate: true
|
||||||
|
kubeApiserverHistogram: true
|
||||||
|
kubeApiserverSlos: true
|
||||||
|
kubeControllerManager: true
|
||||||
|
kubelet: true
|
||||||
|
kubeProxy: true
|
||||||
|
kubePrometheusGeneral: true
|
||||||
|
kubePrometheusNodeRecording: true
|
||||||
|
kubernetesApps: true
|
||||||
|
kubernetesResources: true
|
||||||
|
kubernetesStorage: true
|
||||||
|
kubernetesSystem: true
|
||||||
|
kubeSchedulerAlerting: true
|
||||||
|
kubeSchedulerRecording: true
|
||||||
|
kubeStateMetrics: true
|
||||||
|
network: true
|
||||||
|
node: true
|
||||||
|
nodeExporterAlerting: true
|
||||||
|
nodeExporterRecording: true
|
||||||
|
prometheus: true
|
||||||
|
prometheusOperator: true
|
||||||
|
windows: true
|
||||||
|
windowsMonitoring:
|
||||||
|
enabled: {windows_monitoring}
|
||||||
|
grafana:
|
||||||
|
enabled: {grafana}
|
||||||
|
kubernetesServiceMonitors:
|
||||||
|
enabled: {kubernetes_service_monitors}
|
||||||
|
kubeApiServer:
|
||||||
|
enabled: {kubernetes_api_server}
|
||||||
|
kubelet:
|
||||||
|
enabled: {kubelet}
|
||||||
|
kubeControllerManager:
|
||||||
|
enabled: {kube_controller_manager}
|
||||||
|
coreDns:
|
||||||
|
enabled: {core_dns}
|
||||||
|
kubeEtcd:
|
||||||
|
enabled: {kube_etcd}
|
||||||
|
kubeScheduler:
|
||||||
|
enabled: {kube_scheduler}
|
||||||
|
kubeProxy:
|
||||||
|
enabled: {kube_proxy}
|
||||||
|
kubeStateMetrics:
|
||||||
|
enabled: {kube_state_metrics}
|
||||||
|
nodeExporter:
|
||||||
|
enabled: {node_exporter}
|
||||||
|
prometheusOperator:
|
||||||
|
enabled: {prometheus_operator}
|
||||||
|
prometheus:
|
||||||
|
enabled: {prometheus}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let alertmanager_config = alert_manager_yaml_builder(&config);
|
||||||
|
values.push_str(&alertmanager_config);
|
||||||
|
|
||||||
|
fn alert_manager_yaml_builder(config: &KubePrometheusConfig) -> String {
|
||||||
|
let mut receivers = String::new();
|
||||||
|
let mut routes = String::new();
|
||||||
|
let mut global_configs = String::new();
|
||||||
|
let alert_manager = config.alert_manager;
|
||||||
|
for alert_channel in &config.alert_channel {
|
||||||
|
match alert_channel {
|
||||||
|
AlertChannel::Discord { name, .. } => {
|
||||||
|
let (receiver, route) = discord_alert_builder(name);
|
||||||
|
info!("discord receiver: {} \nroute: {}", receiver, route);
|
||||||
|
receivers.push_str(&receiver);
|
||||||
|
routes.push_str(&route);
|
||||||
|
}
|
||||||
|
AlertChannel::Slack {
|
||||||
|
slack_channel,
|
||||||
|
webhook_url,
|
||||||
|
} => {
|
||||||
|
let (receiver, route) = slack_alert_builder(slack_channel);
|
||||||
|
info!("slack receiver: {} \nroute: {}", receiver, route);
|
||||||
|
receivers.push_str(&receiver);
|
||||||
|
|
||||||
|
routes.push_str(&route);
|
||||||
|
let global_config = format!(
|
||||||
|
r#"
|
||||||
|
global:
|
||||||
|
slack_api_url: {webhook_url}"#
|
||||||
|
);
|
||||||
|
|
||||||
|
global_configs.push_str(&global_config);
|
||||||
|
}
|
||||||
|
AlertChannel::Smpt { .. } => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("after alert receiver: {}", receivers);
|
||||||
|
info!("after alert routes: {}", routes);
|
||||||
|
|
||||||
|
let alertmanager_config = format!(
|
||||||
|
r#"
|
||||||
|
alertmanager:
|
||||||
|
enabled: {alert_manager}
|
||||||
|
config: {global_configs}
|
||||||
|
route:
|
||||||
|
group_by: ['job']
|
||||||
|
group_wait: 30s
|
||||||
|
group_interval: 5m
|
||||||
|
repeat_interval: 12h
|
||||||
|
routes:
|
||||||
|
{routes}
|
||||||
|
receivers:
|
||||||
|
- name: 'null'
|
||||||
|
{receivers}"#
|
||||||
|
);
|
||||||
|
|
||||||
|
info!("alert manager config: {}", alertmanager_config);
|
||||||
|
alertmanager_config
|
||||||
|
}
|
||||||
|
|
||||||
|
HelmChartScore {
|
||||||
|
namespace: Some(NonBlankString::from_str(&config.namespace).unwrap()),
|
||||||
|
release_name: NonBlankString::from_str("kube-prometheus").unwrap(),
|
||||||
|
chart_name: NonBlankString::from_str(
|
||||||
|
"oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack",
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
chart_version: None,
|
||||||
|
values_overrides: None,
|
||||||
|
values_yaml: Some(values.to_string()),
|
||||||
|
create_namespace: true,
|
||||||
|
install_only: true,
|
||||||
|
repository: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discord_alert_builder(release_name: &String) -> (String, String) {
|
||||||
|
let discord_receiver_name = format!("Discord-{}", release_name);
|
||||||
|
let receiver = format!(
|
||||||
|
r#"
|
||||||
|
- name: '{discord_receiver_name}'
|
||||||
|
webhook_configs:
|
||||||
|
- url: 'http://{release_name}-alertmanager-discord:9094'
|
||||||
|
send_resolved: true"#,
|
||||||
|
);
|
||||||
|
let route = format!(
|
||||||
|
r#"
|
||||||
|
- receiver: '{discord_receiver_name}'
|
||||||
|
matchers:
|
||||||
|
- alertname!=Watchdog
|
||||||
|
continue: true"#,
|
||||||
|
);
|
||||||
|
(receiver, route)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slack_alert_builder(slack_channel: &String) -> (String, String) {
|
||||||
|
let slack_receiver_name = format!("Slack-{}", slack_channel);
|
||||||
|
let receiver = format!(
|
||||||
|
r#"
|
||||||
|
- name: '{slack_receiver_name}'
|
||||||
|
slack_configs:
|
||||||
|
- channel: '{slack_channel}'
|
||||||
|
send_resolved: true
|
||||||
|
title: '{{{{ .CommonAnnotations.title }}}}'
|
||||||
|
text: '{{{{ .CommonAnnotations.description }}}}'"#,
|
||||||
|
);
|
||||||
|
let route = format!(
|
||||||
|
r#"
|
||||||
|
- receiver: '{slack_receiver_name}'
|
||||||
|
matchers:
|
||||||
|
- alertname!=Watchdog
|
||||||
|
continue: true"#,
|
||||||
|
);
|
||||||
|
(receiver, route)
|
||||||
|
}
|
||||||
108
harmony/src/modules/monitoring/kube_prometheus_monitor.rs
Normal file
108
harmony/src/modules/monitoring/kube_prometheus_monitor.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_yaml::Value;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
data::{Id, Version},
|
||||||
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||||
|
inventory::Inventory,
|
||||||
|
score::Score,
|
||||||
|
topology::{
|
||||||
|
HelmCommand, Topology,
|
||||||
|
oberservability::monitoring::{AlertReceiverDeployment, Monitor},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
config::KubePrometheusConfig, kube_prometheus_helm_chart::kube_prometheus_helm_chart_score,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct KubePrometheus<T> {
|
||||||
|
alert_receivers: Vec<Box<dyn AlertReceiverDeployment<T>>>,
|
||||||
|
config: KubePrometheusConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait AlertManagerConfig<T> {
|
||||||
|
async fn get_alert_manager_config(&self) -> Result<Value, InterpretError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology> KubePrometheus<T> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
alert_receivers: Vec::new(),
|
||||||
|
config: KubePrometheusConfig::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology + HelmCommand + std::fmt::Debug> Monitor<T> for KubePrometheus<T> {
|
||||||
|
async fn deploy_monitor(&self, topology: &T) -> Result<Outcome, InterpretError> {
|
||||||
|
for alert_receiver in &self.alert_receivers {
|
||||||
|
alert_receiver.deploy_alert_receiver(topology).await?;
|
||||||
|
}
|
||||||
|
let score = KubePrometheusScore {
|
||||||
|
config: self.config.clone(),
|
||||||
|
};
|
||||||
|
let inventory = Inventory::autoload();
|
||||||
|
score.create_interpret().execute(&inventory, topology).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_monitor(&self, _topolgy: &T) -> Result<Outcome, InterpretError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
struct KubePrometheusScore {
|
||||||
|
config: KubePrometheusConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology + HelmCommand> Score<T> for KubePrometheusScore {
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(KubePromethusScoreInterpret {
|
||||||
|
score: self.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
struct KubePromethusScoreInterpret {
|
||||||
|
score: KubePrometheusScore,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology + HelmCommand> Interpret<T> for KubePromethusScoreInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
topology: &T,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
kube_prometheus_helm_chart_score(&self.score.config)
|
||||||
|
.create_interpret()
|
||||||
|
.execute(inventory, topology)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_name(&self) -> InterpretName {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_version(&self) -> Version {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_status(&self) -> InterpretStatus {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_children(&self) -> Vec<Id> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
pub mod alert_channel;
|
pub mod alertmanager_types;
|
||||||
pub mod alert_rule;
|
mod config;
|
||||||
pub mod kube_prometheus;
|
mod discord_alert_manager;
|
||||||
|
pub mod discord_webhook_sender;
|
||||||
|
mod kube_prometheus_helm_chart;
|
||||||
|
pub mod kube_prometheus_monitor;
|
||||||
|
pub mod monitoring_alerting;
|
||||||
|
|||||||
160
harmony/src/modules/monitoring/monitoring_alerting.rs
Normal file
160
harmony/src/modules/monitoring/monitoring_alerting.rs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use email_address::EmailAddress;
|
||||||
|
|
||||||
|
use log::info;
|
||||||
|
use serde::Serialize;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
data::{Id, Version},
|
||||||
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||||
|
inventory::Inventory,
|
||||||
|
score::Score,
|
||||||
|
topology::{HelmCommand, Topology},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
config::KubePrometheusConfig, kube_prometheus_helm_chart::kube_prometheus_helm_chart_score,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub enum AlertChannel {
|
||||||
|
Discord {
|
||||||
|
name: String,
|
||||||
|
webhook_url: Url,
|
||||||
|
},
|
||||||
|
Slack {
|
||||||
|
slack_channel: String,
|
||||||
|
webhook_url: Url,
|
||||||
|
},
|
||||||
|
//TODO test and implement in helm chart
|
||||||
|
//currently does not work
|
||||||
|
Smpt {
|
||||||
|
email_address: EmailAddress,
|
||||||
|
service_name: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct MonitoringAlertingStackScore {
|
||||||
|
pub alert_channel: Vec<AlertChannel>,
|
||||||
|
pub namespace: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MonitoringAlertingStackScore {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
alert_channel: Vec::new(),
|
||||||
|
namespace: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Topology + HelmCommand> Score<T> for MonitoringAlertingStackScore {
|
||||||
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
||||||
|
Box::new(MonitoringAlertingStackInterpret {
|
||||||
|
score: self.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn name(&self) -> String {
|
||||||
|
format!("MonitoringAlertingStackScore")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
struct MonitoringAlertingStackInterpret {
|
||||||
|
score: MonitoringAlertingStackScore,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MonitoringAlertingStackInterpret {
|
||||||
|
async fn build_kube_prometheus_helm_chart_config(&self) -> KubePrometheusConfig {
|
||||||
|
let mut config = KubePrometheusConfig::new();
|
||||||
|
if let Some(ns) = &self.score.namespace {
|
||||||
|
config.namespace = ns.clone();
|
||||||
|
}
|
||||||
|
config.alert_channel = self.score.alert_channel.clone();
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn deploy_kube_prometheus_helm_chart_score<T: Topology + HelmCommand>(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
topology: &T,
|
||||||
|
config: &KubePrometheusConfig,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
let helm_chart = kube_prometheus_helm_chart_score(config);
|
||||||
|
helm_chart
|
||||||
|
.create_interpret()
|
||||||
|
.execute(inventory, topology)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn deploy_alert_channel_service<T: Topology + HelmCommand>(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
topology: &T,
|
||||||
|
config: &KubePrometheusConfig,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
//let mut outcomes = vec![];
|
||||||
|
|
||||||
|
//for channel in &self.score.alert_channel {
|
||||||
|
// let outcome = match channel {
|
||||||
|
// AlertChannel::Discord { .. } => {
|
||||||
|
// discord_alert_manager_score(config)
|
||||||
|
// .create_interpret()
|
||||||
|
// .execute(inventory, topology)
|
||||||
|
// .await
|
||||||
|
// }
|
||||||
|
// AlertChannel::Slack { .. } => Ok(Outcome::success(
|
||||||
|
// "No extra configs for slack alerting".to_string(),
|
||||||
|
// )),
|
||||||
|
// AlertChannel::Smpt { .. } => {
|
||||||
|
// todo!()
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// outcomes.push(outcome);
|
||||||
|
//}
|
||||||
|
//for result in outcomes {
|
||||||
|
// result?;
|
||||||
|
//}
|
||||||
|
|
||||||
|
Ok(Outcome::success("All alert channels deployed".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Topology + HelmCommand> Interpret<T> for MonitoringAlertingStackInterpret {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
inventory: &Inventory,
|
||||||
|
topology: &T,
|
||||||
|
) -> Result<Outcome, InterpretError> {
|
||||||
|
let config = self.build_kube_prometheus_helm_chart_config().await;
|
||||||
|
info!("Built kube prometheus config");
|
||||||
|
info!("Installing kube prometheus chart");
|
||||||
|
self.deploy_kube_prometheus_helm_chart_score(inventory, topology, &config)
|
||||||
|
.await?;
|
||||||
|
info!("Installing alert channel service");
|
||||||
|
self.deploy_alert_channel_service(inventory, topology, &config)
|
||||||
|
.await?;
|
||||||
|
Ok(Outcome::success(format!(
|
||||||
|
"succesfully deployed monitoring and alerting stack"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_name(&self) -> InterpretName {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_version(&self) -> Version {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_status(&self) -> InterpretStatus {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_children(&self) -> Vec<Id> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
use crate::modules::monitoring::alert_rule::prometheus_alert_rule::PrometheusAlertRule;
|
|
||||||
|
|
||||||
pub fn global_storage_status_degraded_non_critical() -> PrometheusAlertRule {
|
|
||||||
PrometheusAlertRule::new("GlobalStorageStatusNonCritical", "globalStorageStatus == 4")
|
|
||||||
.for_duration("5m")
|
|
||||||
.label("severity", "warning")
|
|
||||||
.annotation(
|
|
||||||
"description",
|
|
||||||
"- **system**: {{ $labels.instance }}\n- **Status**: nonCritical\n- **Value**: {{ $value }}\n- **Job**: {{ $labels.job }}",
|
|
||||||
)
|
|
||||||
.annotation("title", " System storage status is in degraded state")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn alert_global_storage_status_critical() -> PrometheusAlertRule {
|
|
||||||
PrometheusAlertRule::new(
|
|
||||||
"GlobalStorageStatus critical",
|
|
||||||
"globalStorageStatus == 5",
|
|
||||||
)
|
|
||||||
.for_duration("5m")
|
|
||||||
.label("severity", "warning")
|
|
||||||
.annotation("title", "System storage status is critical at {{ $labels.instance }}")
|
|
||||||
.annotation(
|
|
||||||
"description",
|
|
||||||
"- **System**: {{ $labels.instance }}\n- **Status**: Critical\n- **Value**: {{ $value }}\n- **Job**: {{ $labels.job }}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn alert_global_storage_status_non_recoverable() -> PrometheusAlertRule {
|
|
||||||
PrometheusAlertRule::new(
|
|
||||||
"GlobalStorageStatus nonRecoverable",
|
|
||||||
"globalStorageStatus == 6",
|
|
||||||
)
|
|
||||||
.for_duration("5m")
|
|
||||||
.label("severity", "warning")
|
|
||||||
.annotation("title", "System storage status is nonRecoverable at {{ $labels.instance }}")
|
|
||||||
.annotation(
|
|
||||||
"description",
|
|
||||||
"- **System**: {{ $labels.instance }}\n- **Status**: nonRecoverable\n- **Value**: {{ $value }}\n- **Job**: {{ $labels.job }}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub mod dell_server;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub mod pvc;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
use crate::modules::monitoring::alert_rule::prometheus_alert_rule::PrometheusAlertRule;
|
|
||||||
|
|
||||||
pub fn high_pvc_fill_rate_over_two_days() -> PrometheusAlertRule {
|
|
||||||
PrometheusAlertRule::new(
|
|
||||||
"PVC Fill Over 95 Percent In 2 Days",
|
|
||||||
"(kubelet_volume_stats_used_bytes/kubelet_volume_stats_capacity_bytes) > 0.95 AND predict_linear(kubelet_volume_stats_used_bytes[2d], 2 * 24 * 60 * 60)/kubelet_volume_stats_capacity_bytes > 0.95",)
|
|
||||||
.for_duration("1m")
|
|
||||||
.label("severity", "warning")
|
|
||||||
.annotation("summary", "The PVC {{ $labels.persistentvolumeclaim }} in namespace {{ $labels.namespace }} is predicted to fill over 95% in less than 2 days.")
|
|
||||||
.annotation("description", "PVC {{ $labels.persistentvolumeclaim }} in namespace {{ $labels.namespace }} will fill over 95% in less than 2 days",)
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pub mod infra;
|
|
||||||
pub mod k8s;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub mod alerts;
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use crate::{interpret::InterpretError, score::Score, topology::Topology};
|
|
||||||
|
|
||||||
/// Create and manage Tenant Credentials.
|
|
||||||
///
|
|
||||||
/// This is meant to be used by cluster administrators who need to provide their tenant users and
|
|
||||||
/// services with credentials to access their resources.
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct TenantCredentialScore;
|
|
||||||
|
|
||||||
impl<T: Topology + TenantCredentialManager> Score<T> for TenantCredentialScore {
|
|
||||||
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> String {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait TenantCredentialManager {
|
|
||||||
async fn create_user(&self) -> Result<TenantCredentialBundle, InterpretError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CredentialMetadata {
|
|
||||||
pub tenant_id: String,
|
|
||||||
pub credential_id: String,
|
|
||||||
pub description: String,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub expires_at: Option<DateTime<Utc>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum CredentialData {
|
|
||||||
/// Used to store login instructions destined to a human. Akin to AWS login instructions email
|
|
||||||
/// upon new console user creation.
|
|
||||||
PlainText(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TenantCredentialBundle {
|
|
||||||
_metadata: CredentialMetadata,
|
|
||||||
_content: CredentialData,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TenantCredentialBundle {}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
mod credentials;
|
|
||||||
pub use credentials::*;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
data::{Id, Version},
|
|
||||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
|
||||||
inventory::Inventory,
|
|
||||||
score::Score,
|
|
||||||
topology::{
|
|
||||||
Topology,
|
|
||||||
tenant::{TenantConfig, TenantManager},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone)]
|
|
||||||
pub struct TenantScore {
|
|
||||||
pub config: TenantConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Topology + TenantManager> Score<T> for TenantScore {
|
|
||||||
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
|
|
||||||
Box::new(TenantInterpret {
|
|
||||||
tenant_config: self.config.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> String {
|
|
||||||
format!("{} TenantScore", self.config.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TenantInterpret {
|
|
||||||
tenant_config: TenantConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<T: Topology + TenantManager> Interpret<T> for TenantInterpret {
|
|
||||||
async fn execute(
|
|
||||||
&self,
|
|
||||||
_inventory: &Inventory,
|
|
||||||
topology: &T,
|
|
||||||
) -> Result<Outcome, InterpretError> {
|
|
||||||
topology.provision_tenant(&self.tenant_config).await?;
|
|
||||||
|
|
||||||
Ok(Outcome::success(format!(
|
|
||||||
"Successfully provisioned tenant {} with id {}",
|
|
||||||
self.tenant_config.name, self.tenant_config.id
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_name(&self) -> InterpretName {
|
|
||||||
InterpretName::TenantInterpret
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_version(&self) -> Version {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_status(&self) -> InterpretStatus {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_children(&self) -> Vec<Id> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
## Quick demo
|
|
||||||
|
|
||||||
`cargo run -p example-tui`
|
|
||||||
|
|
||||||
This will launch Harmony's minimalist terminal ui which embeds a few demo scores.
|
|
||||||
|
|
||||||
Usage instructions will be displayed at the bottom of the TUI.
|
|
||||||
|
|
||||||
`cargo run --bin example-cli -- --help`
|
|
||||||
|
|
||||||
This is the harmony CLI, a minimal implementation
|
|
||||||
|
|
||||||
The current help text:
|
|
||||||
|
|
||||||
```
|
|
||||||
Usage: example-cli [OPTIONS]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-y, --yes Run score(s) or not
|
|
||||||
-f, --filter <FILTER> Filter query
|
|
||||||
-i, --interactive Run interactive TUI or not
|
|
||||||
-a, --all Run all or nth, defaults to all
|
|
||||||
-n, --number <NUMBER> Run nth matching, zero indexed [default: 0]
|
|
||||||
-l, --list list scores, will also be affected by run filter
|
|
||||||
-h, --help Print help
|
|
||||||
-V, --version Print version```
|
|
||||||
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "harmony_composer"
|
|
||||||
edition = "2024"
|
|
||||||
version.workspace = true
|
|
||||||
readme.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
clap = { version = "4.5.35", features = ["derive"] }
|
|
||||||
tokio.workspace = true
|
|
||||||
env_logger.workspace = true
|
|
||||||
log.workspace = true
|
|
||||||
bollard = "0.19.0"
|
|
||||||
current_platform = "0.2.0"
|
|
||||||
futures-util = "0.3.31"
|
|
||||||
serde_json = "1.0.140"
|
|
||||||
cargo_metadata = "0.20.0"
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# harmony_composer
|
|
||||||
|
|
||||||
`harmony_composer` is a command-line utility for compiling and bootstrapping deployments for the Harmony orchestration framework.
|
|
||||||
|
|
||||||
It's designed to simplify the build process by either compiling a Harmony project found in a local harmony directory or by bootstrapping a new deployment through auto-detection of the current project type.
|
|
||||||
|
|
||||||
## ⚡ Quick Install & Run (Linux x86-64)
|
|
||||||
|
|
||||||
You can download and run the latest snapshot build with a single command. This will place the binary in ~/.local/bin, which should be in your PATH on most modern Linux distributions.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
|
|
||||||
curl -Ls https://git.nationtech.io/NationTech/harmony/releases/download/snapshot-latest/harmony_composer \
|
|
||||||
-o ~/.local/bin/harmony_composer && \
|
|
||||||
chmod +x ~/.local/bin/harmony_composer
|
|
||||||
```
|
|
||||||
|
|
||||||
> ⚠️ Warning: Unstable Builds
|
|
||||||
> The snapshot-latest tag points to the latest build from the master branch. It is unstable, unsupported, and intended only for early testing of new features. Please do not use it in production environments.
|
|
||||||
|
|
||||||
## ⚙️ How It Works
|
|
||||||
|
|
||||||
harmony_composer requires either cargo or docker to be available on your system to compile the Harmony project.
|
|
||||||
|
|
||||||
- If cargo is found: It will be used to compile the project locally.
|
|
||||||
- If cargo is not found: It will automatically download and run the harmony_composer Docker image. This image is a self-contained build environment with the required Cargo binary and build targets for both Linux and Windows.
|
|
||||||
- If both cargo and docker are unavailable, `harmony_composer` will fail. Please install one of them.
|
|
||||||
|
|
||||||
## 📖 Basic Usage
|
|
||||||
|
|
||||||
Here are some common commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
|
|
||||||
# Compile the repo's Harmony module
|
|
||||||
harmony_composer compile
|
|
||||||
|
|
||||||
# Run check script on the project
|
|
||||||
harmony_composer check
|
|
||||||
|
|
||||||
# Run the repo's entire harmony deployment sequence
|
|
||||||
harmony_composer deploy
|
|
||||||
|
|
||||||
# Run the full check, compile, and deploy pipeline
|
|
||||||
harmony_composer all
|
|
||||||
```
|
|
||||||
|
|
||||||
For a full list of commands and their options, run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
|
|
||||||
harmony_composer --help
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🏗️ Supported Architectures
|
|
||||||
|
|
||||||
The build system currently supports compiling for:
|
|
||||||
|
|
||||||
x86_64-unknown-linux-gnu
|
|
||||||
x86_64-pc-windows-gnu
|
|
||||||
|
|
||||||
More target architectures are planned. If your platform is not yet supported, please open a feature request in the main repository.
|
|
||||||
|
|
||||||
## 🔗 Main Project
|
|
||||||
|
|
||||||
This tool is a small part of the main Harmony project. For complete documentation, contribution guidelines, and license information, please refer to the main repository.
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
use bollard::models::ContainerCreateBody;
|
|
||||||
use bollard::query_parameters::{
|
|
||||||
CreateContainerOptionsBuilder, ListContainersOptionsBuilder, LogsOptions,
|
|
||||||
RemoveContainerOptions, StartContainerOptions, WaitContainerOptions,
|
|
||||||
};
|
|
||||||
use bollard::secret::HostConfig;
|
|
||||||
use cargo_metadata::{Artifact, Message, MetadataCommand};
|
|
||||||
use clap::{Args, Parser, Subcommand};
|
|
||||||
use futures_util::StreamExt;
|
|
||||||
use log::info;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
use tokio::fs;
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(version, about, long_about = None, flatten_help = true, propagate_version = true)]
|
|
||||||
struct GlobalArgs {
|
|
||||||
#[arg(long, default_value = "harmony")]
|
|
||||||
harmony_path: String,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
compile_method: Option<CompileMethod>,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
compile_platform: Option<String>,
|
|
||||||
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: Option<Commands>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand, Clone, Debug)]
|
|
||||||
enum Commands {
|
|
||||||
Check(CheckArgs),
|
|
||||||
Compile,
|
|
||||||
Deploy(DeployArgs),
|
|
||||||
All(AllArgs),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Args, Clone, Debug)]
|
|
||||||
struct CheckArgs {
|
|
||||||
#[arg(long, default_value = "check.sh")]
|
|
||||||
check_script_path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Args, Clone, Debug)]
|
|
||||||
struct DeployArgs {
|
|
||||||
#[arg(long, default_value_t = false)]
|
|
||||||
staging: bool,
|
|
||||||
|
|
||||||
#[arg(long, default_value_t = false)]
|
|
||||||
prod: bool,
|
|
||||||
|
|
||||||
#[arg(long, default_value_t = false)]
|
|
||||||
smoke_test: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Args, Clone, Debug)]
|
|
||||||
struct AllArgs {
|
|
||||||
#[command(flatten)]
|
|
||||||
check: CheckArgs,
|
|
||||||
|
|
||||||
#[command(flatten)]
|
|
||||||
deploy: DeployArgs,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
env_logger::init();
|
|
||||||
let cli_args = GlobalArgs::parse();
|
|
||||||
|
|
||||||
let harmony_path = Path::new(&cli_args.harmony_path)
|
|
||||||
.try_exists()
|
|
||||||
.expect("couldn't check if path exists");
|
|
||||||
|
|
||||||
let harmony_bin_path: PathBuf;
|
|
||||||
match harmony_path {
|
|
||||||
true => {
|
|
||||||
harmony_bin_path = compile_harmony(
|
|
||||||
cli_args.compile_method,
|
|
||||||
cli_args.compile_platform,
|
|
||||||
cli_args.harmony_path.clone(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
false => todo!("implement autodetect code"),
|
|
||||||
}
|
|
||||||
|
|
||||||
match cli_args.command {
|
|
||||||
Some(command) => match command {
|
|
||||||
Commands::Check(args) => {
|
|
||||||
let check_script_str =
|
|
||||||
format!("{}/{}", cli_args.harmony_path, args.check_script_path);
|
|
||||||
|
|
||||||
let check_script = Path::new(&check_script_str);
|
|
||||||
|
|
||||||
match check_script
|
|
||||||
.try_exists()
|
|
||||||
.expect("couldn't check if path exists")
|
|
||||||
{
|
|
||||||
true => (),
|
|
||||||
false => todo!("implement couldn't find path logic"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let check_output = Command::new(check_script)
|
|
||||||
.output()
|
|
||||||
.expect("failed to run check script");
|
|
||||||
info!(
|
|
||||||
"check stdout: {}, check stderr: {}",
|
|
||||||
String::from_utf8(check_output.stdout).expect("couldn't parse from utf8"),
|
|
||||||
String::from_utf8(check_output.stderr).expect("couldn't parse from utf8")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Commands::Deploy(args) => {
|
|
||||||
if args.staging {
|
|
||||||
todo!("implement staging deployment");
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.prod {
|
|
||||||
todo!("implement prod deployment");
|
|
||||||
}
|
|
||||||
let deploy_output = Command::new(harmony_bin_path)
|
|
||||||
.arg("-y")
|
|
||||||
.arg("-a")
|
|
||||||
.output()
|
|
||||||
.expect("failed to run harmony deploy");
|
|
||||||
println!(
|
|
||||||
"deploy output: {}",
|
|
||||||
String::from_utf8(deploy_output.stdout).expect("couldn't parse from utf8")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Commands::All(_args) => todo!(
|
|
||||||
"take all previous match arms and turn them into separate functions, and call them all one after the other"
|
|
||||||
),
|
|
||||||
Commands::Compile => return,
|
|
||||||
},
|
|
||||||
None => todo!("run interactively, ask for info on CLI"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, clap::ValueEnum)]
|
|
||||||
enum CompileMethod {
|
|
||||||
LocalCargo,
|
|
||||||
Docker,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn compile_harmony(
|
|
||||||
method: Option<CompileMethod>,
|
|
||||||
platform: Option<String>,
|
|
||||||
harmony_location: String,
|
|
||||||
) -> PathBuf {
|
|
||||||
let platform = match platform {
|
|
||||||
Some(p) => p,
|
|
||||||
None => current_platform::CURRENT_PLATFORM.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let cargo_exists = Command::new("which")
|
|
||||||
.arg("cargo")
|
|
||||||
.status()
|
|
||||||
.expect("couldn't get `which cargo` status")
|
|
||||||
.success();
|
|
||||||
|
|
||||||
let method = match method {
|
|
||||||
Some(m) => m,
|
|
||||||
None => {
|
|
||||||
if cargo_exists {
|
|
||||||
return compile_cargo(platform, harmony_location).await;
|
|
||||||
} else {
|
|
||||||
return compile_docker(platform, harmony_location).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match method {
|
|
||||||
CompileMethod::LocalCargo => return compile_cargo(platform, harmony_location).await,
|
|
||||||
CompileMethod::Docker => return compile_docker(platform, harmony_location).await,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: make sure this works with cargo workspaces
|
|
||||||
async fn compile_cargo(platform: String, harmony_location: String) -> PathBuf {
|
|
||||||
let metadata = MetadataCommand::new()
|
|
||||||
.manifest_path(format!("{}/Cargo.toml", harmony_location))
|
|
||||||
.exec()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut cargo_build = Command::new("cargo")
|
|
||||||
.current_dir(&harmony_location)
|
|
||||||
.args(vec![
|
|
||||||
"build",
|
|
||||||
format!("--target={}", platform).as_str(),
|
|
||||||
"--release",
|
|
||||||
"--message-format=json-render-diagnostics",
|
|
||||||
])
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.spawn()
|
|
||||||
.expect("run cargo command failed");
|
|
||||||
|
|
||||||
let mut artifacts: Vec<Artifact> = vec![];
|
|
||||||
let reader = std::io::BufReader::new(cargo_build.stdout.take().unwrap());
|
|
||||||
for message in cargo_metadata::Message::parse_stream(reader) {
|
|
||||||
match message.unwrap() {
|
|
||||||
Message::CompilerMessage(_msg) => (),
|
|
||||||
Message::CompilerArtifact(artifact) => {
|
|
||||||
if artifact.manifest_path
|
|
||||||
== metadata
|
|
||||||
.root_package()
|
|
||||||
.expect("failed to get root package")
|
|
||||||
.manifest_path
|
|
||||||
{
|
|
||||||
println!("{:?}", artifact);
|
|
||||||
artifacts.push(artifact);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Message::BuildScriptExecuted(_script) => (),
|
|
||||||
Message::BuildFinished(finished) => {
|
|
||||||
println!("{:?}", finished);
|
|
||||||
}
|
|
||||||
_ => (), // Unknown message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let bin = artifacts
|
|
||||||
.last()
|
|
||||||
.expect("no binaries built")
|
|
||||||
.filenames
|
|
||||||
.first()
|
|
||||||
.expect("couldn't get filename");
|
|
||||||
|
|
||||||
let bin_out;
|
|
||||||
if let Some(ext) = bin.extension() {
|
|
||||||
bin_out = PathBuf::from(format!("{}/harmony.{}", harmony_location, ext));
|
|
||||||
let _copy_res = fs::copy(&bin, &bin_out).await;
|
|
||||||
} else {
|
|
||||||
bin_out = PathBuf::from(format!("{}/harmony", harmony_location));
|
|
||||||
let _copy_res = fs::copy(&bin, &bin_out).await;
|
|
||||||
}
|
|
||||||
return bin_out;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn compile_docker(platform: String, harmony_location: String) -> PathBuf {
|
|
||||||
let docker_client =
|
|
||||||
bollard::Docker::connect_with_local_defaults().expect("couldn't connect to docker");
|
|
||||||
|
|
||||||
let mut filters = HashMap::new();
|
|
||||||
filters.insert(String::from("name"), vec![String::from("harmony_build")]);
|
|
||||||
let list_containers_options = ListContainersOptionsBuilder::new()
|
|
||||||
.all(true)
|
|
||||||
.filters(&filters)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let containers = &docker_client
|
|
||||||
.list_containers(Some(list_containers_options))
|
|
||||||
.await
|
|
||||||
.expect("list containers failed");
|
|
||||||
|
|
||||||
if containers.len() > 0 {
|
|
||||||
docker_client
|
|
||||||
.remove_container("harmony_build", None::<RemoveContainerOptions>)
|
|
||||||
.await
|
|
||||||
.expect("failed to remove container");
|
|
||||||
}
|
|
||||||
|
|
||||||
let options = Some(
|
|
||||||
CreateContainerOptionsBuilder::new()
|
|
||||||
.name("harmony_build")
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let config = ContainerCreateBody {
|
|
||||||
image: Some("hub.nationtech.io/harmony/harmony_composer".to_string()),
|
|
||||||
working_dir: Some("/mnt".to_string()),
|
|
||||||
cmd: Some(vec![
|
|
||||||
format!("--compile-platform={}", platform),
|
|
||||||
format!("--harmony-path=/mnt"),
|
|
||||||
"compile".to_string(),
|
|
||||||
]),
|
|
||||||
host_config: Some(HostConfig {
|
|
||||||
binds: Some(vec![format!("{}:/mnt", harmony_location)]),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
docker_client
|
|
||||||
.create_container(options, config)
|
|
||||||
.await
|
|
||||||
.expect("couldn't create container");
|
|
||||||
|
|
||||||
docker_client
|
|
||||||
.start_container("harmony_build", None::<StartContainerOptions>)
|
|
||||||
.await
|
|
||||||
.expect("couldn't start container");
|
|
||||||
|
|
||||||
let wait_options = WaitContainerOptions {
|
|
||||||
condition: "not-running".to_string(),
|
|
||||||
};
|
|
||||||
let mut wait = docker_client
|
|
||||||
.wait_container("harmony_build", Some(wait_options))
|
|
||||||
.boxed();
|
|
||||||
|
|
||||||
let mut logs_stream = docker_client.logs(
|
|
||||||
"harmony_build",
|
|
||||||
Some(LogsOptions {
|
|
||||||
follow: true,
|
|
||||||
stdout: true,
|
|
||||||
stderr: true,
|
|
||||||
tail: "all".to_string(),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
while let Some(l) = logs_stream.next().await {
|
|
||||||
let l_str = l.expect("couldn't unwrap logoutput").to_string();
|
|
||||||
println!("{}", l_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait until container is no longer running
|
|
||||||
while let Some(_) = wait.next().await {}
|
|
||||||
|
|
||||||
// hack that should be cleaned up
|
|
||||||
if platform.contains("windows") {
|
|
||||||
return PathBuf::from(format!("{}/harmony.exe", harmony_location));
|
|
||||||
} else {
|
|
||||||
return PathBuf::from(format!("{}/harmony", harmony_location));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,6 @@ quote = "1.0.37"
|
|||||||
serde = "1.0.217"
|
serde = "1.0.217"
|
||||||
serde_yaml = "0.9.34"
|
serde_yaml = "0.9.34"
|
||||||
syn = "2.0.90"
|
syn = "2.0.90"
|
||||||
cidr.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde = { version = "1.0.217", features = ["derive"] }
|
serde = { version = "1.0.217", features = ["derive"] }
|
||||||
|
|||||||
@@ -132,16 +132,3 @@ pub fn ingress_path(input: TokenStream) -> TokenStream {
|
|||||||
false => panic!("Invalid ingress path"),
|
false => panic!("Invalid ingress path"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro]
|
|
||||||
pub fn cidrv4(input: TokenStream) -> TokenStream {
|
|
||||||
let input = parse_macro_input!(input as LitStr);
|
|
||||||
let cidr_str = input.value();
|
|
||||||
|
|
||||||
if let Ok(_) = cidr_str.parse::<cidr::Ipv4Cidr>() {
|
|
||||||
let expanded = quote! { #cidr_str.parse::<cidr::Ipv4Cidr>().unwrap() };
|
|
||||||
return TokenStream::from(expanded);
|
|
||||||
}
|
|
||||||
|
|
||||||
panic!("Invalid IPv4 CIDR : {}", cidr_str);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
## Supporting a new field in OPNSense `config.xml`
|
|
||||||
|
|
||||||
Two steps:
|
|
||||||
- Supporting the field in `opnsense-config-xml`
|
|
||||||
- Enabling Harmony to control the field
|
|
||||||
|
|
||||||
We'll use the `filename` field in the `dhcpcd` section of the file as an example.
|
|
||||||
|
|
||||||
### Supporting the field
|
|
||||||
|
|
||||||
As type checking if enforced, every field from `config.xml` must be known by the code. Each subsection of `config.xml` has its `.rs` file. For the `dhcpcd` section, we'll modify `opnsense-config-xml/src/data/dhcpd.rs`.
|
|
||||||
|
|
||||||
When a new field appears in the xml file, an error like this will be thrown and Harmony will panic :
|
|
||||||
```
|
|
||||||
Running `/home/stremblay/nt/dir/harmony/target/debug/example-nanodc`
|
|
||||||
Found unauthorized element filename
|
|
||||||
thread 'main' panicked at opnsense-config-xml/src/data/opnsense.rs:54:14:
|
|
||||||
OPNSense received invalid string, should be full XML: ()
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Define the missing field (`filename`) in the `DhcpInterface` struct of `opnsense-config-xml/src/data/dhcpd.rs`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct DhcpInterface {
|
|
||||||
...
|
|
||||||
pub filename: Option<String>,
|
|
||||||
```
|
|
||||||
|
|
||||||
Harmony should now be fixed, build and run.
|
|
||||||
|
|
||||||
### Controlling the field
|
|
||||||
|
|
||||||
Define the `xml field setter` in `opnsense-config/src/modules/dhcpd.rs`.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
impl<'a> DhcpConfig<'a> {
|
|
||||||
...
|
|
||||||
pub fn set_filename(&mut self, filename: &str) {
|
|
||||||
self.enable_netboot();
|
|
||||||
self.get_lan_dhcpd().filename = Some(filename.to_string());
|
|
||||||
}
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Define the `value setter` in the `DhcpServer trait` in `domain/topology/network.rs`
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[async_trait]
|
|
||||||
pub trait DhcpServer: Send + Sync {
|
|
||||||
...
|
|
||||||
async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError>;
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Implement the `value setter` in each `DhcpServer` implementation.
|
|
||||||
`infra/opnsense/dhcp.rs`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[async_trait]
|
|
||||||
impl DhcpServer for OPNSenseFirewall {
|
|
||||||
...
|
|
||||||
async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError> {
|
|
||||||
{
|
|
||||||
let mut writable_opnsense = self.opnsense_config.write().await;
|
|
||||||
writable_opnsense.dhcp().set_filename(filename);
|
|
||||||
debug!("OPNsense dhcp server set filename {filename}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
`domain/topology/ha_cluster.rs`
|
|
||||||
```rust
|
|
||||||
#[async_trait]
|
|
||||||
impl DhcpServer for DummyInfra {
|
|
||||||
...
|
|
||||||
async fn set_filename(&self, _filename: &str) -> Result<(), ExecutorError> {
|
|
||||||
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
|
|
||||||
}
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Add the new field to the DhcpScore in `modules/dhcp.rs`
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct DhcpScore {
|
|
||||||
...
|
|
||||||
pub filename: Option<String>,
|
|
||||||
```
|
|
||||||
|
|
||||||
Define it in its implementation in `modules/okd/dhcp.rs`
|
|
||||||
|
|
||||||
```rust
|
|
||||||
impl OKDDhcpScore {
|
|
||||||
...
|
|
||||||
Self {
|
|
||||||
dhcp_score: DhcpScore {
|
|
||||||
...
|
|
||||||
filename: Some("undionly.kpxe".to_string()),
|
|
||||||
```
|
|
||||||
|
|
||||||
Define it in its implementation in `modules/okd/bootstrap_dhcp.rs`
|
|
||||||
|
|
||||||
```rust
|
|
||||||
impl OKDDhcpScore {
|
|
||||||
...
|
|
||||||
Self {
|
|
||||||
dhcp_score: DhcpScore::new(
|
|
||||||
...
|
|
||||||
Some("undionly.kpxe".to_string()),
|
|
||||||
```
|
|
||||||
|
|
||||||
Update the interpret (function called by the `execute` fn of the interpret) so it now updates the `filename` field value in `modules/dhcp.rs`
|
|
||||||
|
|
||||||
```rust
|
|
||||||
impl DhcpInterpret {
|
|
||||||
...
|
|
||||||
let filename_outcome = match &self.score.filename {
|
|
||||||
Some(filename) => {
|
|
||||||
let dhcp_server = Arc::new(topology.dhcp_server.clone());
|
|
||||||
dhcp_server.set_filename(&filename).await?;
|
|
||||||
Outcome::new(
|
|
||||||
InterpretStatus::SUCCESS,
|
|
||||||
format!("Dhcp Interpret Set filename to {filename}"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
None => Outcome::noop(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if next_server_outcome.status == InterpretStatus::NOOP
|
|
||||||
&& boot_filename_outcome.status == InterpretStatus::NOOP
|
|
||||||
&& filename_outcome.status == InterpretStatus::NOOP
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
Ok(Outcome::new(
|
|
||||||
InterpretStatus::SUCCESS,
|
|
||||||
format!(
|
|
||||||
"Dhcp Interpret Set next boot to [{:?}], boot_filename to [{:?}], filename to [{:?}]",
|
|
||||||
self.score.boot_filename, self.score.boot_filename, self.score.filename
|
|
||||||
)
|
|
||||||
...
|
|
||||||
```
|
|
||||||
Reference in New Issue
Block a user