mirror of
https://github.com/edera-dev/krata.git
synced 2025-08-03 05:10:55 +00:00
krata: reorganize crates
This commit is contained in:
28
crates/kratad/Cargo.toml
Normal file
28
crates/kratad/Cargo.toml
Normal file
@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "kratad"
|
||||
version.workspace = true
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
async-stream = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
krata = { path = "../krata" }
|
||||
kratart = { path = "../kratart" }
|
||||
log = { workspace = true }
|
||||
signal-hook = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
tonic = { workspace = true, features = ["tls"] }
|
||||
uuid = { workspace = true }
|
||||
|
||||
[lib]
|
||||
name = "kratad"
|
||||
|
||||
[[bin]]
|
||||
name = "kratad"
|
||||
path = "bin/daemon.rs"
|
37
crates/kratad/bin/daemon.rs
Normal file
37
crates/kratad/bin/daemon.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use env_logger::Env;
|
||||
use krata::dial::ControlDialAddress;
|
||||
use kratad::Daemon;
|
||||
use kratart::Runtime;
|
||||
use std::{
|
||||
str::FromStr,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[arg(short, long, default_value = "unix:///var/lib/krata/daemon.socket")]
|
||||
listen: String,
|
||||
#[arg(short, long, default_value = "/var/lib/krata")]
|
||||
store: String,
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "multi_thread", worker_threads = 10)]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("warn")).init();
|
||||
mask_sighup()?;
|
||||
|
||||
let args = Args::parse();
|
||||
let addr = ControlDialAddress::from_str(&args.listen)?;
|
||||
let runtime = Runtime::new(args.store.clone()).await?;
|
||||
let mut daemon = Daemon::new(args.store.clone(), runtime).await?;
|
||||
daemon.listen(addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mask_sighup() -> Result<()> {
|
||||
let flag = Arc::new(AtomicBool::new(false));
|
||||
signal_hook::flag::register(signal_hook::consts::SIGHUP, flag)?;
|
||||
Ok(())
|
||||
}
|
189
crates/kratad/src/control.rs
Normal file
189
crates/kratad/src/control.rs
Normal file
@ -0,0 +1,189 @@
|
||||
use std::{io, pin::Pin};
|
||||
|
||||
use async_stream::try_stream;
|
||||
use futures::Stream;
|
||||
use krata::control::{
|
||||
control_service_server::ControlService, ConsoleDataReply, ConsoleDataRequest,
|
||||
DestroyGuestReply, DestroyGuestRequest, GuestInfo, LaunchGuestReply, LaunchGuestRequest,
|
||||
ListGuestsReply, ListGuestsRequest, WatchEventsReply, WatchEventsRequest,
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
select,
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
use tonic::{Request, Response, Status, Streaming};
|
||||
|
||||
use crate::event::DaemonEventContext;
|
||||
use kratart::{launch::GuestLaunchRequest, Runtime};
|
||||
|
||||
pub struct ApiError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for ApiError {
|
||||
fn from(value: anyhow::Error) -> Self {
|
||||
ApiError {
|
||||
message: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiError> for Status {
|
||||
fn from(value: ApiError) -> Self {
|
||||
Status::unknown(value.message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RuntimeControlService {
|
||||
events: DaemonEventContext,
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
impl RuntimeControlService {
|
||||
pub fn new(events: DaemonEventContext, runtime: Runtime) -> Self {
|
||||
Self { events, runtime }
|
||||
}
|
||||
}
|
||||
|
||||
enum ConsoleDataSelect {
|
||||
Read(io::Result<usize>),
|
||||
Write(Option<Result<ConsoleDataRequest, tonic::Status>>),
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl ControlService for RuntimeControlService {
|
||||
type ConsoleDataStream =
|
||||
Pin<Box<dyn Stream<Item = Result<ConsoleDataReply, Status>> + Send + 'static>>;
|
||||
|
||||
type WatchEventsStream =
|
||||
Pin<Box<dyn Stream<Item = Result<WatchEventsReply, Status>> + Send + 'static>>;
|
||||
|
||||
async fn launch_guest(
|
||||
&self,
|
||||
request: Request<LaunchGuestRequest>,
|
||||
) -> Result<Response<LaunchGuestReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
let guest: GuestInfo = convert_guest_info(
|
||||
self.runtime
|
||||
.launch(GuestLaunchRequest {
|
||||
image: &request.image,
|
||||
vcpus: request.vcpus,
|
||||
mem: request.mem,
|
||||
env: empty_vec_optional(request.env),
|
||||
run: empty_vec_optional(request.run),
|
||||
debug: false,
|
||||
})
|
||||
.await
|
||||
.map_err(ApiError::from)?,
|
||||
);
|
||||
Ok(Response::new(LaunchGuestReply { guest: Some(guest) }))
|
||||
}
|
||||
|
||||
async fn destroy_guest(
|
||||
&self,
|
||||
request: Request<DestroyGuestRequest>,
|
||||
) -> Result<Response<DestroyGuestReply>, Status> {
|
||||
let request = request.into_inner();
|
||||
self.runtime
|
||||
.destroy(&request.guest_id)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
Ok(Response::new(DestroyGuestReply {}))
|
||||
}
|
||||
|
||||
async fn list_guests(
|
||||
&self,
|
||||
request: Request<ListGuestsRequest>,
|
||||
) -> Result<Response<ListGuestsReply>, Status> {
|
||||
let _ = request.into_inner();
|
||||
let guests = self.runtime.list().await.map_err(ApiError::from)?;
|
||||
let guests = guests
|
||||
.into_iter()
|
||||
.map(convert_guest_info)
|
||||
.collect::<Vec<GuestInfo>>();
|
||||
Ok(Response::new(ListGuestsReply { guests }))
|
||||
}
|
||||
|
||||
async fn console_data(
|
||||
&self,
|
||||
request: Request<Streaming<ConsoleDataRequest>>,
|
||||
) -> Result<Response<Self::ConsoleDataStream>, Status> {
|
||||
let mut input = request.into_inner();
|
||||
let Some(request) = input.next().await else {
|
||||
return Err(ApiError {
|
||||
message: "expected to have at least one request".to_string(),
|
||||
}
|
||||
.into());
|
||||
};
|
||||
let request = request?;
|
||||
let mut console = self
|
||||
.runtime
|
||||
.console(&request.guest_id)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
let output = try_stream! {
|
||||
let mut buffer: Vec<u8> = vec![0u8; 256];
|
||||
loop {
|
||||
let what = select! {
|
||||
x = console.read_handle.read(&mut buffer) => ConsoleDataSelect::Read(x),
|
||||
x = input.next() => ConsoleDataSelect::Write(x),
|
||||
};
|
||||
|
||||
match what {
|
||||
ConsoleDataSelect::Read(result) => {
|
||||
let size = result?;
|
||||
let data = buffer[0..size].to_vec();
|
||||
yield ConsoleDataReply { data, };
|
||||
},
|
||||
|
||||
ConsoleDataSelect::Write(Some(request)) => {
|
||||
let request = request?;
|
||||
if !request.data.is_empty() {
|
||||
console.write_handle.write_all(&request.data).await?;
|
||||
}
|
||||
},
|
||||
|
||||
ConsoleDataSelect::Write(None) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Response::new(Box::pin(output) as Self::ConsoleDataStream))
|
||||
}
|
||||
|
||||
async fn watch_events(
|
||||
&self,
|
||||
request: Request<WatchEventsRequest>,
|
||||
) -> Result<Response<Self::WatchEventsStream>, Status> {
|
||||
let _ = request.into_inner();
|
||||
let mut events = self.events.subscribe();
|
||||
let output = try_stream! {
|
||||
while let Ok(event) = events.recv().await {
|
||||
yield WatchEventsReply { event: Some(event), };
|
||||
}
|
||||
};
|
||||
Ok(Response::new(Box::pin(output) as Self::WatchEventsStream))
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_vec_optional<T>(value: Vec<T>) -> Option<Vec<T>> {
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_guest_info(value: kratart::GuestInfo) -> GuestInfo {
|
||||
GuestInfo {
|
||||
id: value.uuid.to_string(),
|
||||
image: value.image,
|
||||
ipv4: value.ipv4.map(|x| x.ip().to_string()).unwrap_or_default(),
|
||||
ipv6: value.ipv6.map(|x| x.ip().to_string()).unwrap_or_default(),
|
||||
}
|
||||
}
|
112
crates/kratad/src/event.rs
Normal file
112
crates/kratad/src/event.rs
Normal file
@ -0,0 +1,112 @@
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
use anyhow::Result;
|
||||
use krata::control::{GuestDestroyedEvent, GuestExitedEvent, GuestLaunchedEvent};
|
||||
use log::error;
|
||||
use tokio::{sync::broadcast, task::JoinHandle, time};
|
||||
use uuid::Uuid;
|
||||
|
||||
use kratart::{GuestInfo, Runtime};
|
||||
|
||||
pub type DaemonEvent = krata::control::watch_events_reply::Event;
|
||||
|
||||
const EVENT_CHANNEL_QUEUE_LEN: usize = 1000;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonEventContext {
|
||||
sender: broadcast::Sender<DaemonEvent>,
|
||||
}
|
||||
|
||||
impl DaemonEventContext {
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<DaemonEvent> {
|
||||
self.sender.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DaemonEventGenerator {
|
||||
runtime: Runtime,
|
||||
last: HashMap<Uuid, GuestInfo>,
|
||||
sender: broadcast::Sender<DaemonEvent>,
|
||||
}
|
||||
|
||||
impl DaemonEventGenerator {
|
||||
pub async fn new(runtime: Runtime) -> Result<(DaemonEventContext, DaemonEventGenerator)> {
|
||||
let (sender, _) = broadcast::channel(EVENT_CHANNEL_QUEUE_LEN);
|
||||
let generator = DaemonEventGenerator {
|
||||
runtime,
|
||||
last: HashMap::new(),
|
||||
sender: sender.clone(),
|
||||
};
|
||||
let context = DaemonEventContext { sender };
|
||||
Ok((context, generator))
|
||||
}
|
||||
|
||||
async fn evaluate(&mut self) -> Result<()> {
|
||||
let guests = self.runtime.list().await?;
|
||||
let guests = {
|
||||
let mut map = HashMap::new();
|
||||
for guest in guests {
|
||||
map.insert(guest.uuid, guest);
|
||||
}
|
||||
map
|
||||
};
|
||||
|
||||
let mut events: Vec<DaemonEvent> = Vec::new();
|
||||
|
||||
for uuid in guests.keys() {
|
||||
if !self.last.contains_key(uuid) {
|
||||
events.push(DaemonEvent::GuestLaunched(GuestLaunchedEvent {
|
||||
guest_id: uuid.to_string(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
for uuid in self.last.keys() {
|
||||
if !guests.contains_key(uuid) {
|
||||
events.push(DaemonEvent::GuestDestroyed(GuestDestroyedEvent {
|
||||
guest_id: uuid.to_string(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
for (uuid, guest) in &guests {
|
||||
let Some(last) = self.last.get(uuid) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if last.state.exit_code.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(code) = guest.state.exit_code else {
|
||||
continue;
|
||||
};
|
||||
|
||||
events.push(DaemonEvent::GuestExited(GuestExitedEvent {
|
||||
guest_id: uuid.to_string(),
|
||||
code,
|
||||
}));
|
||||
}
|
||||
|
||||
self.last = guests;
|
||||
|
||||
for event in events {
|
||||
let _ = self.sender.send(event);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn launch(mut self) -> Result<JoinHandle<()>> {
|
||||
Ok(tokio::task::spawn(async move {
|
||||
loop {
|
||||
if let Err(error) = self.evaluate().await {
|
||||
error!("failed to evaluate daemon events: {}", error);
|
||||
time::sleep(Duration::from_secs(5)).await;
|
||||
} else {
|
||||
time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
90
crates/kratad/src/lib.rs
Normal file
90
crates/kratad/src/lib.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use std::{net::SocketAddr, path::PathBuf, str::FromStr};
|
||||
|
||||
use anyhow::Result;
|
||||
use control::RuntimeControlService;
|
||||
use event::{DaemonEventContext, DaemonEventGenerator};
|
||||
use krata::{control::control_service_server::ControlServiceServer, dial::ControlDialAddress};
|
||||
use kratart::Runtime;
|
||||
use log::info;
|
||||
use tokio::{net::UnixListener, task::JoinHandle};
|
||||
use tokio_stream::wrappers::UnixListenerStream;
|
||||
use tonic::transport::{Identity, Server, ServerTlsConfig};
|
||||
|
||||
pub mod control;
|
||||
pub mod event;
|
||||
|
||||
pub struct Daemon {
|
||||
store: String,
|
||||
runtime: Runtime,
|
||||
events: DaemonEventContext,
|
||||
task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Daemon {
|
||||
pub async fn new(store: String, runtime: Runtime) -> Result<Self> {
|
||||
let runtime_for_events = runtime.dupe().await?;
|
||||
let (events, generator) = DaemonEventGenerator::new(runtime_for_events).await?;
|
||||
Ok(Self {
|
||||
store,
|
||||
runtime,
|
||||
events,
|
||||
task: generator.launch().await?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn listen(&mut self, addr: ControlDialAddress) -> Result<()> {
|
||||
let control_service = RuntimeControlService::new(self.events.clone(), self.runtime.clone());
|
||||
|
||||
let mut server = Server::builder();
|
||||
|
||||
if let ControlDialAddress::Tls {
|
||||
host: _,
|
||||
port: _,
|
||||
insecure,
|
||||
} = &addr
|
||||
{
|
||||
let mut tls_config = ServerTlsConfig::new();
|
||||
if !insecure {
|
||||
let certificate_path = format!("{}/tls/daemon.pem", self.store);
|
||||
let key_path = format!("{}/tls/daemon.key", self.store);
|
||||
tls_config = tls_config.identity(Identity::from_pem(certificate_path, key_path));
|
||||
}
|
||||
server = server.tls_config(tls_config)?;
|
||||
}
|
||||
|
||||
let server = server.add_service(ControlServiceServer::new(control_service));
|
||||
info!("listening on address {}", addr);
|
||||
match addr {
|
||||
ControlDialAddress::UnixSocket { path } => {
|
||||
let path = PathBuf::from(path);
|
||||
if path.exists() {
|
||||
tokio::fs::remove_file(&path).await?;
|
||||
}
|
||||
let listener = UnixListener::bind(path)?;
|
||||
let stream = UnixListenerStream::new(listener);
|
||||
server.serve_with_incoming(stream).await?;
|
||||
}
|
||||
|
||||
ControlDialAddress::Tcp { host, port } => {
|
||||
let address = format!("{}:{}", host, port);
|
||||
server.serve(SocketAddr::from_str(&address)?).await?;
|
||||
}
|
||||
|
||||
ControlDialAddress::Tls {
|
||||
host,
|
||||
port,
|
||||
insecure: _,
|
||||
} => {
|
||||
let address = format!("{}:{}", host, port);
|
||||
server.serve(SocketAddr::from_str(&address)?).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Daemon {
|
||||
fn drop(&mut self) {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user