feat: add tui

This commit is contained in:
2026-01-06 19:06:29 +01:00
parent 0c318ebddb
commit 2d59c7fd69
8 changed files with 1067 additions and 106 deletions

View File

@@ -18,6 +18,7 @@ tracing.workspace = true
uuid = { version = "1.19.0", features = ["serde", "v4", "v7"] }
[dev-dependencies]
nocontrol-tui = { path = "../nocontrol-tui" }
insta = "1.46.0"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
tracing-test = { version = "0.2.5", features = ["no-env-filter"] }

View File

@@ -1,4 +1,4 @@
use std::io::{BufRead, Write};
use std::time::Duration;
use async_trait::async_trait;
use nocontrol::{
@@ -10,10 +10,10 @@ use tracing_subscriber::EnvFilter;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Setup logging to file
let output_file = std::fs::File::create("target/nocontrol.log")?;
tracing_subscriber::fmt()
// .pretty()
.with_env_filter(EnvFilter::from_default_env())
.with_writer(output_file)
.with_file(false)
@@ -23,109 +23,57 @@ async fn main() -> anyhow::Result<()> {
.init();
let operator = MyOperator {};
let control_plane = nocontrol::ControlPlane::new(operator);
let mut control_plane = nocontrol::ControlPlane::new(operator);
// control_plane.with_deadline(std::time::Duration::from_secs(4));
// Add initial manifest
control_plane
.add_manifest(Manifest {
name: "initial-deployment".into(),
metadata: ManifestMetadata {},
spec: Specifications::Deployment(DeploymentControllerManifest {
name: "initial-app".into(),
}),
})
.await?;
// Spawn random manifest updater
tokio::spawn({
let control_plane = control_plane.clone();
async move {
control_plane
.add_manifest(Manifest {
name: "some-manifest".into(),
metadata: ManifestMetadata {},
spec: Specifications::Deployment(DeploymentControllerManifest {
name: "some-name".into(),
}),
})
.await
.unwrap();
loop {
let rand = {
use rand::prelude::*;
use rand::Rng;
let mut rng = rand::rng();
rng.random_range(2..5)
rng.random_range(3..8)
};
tokio::time::sleep(std::time::Duration::from_secs(rand)).await;
tokio::time::sleep(Duration::from_secs(rand)).await;
let random = uuid::Uuid::now_v7();
control_plane
let _ = control_plane
.add_manifest(Manifest {
name: "some-manifest".into(),
name: "initial-deployment".into(),
metadata: ManifestMetadata {},
spec: Specifications::Deployment(DeploymentControllerManifest {
name: format!("some-changed-name: {}", random),
name: format!("app-{}", &random.to_string()[..8]),
}),
})
.await
.unwrap();
.await;
}
}
});
// Debugging shell
// Spawn control plane
tokio::spawn({
let control_plane = control_plane.clone();
async move {
let ui = Ui {};
loop {
ui.write("> ");
let cmd = ui.read_line();
let items = cmd.split(" ").map(|t| t.to_string()).collect::<Vec<_>>();
let (command, args) = match &items[..] {
[first, rest @ ..] => (first, rest.to_vec()),
//[first] => (first, vec![]),
_ => {
ui.writeln("invalid command");
continue;
}
};
match (command.as_str(), args.as_slice()) {
("get", _) => {
// get all for now
let manifests = control_plane
.get_manifests()
.await
.inspect_err(|e| ui.writeln(format!("get failed: {e:#}")))
.unwrap();
ui.writeln("listing manifests");
for manifest in manifests {
ui.writeln(format!(" - {}", manifest.manifest.name));
}
}
("describe", [manifest_name, ..]) => {
let manifests = control_plane
.get_manifests()
.await
.inspect_err(|e| ui.writeln(format!("get failed: {e:#}")))
.unwrap();
if let Some(manifest) =
manifests.iter().find(|m| &m.manifest.name == manifest_name)
{
let output = serde_json::to_string_pretty(&manifest).unwrap();
ui.writeln(output);
}
}
(cmd, _) => ui.writeln(format!("command is not implemented: {}", cmd)),
}
ui.writeln("");
}
let _ = control_plane.execute().await;
}
});
control_plane.execute().await?;
// Run TUI
nocontrol_tui::run(control_plane).await?;
Ok(())
}
@@ -180,29 +128,3 @@ impl Specification for Specifications {
pub struct DeploymentControllerManifest {
name: String,
}
pub struct Ui {}
impl Ui {
pub fn write(&self, msg: &str) {
let mut stderr = std::io::stderr().lock();
stderr.write_all(msg.as_bytes()).unwrap();
stderr.flush().unwrap()
}
pub fn writeln(&self, msg: impl AsRef<str>) {
let msg = msg.as_ref();
let mut stderr = std::io::stderr().lock();
stderr.write_all(msg.as_bytes()).unwrap();
writeln!(stderr).unwrap();
stderr.flush().unwrap()
}
pub fn read_line(&self) -> String {
let mut stdin = std::io::stdin().lock();
let mut output = String::new();
stdin.read_line(&mut output).unwrap();
output.trim().to_string()
}
}

View File

@@ -122,6 +122,7 @@ impl<T: Specification> BackingStore<T> {
events: Vec::default(),
changes: vec![ManifestChangeEvent {
created: now,
handled: false,
event: ManifestChangeEventType::Changed,
}],
},

View File

@@ -60,11 +60,22 @@ impl<T: Operator> Reconciler<T> {
}
// 4. Check desired vs actual
for manifest in our_manifests.iter_mut() {
'manifest: for manifest in our_manifests.iter_mut() {
// Currently periodic sync,
// TODO: this should also be made event based
if let Some(change) = manifest.status.changes.first() {
if change.handled {
continue 'manifest;
}
}
self.operator.reconcile(manifest).await?;
self.store.update_state(manifest).await?;
if let Some(change) = manifest.status.changes.first_mut() {
change.handled = true
}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;

View File

@@ -58,6 +58,7 @@ pub enum ManifestStatusState {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ManifestChangeEvent {
pub created: jiff::Timestamp,
pub handled: bool,
pub event: ManifestChangeEventType,
}