kratactl: implement guest resolution and rename console to attach

This commit is contained in:
Alex Zenla 2024-03-23 09:48:53 +00:00
parent 7b2de22320
commit e25cbf087d
No known key found for this signature in database
GPG Key ID: 067B238899B51269
10 changed files with 136 additions and 58 deletions

View File

@ -22,6 +22,14 @@ message ListGuestsReply {
repeated krata.common.Guest guests = 1;
}
message ResolveGuestRequest {
string name = 1;
}
message ResolveGuestReply {
krata.common.Guest guest = 1;
}
message DestroyGuestRequest {
string guest_id = 1;
}
@ -53,6 +61,7 @@ service ControlService {
rpc CreateGuest(CreateGuestRequest) returns (CreateGuestReply);
rpc DestroyGuest(DestroyGuestRequest) returns (DestroyGuestReply);
rpc ListGuests(ListGuestsRequest) returns (ListGuestsReply);
rpc ResolveGuest(ResolveGuestRequest) returns (ResolveGuestReply);
rpc ConsoleData(stream ConsoleDataRequest) returns (stream ConsoleDataReply);
rpc WatchEvents(WatchEventsRequest) returns (stream WatchEventsReply);
}

View File

@ -7,24 +7,26 @@ use tonic::transport::Channel;
use crate::{console::StdioConsoleStream, events::EventStream};
use super::resolve_guest;
#[derive(Parser)]
pub struct ConsoleCommand {
pub struct AttachCommand {
#[arg()]
guest: String,
}
impl ConsoleCommand {
impl AttachCommand {
pub async fn run(
self,
mut client: ControlServiceClient<Channel>,
events: EventStream,
) -> Result<()> {
let input = StdioConsoleStream::stdin_stream(self.guest.clone()).await;
let guest_id: String = resolve_guest(&mut client, &self.guest).await?;
let input = StdioConsoleStream::stdin_stream(guest_id.clone()).await;
let output = client.console_data(input).await?.into_inner();
let stdout_handle =
tokio::task::spawn(async move { StdioConsoleStream::stdout(output).await });
let exit_hook_task =
StdioConsoleStream::guest_exit_hook(self.guest.clone(), events).await?;
let exit_hook_task = StdioConsoleStream::guest_exit_hook(guest_id.clone(), events).await?;
let code = select! {
x = stdout_handle => {
x??;

View File

@ -4,7 +4,7 @@ use krata::control::{control_service_client::ControlServiceClient, DestroyGuestR
use tonic::{transport::Channel, Request};
use crate::events::EventStream;
use crate::cli::resolve_guest;
#[derive(Parser)]
pub struct DestroyCommand {
@ -13,15 +13,10 @@ pub struct DestroyCommand {
}
impl DestroyCommand {
pub async fn run(
self,
mut client: ControlServiceClient<Channel>,
_events: EventStream,
) -> Result<()> {
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
let guest_id: String = resolve_guest(&mut client, &self.guest).await?;
let _ = client
.destroy_guest(Request::new(DestroyGuestRequest {
guest_id: self.guest.clone(),
}))
.destroy_guest(Request::new(DestroyGuestRequest { guest_id }))
.await?
.into_inner();
println!("destroyed guest: {}", self.guest);

View File

@ -11,11 +11,9 @@ use tonic::{transport::Channel, Request};
use crate::{
events::EventStream,
format::{kv2line, proto2dynamic, proto2kv},
format::{guest_state_text, kv2line, proto2dynamic, proto2kv},
};
use super::pretty::guest_state_text;
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
enum ListFormat {
CliTable,

View File

@ -1,19 +1,22 @@
pub mod console;
pub mod attach;
pub mod destroy;
pub mod launch;
pub mod list;
pub mod pretty;
pub mod resolve;
pub mod watch;
use anyhow::Result;
use anyhow::{anyhow, Result};
use clap::{Parser, Subcommand};
use krata::control::WatchEventsRequest;
use krata::control::{
control_service_client::ControlServiceClient, ResolveGuestRequest, WatchEventsRequest,
};
use tonic::{transport::Channel, Request};
use crate::{client::ControlClientProvider, events::EventStream};
use self::{
console::ConsoleCommand, destroy::DestroyCommand, launch::LauchCommand, list::ListCommand,
watch::WatchCommand,
attach::AttachCommand, destroy::DestroyCommand, launch::LauchCommand, list::ListCommand,
resolve::ResolveCommand, watch::WatchCommand,
};
#[derive(Parser)]
@ -31,8 +34,9 @@ pub enum Commands {
Launch(LauchCommand),
Destroy(DestroyCommand),
List(ListCommand),
Console(ConsoleCommand),
Attach(AttachCommand),
Watch(WatchCommand),
Resolve(ResolveCommand),
}
impl ControlCommand {
@ -52,11 +56,11 @@ impl ControlCommand {
}
Commands::Destroy(destroy) => {
destroy.run(client, events).await?;
destroy.run(client).await?;
}
Commands::Console(console) => {
console.run(client, events).await?;
Commands::Attach(attach) => {
attach.run(client, events).await?;
}
Commands::List(list) => {
@ -66,7 +70,29 @@ impl ControlCommand {
Commands::Watch(watch) => {
watch.run(events).await?;
}
Commands::Resolve(resolve) => {
resolve.run(client).await?;
}
}
Ok(())
}
}
pub async fn resolve_guest(
client: &mut ControlServiceClient<Channel>,
name: &str,
) -> Result<String> {
let reply = client
.resolve_guest(Request::new(ResolveGuestRequest {
name: name.to_string(),
}))
.await?
.into_inner();
if let Some(guest) = reply.guest {
Ok(guest.id)
} else {
Err(anyhow!("unable to resolve guest {}", name))
}
}

View File

@ -1,28 +0,0 @@
use krata::common::{GuestState, GuestStatus};
pub fn guest_status_text(status: GuestStatus) -> String {
match status {
GuestStatus::Starting => "starting",
GuestStatus::Started => "started",
GuestStatus::Destroying => "destroying",
GuestStatus::Destroyed => "destroyed",
GuestStatus::Exited => "exited",
GuestStatus::Failed => "failed",
_ => "unknown",
}
.to_string()
}
pub fn guest_state_text(state: Option<&GuestState>) -> String {
let state = state.cloned().unwrap_or_default();
let mut text = guest_status_text(state.status());
if let Some(exit) = state.exit_info {
text.push_str(&format!(" (exit code: {})", exit.code));
}
if let Some(error) = state.error_info {
text.push_str(&format!(" (error: {})", error.message));
}
text
}

View File

@ -0,0 +1,28 @@
use anyhow::Result;
use clap::Parser;
use krata::control::{control_service_client::ControlServiceClient, ResolveGuestRequest};
use tonic::{transport::Channel, Request};
#[derive(Parser)]
pub struct ResolveCommand {
#[arg()]
guest: String,
}
impl ResolveCommand {
pub async fn run(self, mut client: ControlServiceClient<Channel>) -> Result<()> {
let reply = client
.resolve_guest(Request::new(ResolveGuestRequest {
name: self.guest.clone(),
}))
.await?
.into_inner();
if let Some(guest) = reply.guest {
println!("{}", guest.id);
} else {
std::process::exit(1);
}
Ok(())
}
}

View File

@ -5,9 +5,8 @@ use prost_reflect::ReflectMessage;
use serde_json::Value;
use crate::{
cli::pretty::guest_state_text,
events::EventStream,
format::{kv2line, proto2dynamic, proto2kv},
format::{guest_state_text, kv2line, proto2dynamic, proto2kv},
};
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]

View File

@ -1,6 +1,7 @@
use std::collections::HashMap;
use anyhow::Result;
use krata::common::{GuestState, GuestStatus};
use prost_reflect::{DynamicMessage, ReflectMessage, Value};
pub fn proto2dynamic(proto: impl ReflectMessage) -> Result<DynamicMessage> {
@ -56,3 +57,30 @@ pub fn kv2line(map: HashMap<String, String>) -> String {
.collect::<Vec<_>>()
.join(" ")
}
pub fn guest_status_text(status: GuestStatus) -> String {
match status {
GuestStatus::Starting => "starting",
GuestStatus::Started => "started",
GuestStatus::Destroying => "destroying",
GuestStatus::Destroyed => "destroyed",
GuestStatus::Exited => "exited",
GuestStatus::Failed => "failed",
_ => "unknown",
}
.to_string()
}
pub fn guest_state_text(state: Option<&GuestState>) -> String {
let state = state.cloned().unwrap_or_default();
let mut text = guest_status_text(state.status());
if let Some(exit) = state.exit_info {
text.push_str(&format!(" (exit code: {})", exit.code));
}
if let Some(error) = state.error_info {
text.push_str(&format!(" (error: {})", error.message));
}
text
}

View File

@ -7,7 +7,8 @@ use krata::{
control::{
control_service_server::ControlService, ConsoleDataReply, ConsoleDataRequest,
CreateGuestReply, CreateGuestRequest, DestroyGuestReply, DestroyGuestRequest,
ListGuestsReply, ListGuestsRequest, WatchEventsReply, WatchEventsRequest,
ListGuestsReply, ListGuestsRequest, ResolveGuestReply, ResolveGuestRequest,
WatchEventsReply, WatchEventsRequest,
},
};
use kratart::Runtime;
@ -179,6 +180,26 @@ impl ControlService for RuntimeControlService {
Ok(Response::new(ListGuestsReply { guests }))
}
async fn resolve_guest(
&self,
request: Request<ResolveGuestRequest>,
) -> Result<Response<ResolveGuestReply>, Status> {
let request = request.into_inner();
let guests = self.guests.list().await.map_err(ApiError::from)?;
let guests = guests
.into_values()
.filter_map(|entry| entry.guest)
.filter(|x| {
let comparison_spec = x.spec.as_ref().cloned().unwrap_or_default();
(!request.name.is_empty() && comparison_spec.name == request.name)
|| x.id == request.name
})
.collect::<Vec<Guest>>();
Ok(Response::new(ResolveGuestReply {
guest: guests.first().cloned(),
}))
}
async fn console_data(
&self,
request: Request<Streaming<ConsoleDataRequest>>,