mirror of
https://github.com/edera-dev/krata.git
synced 2025-08-07 15:11:32 +00:00
feature(kratactl): rework cli to use subcommands (#268)
This commit is contained in:
41
crates/ctl/src/cli/zone/attach.rs
Normal file
41
crates/ctl/src/cli/zone/attach.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::{events::EventStream, v1::control::control_service_client::ControlServiceClient};
|
||||
|
||||
use tokio::select;
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crate::console::StdioConsoleStream;
|
||||
|
||||
use crate::cli::resolve_zone;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Attach to the zone console")]
|
||||
pub struct ZoneAttachCommand {
|
||||
#[arg(help = "Zone to attach to, either the name or the uuid")]
|
||||
zone: String,
|
||||
}
|
||||
|
||||
impl ZoneAttachCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let zone_id: String = resolve_zone(&mut client, &self.zone).await?;
|
||||
let input = StdioConsoleStream::stdin_stream(zone_id.clone()).await;
|
||||
let output = client.attach_zone_console(input).await?.into_inner();
|
||||
let stdout_handle =
|
||||
tokio::task::spawn(async move { StdioConsoleStream::stdout(output).await });
|
||||
let exit_hook_task = StdioConsoleStream::zone_exit_hook(zone_id.clone(), events).await?;
|
||||
let code = select! {
|
||||
x = stdout_handle => {
|
||||
x??;
|
||||
None
|
||||
},
|
||||
x = exit_hook_task => x?
|
||||
};
|
||||
StdioConsoleStream::restore_terminal_mode();
|
||||
std::process::exit(code.unwrap_or(0));
|
||||
}
|
||||
}
|
82
crates/ctl/src/cli/zone/destroy.rs
Normal file
82
crates/ctl/src/cli/zone/destroy.rs
Normal file
@ -0,0 +1,82 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::ZoneStatus,
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, watch_events_reply::Event,
|
||||
DestroyZoneRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use log::error;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
use crate::cli::resolve_zone;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Destroy a zone")]
|
||||
pub struct ZoneDestroyCommand {
|
||||
#[arg(
|
||||
short = 'W',
|
||||
long,
|
||||
help = "Wait for the destruction of the zone to complete"
|
||||
)]
|
||||
wait: bool,
|
||||
#[arg(help = "Zone to destroy, either the name or the uuid")]
|
||||
zone: String,
|
||||
}
|
||||
|
||||
impl ZoneDestroyCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let zone_id: String = resolve_zone(&mut client, &self.zone).await?;
|
||||
let _ = client
|
||||
.destroy_zone(Request::new(DestroyZoneRequest {
|
||||
zone_id: zone_id.clone(),
|
||||
}))
|
||||
.await?
|
||||
.into_inner();
|
||||
if self.wait {
|
||||
wait_zone_destroyed(&zone_id, events).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_zone_destroyed(id: &str, events: EventStream) -> Result<()> {
|
||||
let mut stream = events.subscribe();
|
||||
while let Ok(event) = stream.recv().await {
|
||||
let Event::ZoneChanged(changed) = event;
|
||||
let Some(zone) = changed.zone else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if zone.id != id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(state) = zone.state else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(ref error) = state.error_info {
|
||||
if state.status() == ZoneStatus::Failed {
|
||||
error!("destroy failed: {}", error.message);
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
error!("zone error: {}", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if state.status() == ZoneStatus::Destroyed {
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
70
crates/ctl/src/cli/zone/exec.rs
Normal file
70
crates/ctl/src/cli/zone/exec.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use clap::Parser;
|
||||
use krata::v1::{
|
||||
common::{ZoneTaskSpec, ZoneTaskSpecEnvVar},
|
||||
control::{control_service_client::ControlServiceClient, ExecZoneRequest},
|
||||
};
|
||||
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
use crate::console::StdioConsoleStream;
|
||||
|
||||
use crate::cli::resolve_zone;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Execute a command inside the zone")]
|
||||
pub struct ZoneExecCommand {
|
||||
#[arg[short, long, help = "Environment variables"]]
|
||||
env: Option<Vec<String>>,
|
||||
#[arg(short = 'w', long, help = "Working directory")]
|
||||
working_directory: Option<String>,
|
||||
#[arg(help = "Zone to exec inside, either the name or the uuid")]
|
||||
zone: String,
|
||||
#[arg(
|
||||
allow_hyphen_values = true,
|
||||
trailing_var_arg = true,
|
||||
help = "Command to run inside the zone"
|
||||
)]
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
impl ZoneExecCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let zone_id: String = resolve_zone(&mut client, &self.zone).await?;
|
||||
let initial = ExecZoneRequest {
|
||||
zone_id,
|
||||
task: Some(ZoneTaskSpec {
|
||||
environment: env_map(&self.env.unwrap_or_default())
|
||||
.iter()
|
||||
.map(|(key, value)| ZoneTaskSpecEnvVar {
|
||||
key: key.clone(),
|
||||
value: value.clone(),
|
||||
})
|
||||
.collect(),
|
||||
command: self.command,
|
||||
working_directory: self.working_directory.unwrap_or_default(),
|
||||
}),
|
||||
data: vec![],
|
||||
};
|
||||
|
||||
let stream = StdioConsoleStream::stdin_stream_exec(initial).await;
|
||||
|
||||
let response = client.exec_zone(Request::new(stream)).await?.into_inner();
|
||||
|
||||
let code = StdioConsoleStream::exec_output(response).await?;
|
||||
std::process::exit(code);
|
||||
}
|
||||
}
|
||||
|
||||
fn env_map(env: &[String]) -> HashMap<String, String> {
|
||||
let mut map = HashMap::<String, String>::new();
|
||||
for item in env {
|
||||
if let Some((key, value)) = item.split_once('=') {
|
||||
map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
244
crates/ctl/src/cli/zone/launch.rs
Normal file
244
crates/ctl/src/cli/zone/launch.rs
Normal file
@ -0,0 +1,244 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::{
|
||||
zone_image_spec::Image, OciImageFormat, ZoneImageSpec, ZoneOciImageSpec, ZoneSpec,
|
||||
ZoneSpecDevice, ZoneStatus, ZoneTaskSpec, ZoneTaskSpecEnvVar,
|
||||
},
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, watch_events_reply::Event,
|
||||
CreateZoneRequest, PullImageRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
use log::error;
|
||||
use tokio::select;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
use crate::{console::StdioConsoleStream, pull::pull_interactive_progress};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LaunchImageFormat {
|
||||
Squashfs,
|
||||
Erofs,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Launch a new zone")]
|
||||
pub struct ZoneLaunchCommand {
|
||||
#[arg(long, default_value = "squashfs", help = "Image format")]
|
||||
image_format: LaunchImageFormat,
|
||||
#[arg(long, help = "Overwrite image cache on pull")]
|
||||
pull_overwrite_cache: bool,
|
||||
#[arg(short, long, help = "Name of the zone")]
|
||||
name: Option<String>,
|
||||
#[arg(short, long, default_value_t = 1, help = "vCPUs available to the zone")]
|
||||
cpus: u32,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
default_value_t = 512,
|
||||
help = "Memory available to the zone, in megabytes"
|
||||
)]
|
||||
mem: u64,
|
||||
#[arg[short = 'D', long = "device", help = "Devices to request for the zone"]]
|
||||
device: Vec<String>,
|
||||
#[arg[short, long, help = "Environment variables set in the zone"]]
|
||||
env: Option<Vec<String>>,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = "Attach to the zone after zone starts, implies --wait"
|
||||
)]
|
||||
attach: bool,
|
||||
#[arg(
|
||||
short = 'W',
|
||||
long,
|
||||
help = "Wait for the zone to start, implied by --attach"
|
||||
)]
|
||||
wait: bool,
|
||||
#[arg(short = 'k', long, help = "OCI kernel image for zone to use")]
|
||||
kernel: Option<String>,
|
||||
#[arg(short = 'I', long, help = "OCI initrd image for zone to use")]
|
||||
initrd: Option<String>,
|
||||
#[arg(short = 'w', long, help = "Working directory")]
|
||||
working_directory: Option<String>,
|
||||
#[arg(help = "Container image for zone to use")]
|
||||
oci: String,
|
||||
#[arg(
|
||||
allow_hyphen_values = true,
|
||||
trailing_var_arg = true,
|
||||
help = "Command to run inside the zone"
|
||||
)]
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
impl ZoneLaunchCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let image = self
|
||||
.pull_image(
|
||||
&mut client,
|
||||
&self.oci,
|
||||
match self.image_format {
|
||||
LaunchImageFormat::Squashfs => OciImageFormat::Squashfs,
|
||||
LaunchImageFormat::Erofs => OciImageFormat::Erofs,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let kernel = if let Some(ref kernel) = self.kernel {
|
||||
let kernel_image = self
|
||||
.pull_image(&mut client, kernel, OciImageFormat::Tar)
|
||||
.await?;
|
||||
Some(kernel_image)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let initrd = if let Some(ref initrd) = self.initrd {
|
||||
let kernel_image = self
|
||||
.pull_image(&mut client, initrd, OciImageFormat::Tar)
|
||||
.await?;
|
||||
Some(kernel_image)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let request = CreateZoneRequest {
|
||||
spec: Some(ZoneSpec {
|
||||
name: self.name.unwrap_or_default(),
|
||||
image: Some(image),
|
||||
kernel,
|
||||
initrd,
|
||||
vcpus: self.cpus,
|
||||
mem: self.mem,
|
||||
task: Some(ZoneTaskSpec {
|
||||
environment: env_map(&self.env.unwrap_or_default())
|
||||
.iter()
|
||||
.map(|(key, value)| ZoneTaskSpecEnvVar {
|
||||
key: key.clone(),
|
||||
value: value.clone(),
|
||||
})
|
||||
.collect(),
|
||||
command: self.command,
|
||||
working_directory: self.working_directory.unwrap_or_default(),
|
||||
}),
|
||||
annotations: vec![],
|
||||
devices: self
|
||||
.device
|
||||
.iter()
|
||||
.map(|name| ZoneSpecDevice { name: name.clone() })
|
||||
.collect(),
|
||||
}),
|
||||
};
|
||||
let response = client
|
||||
.create_zone(Request::new(request))
|
||||
.await?
|
||||
.into_inner();
|
||||
let id = response.zone_id;
|
||||
|
||||
if self.wait || self.attach {
|
||||
wait_zone_started(&id, events.clone()).await?;
|
||||
}
|
||||
|
||||
let code = if self.attach {
|
||||
let input = StdioConsoleStream::stdin_stream(id.clone()).await;
|
||||
let output = client.attach_zone_console(input).await?.into_inner();
|
||||
let stdout_handle =
|
||||
tokio::task::spawn(async move { StdioConsoleStream::stdout(output).await });
|
||||
let exit_hook_task = StdioConsoleStream::zone_exit_hook(id.clone(), events).await?;
|
||||
select! {
|
||||
x = stdout_handle => {
|
||||
x??;
|
||||
None
|
||||
},
|
||||
x = exit_hook_task => x?
|
||||
}
|
||||
} else {
|
||||
println!("{}", id);
|
||||
None
|
||||
};
|
||||
StdioConsoleStream::restore_terminal_mode();
|
||||
std::process::exit(code.unwrap_or(0));
|
||||
}
|
||||
|
||||
async fn pull_image(
|
||||
&self,
|
||||
client: &mut ControlServiceClient<Channel>,
|
||||
image: &str,
|
||||
format: OciImageFormat,
|
||||
) -> Result<ZoneImageSpec> {
|
||||
let response = client
|
||||
.pull_image(PullImageRequest {
|
||||
image: image.to_string(),
|
||||
format: format.into(),
|
||||
overwrite_cache: self.pull_overwrite_cache,
|
||||
})
|
||||
.await?;
|
||||
let reply = pull_interactive_progress(response.into_inner()).await?;
|
||||
Ok(ZoneImageSpec {
|
||||
image: Some(Image::Oci(ZoneOciImageSpec {
|
||||
digest: reply.digest,
|
||||
format: reply.format,
|
||||
})),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_zone_started(id: &str, events: EventStream) -> Result<()> {
|
||||
let mut stream = events.subscribe();
|
||||
while let Ok(event) = stream.recv().await {
|
||||
match event {
|
||||
Event::ZoneChanged(changed) => {
|
||||
let Some(zone) = changed.zone else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if zone.id != id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(state) = zone.state else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(ref error) = state.error_info {
|
||||
if state.status() == ZoneStatus::Failed {
|
||||
error!("launch failed: {}", error.message);
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
error!("zone error: {}", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if state.status() == ZoneStatus::Destroyed {
|
||||
error!("zone destroyed");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if state.status() == ZoneStatus::Started {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn env_map(env: &[String]) -> HashMap<String, String> {
|
||||
let mut map = HashMap::<String, String>::new();
|
||||
for item in env {
|
||||
if let Some((key, value)) = item.split_once('=') {
|
||||
map.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
170
crates/ctl/src/cli/zone/list.rs
Normal file
170
crates/ctl/src/cli/zone/list.rs
Normal file
@ -0,0 +1,170 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use comfy_table::{presets::UTF8_FULL_CONDENSED, Cell, Color, Table};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::{Zone, ZoneStatus},
|
||||
control::{
|
||||
control_service_client::ControlServiceClient, ListZonesRequest, ResolveZoneRequest,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use serde_json::Value;
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
use crate::format::{kv2line, proto2dynamic, proto2kv, zone_simple_line, zone_status_text};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum ZoneListFormat {
|
||||
Table,
|
||||
Json,
|
||||
JsonPretty,
|
||||
Jsonl,
|
||||
Yaml,
|
||||
KeyValue,
|
||||
Simple,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "List the zones on the isolation engine")]
|
||||
pub struct ZoneListCommand {
|
||||
#[arg(short, long, default_value = "table", help = "Output format")]
|
||||
format: ZoneListFormat,
|
||||
#[arg(help = "Limit to a single zone, either the name or the uuid")]
|
||||
zone: Option<String>,
|
||||
}
|
||||
|
||||
impl ZoneListCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
_events: EventStream,
|
||||
) -> Result<()> {
|
||||
let mut zones = if let Some(ref zone) = self.zone {
|
||||
let reply = client
|
||||
.resolve_zone(Request::new(ResolveZoneRequest { name: zone.clone() }))
|
||||
.await?
|
||||
.into_inner();
|
||||
if let Some(zone) = reply.zone {
|
||||
vec![zone]
|
||||
} else {
|
||||
return Err(anyhow!("unable to resolve zone '{}'", zone));
|
||||
}
|
||||
} else {
|
||||
client
|
||||
.list_zones(Request::new(ListZonesRequest {}))
|
||||
.await?
|
||||
.into_inner()
|
||||
.zones
|
||||
};
|
||||
|
||||
zones.sort_by(|a, b| {
|
||||
a.spec
|
||||
.as_ref()
|
||||
.map(|x| x.name.as_str())
|
||||
.unwrap_or("")
|
||||
.cmp(b.spec.as_ref().map(|x| x.name.as_str()).unwrap_or(""))
|
||||
});
|
||||
|
||||
match self.format {
|
||||
ZoneListFormat::Table => {
|
||||
self.print_zone_table(zones)?;
|
||||
}
|
||||
|
||||
ZoneListFormat::Simple => {
|
||||
for zone in zones {
|
||||
println!("{}", zone_simple_line(&zone));
|
||||
}
|
||||
}
|
||||
|
||||
ZoneListFormat::Json | ZoneListFormat::JsonPretty | ZoneListFormat::Yaml => {
|
||||
let mut values = Vec::new();
|
||||
for zone in zones {
|
||||
let message = proto2dynamic(zone)?;
|
||||
values.push(serde_json::to_value(message)?);
|
||||
}
|
||||
let value = Value::Array(values);
|
||||
let encoded = if self.format == ZoneListFormat::JsonPretty {
|
||||
serde_json::to_string_pretty(&value)?
|
||||
} else if self.format == ZoneListFormat::Yaml {
|
||||
serde_yaml::to_string(&value)?
|
||||
} else {
|
||||
serde_json::to_string(&value)?
|
||||
};
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
ZoneListFormat::Jsonl => {
|
||||
for zone in zones {
|
||||
let message = proto2dynamic(zone)?;
|
||||
println!("{}", serde_json::to_string(&message)?);
|
||||
}
|
||||
}
|
||||
|
||||
ZoneListFormat::KeyValue => {
|
||||
self.print_key_value(zones)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_zone_table(&self, zones: Vec<Zone>) -> Result<()> {
|
||||
let mut table = Table::new();
|
||||
table.load_preset(UTF8_FULL_CONDENSED);
|
||||
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
|
||||
table.set_header(vec!["name", "uuid", "status", "ipv4", "ipv6"]);
|
||||
for zone in zones {
|
||||
let ipv4 = zone
|
||||
.state
|
||||
.as_ref()
|
||||
.and_then(|x| x.network.as_ref())
|
||||
.map(|x| x.zone_ipv4.as_str())
|
||||
.unwrap_or("n/a");
|
||||
let ipv6 = zone
|
||||
.state
|
||||
.as_ref()
|
||||
.and_then(|x| x.network.as_ref())
|
||||
.map(|x| x.zone_ipv6.as_str())
|
||||
.unwrap_or("n/a");
|
||||
let Some(spec) = zone.spec else {
|
||||
continue;
|
||||
};
|
||||
let status = zone.state.as_ref().cloned().unwrap_or_default().status();
|
||||
let status_text = zone_status_text(status);
|
||||
|
||||
let status_color = match status {
|
||||
ZoneStatus::Destroyed | ZoneStatus::Failed => Color::Red,
|
||||
ZoneStatus::Destroying | ZoneStatus::Exited | ZoneStatus::Starting => Color::Yellow,
|
||||
ZoneStatus::Started => Color::Green,
|
||||
_ => Color::Reset,
|
||||
};
|
||||
|
||||
table.add_row(vec![
|
||||
Cell::new(spec.name),
|
||||
Cell::new(zone.id),
|
||||
Cell::new(status_text).fg(status_color),
|
||||
Cell::new(ipv4.to_string()),
|
||||
Cell::new(ipv6.to_string()),
|
||||
]);
|
||||
}
|
||||
if table.is_empty() {
|
||||
if self.zone.is_none() {
|
||||
println!("no zones have been launched");
|
||||
}
|
||||
} else {
|
||||
println!("{}", table);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_key_value(&self, zones: Vec<Zone>) -> Result<()> {
|
||||
for zone in zones {
|
||||
let kvs = proto2kv(zone)?;
|
||||
println!("{}", kv2line(kvs),);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
58
crates/ctl/src/cli/zone/logs.rs
Normal file
58
crates/ctl/src/cli/zone/logs.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use anyhow::Result;
|
||||
use async_stream::stream;
|
||||
use clap::Parser;
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::control::{control_service_client::ControlServiceClient, ZoneConsoleRequest},
|
||||
};
|
||||
|
||||
use tokio::select;
|
||||
use tokio_stream::{pending, StreamExt};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crate::console::StdioConsoleStream;
|
||||
|
||||
use crate::cli::resolve_zone;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "View the logs of a zone")]
|
||||
pub struct ZoneLogsCommand {
|
||||
#[arg(short, long, help = "Follow output from the zone")]
|
||||
follow: bool,
|
||||
#[arg(help = "Zone to show logs for, either the name or the uuid")]
|
||||
zone: String,
|
||||
}
|
||||
|
||||
impl ZoneLogsCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let zone_id: String = resolve_zone(&mut client, &self.zone).await?;
|
||||
let zone_id_stream = zone_id.clone();
|
||||
let follow = self.follow;
|
||||
let input = stream! {
|
||||
yield ZoneConsoleRequest { zone_id: zone_id_stream, data: Vec::new() };
|
||||
if follow {
|
||||
let mut pending = pending::<ZoneConsoleRequest>();
|
||||
while let Some(x) = pending.next().await {
|
||||
yield x;
|
||||
}
|
||||
}
|
||||
};
|
||||
let output = client.attach_zone_console(input).await?.into_inner();
|
||||
let stdout_handle =
|
||||
tokio::task::spawn(async move { StdioConsoleStream::stdout(output).await });
|
||||
let exit_hook_task = StdioConsoleStream::zone_exit_hook(zone_id.clone(), events).await?;
|
||||
let code = select! {
|
||||
x = stdout_handle => {
|
||||
x??;
|
||||
None
|
||||
},
|
||||
x = exit_hook_task => x?
|
||||
};
|
||||
StdioConsoleStream::restore_terminal_mode();
|
||||
std::process::exit(code.unwrap_or(0));
|
||||
}
|
||||
}
|
83
crates/ctl/src/cli/zone/metrics.rs
Normal file
83
crates/ctl/src/cli/zone/metrics.rs
Normal file
@ -0,0 +1,83 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{
|
||||
common::ZoneMetricNode,
|
||||
control::{control_service_client::ControlServiceClient, ReadZoneMetricsRequest},
|
||||
},
|
||||
};
|
||||
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crate::format::{kv2line, metrics_flat, metrics_tree, proto2dynamic};
|
||||
|
||||
use crate::cli::resolve_zone;
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum ZoneMetricsFormat {
|
||||
Tree,
|
||||
Json,
|
||||
JsonPretty,
|
||||
Yaml,
|
||||
KeyValue,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Read metrics from the zone")]
|
||||
pub struct ZoneMetricsCommand {
|
||||
#[arg(short, long, default_value = "tree", help = "Output format")]
|
||||
format: ZoneMetricsFormat,
|
||||
#[arg(help = "Zone to read metrics for, either the name or the uuid")]
|
||||
zone: String,
|
||||
}
|
||||
|
||||
impl ZoneMetricsCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
mut client: ControlServiceClient<Channel>,
|
||||
_events: EventStream,
|
||||
) -> Result<()> {
|
||||
let zone_id: String = resolve_zone(&mut client, &self.zone).await?;
|
||||
let root = client
|
||||
.read_zone_metrics(ReadZoneMetricsRequest { zone_id })
|
||||
.await?
|
||||
.into_inner()
|
||||
.root
|
||||
.unwrap_or_default();
|
||||
match self.format {
|
||||
ZoneMetricsFormat::Tree => {
|
||||
self.print_metrics_tree(root)?;
|
||||
}
|
||||
|
||||
ZoneMetricsFormat::Json | ZoneMetricsFormat::JsonPretty | ZoneMetricsFormat::Yaml => {
|
||||
let value = serde_json::to_value(proto2dynamic(root)?)?;
|
||||
let encoded = if self.format == ZoneMetricsFormat::JsonPretty {
|
||||
serde_json::to_string_pretty(&value)?
|
||||
} else if self.format == ZoneMetricsFormat::Yaml {
|
||||
serde_yaml::to_string(&value)?
|
||||
} else {
|
||||
serde_json::to_string(&value)?
|
||||
};
|
||||
println!("{}", encoded.trim());
|
||||
}
|
||||
|
||||
ZoneMetricsFormat::KeyValue => {
|
||||
self.print_key_value(root)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_metrics_tree(&self, root: ZoneMetricNode) -> Result<()> {
|
||||
print!("{}", metrics_tree(root));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_key_value(&self, metrics: ZoneMetricNode) -> Result<()> {
|
||||
let kvs = metrics_flat(metrics);
|
||||
println!("{}", kv2line(kvs));
|
||||
Ok(())
|
||||
}
|
||||
}
|
89
crates/ctl/src/cli/zone/mod.rs
Normal file
89
crates/ctl/src/cli/zone/mod.rs
Normal file
@ -0,0 +1,89 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use krata::events::EventStream;
|
||||
use krata::v1::control::control_service_client::ControlServiceClient;
|
||||
|
||||
use crate::cli::zone::attach::ZoneAttachCommand;
|
||||
use crate::cli::zone::destroy::ZoneDestroyCommand;
|
||||
use crate::cli::zone::exec::ZoneExecCommand;
|
||||
use crate::cli::zone::launch::ZoneLaunchCommand;
|
||||
use crate::cli::zone::list::ZoneListCommand;
|
||||
use crate::cli::zone::logs::ZoneLogsCommand;
|
||||
use crate::cli::zone::metrics::ZoneMetricsCommand;
|
||||
use crate::cli::zone::resolve::ZoneResolveCommand;
|
||||
use crate::cli::zone::top::ZoneTopCommand;
|
||||
use crate::cli::zone::watch::ZoneWatchCommand;
|
||||
|
||||
pub mod attach;
|
||||
pub mod destroy;
|
||||
pub mod exec;
|
||||
pub mod launch;
|
||||
pub mod list;
|
||||
pub mod logs;
|
||||
pub mod metrics;
|
||||
pub mod resolve;
|
||||
pub mod top;
|
||||
pub mod watch;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Manage the zones on the isolation engine")]
|
||||
pub struct ZoneCommand {
|
||||
#[command(subcommand)]
|
||||
subcommand: ZoneCommands,
|
||||
}
|
||||
|
||||
impl ZoneCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
self.subcommand.run(client, events).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum ZoneCommands {
|
||||
Attach(ZoneAttachCommand),
|
||||
List(ZoneListCommand),
|
||||
Launch(ZoneLaunchCommand),
|
||||
Destroy(ZoneDestroyCommand),
|
||||
Exec(ZoneExecCommand),
|
||||
Logs(ZoneLogsCommand),
|
||||
Metrics(ZoneMetricsCommand),
|
||||
Resolve(ZoneResolveCommand),
|
||||
Top(ZoneTopCommand),
|
||||
Watch(ZoneWatchCommand),
|
||||
}
|
||||
|
||||
impl ZoneCommands {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
ZoneCommands::Launch(launch) => launch.run(client, events).await,
|
||||
|
||||
ZoneCommands::Destroy(destroy) => destroy.run(client, events).await,
|
||||
|
||||
ZoneCommands::Attach(attach) => attach.run(client, events).await,
|
||||
|
||||
ZoneCommands::Logs(logs) => logs.run(client, events).await,
|
||||
|
||||
ZoneCommands::List(list) => list.run(client, events).await,
|
||||
|
||||
ZoneCommands::Watch(watch) => watch.run(events).await,
|
||||
|
||||
ZoneCommands::Resolve(resolve) => resolve.run(client).await,
|
||||
|
||||
ZoneCommands::Metrics(metrics) => metrics.run(client, events).await,
|
||||
|
||||
ZoneCommands::Top(top) => top.run(client, events).await,
|
||||
|
||||
ZoneCommands::Exec(exec) => exec.run(client).await,
|
||||
}
|
||||
}
|
||||
}
|
29
crates/ctl/src/cli/zone/resolve.rs
Normal file
29
crates/ctl/src/cli/zone/resolve.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::v1::control::{control_service_client::ControlServiceClient, ResolveZoneRequest};
|
||||
|
||||
use tonic::{transport::Channel, Request};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Resolve a zone name to a uuid")]
|
||||
pub struct ZoneResolveCommand {
|
||||
#[arg(help = "Zone name")]
|
||||
zone: String,
|
||||
}
|
||||
|
||||
impl ZoneResolveCommand {
|
||||
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
|
||||
let reply = client
|
||||
.resolve_zone(Request::new(ResolveZoneRequest {
|
||||
name: self.zone.clone(),
|
||||
}))
|
||||
.await?
|
||||
.into_inner();
|
||||
if let Some(zone) = reply.zone {
|
||||
println!("{}", zone.id);
|
||||
} else {
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
215
crates/ctl/src/cli/zone/top.rs
Normal file
215
crates/ctl/src/cli/zone/top.rs
Normal file
@ -0,0 +1,215 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use krata::{events::EventStream, v1::control::control_service_client::ControlServiceClient};
|
||||
use std::{
|
||||
io::{self, stdout, Stdout},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::select;
|
||||
use tokio_stream::StreamExt;
|
||||
use tonic::transport::Channel;
|
||||
|
||||
use crossterm::{
|
||||
event::{Event, KeyCode, KeyEvent, KeyEventKind},
|
||||
execute,
|
||||
terminal::*,
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
symbols::border,
|
||||
widgets::{
|
||||
block::{Position, Title},
|
||||
Block, Borders, Row, Table, TableState,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
format::zone_status_text,
|
||||
metrics::{
|
||||
lookup_metric_value, MultiMetricCollector, MultiMetricCollectorHandle, MultiMetricState,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Dashboard for running zones")]
|
||||
pub struct ZoneTopCommand {}
|
||||
|
||||
pub type Tui = Terminal<CrosstermBackend<Stdout>>;
|
||||
|
||||
impl ZoneTopCommand {
|
||||
pub async fn run(
|
||||
self,
|
||||
client: ControlServiceClient<Channel>,
|
||||
events: EventStream,
|
||||
) -> Result<()> {
|
||||
let collector = MultiMetricCollector::new(client, events, Duration::from_millis(200))?;
|
||||
let collector = collector.launch().await?;
|
||||
let mut tui = ZoneTopCommand::init()?;
|
||||
let mut app = ZoneTopApp {
|
||||
metrics: MultiMetricState { zones: vec![] },
|
||||
exit: false,
|
||||
table: TableState::new(),
|
||||
};
|
||||
app.run(collector, &mut tui).await?;
|
||||
ZoneTopCommand::restore()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init() -> io::Result<Tui> {
|
||||
execute!(stdout(), EnterAlternateScreen)?;
|
||||
enable_raw_mode()?;
|
||||
Terminal::new(CrosstermBackend::new(stdout()))
|
||||
}
|
||||
|
||||
pub fn restore() -> io::Result<()> {
|
||||
execute!(stdout(), LeaveAlternateScreen)?;
|
||||
disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ZoneTopApp {
|
||||
table: TableState,
|
||||
metrics: MultiMetricState,
|
||||
exit: bool,
|
||||
}
|
||||
|
||||
impl ZoneTopApp {
|
||||
pub async fn run(
|
||||
&mut self,
|
||||
mut collector: MultiMetricCollectorHandle,
|
||||
terminal: &mut Tui,
|
||||
) -> Result<()> {
|
||||
let mut events = crossterm::event::EventStream::new();
|
||||
|
||||
while !self.exit {
|
||||
terminal.draw(|frame| self.render_frame(frame))?;
|
||||
|
||||
select! {
|
||||
x = collector.receiver.recv() => match x {
|
||||
Some(state) => {
|
||||
self.metrics = state;
|
||||
},
|
||||
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
x = events.next() => match x {
|
||||
Some(event) => {
|
||||
let event = event?;
|
||||
self.handle_event(event)?;
|
||||
},
|
||||
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_frame(&mut self, frame: &mut Frame) {
|
||||
frame.render_widget(self, frame.size());
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: Event) -> io::Result<()> {
|
||||
match event {
|
||||
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
|
||||
self.handle_key_event(key_event)
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exit(&mut self) {
|
||||
self.exit = true;
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||
if let KeyCode::Char('q') = key_event.code {
|
||||
self.exit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut ZoneTopApp {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let title = Title::from(" krata isolation engine ".bold());
|
||||
let instructions = Title::from(vec![" Quit ".into(), "<Q> ".blue().bold()]);
|
||||
let block = Block::default()
|
||||
.title(title.alignment(Alignment::Center))
|
||||
.title(
|
||||
instructions
|
||||
.alignment(Alignment::Center)
|
||||
.position(Position::Bottom),
|
||||
)
|
||||
.borders(Borders::ALL)
|
||||
.border_set(border::THICK);
|
||||
|
||||
let mut rows = vec![];
|
||||
|
||||
for ms in &self.metrics.zones {
|
||||
let Some(ref spec) = ms.zone.spec else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(ref state) = ms.zone.state else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let memory_total = ms
|
||||
.root
|
||||
.as_ref()
|
||||
.and_then(|root| lookup_metric_value(root, "system/memory/total"));
|
||||
let memory_used = ms
|
||||
.root
|
||||
.as_ref()
|
||||
.and_then(|root| lookup_metric_value(root, "system/memory/used"));
|
||||
let memory_free = ms
|
||||
.root
|
||||
.as_ref()
|
||||
.and_then(|root| lookup_metric_value(root, "system/memory/free"));
|
||||
|
||||
let row = Row::new(vec![
|
||||
spec.name.clone(),
|
||||
ms.zone.id.clone(),
|
||||
zone_status_text(state.status()),
|
||||
memory_total.unwrap_or_default(),
|
||||
memory_used.unwrap_or_default(),
|
||||
memory_free.unwrap_or_default(),
|
||||
]);
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
let widths = [
|
||||
Constraint::Min(8),
|
||||
Constraint::Min(8),
|
||||
Constraint::Min(8),
|
||||
Constraint::Min(8),
|
||||
Constraint::Min(8),
|
||||
Constraint::Min(8),
|
||||
];
|
||||
|
||||
let table = Table::new(rows, widths)
|
||||
.header(
|
||||
Row::new(vec![
|
||||
"name",
|
||||
"id",
|
||||
"status",
|
||||
"total memory",
|
||||
"used memory",
|
||||
"free memory",
|
||||
])
|
||||
.style(Style::new().bold())
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.column_spacing(1)
|
||||
.block(block);
|
||||
|
||||
StatefulWidget::render(table, area, buf, &mut self.table);
|
||||
}
|
||||
}
|
63
crates/ctl/src/cli/zone/watch.rs
Normal file
63
crates/ctl/src/cli/zone/watch.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use krata::{
|
||||
events::EventStream,
|
||||
v1::{common::Zone, control::watch_events_reply::Event},
|
||||
};
|
||||
use prost_reflect::ReflectMessage;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::format::{kv2line, proto2dynamic, proto2kv, zone_simple_line};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
|
||||
enum ZoneWatchFormat {
|
||||
Simple,
|
||||
Json,
|
||||
KeyValue,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Watch for zone changes")]
|
||||
pub struct ZoneWatchCommand {
|
||||
#[arg(short, long, default_value = "simple", help = "Output format")]
|
||||
format: ZoneWatchFormat,
|
||||
}
|
||||
|
||||
impl ZoneWatchCommand {
|
||||
pub async fn run(self, events: EventStream) -> Result<()> {
|
||||
let mut stream = events.subscribe();
|
||||
loop {
|
||||
let event = stream.recv().await?;
|
||||
|
||||
let Event::ZoneChanged(changed) = event;
|
||||
let zone = changed.zone.clone();
|
||||
self.print_event("zone.changed", changed, zone)?;
|
||||
}
|
||||
}
|
||||
|
||||
fn print_event(&self, typ: &str, event: impl ReflectMessage, zone: Option<Zone>) -> Result<()> {
|
||||
match self.format {
|
||||
ZoneWatchFormat::Simple => {
|
||||
if let Some(zone) = zone {
|
||||
println!("{}", zone_simple_line(&zone));
|
||||
}
|
||||
}
|
||||
|
||||
ZoneWatchFormat::Json => {
|
||||
let message = proto2dynamic(event)?;
|
||||
let mut value = serde_json::to_value(&message)?;
|
||||
if let Value::Object(ref mut map) = value {
|
||||
map.insert("event.type".to_string(), Value::String(typ.to_string()));
|
||||
}
|
||||
println!("{}", serde_json::to_string(&value)?);
|
||||
}
|
||||
|
||||
ZoneWatchFormat::KeyValue => {
|
||||
let mut map = proto2kv(event)?;
|
||||
map.insert("event.type".to_string(), typ.to_string());
|
||||
println!("{}", kv2line(map),);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user