Compare commits

...

76 Commits

Author SHA1 Message Date
06a2be4496 doc: Add README explaining how to build harmony_inventory_agent statically with musl target
Some checks failed
Run Check Script / check (pull_request) Failing after 35s
2025-08-21 21:58:35 -04:00
e2a09efdee Merge remote-tracking branch 'origin/master' into doc/pxe_test_setup 2025-08-21 21:56:09 -04:00
d36c574590 Merge pull request 'feat/inventory_agent' (#119) from feat/inventory_agent into master
Some checks failed
Run Check Script / check (push) Failing after 38s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 5m48s
Reviewed-on: #119
2025-08-22 01:55:52 +00:00
2618441de3 fix: Make sure directory exists before uploading file in opnsense http
Some checks failed
Run Check Script / check (pull_request) Failing after 31s
2025-08-21 17:31:43 -04:00
da6610c625 wip: PXE setup for ipxe and okd files in progress
Some checks failed
Run Check Script / check (pull_request) Failing after 36s
2025-08-21 17:28:17 -04:00
e956772593 feat: Add pxe example and new data files structure 2025-08-20 22:00:56 -04:00
27c51e0ec5 feat(wip): Support opnsense 25.7 which defaults to dnsmasq instead of isc dhcp 2025-08-20 21:54:46 -04:00
bfca9cf163 Merge pull request 'feat/ceph-osd-score' (#116) from feat/ceph-osd-score into master
Some checks failed
Run Check Script / check (push) Failing after 36s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 15m5s
Reviewed-on: #116
Reviewed-by: johnride <jg@nationtech.io>
2025-08-20 18:19:42 +00:00
597dcbc848 doc: PXE test setup script and README file to explain what it does and how to use it
Some checks failed
Run Check Script / check (pull_request) Failing after 40s
2025-08-20 13:14:00 -04:00
cd3ea6fc10 fix: added check to ensure that rook-ceph-tools is available in the designated namespace
All checks were successful
Run Check Script / check (pull_request) Successful in 1m16s
2025-08-20 12:54:19 -04:00
a53e8552e9 wip: pxe test setup still has a few kinks with serial console 2025-08-20 12:14:17 -04:00
89eb88d10e feat: socre to remove an osd from the ceph osd tree using K8sClient to interact with rook-ceph-toolbox pod 2025-08-20 12:09:55 -04:00
72fb05b5cc fix(inventory_agent) : Agent now retreives correct dmidecode fields, fixed uuid generation which is unacceptable, fixed storage drive parsing, much better error handling, much more strict behavior which also leads to more complete output as missing fields will raise errors unless explicitely optional 2025-08-19 17:56:06 -04:00
6685b05cc5 wip(inventory_agent): Refactoring for better error handling in progress 2025-08-19 17:05:23 -04:00
07116eb8a6 Merge pull request 'feat: Harmony inventory agent crate that exposes an endpoint listing the host hardware. Has to be reviewed, generated 99% by GLM-4.5' (#115) from feat/inventory_agent into master
Some checks failed
Run Check Script / check (push) Failing after 27s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 5m34s
Reviewed-on: #115
2025-08-19 16:58:00 +00:00
3f34f868eb Merge remote-tracking branch 'origin/master' into feat/inventory_agent
Some checks failed
Run Check Script / check (pull_request) Failing after 29s
2025-08-19 12:56:10 -04:00
bc6f7336d2 feat(inventory_agent): use HARMONY_INVENTORY_AGENT_PORT as environment variable to set port
Some checks failed
Run Check Script / check (pull_request) Failing after 25s
2025-08-19 12:55:03 -04:00
01da8631da chore(inventory_agent): Cargo fmt
Some checks failed
Run Check Script / check (pull_request) Failing after 24s
2025-08-19 12:44:49 -04:00
67b5c2df07 Merge pull request 'feat: Add iobench project and python dashboard' (#112) from feat/iobench into master
All checks were successful
Run Check Script / check (push) Successful in 1m11s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 5m41s
Reviewed-on: #112
2025-08-19 16:24:31 +00:00
1eaf63417b Merge pull request 'feat/secrets' (#111) from feat/secrets into master
Some checks failed
Compile and package harmony_composer / package_harmony_composer (push) Waiting to run
Run Check Script / check (push) Has been cancelled
Reviewed-on: #111

This pull request introduces a comprehensive and ergonomic secret management system via a new harmony-secret crate.
What's Done

    New harmony-secret Crate:
        A new crate dedicated to secret management, providing a clean, static API: SecretManager::get::<MySecret>() and SecretManager::set(&my_secret).
        A #[derive(Secret)] procedural macro that automatically uses the struct's name as the secret key, simplifying usage.
        An async SecretStore trait to support various backend implementations.

    Two Secret Store Implementations:
        LocalFileSecretStore: A simple file-based store that saves secrets as JSON in the user's data directory. Ideal for local development and testing.
        InfisicalSecretStore: A production-ready implementation that integrates with Infisical for centralized, secure secret management.

    Configuration via Environment Variables:
        The secret store is selected at runtime via the HARMONY_SECRET_STORE environment variable (file or infisical).
        Infisical integration is configured through HARMONY_SECRET_INFISICAL_* variables.

What's Not Done (Future Work)

    Automated Infisical Setup: The initial configuration for the Infisical backend is currently manual. Developers must create a project and a Universal Auth identity in Infisical and set the corresponding environment variables to run tests or use the backend. The new test_harmony_secret_infisical.sh script serves as a clear example of the required variables.

This new secrets module provides a solid and secure foundation for managing credentials for components like OPNsense, Kubernetes, and other infrastructure services going forward. Even with the manual first-time setup for Infisical, this architecture is robust enough to serve our needs for the foreseeable future.
2025-08-19 16:23:45 +00:00
5e7803d2ba chore(iobench-dash): Delete older revisions and rename to iobench-dash.py for clarity
All checks were successful
Run Check Script / check (pull_request) Successful in 1m3s
2025-08-19 12:21:42 -04:00
9a610661c7 chore: Add description and license fields to Cargo.toml to allow publishing the crate
All checks were successful
Run Check Script / check (pull_request) Successful in 1m1s
2025-08-19 12:12:41 -04:00
70a65ed5d0 Merge remote-tracking branch 'origin/master' into feat/secrets
All checks were successful
Run Check Script / check (pull_request) Successful in 1m9s
2025-08-19 12:00:19 -04:00
26e8e386b9 feat: Secret module works with infisical and local file storage backends
All checks were successful
Run Check Script / check (pull_request) Successful in 1m9s
2025-08-19 11:59:21 -04:00
19cb7f73bc feat: Harmony inventory agent crate that exposes an endpoint listing the host hardware. Has to be reviewed, generated 99% by GLM-4.5
Some checks failed
Run Check Script / check (pull_request) Failing after 29s
2025-08-19 11:24:20 -04:00
84f38974b1 Merge pull request 'fix: bring back the TUI' (#110) from fix-tui into master
All checks were successful
Run Check Script / check (push) Successful in 1m15s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 5m34s
Reviewed-on: #110
2025-08-15 20:01:59 +00:00
7d027bcfc4 Merge pull request 'fix: remove indicatif in harmony_cli to simplify logging and fixing interactions' (#109) from rip-indicatif into master
Some checks failed
Compile and package harmony_composer / package_harmony_composer (push) Waiting to run
Run Check Script / check (push) Has been cancelled
Reviewed-on: #109
2025-08-15 20:01:13 +00:00
d1a274b705 fix: checks deployment status ready replicas rather than pod name since the pod name is not necessarily matching the deployment name and often has a random generated number in it 2025-08-15 15:44:06 -04:00
b43ca7c740 feat: score for preparing rook ceph cluster to remove drive based on rook-ceph-osd deployment name added functions to K8sclient to be able to scale deployment to a desired replicaset number and get pod based on name and namespace 2025-08-15 14:51:16 -04:00
2a6a233fb2 feat: WIP add secrets module and macro crate 2025-08-15 14:40:39 -04:00
Ian Letourneau
610ce84280 fix: bring back to TUI
All checks were successful
Run Check Script / check (pull_request) Successful in 1m20s
2025-08-15 12:47:36 -04:00
Ian Letourneau
8bb4a9d3f6 fix: remove indicatif in harmony_cli to simplify logging and fixing interactions
All checks were successful
Run Check Script / check (pull_request) Successful in 1m7s
2025-08-15 11:26:54 -04:00
Ian Letourneau
67f3a23071 chore: cleanup unused imports
All checks were successful
Run Check Script / check (push) Successful in 1m30s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 5m33s
2025-08-14 16:44:22 -04:00
d86970f81b fix: make sure demo works on both local & remote target (#107)
Some checks failed
Compile and package harmony_composer / package_harmony_composer (push) Waiting to run
Run Check Script / check (push) Has been cancelled
* define Ntfy ingress (naive implementation) based on current target
* use patched Ntfy Helm Chart
* create Ntfy main user only if needed
* add info logs
* better error bubbling
* instrument feature installations
* upgrade prometheus alerting charts if already installed
* harmony_composer params to control deployment `target` and `profile`

Co-authored-by: Ian Letourneau <letourneau.ian@gmail.com>
Co-authored-by: Jean-Gabriel Gill-Couture <jg@nationtech.io>
Reviewed-on: #107
2025-08-14 20:42:09 +00:00
623a3f019b fix: apply different network policies based on current target (#97)
Some checks failed
Compile and package harmony_composer / package_harmony_composer (push) Waiting to run
Run Check Script / check (push) Has been cancelled
Fixes #94

Co-authored-by: Ian Letourneau <letourneau.ian@gmail.com>
Reviewed-on: #97
Reviewed-by: johnride <jg@nationtech.io>
2025-08-14 20:36:19 +00:00
fd8f643a8f feat: Add iobench project and python dashboard
All checks were successful
Run Check Script / check (pull_request) Successful in 1m3s
2025-08-14 10:37:30 -04:00
Ian Letourneau
bd214f8fb8 fix: remove sha256 for harmony composer image in harmony_composer workflow
All checks were successful
Run Check Script / check (push) Successful in 1m9s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 6m11s
2025-08-11 19:49:06 -04:00
f0ed548755 fix: improve usage of indicatif for tracking progress (#101)
Some checks failed
Run Check Script / check (push) Successful in 1m18s
Compile and package harmony_composer / package_harmony_composer (push) Failing after -1s
The multiprogress wasn't used properly and leading to conflicting progress bars (within our own progress bars, as well as the log wrapper).

This PR introduce a layer on top of `indicatif::MultiProgress` to properly handle sections of progress bars, where we can dynamically add/update/remove progress bars from any sections.

We can see in the demo that new sections + progress bars are added on the fly and that extra logs (e.g. info logs) are appended on top of the progress bars.

Progress are also grouped together based on their parent score.

Co-authored-by: Ian Letourneau <letourneau.ian@gmail.com>
Co-authored-by: johnride <jg@nationtech.io>
Reviewed-on: #101
2025-08-11 23:47:11 +00:00
1de96027a1 fix: prevent instrumentation to run in test mode (#102)
Some checks failed
Run Check Script / check (push) Successful in 1m16s
Compile and package harmony_composer / package_harmony_composer (push) Failing after -1s
The CI pipeline (`./check.sh`) was failing because of test errors, which was caused by the instrumentation framework complaining that no subscribers/listeners were registered.

Instead of setting up all tests to run with a dummy subscriber, move the implementation of the instrumentation behind a feature flag so that it runs only for tests.

There's a catch though: the `#[cfg(test)]` directive works only when directly testing the crate. If a crate `A` depends on another crate `B`, `B` will be compiled as usual (aka not in test mode) which will not trigger the `test` flag.

So we need to introduce our own `testing` feature flag for `harmony` core and import it with that flag (only during dev/test).

More info: https://github.com/rust-lang/rust/issues/59168

Co-authored-by: Ian Letourneau <letourneau.ian@gmail.com>
Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/102
2025-08-11 23:42:08 +00:00
0812937a67 fix(ci): Remove specific sha256 for harmony composer image, just always run on latest
Some checks failed
Compile and package harmony_composer / package_harmony_composer (push) Successful in 7m22s
Run Check Script / check (push) Failing after 2m0s
2025-08-11 15:52:37 -04:00
29a261575b refactor: Interpret score with a provided method on Score (#100)
Some checks failed
Compile and package harmony_composer / package_harmony_composer (push) Successful in 6m49s
Run Check Script / check (push) Failing after 41s
First step in a direction to better orchestrate the core flow, even though it feels weird to move this logic into the `Score`. We'll refactor this as soon as we have a better solution.

Co-authored-by: Ian Letourneau <letourneau.ian@gmail.com>
Reviewed-on: #100
2025-08-09 22:56:23 +00:00
dcf8335240 Merge pull request 'refactor: Remove InterpretStatus/Error & Outcome from Topology' (#99) from remove-interpret-status-from-topology into master
Some checks are pending
Run Check Script / check (push) Waiting to run
Compile and package harmony_composer / package_harmony_composer (push) Waiting to run
Reviewed-on: #99
Reviewed-by: johnride <jg@nationtech.io>
2025-08-09 22:52:21 +00:00
Ian Letourneau
f876b5e67b refactor: Remove InterpretStatus/Error & Outcome from Topology
Some checks failed
Run Check Script / check (pull_request) Has been cancelled
2025-08-06 22:29:00 -04:00
440c1bce12 chore: reformat & clippy cleanup (#96)
Some checks failed
Run Check Script / check (pull_request) Has been cancelled
Run Check Script / check (push) Has been cancelled
Compile and package harmony_composer / package_harmony_composer (push) Has been cancelled
Clippy is now added to the `check` in the pipeline

Co-authored-by: Ian Letourneau <letourneau.ian@gmail.com>
Reviewed-on: #96
2025-08-06 15:57:14 +00:00
024084859e Monitor an application within a tenant (#86)
All checks were successful
Run Check Script / check (push) Successful in -45s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 4m35s
WIP: added implementation to deploy crd-alertmanagerconfigs
Co-authored-by: Ian Letourneau <letourneau.ian@gmail.com>
Reviewed-on: #86
Co-authored-by: Willem <wrolleman@nationtech.io>
Co-committed-by: Willem <wrolleman@nationtech.io>
2025-08-04 21:42:01 +00:00
54990cd1a5 fix(cli): simplify running the CLI by hiding the maestro inside the implemtation (#93)
All checks were successful
Run Check Script / check (push) Successful in -46s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 4m36s
Co-authored-by: Ian Letourneau <letourneau.ian@gmail.com>
Reviewed-on: #93
2025-08-04 20:59:07 +00:00
06aab1f57f fix(cli): reduce noise & better track progress within Harmony (#91)
All checks were successful
Run Check Script / check (push) Successful in -37s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 9m6s
Introduce a way to instrument what happens within Harmony and around Harmony (e.g. in the CLI or in Composer).

The goal is to provide visual feedback to the end users and inform them of the progress of their tasks (e.g. deployment) as clearly as possible. It is important to also let them know of the outcome of their tasks (what was created, where to access stuff, etc.).

<img src="https://media.discordapp.net/attachments/1295353830300713062/1400289618636574741/demo.gif?ex=688c18d5&is=688ac755&hm=2c70884aacb08f7bd15cbb65a7562a174846906718aa15294bbb238e64febbce&=" />

## Changes

### Instrumentation architecture
Extensibility and ease of use is key here, while preserving type safety as much as possible.

The proposed API is quite simple:
```rs
// Emit an event
instrumentation::instrument(
    HarmonyEvent::TopologyPrepared {
        topology: "k8s-anywhere",
        outcome: Outcome::success("yay")
    }
);

// Consume events
instrumentation::subscribe("Harmony CLI Logger", async |event| {
    match event {
        HarmonyEvent::TopologyPrepared { name, outcome } => todo!(),
    }
});
```

#### Current limitations
* this API is not very extensible, but it could be easily changed to allow end users to define custom events in addition to Harmony core events
* we use a tokio broadcast channel behind the scene so only in process communication can happen, but it could be easily changed to a more flexible communication mechanism as implementation details are hidden

### `harmony_composer` VS `harmony_cli`
As Harmony Composer launches commands from Harmony (CLI), they both live in different processes. And because of this, we cannot easily make all the logging happens in one place (Harmony Composer) and get rid of Harmony CLI. At least not without introducing additional complexity such as communication through a server, unix socket, etc.

So for the time being, it was decided to preserve both `harmony_composer` and `harmony_cli` and let them independently log their stuff and handle their own responsibilities:
* `harmony_composer`: takes care only of setting up & packaging a project, delegates everything else to `harmony_cli`
* `harmony_cli`: takes care of configuring & running Harmony

### Logging & prompts
* [indicatif](https://github.com/console-rs/indicatif) is used to create progress bars and track progress within Harmony, Harmony CLI, and Harmony Composer
* [inquire](https://github.com/mikaelmello/inquire) is preserved, but was removed from `harmony` (core) as UI concerns shouldn't go that deep
  * note: for now the only prompt we had was simply deleted, we'll have to find a better way to prompt stuff in the future

## Todos
* [ ] Update/Create ADRs
* [ ] Continue instrumentation for missing branches
* [ ] Allow instrumentation to emit and subscribe to custom events

Co-authored-by: Ian Letourneau <letourneau.ian@gmail.com>
Reviewed-on: #91
Reviewed-by: johnride <jg@nationtech.io>
2025-07-31 19:35:33 +00:00
1ab66af718 Merge pull request 'refactor(topo/k8s_anywhere): simplify local installation of k3d' (#90) from simply-k3d-installation into master
Some checks failed
Run Check Script / check (push) Failing after -1m14s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 3m23s
Reviewed-on: #90
Reviewed-by: wjro <wrolleman@nationtech.io>
2025-07-31 13:22:25 +00:00
Ian Letourneau
0fff4ef566 refactor(topo/k8s_anywhere): simplify local installation of k3d
All checks were successful
Run Check Script / check (pull_request) Successful in -37s
A Maestro was initialized with a new inventory simply to provide a
localhost topology to install K3D locally. But in practice, the K3D
installation wasn't actually using the topology nor the inventory.

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

View File

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

View File

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

29
.gitignore vendored
View File

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

879
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,9 @@ members = [
"harmony_cli", "harmony_cli",
"k3d", "k3d",
"harmony_composer", "harmony_composer",
"harmony_inventory_agent",
"harmony_secret_derive",
"harmony_secret",
] ]
[workspace.package] [workspace.package]
@@ -20,7 +23,7 @@ readme = "README.md"
license = "GNU AGPL v3" license = "GNU AGPL v3"
[workspace.dependencies] [workspace.dependencies]
log = "0.4" log = { version = "0.4", features = ["kv"] }
env_logger = "0.11" env_logger = "0.11"
derive-new = "0.7" derive-new = "0.7"
async-trait = "0.1" async-trait = "0.1"
@@ -52,3 +55,13 @@ convert_case = "0.8"
chrono = "0.4" chrono = "0.4"
similar = "2" similar = "2"
uuid = { version = "1.11", features = ["v4", "fast-rng", "macro-diagnostics"] } uuid = { version = "1.11", features = ["v4", "fast-rng", "macro-diagnostics"] }
pretty_assertions = "1.4.1"
tempfile = "3.20.0"
bollard = "0.19.1"
base64 = "0.22.1"
tar = "0.4.44"
lazy_static = "1.5.0"
directories = "6.0.0"
thiserror = "2.0.14"
serde = { version = "1.0.209", features = ["derive", "rc"] }
serde_json = "1.0.127"

View File

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

View File

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

View File

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

3
data/pxe/okd/README.md Normal file
View File

@@ -0,0 +1,3 @@
Here lies all the data files required for an OKD cluster PXE boot setup.
This inclues ISO files, binary boot files, ipxe, etc.

Binary file not shown.

Binary file not shown.

108
docs/pxe_test/README.md Normal file
View File

@@ -0,0 +1,108 @@
# OPNsense PXE Lab Environment
This project contains a script to automatically set up a virtual lab environment for testing PXE boot services managed by an OPNsense firewall.
## Overview
The `pxe_vm_lab_setup.sh` script will create the following resources using libvirt/KVM:
1. **A Virtual Network**: An isolated network named `harmonylan` (`virbr1`) for the lab.
2. **Two Virtual Machines**:
* `opnsense-pxe`: A firewall VM that will act as the gateway and PXE server.
* `pxe-node-1`: A client VM configured to boot from the network.
## Prerequisites
Ensure you have the following software installed on your Arch Linux host:
* `libvirt`
* `qemu`
* `virt-install` (from the `virt-install` package)
* `curl`
* `bzip2`
## Usage
### 1. Create the Environment
Run the `up` command to download the necessary images and create the network and VMs.
```bash
sudo ./pxe_vm_lab_setup.sh up
```
### 2. Install and Configure OPNsense
The OPNsense VM is created but the OS needs to be installed manually via the console.
1. **Connect to the VM console**:
```bash
sudo virsh console opnsense-pxe
```
2. **Log in as the installer**:
* Username: `installer`
* Password: `opnsense`
3. **Follow the on-screen installation wizard**. When prompted to assign network interfaces (`WAN` and `LAN`):
* Find the MAC address for the `harmonylan` interface by running this command in another terminal:
```bash
virsh domiflist opnsense-pxe
# Example output:
# Interface Type Source Model MAC
# ---------------------------------------------------------
# vnet18 network default virtio 52:54:00:b5:c4:6d
# vnet19 network harmonylan virtio 52:54:00:21:f9:ba
```
* Assign the interface connected to `harmonylan` (e.g., `vtnet1` with MAC `52:54:00:21:f9:ba`) as your **LAN**.
* Assign the other interface as your **WAN**.
4. After the installation is complete, **shut down** the VM from the console menu.
5. **Detach the installation media** by editing the VM's configuration:
```bash
sudo virsh edit opnsense-pxe
```
Find and **delete** the entire `<disk>` block corresponding to the `.img` file (the one with `<target ... bus='usb'/>`).
6. **Start the VM** to boot into the newly installed system:
```bash
sudo virsh start opnsense-pxe
```
### 3. Connect to OPNsense from Your Host
To configure OPNsense, you need to connect your host to the `harmonylan` network.
1. By default, OPNsense configures its LAN interface with the IP `192.168.1.1`.
2. Assign a compatible IP address to your host's `virbr1` bridge interface:
```bash
sudo ip addr add 192.168.1.5/24 dev virbr1
```
3. You can now access the OPNsense VM from your host:
* **SSH**: `ssh root@192.168.1.1` (password: `opnsense`)
* **Web UI**: `https://192.168.1.1`
### 4. Configure PXE Services with Harmony
With connectivity established, you can now use Harmony to configure the OPNsense firewall for PXE booting. Point your Harmony OPNsense scores to the firewall using these details:
* **Hostname/IP**: `192.168.1.1`
* **Credentials**: `root` / `opnsense`
### 5. Boot the PXE Client
Once your Harmony configuration has been applied and OPNsense is serving DHCP/TFTP, start the client VM. It will automatically attempt to boot from the network.
```bash
sudo virsh start pxe-node-1
sudo virsh console pxe-node-1
```
## Cleanup
To destroy all VMs and networks created by the script, run the `clean` command:
```bash
sudo ./pxe_vm_lab_setup.sh clean
```

191
docs/pxe_test/pxe_vm_lab_setup.sh Executable file
View File

@@ -0,0 +1,191 @@
#!/usr/bin/env bash
set -euo pipefail
# --- Configuration ---
LAB_DIR="/var/lib/harmony_pxe_test"
IMG_DIR="${LAB_DIR}/images"
STATE_DIR="${LAB_DIR}/state"
VM_OPN="opnsense-pxe"
VM_PXE="pxe-node-1"
NET_HARMONYLAN="harmonylan"
# Network settings for the isolated LAN
VLAN_CIDR="192.168.150.0/24"
VLAN_GW="192.168.150.1"
VLAN_MASK="255.255.255.0"
# VM Specifications
RAM_OPN="2048"
VCPUS_OPN="2"
DISK_OPN_GB="10"
OS_VARIANT_OPN="freebsd14.0" # Updated to a more recent FreeBSD variant
RAM_PXE="4096"
VCPUS_PXE="2"
DISK_PXE_GB="40"
OS_VARIANT_LINUX="centos-stream9"
OPN_IMG_URL="https://mirror.ams1.nl.leaseweb.net/opnsense/releases/25.7/OPNsense-25.7-serial-amd64.img.bz2"
OPN_IMG_PATH="${IMG_DIR}/OPNsense-25.7-serial-amd64.img"
CENTOS_ISO_URL="https://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os/images/boot.iso"
CENTOS_ISO_PATH="${IMG_DIR}/CentOS-Stream-9-latest-boot.iso"
CONNECT_URI="qemu:///system"
download_if_missing() {
local url="$1"
local dest="$2"
if [[ ! -f "$dest" ]]; then
echo "Downloading $url to $dest"
mkdir -p "$(dirname "$dest")"
local tmp
tmp="$(mktemp)"
curl -L --progress-bar "$url" -o "$tmp"
case "$url" in
*.bz2) bunzip2 -c "$tmp" > "$dest" && rm -f "$tmp" ;;
*) mv "$tmp" "$dest" ;;
esac
else
echo "Already present: $dest"
fi
}
# Ensures a libvirt network is defined and active
ensure_network() {
local net_name="$1"
local net_xml_path="$2"
if virsh --connect "${CONNECT_URI}" net-info "${net_name}" >/dev/null 2>&1; then
echo "Network ${net_name} already exists."
else
echo "Defining network ${net_name} from ${net_xml_path}"
virsh --connect "${CONNECT_URI}" net-define "${net_xml_path}"
fi
if ! virsh --connect "${CONNECT_URI}" net-info "${net_name}" | grep "Active: *yes"; then
echo "Starting network ${net_name}..."
virsh --connect "${CONNECT_URI}" net-start "${net_name}"
virsh --connect "${CONNECT_URI}" net-autostart "${net_name}"
fi
}
# Destroys a VM completely
destroy_vm() {
local vm_name="$1"
if virsh --connect "${CONNECT_URI}" dominfo "$vm_name" >/dev/null 2>&1; then
echo "Destroying and undefining VM: ${vm_name}"
virsh --connect "${CONNECT_URI}" destroy "$vm_name" || true
virsh --connect "${CONNECT_URI}" undefine "$vm_name" --nvram
fi
}
# Destroys a libvirt network
destroy_network() {
local net_name="$1"
if virsh --connect "${CONNECT_URI}" net-info "$net_name" >/dev/null 2>&1; then
echo "Destroying and undefining network: ${net_name}"
virsh --connect "${CONNECT_URI}" net-destroy "$net_name" || true
virsh --connect "${CONNECT_URI}" net-undefine "$net_name"
fi
}
# --- Main Logic ---
create_lab_environment() {
# Create network definition files
cat > "${STATE_DIR}/default.xml" <<EOF
<network>
<name>default</name>
<forward mode='nat'/>
<bridge name='virbr0' stp='on' delay='0'/>
<ip address='192.168.122.1' netmask='255.255.255.0'>
<dhcp>
<range start='192.168.122.100' end='192.168.122.200'/>
</dhcp>
</ip>
</network>
EOF
cat > "${STATE_DIR}/${NET_HARMONYLAN}.xml" <<EOF
<network>
<name>${NET_HARMONYLAN}</name>
<bridge name='virbr1' stp='on' delay='0'/>
</network>
EOF
# Ensure both networks exist and are active
ensure_network "default" "${STATE_DIR}/default.xml"
ensure_network "${NET_HARMONYLAN}" "${STATE_DIR}/${NET_HARMONYLAN}.xml"
# --- Create OPNsense VM (MODIFIED SECTION) ---
local disk_opn="${IMG_DIR}/${VM_OPN}.qcow2"
if [[ ! -f "$disk_opn" ]]; then
qemu-img create -f qcow2 "$disk_opn" "${DISK_OPN_GB}G"
fi
echo "Creating OPNsense VM using serial image..."
virt-install \
--connect "${CONNECT_URI}" \
--name "${VM_OPN}" \
--ram "${RAM_OPN}" \
--vcpus "${VCPUS_OPN}" \
--cpu host-passthrough \
--os-variant "${OS_VARIANT_OPN}" \
--graphics none \
--noautoconsole \
--disk path="${disk_opn}",device=disk,bus=virtio,boot.order=1 \
--disk path="${OPN_IMG_PATH}",device=disk,bus=usb,readonly=on,boot.order=2 \
--network network=default,model=virtio \
--network network="${NET_HARMONYLAN}",model=virtio \
--boot uefi,menu=on
echo "OPNsense VM created. Connect with: sudo virsh console ${VM_OPN}"
echo "The VM will boot from the serial installation image."
echo "Login with user 'installer' and password 'opnsense' to start the installation."
echo "Install onto the VirtIO disk (vtbd0)."
echo "After installation, shutdown the VM, then run 'sudo virsh edit ${VM_OPN}' and remove the USB disk block to boot from the installed system."
# --- Create PXE Client VM ---
local disk_pxe="${IMG_DIR}/${VM_PXE}.qcow2"
if [[ ! -f "$disk_pxe" ]]; then
qemu-img create -f qcow2 "$disk_pxe" "${DISK_PXE_GB}G"
fi
echo "Creating PXE client VM..."
virt-install \
--connect "${CONNECT_URI}" \
--name "${VM_PXE}" \
--ram "${RAM_PXE}" \
--vcpus "${VCPUS_PXE}" \
--cpu host-passthrough \
--os-variant "${OS_VARIANT_LINUX}" \
--graphics none \
--noautoconsole \
--disk path="${disk_pxe}",format=qcow2,bus=virtio \
--network network="${NET_HARMONYLAN}",model=virtio \
--pxe \
--boot uefi,menu=on
echo "PXE VM created. It will attempt to netboot on ${NET_HARMONYLAN}."
}
# --- Script Entrypoint ---
case "${1:-}" in
up)
mkdir -p "${IMG_DIR}" "${STATE_DIR}"
download_if_missing "$OPN_IMG_URL" "$OPN_IMG_PATH"
download_if_missing "$CENTOS_ISO_URL" "$CENTOS_ISO_PATH"
create_lab_environment
echo "Lab setup complete. Use 'sudo virsh list --all' to see VMs."
;;
clean)
destroy_vm "${VM_PXE}"
destroy_vm "${VM_OPN}"
destroy_network "${NET_HARMONYLAN}"
# Optionally destroy the default network if you want a full reset
# destroy_network "default"
echo "Cleanup complete."
;;
*)
echo "Usage: sudo $0 {up|clean}"
exit 1
;;
esac

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,9 +8,8 @@ use harmony::{
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup}, hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
infra::opnsense::OPNSenseManagementInterface, infra::opnsense::OPNSenseManagementInterface,
inventory::Inventory, inventory::Inventory,
maestro::Maestro,
modules::{ modules::{
http::HttpScore, http::StaticFilesHttpScore,
ipxe::IpxeScore, ipxe::IpxeScore,
okd::{ okd::{
bootstrap_dhcp::OKDBootstrapDhcpScore, bootstrap_dhcp::OKDBootstrapDhcpScore,
@@ -126,20 +125,26 @@ async fn main() {
harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology); harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string())); let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string()));
let http_score = HttpScore::new(Url::LocalFolder( let http_score = StaticFilesHttpScore {
"./data/watchguard/pxe-http-files".to_string(), folder_to_serve: Some(Url::LocalFolder("./data/watchguard/pxe-http-files".to_string())),
)); files: vec![],
};
let ipxe_score = IpxeScore::new(); let ipxe_score = IpxeScore::new();
let mut maestro = Maestro::initialize(inventory, topology).await.unwrap();
maestro.register_all(vec![ harmony_tui::run(
Box::new(dns_score), inventory,
Box::new(bootstrap_dhcp_score), topology,
Box::new(bootstrap_load_balancer_score), vec![
Box::new(load_balancer_score), Box::new(dns_score),
Box::new(tftp_score), Box::new(bootstrap_dhcp_score),
Box::new(http_score), Box::new(bootstrap_load_balancer_score),
Box::new(ipxe_score), Box::new(load_balancer_score),
Box::new(dhcp_score), Box::new(tftp_score),
]); Box::new(http_score),
harmony_tui::init(maestro).await.unwrap(); Box::new(ipxe_score),
Box::new(dhcp_score),
],
)
.await
.unwrap();
} }

View File

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

View File

@@ -0,0 +1,18 @@
[package]
name = "example-pxe"
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 }

View File

@@ -0,0 +1,50 @@
mod topology;
use harmony::{
data::{FileContent, FilePath},
modules::{dhcp::DhcpScore, http::StaticFilesHttpScore, tftp::TftpScore},
score::Score,
topology::{HAClusterTopology, Url},
};
use crate::topology::{get_inventory, get_topology};
#[tokio::main]
async fn main() {
let inventory = get_inventory();
let topology = get_topology().await;
let gateway_ip = topology.router.get_gateway();
// TODO this should be a single IPXEScore instead of having the user do this step by step
let scores: Vec<Box<dyn Score<HAClusterTopology>>> = vec![
Box::new(DhcpScore {
host_binding: vec![],
next_server: Some(topology.router.get_gateway()),
boot_filename: None,
filename: Some("undionly.kpxe".to_string()),
filename64: Some("ipxe.efi".to_string()),
filenameipxe: Some(format!("http://{gateway_ip}:8080/boot.ipxe").to_string()),
}),
Box::new(TftpScore {
files_to_serve: Url::LocalFolder("./data/pxe/okd/tftpboot/".to_string()),
}),
Box::new(StaticFilesHttpScore {
folder_to_serve: None,
files: vec![FileContent {
path: FilePath::Relative("boot.ipxe".to_string()),
content: format!(
"#!ipxe
set base-url http://{gateway_ip}:8080
set hostfile ${{base-url}}/byMAC/01-${{mac:hexhyp}}.ipxe
chain ${{hostfile}} || chain ${{base-url}}/default.ipxe"
),
}],
}),
];
harmony_cli::run(inventory, topology, scores, None)
.await
.unwrap();
}

View File

@@ -0,0 +1,65 @@
use std::{
net::{IpAddr, Ipv4Addr},
sync::Arc,
};
use cidr::Ipv4Cidr;
use harmony::{
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
infra::opnsense::OPNSenseManagementInterface,
inventory::Inventory,
topology::{HAClusterTopology, LogicalHost, UnmanagedRouter},
};
use harmony_macros::{ip, ipv4};
pub async fn get_topology() -> HAClusterTopology {
let firewall = harmony::topology::LogicalHost {
ip: ip!("192.168.1.1"),
name: String::from("opnsense-1"),
};
let opnsense = Arc::new(
harmony::infra::opnsense::OPNSenseFirewall::new(firewall, None, "root", "opnsense").await,
);
let lan_subnet = ipv4!("192.168.1.0");
let gateway_ipv4 = ipv4!("192.168.1.1");
let gateway_ip = IpAddr::V4(gateway_ipv4);
harmony::topology::HAClusterTopology {
domain_name: "demo.harmony.mcd".to_string(),
router: Arc::new(UnmanagedRouter::new(
gateway_ip,
Ipv4Cidr::new(lan_subnet, 24).unwrap(),
)),
load_balancer: opnsense.clone(),
firewall: opnsense.clone(),
tftp_server: opnsense.clone(),
http_server: opnsense.clone(),
dhcp_server: opnsense.clone(),
dns_server: opnsense.clone(),
control_plane: vec![LogicalHost {
ip: ip!("10.100.8.20"),
name: "cp0".to_string(),
}],
bootstrap_host: LogicalHost {
ip: ip!("10.100.8.20"),
name: "cp0".to_string(),
},
workers: vec![],
switch: vec![],
}
}
pub fn get_inventory() -> Inventory {
Inventory {
location: Location::new(
"Some virtual machine or maybe a physical machine if you're cool".to_string(),
"testopnsense".to_string(),
),
switch: SwitchGroup::from([]),
firewall: FirewallGroup::from([PhysicalHost::empty(HostCategory::Firewall)
.management(Arc::new(OPNSenseManagementInterface::new()))]),
storage_host: vec![],
worker_host: vec![],
control_plane_host: vec![],
}
}

View File

@@ -8,10 +8,9 @@ use harmony::{
hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup}, hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup},
infra::opnsense::OPNSenseManagementInterface, infra::opnsense::OPNSenseManagementInterface,
inventory::Inventory, inventory::Inventory,
maestro::Maestro,
modules::{ modules::{
dummy::{ErrorScore, PanicScore, SuccessScore}, dummy::{ErrorScore, PanicScore, SuccessScore},
http::HttpScore, http::StaticFilesHttpScore,
okd::{dhcp::OKDDhcpScore, dns::OKDDnsScore, load_balancer::OKDLoadBalancerScore}, okd::{dhcp::OKDDhcpScore, dns::OKDDnsScore, load_balancer::OKDLoadBalancerScore},
opnsense::OPNsenseShellCommandScore, opnsense::OPNsenseShellCommandScore,
tftp::TftpScore, tftp::TftpScore,
@@ -81,23 +80,31 @@ async fn main() {
let load_balancer_score = OKDLoadBalancerScore::new(&topology); let load_balancer_score = OKDLoadBalancerScore::new(&topology);
let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string())); let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string()));
let http_score = HttpScore::new(Url::LocalFolder( let http_score = StaticFilesHttpScore {
"./data/watchguard/pxe-http-files".to_string(), folder_to_serve: Some(Url::LocalFolder(
)); "./data/watchguard/pxe-http-files".to_string(),
let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); )),
maestro.register_all(vec![ files: vec![],
Box::new(dns_score), };
Box::new(dhcp_score),
Box::new(load_balancer_score), harmony_tui::run(
Box::new(tftp_score), inventory,
Box::new(http_score), topology,
Box::new(OPNsenseShellCommandScore { vec![
opnsense: opnsense.get_opnsense_config(), Box::new(dns_score),
command: "touch /tmp/helloharmonytouching".to_string(), Box::new(dhcp_score),
}), Box::new(load_balancer_score),
Box::new(SuccessScore {}), Box::new(tftp_score),
Box::new(ErrorScore {}), Box::new(http_score),
Box::new(PanicScore {}), Box::new(OPNsenseShellCommandScore {
]); opnsense: opnsense.get_opnsense_config(),
harmony_tui::init(maestro).await.unwrap(); command: "touch /tmp/helloharmonytouching".to_string(),
}),
Box::new(SuccessScore {}),
Box::new(ErrorScore {}),
Box::new(PanicScore {}),
],
)
.await
.unwrap();
} }

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ use std::net::{SocketAddr, SocketAddrV4};
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,
maestro::Maestro,
modules::{ modules::{
dns::DnsScore, dns::DnsScore,
dummy::{ErrorScore, PanicScore, SuccessScore}, dummy::{ErrorScore, PanicScore, SuccessScore},
@@ -16,18 +15,19 @@ use harmony_macros::ipv4;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let inventory = Inventory::autoload(); harmony_tui::run(
let topology = DummyInfra {}; Inventory::autoload(),
let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); DummyInfra {},
vec![
maestro.register_all(vec![ Box::new(SuccessScore {}),
Box::new(SuccessScore {}), Box::new(ErrorScore {}),
Box::new(ErrorScore {}), Box::new(PanicScore {}),
Box::new(PanicScore {}), Box::new(DnsScore::new(vec![], None)),
Box::new(DnsScore::new(vec![], None)), Box::new(build_large_score()),
Box::new(build_large_score()), ],
]); )
harmony_tui::init(maestro).await.unwrap(); .await
.unwrap();
} }
fn build_large_score() -> LoadBalancerScore { fn build_large_score() -> LoadBalancerScore {

View File

@@ -5,6 +5,9 @@ version.workspace = true
readme.workspace = true readme.workspace = true
license.workspace = true license.workspace = true
[features]
testing = []
[dependencies] [dependencies]
rand = "0.9" rand = "0.9"
hex = "0.4" hex = "0.4"
@@ -13,8 +16,8 @@ reqwest = { version = "0.11", features = ["blocking", "json"] }
russh = "0.45.0" russh = "0.45.0"
rust-ipmi = "0.1.1" rust-ipmi = "0.1.1"
semver = "1.0.23" semver = "1.0.23"
serde = { version = "1.0.209", features = ["derive"] } serde.workspace = true
serde_json = "1.0.127" serde_json.workspace = true
tokio.workspace = true tokio.workspace = true
derive-new.workspace = true derive-new.workspace = true
log.workspace = true log.workspace = true
@@ -27,29 +30,28 @@ harmony_macros = { path = "../harmony_macros" }
harmony_types = { path = "../harmony_types" } harmony_types = { path = "../harmony_types" }
uuid.workspace = true uuid.workspace = true
url.workspace = true url.workspace = true
kube.workspace = true kube = { workspace = true, features = ["derive"] }
k8s-openapi.workspace = true k8s-openapi.workspace = true
serde_yaml.workspace = true serde_yaml.workspace = true
http.workspace = true http.workspace = true
serde-value.workspace = true serde-value.workspace = true
inquire.workspace = true
helm-wrapper-rs = "0.4.0" helm-wrapper-rs = "0.4.0"
non-blank-string-rs = "1.0.4" non-blank-string-rs = "1.0.4"
k3d-rs = { path = "../k3d" } k3d-rs = { path = "../k3d" }
directories = "6.0.0" directories.workspace = true
lazy_static = "1.5.0" lazy_static.workspace = true
dockerfile_builder = "0.1.5" dockerfile_builder = "0.1.5"
temp-file = "0.1.9" temp-file = "0.1.9"
convert_case.workspace = true convert_case.workspace = true
email_address = "0.2.9" email_address = "0.2.9"
chrono.workspace = true chrono.workspace = true
fqdn = { version = "0.4.6", features = [ fqdn = { version = "0.4.6", features = [
"domain-label-cannot-start-or-end-with-hyphen", "domain-label-cannot-start-or-end-with-hyphen",
"domain-label-length-limited-to-63", "domain-label-length-limited-to-63",
"domain-name-without-special-chars", "domain-name-without-special-chars",
"domain-name-length-limited-to-255", "domain-name-length-limited-to-255",
"punycode", "punycode",
"serde", "serde",
] } ] }
temp-dir = "0.1.14" temp-dir = "0.1.14"
dyn-clone = "1.0.19" dyn-clone = "1.0.19"
@@ -57,4 +59,15 @@ similar.workspace = true
futures-util = "0.3.31" futures-util = "0.3.31"
tokio-util = "0.7.15" tokio-util = "0.7.15"
strum = { version = "0.27.1", features = ["derive"] } strum = { version = "0.27.1", features = ["derive"] }
tempfile = "3.20.0" tempfile.workspace = true
serde_with = "3.14.0"
schemars = "0.8.22"
kube-derive = "1.1.0"
bollard.workspace = true
tar.workspace = true
base64.workspace = true
once_cell = "1.21.3"
harmony-secret-derive = { version = "0.1.0", path = "../harmony_secret_derive" }
[dev-dependencies]
pretty_assertions.workspace = true

BIN
harmony/harmony.rlib Normal file

Binary file not shown.

View File

@@ -11,5 +11,5 @@ lazy_static! {
pub static ref REGISTRY_PROJECT: String = pub static ref REGISTRY_PROJECT: String =
std::env::var("HARMONY_REGISTRY_PROJECT").unwrap_or_else(|_| "harmony".to_string()); std::env::var("HARMONY_REGISTRY_PROJECT").unwrap_or_else(|_| "harmony".to_string());
pub static ref DRY_RUN: bool = pub static ref DRY_RUN: bool =
std::env::var("HARMONY_DRY_RUN").map_or(true, |value| value.parse().unwrap_or(true)); std::env::var("HARMONY_DRY_RUN").is_ok_and(|value| value.parse().unwrap_or(false));
} }

View File

@@ -0,0 +1,22 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileContent {
pub path: FilePath,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FilePath {
Relative(String),
Absolute(String),
}
impl std::fmt::Display for FilePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FilePath::Relative(path) => f.write_fmt(format_args!("./{path}")),
FilePath::Absolute(path) => f.write_fmt(format_args!("/{path}")),
}
}
}

View File

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

View File

@@ -1,4 +1,6 @@
mod id; mod id;
mod version; mod version;
mod file;
pub use id::*; pub use id::*;
pub use version::*; pub use version::*;
pub use file::*;

View File

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

View File

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

View File

@@ -0,0 +1,82 @@
use log::debug;
use once_cell::sync::Lazy;
use tokio::sync::broadcast;
use crate::modules::application::ApplicationFeatureStatus;
use super::{
interpret::{InterpretError, Outcome},
topology::TopologyStatus,
};
#[derive(Debug, Clone)]
pub enum HarmonyEvent {
HarmonyStarted,
HarmonyFinished,
InterpretExecutionStarted {
execution_id: String,
topology: String,
interpret: String,
score: String,
message: String,
},
InterpretExecutionFinished {
execution_id: String,
topology: String,
interpret: String,
score: String,
outcome: Result<Outcome, InterpretError>,
},
TopologyStateChanged {
topology: String,
status: TopologyStatus,
message: Option<String>,
},
ApplicationFeatureStateChanged {
topology: String,
application: String,
feature: String,
status: ApplicationFeatureStatus,
},
}
static HARMONY_EVENT_BUS: Lazy<broadcast::Sender<HarmonyEvent>> = Lazy::new(|| {
// TODO: Adjust channel capacity
let (tx, _rx) = broadcast::channel(100);
tx
});
pub fn instrument(event: HarmonyEvent) -> Result<(), &'static str> {
if cfg!(any(test, feature = "testing")) {
let _ = event; // Suppress the "unused variable" warning for `event`
Ok(())
} else {
match HARMONY_EVENT_BUS.send(event) {
Ok(_) => Ok(()),
Err(_) => Err("send error: no subscribers"),
}
}
}
pub async fn subscribe<F, Fut>(name: &str, mut handler: F)
where
F: FnMut(HarmonyEvent) -> Fut + Send + 'static,
Fut: Future<Output = bool> + Send,
{
let mut rx = HARMONY_EVENT_BUS.subscribe();
debug!("[{name}] Service started. Listening for events...");
loop {
match rx.recv().await {
Ok(event) => {
if !handler(event).await {
debug!("[{name}] Handler requested exit.");
break;
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
debug!("[{name}] Lagged behind by {n} messages.");
}
Err(_) => break,
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
use async_trait::async_trait; use async_trait::async_trait;
use harmony_macros::ip; use harmony_macros::ip;
use harmony_types::net::MacAddress; use harmony_types::net::MacAddress;
use log::debug;
use log::info; use log::info;
use crate::data::FileContent;
use crate::executors::ExecutorError; use crate::executors::ExecutorError;
use crate::interpret::InterpretError; use crate::topology::PxeOptions;
use crate::interpret::Outcome;
use super::DHCPStaticEntry; use super::DHCPStaticEntry;
use super::DhcpServer; use super::DhcpServer;
@@ -19,6 +20,8 @@ use super::K8sclient;
use super::LoadBalancer; use super::LoadBalancer;
use super::LoadBalancerService; use super::LoadBalancerService;
use super::LogicalHost; use super::LogicalHost;
use super::PreparationError;
use super::PreparationOutcome;
use super::Router; use super::Router;
use super::TftpServer; use super::TftpServer;
@@ -48,10 +51,11 @@ impl Topology for HAClusterTopology {
fn name(&self) -> &str { fn name(&self) -> &str {
"HAClusterTopology" "HAClusterTopology"
} }
async fn ensure_ready(&self) -> Result<Outcome, InterpretError> { async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> {
todo!( debug!(
"ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready." "ensure_ready, not entirely sure what it should do here, probably something like verify that the hosts are reachable and all services are up and ready."
) );
Ok(PreparationOutcome::Noop)
} }
} }
@@ -153,12 +157,10 @@ impl DhcpServer for HAClusterTopology {
async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)> { async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)> {
self.dhcp_server.list_static_mappings().await self.dhcp_server.list_static_mappings().await
} }
async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError> { async fn set_pxe_options(&self, options: PxeOptions) -> Result<(), ExecutorError> {
self.dhcp_server.set_next_server(ip).await self.dhcp_server.set_pxe_options(options).await
}
async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError> {
self.dhcp_server.set_boot_filename(boot_filename).await
} }
fn get_ip(&self) -> IpAddress { fn get_ip(&self) -> IpAddress {
self.dhcp_server.get_ip() self.dhcp_server.get_ip()
} }
@@ -168,16 +170,6 @@ impl DhcpServer for HAClusterTopology {
async fn commit_config(&self) -> Result<(), ExecutorError> { async fn commit_config(&self) -> Result<(), ExecutorError> {
self.dhcp_server.commit_config().await self.dhcp_server.commit_config().await
} }
async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError> {
self.dhcp_server.set_filename(filename).await
}
async fn set_filename64(&self, filename64: &str) -> Result<(), ExecutorError> {
self.dhcp_server.set_filename64(filename64).await
}
async fn set_filenameipxe(&self, filenameipxe: &str) -> Result<(), ExecutorError> {
self.dhcp_server.set_filenameipxe(filenameipxe).await
}
} }
#[async_trait] #[async_trait]
@@ -221,17 +213,21 @@ impl HttpServer for HAClusterTopology {
self.http_server.serve_files(url).await self.http_server.serve_files(url).await
} }
async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError> {
self.http_server.serve_file_content(file).await
}
fn get_ip(&self) -> IpAddress { fn get_ip(&self) -> IpAddress {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) self.http_server.get_ip()
} }
async fn ensure_initialized(&self) -> Result<(), ExecutorError> { async fn ensure_initialized(&self) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) self.http_server.ensure_initialized().await
} }
async fn commit_config(&self) -> Result<(), ExecutorError> { async fn commit_config(&self) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) self.http_server.commit_config().await
} }
async fn reload_restart(&self) -> Result<(), ExecutorError> { async fn reload_restart(&self) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) self.http_server.reload_restart().await
} }
} }
@@ -241,13 +237,15 @@ pub struct DummyInfra;
#[async_trait] #[async_trait]
impl Topology for DummyInfra { impl Topology for DummyInfra {
fn name(&self) -> &str { fn name(&self) -> &str {
todo!() "DummyInfra"
} }
async fn ensure_ready(&self) -> Result<Outcome, InterpretError> { async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> {
let dummy_msg = "This is a dummy infrastructure that does nothing"; let dummy_msg = "This is a dummy infrastructure that does nothing";
info!("{dummy_msg}"); info!("{dummy_msg}");
Ok(Outcome::success(dummy_msg.to_string())) Ok(PreparationOutcome::Success {
details: dummy_msg.into(),
})
} }
} }
@@ -297,19 +295,7 @@ impl DhcpServer for DummyInfra {
async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)> { async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
} }
async fn set_next_server(&self, _ip: IpAddress) -> Result<(), ExecutorError> { async fn set_pxe_options(&self, _options: PxeOptions) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}
async fn set_boot_filename(&self, _boot_filename: &str) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}
async fn set_filename(&self, _filename: &str) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}
async fn set_filename64(&self, _filename: &str) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}
async fn set_filenameipxe(&self, _filenameipxe: &str) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
} }
fn get_ip(&self) -> IpAddress { fn get_ip(&self) -> IpAddress {
@@ -379,6 +365,9 @@ impl HttpServer for DummyInfra {
async fn serve_files(&self, _url: &Url) -> Result<(), ExecutorError> { async fn serve_files(&self, _url: &Url) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
} }
async fn serve_file_content(&self, _file: &FileContent) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}
fn get_ip(&self) -> IpAddress { fn get_ip(&self) -> IpAddress {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA) unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
} }

View File

@@ -1,4 +1,4 @@
use crate::executors::ExecutorError; use crate::{data::FileContent, executors::ExecutorError};
use async_trait::async_trait; use async_trait::async_trait;
use super::{IpAddress, Url}; use super::{IpAddress, Url};
@@ -6,6 +6,7 @@ use super::{IpAddress, Url};
#[async_trait] #[async_trait]
pub trait HttpServer: Send + Sync { pub trait HttpServer: Send + Sync {
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError>; async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError>;
async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError>;
fn get_ip(&self) -> IpAddress; fn get_ip(&self) -> IpAddress;
// async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError>; // async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError>;

View File

@@ -1,27 +1,40 @@
use derive_new::new; use derive_new::new;
use futures_util::StreamExt;
use k8s_openapi::{ use k8s_openapi::{
ClusterResourceScope, NamespaceResourceScope, ClusterResourceScope, NamespaceResourceScope,
api::{apps::v1::Deployment, core::v1::Pod}, api::{apps::v1::Deployment, core::v1::Pod},
}; };
use kube::runtime::conditions;
use kube::runtime::wait::await_condition;
use kube::{ use kube::{
Client, Config, Error, Resource, Client, Config, Error, Resource,
api::{Api, AttachParams, ListParams, Patch, PatchParams, ResourceExt}, api::{Api, AttachParams, DeleteParams, ListParams, Patch, PatchParams, ResourceExt},
config::{KubeConfigOptions, Kubeconfig}, config::{KubeConfigOptions, Kubeconfig},
core::ErrorResponse, core::ErrorResponse,
runtime::reflector::Lookup, runtime::reflector::Lookup,
}; };
use kube::{api::DynamicObject, runtime::conditions};
use kube::{
api::{ApiResource, GroupVersionKind},
runtime::wait::await_condition,
};
use log::{debug, error, trace}; use log::{debug, error, trace};
use serde::de::DeserializeOwned; use serde::{Serialize, de::DeserializeOwned};
use similar::{DiffableStr, TextDiff}; use serde_json::json;
use similar::TextDiff;
use tokio::io::AsyncReadExt;
#[derive(new, Clone)] #[derive(new, Clone)]
pub struct K8sClient { pub struct K8sClient {
client: Client, client: Client,
} }
impl Serialize for K8sClient {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
todo!()
}
}
impl std::fmt::Debug for K8sClient { impl std::fmt::Debug for K8sClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// This is a poor man's debug implementation for now as kube::Client does not provide much // This is a poor man's debug implementation for now as kube::Client does not provide much
@@ -40,6 +53,66 @@ impl K8sClient {
}) })
} }
pub async fn get_deployment(
&self,
name: &str,
namespace: Option<&str>,
) -> Result<Option<Deployment>, Error> {
let deps: Api<Deployment> = if let Some(ns) = namespace {
Api::namespaced(self.client.clone(), ns)
} else {
Api::default_namespaced(self.client.clone())
};
Ok(deps.get_opt(name).await?)
}
pub async fn get_pod(&self, name: &str, namespace: Option<&str>) -> Result<Option<Pod>, Error> {
let pods: Api<Pod> = if let Some(ns) = namespace {
Api::namespaced(self.client.clone(), ns)
} else {
Api::default_namespaced(self.client.clone())
};
Ok(pods.get_opt(name).await?)
}
pub async fn scale_deployment(
&self,
name: &str,
namespace: Option<&str>,
replicas: u32,
) -> Result<(), Error> {
let deployments: Api<Deployment> = if let Some(ns) = namespace {
Api::namespaced(self.client.clone(), ns)
} else {
Api::default_namespaced(self.client.clone())
};
let patch = json!({
"spec": {
"replicas": replicas
}
});
let pp = PatchParams::default();
let scale = Patch::Apply(&patch);
deployments.patch_scale(name, &pp, &scale).await?;
Ok(())
}
pub async fn delete_deployment(
&self,
name: &str,
namespace: Option<&str>,
) -> Result<(), Error> {
let deployments: Api<Deployment> = if let Some(ns) = namespace {
Api::namespaced(self.client.clone(), ns)
} else {
Api::default_namespaced(self.client.clone())
};
let delete_params = DeleteParams::default();
deployments.delete(name, &delete_params).await?;
Ok(())
}
pub async fn wait_until_deployment_ready( pub async fn wait_until_deployment_ready(
&self, &self,
name: String, name: String,
@@ -55,13 +128,75 @@ impl K8sClient {
} }
let establish = await_condition(api, name.as_str(), conditions::is_deployment_completed()); let establish = await_condition(api, name.as_str(), conditions::is_deployment_completed());
let t = if let Some(t) = timeout { t } else { 300 }; let t = timeout.unwrap_or(300);
let res = tokio::time::timeout(std::time::Duration::from_secs(t), establish).await; let res = tokio::time::timeout(std::time::Duration::from_secs(t), establish).await;
if let Ok(r) = res { if res.is_ok() {
return Ok(()); Ok(())
} else { } else {
return Err("timed out while waiting for deployment".to_string()); Err("timed out while waiting for deployment".to_string())
}
}
/// Will execute a commond in the first pod found that matches the specified label
/// '{label}={name}'
pub async fn exec_app_capture_output(
&self,
name: String,
label: String,
namespace: Option<&str>,
command: Vec<&str>,
) -> Result<String, String> {
let api: Api<Pod>;
if let Some(ns) = namespace {
api = Api::namespaced(self.client.clone(), ns);
} else {
api = Api::default_namespaced(self.client.clone());
}
let pod_list = api
.list(&ListParams::default().labels(format!("{label}={name}").as_str()))
.await
.expect("couldn't get list of pods");
let res = api
.exec(
pod_list
.items
.first()
.expect("couldn't get pod")
.name()
.expect("couldn't get pod name")
.into_owned()
.as_str(),
command,
&AttachParams::default().stdout(true).stderr(true),
)
.await;
match res {
Err(e) => Err(e.to_string()),
Ok(mut process) => {
let status = process
.take_status()
.expect("Couldn't get status")
.await
.expect("Couldn't unwrap status");
if let Some(s) = status.status {
let mut stdout_buf = String::new();
if let Some(mut stdout) = process.stdout().take() {
stdout.read_to_string(&mut stdout_buf).await;
}
debug!("Status: {} - {:?}", s, status.details);
if s == "Success" {
Ok(stdout_buf)
} else {
Err(s)
}
} else {
Err("Couldn't get inner status of pod exec".to_string())
}
}
} }
} }
@@ -100,7 +235,7 @@ impl K8sClient {
.await; .await;
match res { match res {
Err(e) => return Err(e.to_string()), Err(e) => Err(e.to_string()),
Ok(mut process) => { Ok(mut process) => {
let status = process let status = process
.take_status() .take_status()
@@ -109,14 +244,10 @@ impl K8sClient {
.expect("Couldn't unwrap status"); .expect("Couldn't unwrap status");
if let Some(s) = status.status { if let Some(s) = status.status {
debug!("Status: {}", s); debug!("Status: {} - {:?}", s, status.details);
if s == "Success" { if s == "Success" { Ok(()) } else { Err(s) }
return Ok(());
} else {
return Err(s);
}
} else { } else {
return Err("Couldn't get inner status of pod exec".to_string()); Err("Couldn't get inner status of pod exec".to_string())
} }
} }
} }
@@ -157,8 +288,9 @@ impl K8sClient {
trace!("Received current value {current:#?}"); trace!("Received current value {current:#?}");
// The resource exists, so we calculate and display a diff. // The resource exists, so we calculate and display a diff.
println!("\nPerforming dry-run for resource: '{}'", name); println!("\nPerforming dry-run for resource: '{}'", name);
let mut current_yaml = serde_yaml::to_value(&current) let mut current_yaml = serde_yaml::to_value(&current).unwrap_or_else(|_| {
.expect(&format!("Could not serialize current value : {current:#?}")); panic!("Could not serialize current value : {current:#?}")
});
if current_yaml.is_mapping() && current_yaml.get("status").is_some() { if current_yaml.is_mapping() && current_yaml.get("status").is_some() {
let map = current_yaml.as_mapping_mut().unwrap(); let map = current_yaml.as_mapping_mut().unwrap();
let removed = map.remove_entry("status"); let removed = map.remove_entry("status");
@@ -225,7 +357,7 @@ impl K8sClient {
} }
} }
pub async fn apply_many<K>(&self, resource: &Vec<K>, ns: Option<&str>) -> Result<Vec<K>, Error> pub async fn apply_many<K>(&self, resource: &[K], ns: Option<&str>) -> Result<Vec<K>, Error>
where where
K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize, K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize,
<K as Resource>::Scope: ApplyStrategy<K>, <K as Resource>::Scope: ApplyStrategy<K>,
@@ -239,6 +371,70 @@ impl K8sClient {
Ok(result) Ok(result)
} }
pub async fn apply_yaml_many(
&self,
#[allow(clippy::ptr_arg)] yaml: &Vec<serde_yaml::Value>,
ns: Option<&str>,
) -> Result<(), Error> {
for y in yaml.iter() {
self.apply_yaml(y, ns).await?;
}
Ok(())
}
pub async fn apply_yaml(
&self,
yaml: &serde_yaml::Value,
ns: Option<&str>,
) -> Result<(), Error> {
let obj: DynamicObject = serde_yaml::from_value(yaml.clone()).expect("TODO do not unwrap");
let name = obj.metadata.name.as_ref().expect("YAML must have a name");
let api_version = yaml
.get("apiVersion")
.expect("couldn't get apiVersion from YAML")
.as_str()
.expect("couldn't get apiVersion as str");
let kind = yaml
.get("kind")
.expect("couldn't get kind from YAML")
.as_str()
.expect("couldn't get kind as str");
let split: Vec<&str> = api_version.splitn(2, "/").collect();
let g = split[0];
let v = split[1];
let gvk = GroupVersionKind::gvk(g, v, kind);
let api_resource = ApiResource::from_gvk(&gvk);
let namespace = match ns {
Some(n) => n,
None => obj
.metadata
.namespace
.as_ref()
.expect("YAML must have a namespace"),
};
// 5. Create a dynamic API client for this resource type.
let api: Api<DynamicObject> =
Api::namespaced_with(self.client.clone(), namespace, &api_resource);
// 6. Apply the object to the cluster using Server-Side Apply.
// This will create the resource if it doesn't exist, or update it if it does.
println!(
"Applying Argo Application '{}' in namespace '{}'...",
name, namespace
);
let patch_params = PatchParams::apply("harmony"); // Use a unique field manager name
let result = api.patch(name, &patch_params, &Patch::Apply(&obj)).await?;
println!("Successfully applied '{}'.", result.name_any());
Ok(())
}
pub(crate) async fn from_kubeconfig(path: &str) -> Option<K8sClient> { pub(crate) async fn from_kubeconfig(path: &str) -> Option<K8sClient> {
let k = match Kubeconfig::read_from(path) { let k = match Kubeconfig::read_from(path) {
Ok(k) => k, Ok(k) => k,

View File

@@ -1,30 +1,46 @@
use std::{process::Command, sync::Arc}; use std::{process::Command, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use inquire::Confirm;
use log::{debug, info, warn}; use log::{debug, info, warn};
use serde::Serialize; use serde::Serialize;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
use crate::{ use crate::{
executors::ExecutorError, executors::ExecutorError,
interpret::{InterpretError, Outcome}, interpret::InterpretStatus,
inventory::Inventory, inventory::Inventory,
maestro::Maestro, modules::{
modules::k3d::K3DInstallationScore, k3d::K3DInstallationScore,
topology::LocalhostTopology, monitoring::kube_prometheus::crd::{
crd_alertmanager_config::CRDPrometheus,
prometheus_operator::prometheus_operator_helm_chart_score,
},
prometheus::{
k8s_prometheus_alerting_score::K8sPrometheusCRDAlertingScore,
prometheus::PrometheusApplicationMonitoring,
},
},
score::Score,
}; };
use super::{ use super::{
DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology, DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, PreparationError,
PreparationOutcome, Topology,
k8s::K8sClient, k8s::K8sClient,
tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager}, oberservability::monitoring::AlertReceiver,
tenant::{
TenantConfig, TenantManager,
k8s::K8sTenantManager,
network_policy::{
K3dNetworkPolicyStrategy, NetworkPolicyStrategy, NoopNetworkPolicyStrategy,
},
},
}; };
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct K8sState { struct K8sState {
client: Arc<K8sClient>, client: Arc<K8sClient>,
_source: K8sSource, source: K8sSource,
message: String, message: String,
} }
@@ -58,8 +74,42 @@ impl K8sclient for K8sAnywhereTopology {
} }
} }
#[async_trait]
impl PrometheusApplicationMonitoring<CRDPrometheus> for K8sAnywhereTopology {
async fn install_prometheus(
&self,
sender: &CRDPrometheus,
inventory: &Inventory,
receivers: Option<Vec<Box<dyn AlertReceiver<CRDPrometheus>>>>,
) -> Result<PreparationOutcome, PreparationError> {
let po_result = self.ensure_prometheus_operator(sender).await?;
if po_result == PreparationOutcome::Noop {
debug!("Skipping Prometheus CR installation due to missing operator.");
return Ok(po_result);
}
let result = self
.get_k8s_prometheus_application_score(sender.clone(), receivers)
.await
.interpret(inventory, self)
.await;
match result {
Ok(outcome) => match outcome.status {
InterpretStatus::SUCCESS => Ok(PreparationOutcome::Success {
details: outcome.message,
}),
InterpretStatus::NOOP => Ok(PreparationOutcome::Noop),
_ => Err(PreparationError::new(outcome.message)),
},
Err(err) => Err(PreparationError::new(err.to_string())),
}
}
}
impl Serialize for K8sAnywhereTopology { impl Serialize for K8sAnywhereTopology {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where where
S: serde::Serializer, S: serde::Serializer,
{ {
@@ -84,6 +134,19 @@ impl K8sAnywhereTopology {
} }
} }
async fn get_k8s_prometheus_application_score(
&self,
sender: CRDPrometheus,
receivers: Option<Vec<Box<dyn AlertReceiver<CRDPrometheus>>>>,
) -> K8sPrometheusCRDAlertingScore {
K8sPrometheusCRDAlertingScore {
sender,
receivers: receivers.unwrap_or_default(),
service_monitors: vec![],
prometheus_rules: vec![],
}
}
fn is_helm_available(&self) -> Result<(), String> { fn is_helm_available(&self) -> Result<(), String> {
let version_result = Command::new("helm") let version_result = Command::new("helm")
.arg("version") .arg("version")
@@ -94,9 +157,8 @@ impl K8sAnywhereTopology {
return Err("Failed to run 'helm -version'".to_string()); return Err("Failed to run 'helm -version'".to_string());
} }
// Print the version output
let version_output = String::from_utf8_lossy(&version_result.stdout); let version_output = String::from_utf8_lossy(&version_result.stdout);
println!("Helm version: {}", version_output.trim()); debug!("Helm version: {}", version_output.trim());
Ok(()) Ok(())
} }
@@ -113,33 +175,42 @@ impl K8sAnywhereTopology {
K3DInstallationScore::default() K3DInstallationScore::default()
} }
async fn try_install_k3d(&self) -> Result<(), InterpretError> { async fn try_install_k3d(&self) -> Result<(), PreparationError> {
let maestro = Maestro::initialize(Inventory::autoload(), LocalhostTopology::new()).await?; let result = self
let k3d_score = self.get_k3d_installation_score(); .get_k3d_installation_score()
maestro.interpret(Box::new(k3d_score)).await?; .interpret(&Inventory::empty(), self)
Ok(()) .await;
match result {
Ok(outcome) => match outcome.status {
InterpretStatus::SUCCESS => Ok(()),
InterpretStatus::NOOP => Ok(()),
_ => Err(PreparationError::new(outcome.message)),
},
Err(err) => Err(PreparationError::new(err.to_string())),
}
} }
async fn try_get_or_install_k8s_client(&self) -> Result<Option<K8sState>, InterpretError> { async fn try_get_or_install_k8s_client(&self) -> Result<Option<K8sState>, PreparationError> {
let k8s_anywhere_config = &self.config; let k8s_anywhere_config = &self.config;
// TODO this deserves some refactoring, it is becoming a bit hard to figure out // TODO this deserves some refactoring, it is becoming a bit hard to figure out
// be careful when making modifications here // be careful when making modifications here
if k8s_anywhere_config.use_local_k3d { if k8s_anywhere_config.use_local_k3d {
info!("Using local k3d cluster because of use_local_k3d set to true"); debug!("Using local k3d cluster because of use_local_k3d set to true");
} else { } else {
if let Some(kubeconfig) = &k8s_anywhere_config.kubeconfig { if let Some(kubeconfig) = &k8s_anywhere_config.kubeconfig {
debug!("Loading kubeconfig {kubeconfig}"); debug!("Loading kubeconfig {kubeconfig}");
match self.try_load_kubeconfig(&kubeconfig).await { match self.try_load_kubeconfig(kubeconfig).await {
Some(client) => { Some(client) => {
return Ok(Some(K8sState { return Ok(Some(K8sState {
client: Arc::new(client), client: Arc::new(client),
_source: K8sSource::Kubeconfig, source: K8sSource::Kubeconfig,
message: format!("Loaded k8s client from kubeconfig {kubeconfig}"), message: format!("Loaded k8s client from kubeconfig {kubeconfig}"),
})); }));
} }
None => { None => {
return Err(InterpretError::new(format!( return Err(PreparationError::new(format!(
"Failed to load kubeconfig from {kubeconfig}" "Failed to load kubeconfig from {kubeconfig}"
))); )));
} }
@@ -158,22 +229,13 @@ impl K8sAnywhereTopology {
} }
if !k8s_anywhere_config.autoinstall { if !k8s_anywhere_config.autoinstall {
debug!("Autoinstall confirmation prompt"); warn!(
let confirmation = Confirm::new( "Harmony autoinstallation is not activated, do you wish to launch autoinstallation? : ") "Installation cancelled, K8sAnywhere could not initialize a valid Kubernetes client"
.with_default(false) );
.prompt() return Ok(None);
.expect("Unexpected prompt error");
debug!("Autoinstall confirmation {confirmation}");
if !confirmation {
warn!(
"Installation cancelled, K8sAnywhere could not initialize a valid Kubernetes client"
);
return Ok(None);
}
} }
info!("Starting K8sAnywhere installation"); debug!("Starting K8sAnywhere installation");
self.try_install_k3d().await?; self.try_install_k3d().await?;
let k3d_score = self.get_k3d_installation_score(); let k3d_score = self.get_k3d_installation_score();
// I feel like having to rely on the k3d_rs crate here is a smell // I feel like having to rely on the k3d_rs crate here is a smell
@@ -185,8 +247,8 @@ impl K8sAnywhereTopology {
let state = match k3d.get_client().await { let state = match k3d.get_client().await {
Ok(client) => K8sState { Ok(client) => K8sState {
client: Arc::new(K8sClient::new(client)), client: Arc::new(K8sClient::new(client)),
_source: K8sSource::LocalK3d, source: K8sSource::LocalK3d,
message: "Successfully installed K3D cluster and acquired client".to_string(), message: "K8s client ready".to_string(),
}, },
Err(_) => todo!(), Err(_) => todo!(),
}; };
@@ -194,15 +256,21 @@ impl K8sAnywhereTopology {
Ok(Some(state)) Ok(Some(state))
} }
async fn ensure_k8s_tenant_manager(&self) -> Result<(), String> { async fn ensure_k8s_tenant_manager(&self, k8s_state: &K8sState) -> Result<(), String> {
if let Some(_) = self.tenant_manager.get() { if self.tenant_manager.get().is_some() {
return Ok(()); return Ok(());
} }
self.tenant_manager self.tenant_manager
.get_or_try_init(async || -> Result<K8sTenantManager, String> { .get_or_try_init(async || -> Result<K8sTenantManager, String> {
let k8s_client = self.k8s_client().await?; let k8s_client = self.k8s_client().await?;
Ok(K8sTenantManager::new(k8s_client)) let network_policy_strategy: Box<dyn NetworkPolicyStrategy> = match k8s_state.source
{
K8sSource::LocalK3d => Box::new(K3dNetworkPolicyStrategy::new()),
K8sSource::Kubeconfig => Box::new(NoopNetworkPolicyStrategy::new()),
};
Ok(K8sTenantManager::new(k8s_client, network_policy_strategy))
}) })
.await?; .await?;
@@ -217,6 +285,55 @@ impl K8sAnywhereTopology {
)), )),
} }
} }
async fn ensure_prometheus_operator(
&self,
sender: &CRDPrometheus,
) -> Result<PreparationOutcome, PreparationError> {
let status = Command::new("sh")
.args(["-c", "kubectl get crd -A | grep -i prometheuses"])
.status()
.map_err(|e| PreparationError::new(format!("could not connect to cluster: {}", e)))?;
if !status.success() {
if let Some(Some(k8s_state)) = self.k8s_state.get() {
match k8s_state.source {
K8sSource::LocalK3d => {
debug!("installing prometheus operator");
let op_score =
prometheus_operator_helm_chart_score(sender.namespace.clone());
let result = op_score.interpret(&Inventory::empty(), self).await;
return match result {
Ok(outcome) => match outcome.status {
InterpretStatus::SUCCESS => Ok(PreparationOutcome::Success {
details: "installed prometheus operator".into(),
}),
InterpretStatus::NOOP => Ok(PreparationOutcome::Noop),
_ => Err(PreparationError::new(
"failed to install prometheus operator (unknown error)".into(),
)),
},
Err(err) => Err(PreparationError::new(err.to_string())),
};
}
K8sSource::Kubeconfig => {
debug!("unable to install prometheus operator, contact cluster admin");
return Ok(PreparationOutcome::Noop);
}
}
} else {
warn!("Unable to detect k8s_state. Skipping Prometheus Operator install.");
return Ok(PreparationOutcome::Noop);
}
}
debug!("Prometheus operator is already present, skipping install");
Ok(PreparationOutcome::Success {
details: "prometheus operator present in cluster".into(),
})
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -237,7 +354,7 @@ pub struct K8sAnywhereConfig {
/// ///
/// When enabled, autoinstall will setup a K3D cluster on the localhost. https://k3d.io/stable/ /// When enabled, autoinstall will setup a K3D cluster on the localhost. https://k3d.io/stable/
/// ///
/// Default: false /// Default: true
pub autoinstall: bool, pub autoinstall: bool,
/// Whether to use local k3d cluster. /// Whether to use local k3d cluster.
@@ -246,7 +363,7 @@ pub struct K8sAnywhereConfig {
/// ///
/// default: true /// default: true
pub use_local_k3d: bool, pub use_local_k3d: bool,
harmony_profile: String, pub harmony_profile: String,
} }
impl K8sAnywhereConfig { impl K8sAnywhereConfig {
@@ -256,7 +373,7 @@ impl K8sAnywhereConfig {
use_system_kubeconfig: std::env::var("HARMONY_USE_SYSTEM_KUBECONFIG") use_system_kubeconfig: std::env::var("HARMONY_USE_SYSTEM_KUBECONFIG")
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)), .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
autoinstall: std::env::var("HARMONY_AUTOINSTALL") autoinstall: std::env::var("HARMONY_AUTOINSTALL")
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)), .map_or_else(|_| true, |v| v.parse().ok().unwrap_or(false)),
// TODO harmony_profile should be managed at a more core level than this // TODO harmony_profile should be managed at a more core level than this
harmony_profile: std::env::var("HARMONY_PROFILE").map_or_else( harmony_profile: std::env::var("HARMONY_PROFILE").map_or_else(
|_| "dev".to_string(), |_| "dev".to_string(),
@@ -274,26 +391,25 @@ impl Topology for K8sAnywhereTopology {
"K8sAnywhereTopology" "K8sAnywhereTopology"
} }
async fn ensure_ready(&self) -> Result<Outcome, InterpretError> { async fn ensure_ready(&self) -> Result<PreparationOutcome, PreparationError> {
let k8s_state = self let k8s_state = self
.k8s_state .k8s_state
.get_or_try_init(|| self.try_get_or_install_k8s_client()) .get_or_try_init(|| self.try_get_or_install_k8s_client())
.await?; .await?;
let k8s_state: &K8sState = k8s_state.as_ref().ok_or(InterpretError::new( let k8s_state: &K8sState = k8s_state.as_ref().ok_or(PreparationError::new(
"No K8s client could be found or installed".to_string(), "no K8s client could be found or installed".to_string(),
))?; ))?;
self.ensure_k8s_tenant_manager() self.ensure_k8s_tenant_manager(k8s_state)
.await .await
.map_err(|e| InterpretError::new(e))?; .map_err(PreparationError::new)?;
match self.is_helm_available() { match self.is_helm_available() {
Ok(()) => Ok(Outcome::success(format!( Ok(()) => Ok(PreparationOutcome::Success {
"{} + helm available", details: format!("{} + helm available", k8s_state.message.clone()),
k8s_state.message.clone() }),
))), Err(e) => Err(PreparationError::new(format!("helm unavailable: {}", e))),
Err(e) => Err(InterpretError::new(format!("helm unavailable: {}", e))),
} }
} }
} }

View File

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

View File

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

View File

@@ -46,16 +46,19 @@ pub trait K8sclient: Send + Sync {
async fn k8s_client(&self) -> Result<Arc<K8sClient>, String>; async fn k8s_client(&self) -> Result<Arc<K8sClient>, String>;
} }
pub struct PxeOptions {
pub ipxe_filename: String,
pub bios_filename: String,
pub efi_filename: String,
pub tftp_ip: Option<IpAddress>,
}
#[async_trait] #[async_trait]
pub trait DhcpServer: Send + Sync + std::fmt::Debug { pub trait DhcpServer: Send + Sync + std::fmt::Debug {
async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError>; async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError>;
async fn remove_static_mapping(&self, mac: &MacAddress) -> Result<(), ExecutorError>; async fn remove_static_mapping(&self, mac: &MacAddress) -> Result<(), ExecutorError>;
async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)>; async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)>;
async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError>; async fn set_pxe_options(&self, pxe_options: PxeOptions) -> Result<(), ExecutorError>;
async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError>;
async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError>;
async fn set_filename64(&self, filename64: &str) -> Result<(), ExecutorError>;
async fn set_filenameipxe(&self, filenameipxe: &str) -> Result<(), ExecutorError>;
fn get_ip(&self) -> IpAddress; fn get_ip(&self) -> IpAddress;
fn get_host(&self) -> LogicalHost; fn get_host(&self) -> LogicalHost;
async fn commit_config(&self) -> Result<(), ExecutorError>; async fn commit_config(&self) -> Result<(), ExecutorError>;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
use async_trait::async_trait; use async_trait::async_trait;
use harmony_types::net::MacAddress; use harmony_types::net::MacAddress;
use log::debug; use log::{debug, info};
use crate::{ use crate::{
executors::ExecutorError, executors::ExecutorError,
topology::{DHCPStaticEntry, DhcpServer, IpAddress, LogicalHost}, topology::{DHCPStaticEntry, DhcpServer, IpAddress, LogicalHost, PxeOptions},
}; };
use super::OPNSenseFirewall; use super::OPNSenseFirewall;
@@ -26,7 +26,7 @@ impl DhcpServer for OPNSenseFirewall {
.unwrap(); .unwrap();
} }
debug!("Registered {:?}", entry); info!("Registered {:?}", entry);
Ok(()) Ok(())
} }
@@ -46,57 +46,25 @@ impl DhcpServer for OPNSenseFirewall {
self.host.clone() self.host.clone()
} }
async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError> { async fn set_pxe_options(&self, options: PxeOptions) -> Result<(), ExecutorError> {
let ipv4 = match ip { let mut writable_opnsense = self.opnsense_config.write().await;
std::net::IpAddr::V4(ipv4_addr) => ipv4_addr, let PxeOptions {
std::net::IpAddr::V6(_) => todo!("ipv6 not supported yet"), ipxe_filename,
}; bios_filename,
{ efi_filename,
let mut writable_opnsense = self.opnsense_config.write().await; tftp_ip,
writable_opnsense.dhcp().set_next_server(ipv4); } = options;
debug!("OPNsense dhcp server set next server {ipv4}"); writable_opnsense
} .dhcp()
.set_pxe_options(
Ok(()) tftp_ip.map(|i| i.to_string()),
} bios_filename,
efi_filename,
async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError> { ipxe_filename,
{ )
let mut writable_opnsense = self.opnsense_config.write().await; .await
writable_opnsense.dhcp().set_boot_filename(boot_filename); .map_err(|dhcp_error| {
debug!("OPNsense dhcp server set boot filename {boot_filename}"); ExecutorError::UnexpectedError(format!("Failed to set_pxe_options : {dhcp_error}"))
} })
Ok(())
}
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(())
}
async fn set_filename64(&self, filename: &str) -> Result<(), ExecutorError> {
{
let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense.dhcp().set_filename64(filename);
debug!("OPNsense dhcp server set filename {filename}");
}
Ok(())
}
async fn set_filenameipxe(&self, filenameipxe: &str) -> Result<(), ExecutorError> {
{
let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense.dhcp().set_filenameipxe(filenameipxe);
debug!("OPNsense dhcp server set filenameipxe {filenameipxe}");
}
Ok(())
} }
} }

View File

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

View File

@@ -2,23 +2,23 @@ use async_trait::async_trait;
use log::info; use log::info;
use crate::{ use crate::{
data::FileContent,
executors::ExecutorError, executors::ExecutorError,
topology::{HttpServer, IpAddress, Url}, topology::{HttpServer, IpAddress, Url},
}; };
use super::OPNSenseFirewall; use super::OPNSenseFirewall;
const OPNSENSE_HTTP_ROOT_PATH: &str = "/usr/local/http";
#[async_trait] #[async_trait]
impl HttpServer for OPNSenseFirewall { impl HttpServer for OPNSenseFirewall {
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError> { async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError> {
let http_root_path = "/usr/local/http";
let config = self.opnsense_config.read().await; let config = self.opnsense_config.read().await;
info!("Uploading files from url {url} to {http_root_path}"); info!("Uploading files from url {url} to {OPNSENSE_HTTP_ROOT_PATH}");
match url { match url {
Url::LocalFolder(path) => { Url::LocalFolder(path) => {
config config
.upload_files(path, http_root_path) .upload_files(path, OPNSENSE_HTTP_ROOT_PATH)
.await .await
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?; .map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?;
} }
@@ -27,8 +27,29 @@ impl HttpServer for OPNSenseFirewall {
Ok(()) Ok(())
} }
async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError> {
let path = match &file.path {
crate::data::FilePath::Relative(path) => {
format!("{OPNSENSE_HTTP_ROOT_PATH}/{}", path.to_string())
}
crate::data::FilePath::Absolute(path) => {
return Err(ExecutorError::ConfigurationError(format!(
"Cannot serve file from http server with absolute path : {path}"
)));
}
};
let config = self.opnsense_config.read().await;
info!("Uploading file content to {}", path);
config
.upload_file_content(&path, &file.content)
.await
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?;
Ok(())
}
fn get_ip(&self) -> IpAddress { fn get_ip(&self) -> IpAddress {
todo!(); OPNSenseFirewall::get_ip(self)
} }
async fn commit_config(&self) -> Result<(), ExecutorError> { async fn commit_config(&self) -> Result<(), ExecutorError> {
@@ -48,7 +69,7 @@ impl HttpServer for OPNSenseFirewall {
async fn ensure_initialized(&self) -> Result<(), ExecutorError> { async fn ensure_initialized(&self) -> Result<(), ExecutorError> {
let mut config = self.opnsense_config.write().await; let mut config = self.opnsense_config.write().await;
let caddy = config.caddy(); let caddy = config.caddy();
if let None = caddy.get_full_config() { if caddy.get_full_config().is_none() {
info!("Http config not available in opnsense config, installing package"); info!("Http config not available in opnsense config, installing package");
config.install_package("os-caddy").await.map_err(|e| { config.install_package("os-caddy").await.map_err(|e| {
ExecutorError::UnexpectedError(format!( ExecutorError::UnexpectedError(format!(

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ use serde::Serialize;
use crate::topology::Topology; use crate::topology::Topology;
use super::Application;
/// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability, /// An ApplicationFeature provided by harmony, such as Backups, Monitoring, MultisiteAvailability,
/// ContinuousIntegration, ContinuousDelivery /// ContinuousIntegration, ContinuousDelivery
#[async_trait] #[async_trait]
@@ -14,7 +13,7 @@ pub trait ApplicationFeature<T: Topology>:
fn name(&self) -> String; fn name(&self) -> String;
} }
trait ApplicationFeatureClone<T: Topology> { pub trait ApplicationFeatureClone<T: Topology> {
fn clone_box(&self) -> Box<dyn ApplicationFeature<T>>; fn clone_box(&self) -> Box<dyn ApplicationFeature<T>>;
} }
@@ -28,7 +27,7 @@ where
} }
impl<T: Topology> Serialize for Box<dyn ApplicationFeature<T>> { impl<T: Topology> Serialize for Box<dyn ApplicationFeature<T>> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where where
S: serde::Serializer, S: serde::Serializer,
{ {

View File

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

View File

@@ -1,20 +1,20 @@
use std::{io::Write, process::Command, sync::Arc}; use std::{io::Write, process::Command, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use log::{error, info}; use log::info;
use serde_json::Value; use serde_yaml::Value;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use crate::{ use crate::{
config::HARMONY_DATA_DIR, config::HARMONY_DATA_DIR,
data::Version, data::Version,
inventory::Inventory, inventory::Inventory,
modules::{ modules::application::{
application::{Application, ApplicationFeature, HelmPackage, OCICompliant}, ApplicationFeature, HelmPackage, OCICompliant,
helm::chart::HelmChartScore, features::{ArgoApplication, ArgoHelmScore},
}, },
score::Score, score::Score,
topology::{DeploymentTarget, HelmCommand, MultiTargetTopology, Topology, Url}, topology::{DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology},
}; };
/// ContinuousDelivery in Harmony provides this functionality : /// ContinuousDelivery in Harmony provides this functionality :
@@ -56,11 +56,8 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
chart_url: String, chart_url: String,
image_name: String, image_name: String,
) -> Result<(), String> { ) -> Result<(), String> {
error!( // TODO: This works only with local k3d installations, which is fine only for current demo purposes. We assume usage of K8sAnywhereTopology"
"FIXME This works only with local k3d installations, which is fine only for current demo purposes. We assume usage of K8sAnywhereTopology" // https://git.nationtech.io/NationTech/harmony/issues/106
);
error!("TODO hardcoded k3d bin path is wrong");
let k3d_bin_path = (*HARMONY_DATA_DIR).join("k3d").join("k3d"); let k3d_bin_path = (*HARMONY_DATA_DIR).join("k3d").join("k3d");
// --- 1. Import the container image into the k3d cluster --- // --- 1. Import the container image into the k3d cluster ---
info!( info!(
@@ -139,26 +136,24 @@ impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
#[async_trait] #[async_trait]
impl< impl<
A: OCICompliant + HelmPackage + Clone + 'static, A: OCICompliant + HelmPackage + Clone + 'static,
T: Topology + HelmCommand + MultiTargetTopology + 'static, T: Topology + HelmCommand + MultiTargetTopology + K8sclient + 'static,
> ApplicationFeature<T> for ContinuousDelivery<A> > ApplicationFeature<T> for ContinuousDelivery<A>
{ {
async fn ensure_installed(&self, topology: &T) -> Result<(), String> { async fn ensure_installed(&self, topology: &T) -> Result<(), String> {
let image = self.application.image_name(); let image = self.application.image_name();
// TODO // TODO Write CI/CD workflow files
error!( // we can autotedect the CI type using the remote url (default to github action for github
"TODO reverse helm chart packaging and docker image build. I put helm package first for faster iterations" // url, etc..)
); // Or ask for it when unknown
let helm_chart = self.application.build_push_helm_package(&image).await?; let helm_chart = self.application.build_push_helm_package(&image).await?;
info!("Pushed new helm chart {helm_chart}");
// let image = self.application.build_push_oci_image().await?; // TODO: Make building image configurable/skippable if image already exists (prompt)")
// info!("Pushed new docker image {image}"); // https://git.nationtech.io/NationTech/harmony/issues/104
error!("uncomment above"); let image = self.application.build_push_oci_image().await?;
info!("Installing ContinuousDelivery feature"); // TODO: this is a temporary hack for demo purposes, the deployment target should be driven
// TODO this is a temporary hack for demo purposes, the deployment target should be driven
// by the topology only and we should not have to know how to perform tasks like this for // by the topology only and we should not have to know how to perform tasks like this for
// which the topology should be responsible. // which the topology should be responsible.
// //
@@ -171,38 +166,37 @@ impl<
// access it. This forces every Topology to understand the concept of targets though... So // access it. This forces every Topology to understand the concept of targets though... So
// instead I'll create a new Capability which is MultiTargetTopology and we'll see how it // instead I'll create a new Capability which is MultiTargetTopology and we'll see how it
// goes. It still does not feel right though. // goes. It still does not feel right though.
//
// https://git.nationtech.io/NationTech/harmony/issues/106
match topology.current_target() { match topology.current_target() {
DeploymentTarget::LocalDev => { DeploymentTarget::LocalDev => {
info!("Deploying {} locally...", self.application.name());
self.deploy_to_local_k3d(self.application.name(), helm_chart, image) self.deploy_to_local_k3d(self.application.name(), helm_chart, image)
.await?; .await?;
} }
target => { target => {
info!("Deploying to target {target:?}"); info!("Deploying {} to target {target:?}", self.application.name());
let cd_server = HelmChartScore { let score = ArgoHelmScore {
namespace: todo!( namespace: "harmony-example-rust-webapp".to_string(),
"ArgoCD Helm chart with proper understanding of Tenant, see how Will did it for Monitoring for now" openshift: true,
), domain: "argo.harmonydemo.apps.ncd0.harmony.mcd".to_string(),
release_name: todo!("argocd helm chart whatever"), argo_apps: vec![ArgoApplication::from(CDApplicationConfig {
chart_name: todo!(), // helm pull oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart --version 0.1.0
chart_version: todo!(), version: Version::from("0.1.0").unwrap(),
values_overrides: todo!(), helm_chart_repo_url: "hub.nationtech.io/harmony".to_string(),
values_yaml: todo!(), helm_chart_name: "harmony-example-rust-webapp-chart".to_string(),
create_namespace: todo!(), values_overrides: None,
install_only: todo!(), name: "harmony-demo-rust-webapp".to_string(),
repository: todo!(), namespace: "harmony-example-rust-webapp".to_string(),
})],
}; };
let interpret = cd_server.create_interpret(); score
interpret.execute(&Inventory::empty(), topology); .interpret(&Inventory::empty(), topology)
.await
.unwrap();
} }
}; };
Ok(())
todo!("1. Create ArgoCD score that installs argo using helm chart, see if Taha's already done it
- [X] Package app (docker image, helm chart)
- [X] Push to registry
- [ ] Push only if staging or prod
- [ ] Deploy to local k3d when target is local
- [ ] Poke Argo
- [ ] Ensure app is up")
} }
fn name(&self) -> String { fn name(&self) -> String {
"ContinuousDelivery".to_string() "ContinuousDelivery".to_string()
@@ -212,9 +206,12 @@ impl<
/// For now this is entirely bound to K8s / ArgoCD, will have to be revisited when we support /// For now this is entirely bound to K8s / ArgoCD, will have to be revisited when we support
/// more CD systems /// more CD systems
pub struct CDApplicationConfig { pub struct CDApplicationConfig {
version: Version, pub version: Version,
helm_chart_url: Url, pub helm_chart_repo_url: String,
values_overrides: Value, pub helm_chart_name: String,
pub values_overrides: Option<Value>,
pub name: String,
pub namespace: String,
} }
pub trait ContinuousDeliveryApplication { pub trait ContinuousDeliveryApplication {

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,19 +1,105 @@
use async_trait::async_trait; use std::sync::Arc;
use log::info;
use crate::modules::application::{Application, ApplicationFeature};
use crate::modules::monitoring::application_monitoring::application_monitoring_score::ApplicationMonitoringScore;
use crate::modules::monitoring::kube_prometheus::crd::crd_alertmanager_config::CRDPrometheus;
use crate::topology::MultiTargetTopology;
use crate::{ use crate::{
modules::application::{Application, ApplicationFeature}, inventory::Inventory,
topology::{HelmCommand, Topology}, modules::monitoring::{
alert_channel::webhook_receiver::WebhookReceiver, ntfy::ntfy::NtfyScore,
},
score::Score,
topology::{HelmCommand, K8sclient, Topology, Url, tenant::TenantManager},
}; };
use crate::{
modules::prometheus::prometheus::PrometheusApplicationMonitoring,
topology::oberservability::monitoring::AlertReceiver,
};
use async_trait::async_trait;
use base64::{Engine as _, engine::general_purpose};
use log::{debug, info};
#[derive(Debug, Default, Clone)] #[derive(Debug, Clone)]
pub struct Monitoring {} pub struct Monitoring {
pub application: Arc<dyn Application>,
pub alert_receiver: Vec<Box<dyn AlertReceiver<CRDPrometheus>>>,
}
#[async_trait] #[async_trait]
impl<T: Topology + HelmCommand + 'static> ApplicationFeature<T> for Monitoring { impl<
async fn ensure_installed(&self, _topology: &T) -> Result<(), String> { T: Topology
+ HelmCommand
+ 'static
+ TenantManager
+ K8sclient
+ MultiTargetTopology
+ std::fmt::Debug
+ PrometheusApplicationMonitoring<CRDPrometheus>,
> ApplicationFeature<T> for Monitoring
{
async fn ensure_installed(&self, topology: &T) -> Result<(), String> {
info!("Ensuring monitoring is available for application"); info!("Ensuring monitoring is available for application");
todo!("create and execute k8s prometheus score, depends on Will's work") let namespace = topology
.get_tenant_config()
.await
.map(|ns| ns.name.clone())
.unwrap_or_else(|| self.application.name());
let mut alerting_score = ApplicationMonitoringScore {
sender: CRDPrometheus {
namespace: namespace.clone(),
client: topology.k8s_client().await.unwrap(),
},
application: self.application.clone(),
receivers: self.alert_receiver.clone(),
};
let ntfy = NtfyScore {
namespace: namespace.clone(),
host: "ntfy.harmonydemo.apps.ncd0.harmony.mcd".to_string(),
};
ntfy.interpret(&Inventory::empty(), topology)
.await
.map_err(|e| e.to_string())?;
let ntfy_default_auth_username = "harmony";
let ntfy_default_auth_password = "harmony";
let ntfy_default_auth_header = format!(
"Basic {}",
general_purpose::STANDARD.encode(format!(
"{ntfy_default_auth_username}:{ntfy_default_auth_password}"
))
);
debug!("ntfy_default_auth_header: {ntfy_default_auth_header}");
let ntfy_default_auth_param = general_purpose::STANDARD
.encode(ntfy_default_auth_header)
.replace("=", "");
debug!("ntfy_default_auth_param: {ntfy_default_auth_param}");
let ntfy_receiver = WebhookReceiver {
name: "ntfy-webhook".to_string(),
url: Url::Url(
url::Url::parse(
format!(
"http://ntfy.{}.svc.cluster.local/rust-web-app?auth={ntfy_default_auth_param}",
namespace.clone()
)
.as_str(),
)
.unwrap(),
),
};
alerting_score.receivers.push(Box::new(ntfy_receiver));
alerting_score
.interpret(&Inventory::empty(), topology)
.await
.map_err(|e| e.to_string())?;
Ok(())
} }
fn name(&self) -> String { fn name(&self) -> String {
"Monitoring".to_string() "Monitoring".to_string()

View File

@@ -5,38 +5,47 @@ mod rust;
use std::sync::Arc; use std::sync::Arc;
pub use feature::*; pub use feature::*;
use log::info; use log::debug;
pub use oci::*; pub use oci::*;
pub use rust::*; pub use rust::*;
use async_trait::async_trait; use async_trait::async_trait;
use serde::Serialize;
use crate::{ use crate::{
data::{Id, Version}, data::{Id, Version},
instrumentation::{self, HarmonyEvent},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory, inventory::Inventory,
topology::Topology, topology::Topology,
}; };
#[derive(Clone, Debug)]
pub enum ApplicationFeatureStatus {
Installing,
Installed,
Failed { details: String },
}
pub trait Application: std::fmt::Debug + Send + Sync { pub trait Application: std::fmt::Debug + Send + Sync {
fn name(&self) -> String; fn name(&self) -> String;
} }
#[derive(Debug)] #[derive(Debug)]
pub struct ApplicationInterpret<T: Topology + std::fmt::Debug> { pub struct ApplicationInterpret<A: Application, T: Topology + std::fmt::Debug> {
features: Vec<Box<dyn ApplicationFeature<T>>>, features: Vec<Box<dyn ApplicationFeature<T>>>,
application: Arc<Box<dyn Application>>, application: Arc<A>,
} }
#[async_trait] #[async_trait]
impl<T: Topology + std::fmt::Debug> Interpret<T> for ApplicationInterpret<T> { impl<A: Application, T: Topology + std::fmt::Debug> Interpret<T> for ApplicationInterpret<A, T> {
async fn execute( async fn execute(
&self, &self,
_inventory: &Inventory, _inventory: &Inventory,
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
let app_name = self.application.name(); let app_name = self.application.name();
info!( debug!(
"Preparing {} features [{}] for application {app_name}", "Preparing {} features [{}] for application {app_name}",
self.features.len(), self.features.len(),
self.features self.features
@@ -46,22 +55,41 @@ impl<T: Topology + std::fmt::Debug> Interpret<T> for ApplicationInterpret<T> {
.join(", ") .join(", ")
); );
for feature in self.features.iter() { for feature in self.features.iter() {
info!( instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged {
"Installing feature {} for application {app_name}", topology: topology.name().into(),
feature.name() application: self.application.name(),
); feature: feature.name(),
status: ApplicationFeatureStatus::Installing,
})
.unwrap();
let _ = match feature.ensure_installed(topology).await { let _ = match feature.ensure_installed(topology).await {
Ok(()) => (), Ok(()) => {
instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged {
topology: topology.name().into(),
application: self.application.name(),
feature: feature.name(),
status: ApplicationFeatureStatus::Installed,
})
.unwrap();
}
Err(msg) => { Err(msg) => {
instrumentation::instrument(HarmonyEvent::ApplicationFeatureStateChanged {
topology: topology.name().into(),
application: self.application.name(),
feature: feature.name(),
status: ApplicationFeatureStatus::Failed {
details: msg.clone(),
},
})
.unwrap();
return Err(InterpretError::new(format!( return Err(InterpretError::new(format!(
"Application Interpret failed to install feature : {msg}" "Application Interpret failed to install feature : {msg}"
))); )));
} }
}; };
} }
todo!( Ok(Outcome::success("Application created".to_string()))
"Do I need to do anything more than this here?? I feel like the Application trait itself should expose something like ensure_ready but its becoming redundant. We'll see as this evolves."
)
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {
@@ -80,3 +108,12 @@ impl<T: Topology + std::fmt::Debug> Interpret<T> for ApplicationInterpret<T> {
todo!() todo!()
} }
} }
impl Serialize for dyn Application {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
todo!()
}
}

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ use crate::{
domain::{data::Version, interpret::InterpretStatus}, domain::{data::Version, interpret::InterpretStatus},
interpret::{Interpret, InterpretError, InterpretName, Outcome}, interpret::{Interpret, InterpretError, InterpretName, Outcome},
inventory::Inventory, inventory::Inventory,
topology::{DHCPStaticEntry, DhcpServer, HostBinding, IpAddress, Topology}, topology::{DHCPStaticEntry, DhcpServer, HostBinding, IpAddress, PxeOptions, Topology},
}; };
use crate::domain::score::Score; use crate::domain::score::Score;
@@ -98,69 +98,14 @@ impl DhcpInterpret {
_inventory: &Inventory, _inventory: &Inventory,
dhcp_server: &D, dhcp_server: &D,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
let next_server_outcome = match self.score.next_server { let pxe_options = PxeOptions {
Some(next_server) => { ipxe_filename: self.score.filenameipxe.clone().unwrap_or_default(),
dhcp_server.set_next_server(next_server).await?; bios_filename: self.score.filename.clone().unwrap_or_default(),
Outcome::new( efi_filename: self.score.filename64.clone().unwrap_or_default(),
InterpretStatus::SUCCESS, tftp_ip: self.score.next_server,
format!("Dhcp Interpret Set next boot to {next_server}"),
)
}
None => Outcome::noop(),
}; };
let boot_filename_outcome = match &self.score.boot_filename { dhcp_server.set_pxe_options(pxe_options).await?;
Some(boot_filename) => {
dhcp_server.set_boot_filename(&boot_filename).await?;
Outcome::new(
InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set boot filename to {boot_filename}"),
)
}
None => Outcome::noop(),
};
let filename_outcome = match &self.score.filename {
Some(filename) => {
dhcp_server.set_filename(&filename).await?;
Outcome::new(
InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set filename to {filename}"),
)
}
None => Outcome::noop(),
};
let filename64_outcome = match &self.score.filename64 {
Some(filename64) => {
dhcp_server.set_filename64(&filename64).await?;
Outcome::new(
InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set filename64 to {filename64}"),
)
}
None => Outcome::noop(),
};
let filenameipxe_outcome = match &self.score.filenameipxe {
Some(filenameipxe) => {
dhcp_server.set_filenameipxe(&filenameipxe).await?;
Outcome::new(
InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set filenameipxe to {filenameipxe}"),
)
}
None => Outcome::noop(),
};
if next_server_outcome.status == InterpretStatus::NOOP
&& boot_filename_outcome.status == InterpretStatus::NOOP
&& filename_outcome.status == InterpretStatus::NOOP
&& filename64_outcome.status == InterpretStatus::NOOP
&& filenameipxe_outcome.status == InterpretStatus::NOOP
{
return Ok(Outcome::noop());
}
Ok(Outcome::new( Ok(Outcome::new(
InterpretStatus::SUCCESS, InterpretStatus::SUCCESS,
@@ -209,7 +154,7 @@ impl<T: DhcpServer> Interpret<T> for DhcpInterpret {
Ok(Outcome::new( Ok(Outcome::new(
InterpretStatus::SUCCESS, InterpretStatus::SUCCESS,
format!("Dhcp Interpret execution successful"), "Dhcp Interpret execution successful".to_string(),
)) ))
} }
} }

View File

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

View File

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

View File

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

View File

@@ -3,21 +3,33 @@ use derive_new::new;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
data::{Id, Version}, data::{FileContent, Id, Version},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory, inventory::Inventory,
score::Score, score::Score,
topology::{HttpServer, Topology, Url}, topology::{HttpServer, Topology, Url},
}; };
/// Configure an HTTP server that is provided by the Topology
///
/// This Score will let you easily specify a file path to be served by the HTTP server
///
/// For example, if you have a folder of assets at `/var/www/assets` simply do :
///
/// ```rust,ignore
/// StaticFilesHttpScore {
/// files_to_serve: url!("file:///var/www/assets"),
/// }
/// ```
#[derive(Debug, new, Clone, Serialize)] #[derive(Debug, new, Clone, Serialize)]
pub struct HttpScore { pub struct StaticFilesHttpScore {
files_to_serve: Url, pub folder_to_serve: Option<Url>,
pub files: Vec<FileContent>,
} }
impl<T: Topology + HttpServer> Score<T> for HttpScore { impl<T: Topology + HttpServer> Score<T> for StaticFilesHttpScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(HttpInterpret::new(self.clone())) Box::new(StaticFilesHttpInterpret::new(self.clone()))
} }
fn name(&self) -> String { fn name(&self) -> String {
@@ -26,12 +38,12 @@ impl<T: Topology + HttpServer> Score<T> for HttpScore {
} }
#[derive(Debug, new, Clone)] #[derive(Debug, new, Clone)]
pub struct HttpInterpret { pub struct StaticFilesHttpInterpret {
score: HttpScore, score: StaticFilesHttpScore,
} }
#[async_trait] #[async_trait]
impl<T: Topology + HttpServer> Interpret<T> for HttpInterpret { impl<T: Topology + HttpServer> Interpret<T> for StaticFilesHttpInterpret {
async fn execute( async fn execute(
&self, &self,
_inventory: &Inventory, _inventory: &Inventory,
@@ -39,12 +51,20 @@ impl<T: Topology + HttpServer> Interpret<T> for HttpInterpret {
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
http_server.ensure_initialized().await?; http_server.ensure_initialized().await?;
// http_server.set_ip(topology.router.get_gateway()).await?; // http_server.set_ip(topology.router.get_gateway()).await?;
http_server.serve_files(&self.score.files_to_serve).await?; if let Some(folder) = self.score.folder_to_serve.as_ref() {
http_server.serve_files(folder).await?;
}
for f in self.score.files.iter() {
http_server.serve_file_content(&f).await?
}
http_server.commit_config().await?; http_server.commit_config().await?;
http_server.reload_restart().await?; http_server.reload_restart().await?;
Ok(Outcome::success(format!( Ok(Outcome::success(format!(
"Http Server running and serving files from {}", "Http Server running and serving files from folder {:?} and content for {}",
self.score.files_to_serve self.score.folder_to_serve,
self.score.files.iter().map(|f| f.path.to_string()).collect::<Vec<String>>().join(",")
))) )))
} }

View File

@@ -1,7 +1,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use async_trait::async_trait; use async_trait::async_trait;
use log::info; use log::debug;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
@@ -29,14 +29,14 @@ impl Default for K3DInstallationScore {
} }
impl<T: Topology> Score<T> for K3DInstallationScore { impl<T: Topology> Score<T> for K3DInstallationScore {
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(K3dInstallationInterpret { Box::new(K3dInstallationInterpret {
score: self.clone(), score: self.clone(),
}) })
} }
fn name(&self) -> String { fn name(&self) -> String {
todo!() "K3dInstallationScore".into()
} }
} }
@@ -56,14 +56,15 @@ impl<T: Topology> Interpret<T> for K3dInstallationInterpret {
self.score.installation_path.clone(), self.score.installation_path.clone(),
Some(self.score.cluster_name.clone()), Some(self.score.cluster_name.clone()),
); );
match k3d.ensure_installed().await { match k3d.ensure_installed().await {
Ok(_client) => { Ok(_client) => {
let msg = format!("k3d cluster {} is installed ", self.score.cluster_name); let msg = format!("k3d cluster '{}' installed ", self.score.cluster_name);
info!("{msg}"); debug!("{msg}");
Ok(Outcome::success(msg)) Ok(Outcome::success(msg))
} }
Err(msg) => Err(InterpretError::new(format!( Err(msg) => Err(InterpretError::new(format!(
"K3dInstallationInterpret failed to ensure k3d is installed : {msg}" "failed to ensure k3d is installed : {msg}"
))), ))),
} }
} }

View File

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

View File

@@ -128,13 +128,12 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for LAMPInterpret {
info!("Deploying score {deployment_score:#?}"); info!("Deploying score {deployment_score:#?}");
deployment_score deployment_score.interpret(inventory, topology).await?;
.create_interpret()
.execute(inventory, topology)
.await?;
info!("LAMP deployment_score {deployment_score:?}"); info!("LAMP deployment_score {deployment_score:?}");
let ingress_path = ingress_path!("/");
let lamp_ingress = K8sIngressScore { let lamp_ingress = K8sIngressScore {
name: fqdn!("lamp-ingress"), name: fqdn!("lamp-ingress"),
host: fqdn!("test"), host: fqdn!("test"),
@@ -144,17 +143,14 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for LAMPInterpret {
.as_str() .as_str()
), ),
port: 8080, port: 8080,
path: Some(ingress_path!("/")), path: Some(ingress_path),
path_type: None, path_type: None,
namespace: self namespace: self
.get_namespace() .get_namespace()
.map(|nbs| fqdn!(nbs.to_string().as_str())), .map(|nbs| fqdn!(nbs.to_string().as_str())),
}; };
lamp_ingress lamp_ingress.interpret(inventory, topology).await?;
.create_interpret()
.execute(inventory, topology)
.await?;
info!("LAMP lamp_ingress {lamp_ingress:?}"); info!("LAMP lamp_ingress {lamp_ingress:?}");
@@ -164,7 +160,7 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for LAMPInterpret {
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {
todo!() InterpretName::Lamp
} }
fn get_version(&self) -> Version { fn get_version(&self) -> Version {
@@ -213,7 +209,7 @@ impl LAMPInterpret {
repository: None, repository: None,
}; };
score.create_interpret().execute(inventory, topology).await score.interpret(inventory, topology).await
} }
fn build_dockerfile(&self, score: &LAMPScore) -> Result<PathBuf, Box<dyn std::error::Error>> { fn build_dockerfile(&self, score: &LAMPScore) -> Result<PathBuf, Box<dyn std::error::Error>> {
let mut dockerfile = Dockerfile::new(); let mut dockerfile = Dockerfile::new();

View File

@@ -14,5 +14,6 @@ pub mod monitoring;
pub mod okd; pub mod okd;
pub mod opnsense; pub mod opnsense;
pub mod prometheus; pub mod prometheus;
pub mod storage;
pub mod tenant; pub mod tenant;
pub mod tftp; pub mod tftp;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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