krata: reconcile improvements and better kratactl error experience

This commit is contained in:
Alex Zenla 2024-03-23 07:00:12 +00:00
parent df90a4d03f
commit 3d5095c78b
No known key found for this signature in database
GPG Key ID: 067B238899B51269
12 changed files with 119 additions and 62 deletions

View File

@ -40,22 +40,23 @@ message GuestErrorInfo {
enum GuestStatus { enum GuestStatus {
GUEST_STATUS_UNKNOWN = 0; GUEST_STATUS_UNKNOWN = 0;
GUEST_STATUS_START = 1; GUEST_STATUS_STARTING = 1;
GUEST_STATUS_STARTED = 2; GUEST_STATUS_STARTED = 2;
GUEST_STATUS_EXITED = 3; GUEST_STATUS_EXITED = 3;
GUEST_STATUS_DESTROY = 4; GUEST_STATUS_DESTROYING = 4;
GUEST_STATUS_DESTROYED = 5; GUEST_STATUS_DESTROYED = 5;
GUEST_STATUS_FAILED = 6;
} }
message GuestState { message GuestState {
GuestStatus status = 1; GuestStatus status = 1;
GuestExitInfo exit_info = 2; GuestNetworkState network = 2;
GuestErrorInfo error_info = 3; GuestExitInfo exit_info = 3;
GuestErrorInfo error_info = 4;
} }
message Guest { message Guest {
string id = 1; string id = 1;
GuestState state = 2; GuestSpec spec = 2;
GuestSpec spec = 3; GuestState state = 3;
GuestNetworkState network = 4;
} }

View File

@ -94,7 +94,12 @@ async fn wait_guest_started(id: &str, events: EventStream) -> Result<()> {
}; };
if let Some(ref error) = state.error_info { if let Some(ref error) = state.error_info {
error!("guest error: {}", error.message); if state.status() == GuestStatus::Failed {
error!("launch failed: {}", error.message);
std::process::exit(1);
} else {
error!("guest error: {}", error.message);
}
} }
if state.status() == GuestStatus::Destroyed { if state.status() == GuestStatus::Destroyed {

View File

@ -11,14 +11,13 @@ use tonic::{transport::Channel, Request};
use crate::{ use crate::{
events::EventStream, events::EventStream,
format::{proto2dynamic, proto2kv}, format::{kv2line, proto2dynamic, proto2kv},
}; };
use super::pretty::guest_state_text; use super::pretty::guest_state_text;
#[derive(ValueEnum, Clone, Default, Debug, PartialEq, Eq)] #[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
enum ListFormat { enum ListFormat {
#[default]
CliTable, CliTable,
Json, Json,
JsonPretty, JsonPretty,
@ -29,7 +28,7 @@ enum ListFormat {
#[derive(Parser)] #[derive(Parser)]
pub struct ListCommand { pub struct ListCommand {
#[arg(short, long)] #[arg(short, long, default_value = "cli-table")]
format: ListFormat, format: ListFormat,
} }
@ -87,13 +86,15 @@ impl ListCommand {
table.push_row(&header)?; table.push_row(&header)?;
for guest in guests { for guest in guests {
let ipv4 = guest let ipv4 = guest
.network .state
.as_ref() .as_ref()
.and_then(|x| x.network.as_ref())
.map(|x| x.ipv4.as_str()) .map(|x| x.ipv4.as_str())
.unwrap_or("unknown"); .unwrap_or("unknown");
let ipv6 = guest let ipv6 = guest
.network .state
.as_ref() .as_ref()
.and_then(|x| x.network.as_ref())
.map(|x| x.ipv6.as_str()) .map(|x| x.ipv6.as_str())
.unwrap_or("unknown"); .unwrap_or("unknown");
let Some(spec) = guest.spec else { let Some(spec) = guest.spec else {
@ -112,7 +113,7 @@ impl ListCommand {
table.push_row_string(&vec![ table.push_row_string(&vec![
spec.name, spec.name,
guest.id, guest.id,
format!("{}", guest_state_text(guest.state.unwrap_or_default())), format!("{}", guest_state_text(guest.state.as_ref())),
ipv4.to_string(), ipv4.to_string(),
ipv6.to_string(), ipv6.to_string(),
image, image,
@ -129,13 +130,7 @@ impl ListCommand {
fn print_key_value(&self, guests: Vec<Guest>) -> Result<()> { fn print_key_value(&self, guests: Vec<Guest>) -> Result<()> {
for guest in guests { for guest in guests {
let kvs = proto2kv(guest)?; let kvs = proto2kv(guest)?;
println!( println!("{}", kv2line(kvs),);
"{}",
kvs.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join(" ")
);
} }
Ok(()) Ok(())
} }

View File

@ -2,17 +2,19 @@ use krata::common::{GuestState, GuestStatus};
pub fn guest_status_text(status: GuestStatus) -> String { pub fn guest_status_text(status: GuestStatus) -> String {
match status { match status {
GuestStatus::Destroy => "destroying", GuestStatus::Starting => "starting",
GuestStatus::Destroyed => "destroyed",
GuestStatus::Start => "starting",
GuestStatus::Exited => "exited",
GuestStatus::Started => "started", GuestStatus::Started => "started",
GuestStatus::Destroying => "destroying",
GuestStatus::Destroyed => "destroyed",
GuestStatus::Exited => "exited",
GuestStatus::Failed => "failed",
_ => "unknown", _ => "unknown",
} }
.to_string() .to_string()
} }
pub fn guest_state_text(state: GuestState) -> 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()); let mut text = guest_status_text(state.status());
if let Some(exit) = state.exit_info { if let Some(exit) = state.exit_info {

View File

@ -1,11 +1,27 @@
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::{Parser, ValueEnum};
use krata::control::watch_events_reply::Event; use krata::{common::Guest, control::watch_events_reply::Event};
use prost_reflect::ReflectMessage;
use serde_json::Value;
use crate::{cli::pretty::guest_status_text, events::EventStream}; use crate::{
cli::pretty::guest_state_text,
events::EventStream,
format::{kv2line, proto2dynamic, proto2kv},
};
#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
enum WatchFormat {
Simple,
Json,
KeyValue,
}
#[derive(Parser)] #[derive(Parser)]
pub struct WatchCommand {} pub struct WatchCommand {
#[arg(short, long, default_value = "simple")]
format: WatchFormat,
}
impl WatchCommand { impl WatchCommand {
pub async fn run(self, events: EventStream) -> Result<()> { pub async fn run(self, events: EventStream) -> Result<()> {
@ -14,15 +30,46 @@ impl WatchCommand {
let event = stream.recv().await?; let event = stream.recv().await?;
match event { match event {
Event::GuestChanged(changed) => { Event::GuestChanged(changed) => {
if let Some(guest) = changed.guest { let guest = changed.guest.clone();
println!( self.print_event("guest.changed", changed, guest)?;
"event=guest.changed guest={} status={}",
guest.id,
guest_status_text(guest.state.unwrap_or_default().status())
);
}
} }
} }
} }
} }
fn print_event(
&self,
typ: &str,
event: impl ReflectMessage,
guest: Option<Guest>,
) -> Result<()> {
match self.format {
WatchFormat::Simple => {
if let Some(guest) = guest {
println!(
"{} guest={} status=\"{}\"",
typ,
guest.id,
guest_state_text(guest.state.as_ref()).replace('"', "\\\"")
);
}
}
WatchFormat::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)?);
}
WatchFormat::KeyValue => {
let mut map = proto2kv(event)?;
map.insert("event.type".to_string(), typ.to_string());
println!("{}", kv2line(map),);
}
}
Ok(())
}
} }

View File

@ -87,7 +87,7 @@ impl StdioConsoleStream {
} }
let status = state.status(); let status = state.status();
if status == GuestStatus::Destroy || status == GuestStatus::Destroyed { if status == GuestStatus::Destroying || status == GuestStatus::Destroyed {
return Some(10); return Some(10);
} }
} }

View File

@ -49,3 +49,10 @@ pub fn proto2kv(proto: impl ReflectMessage) -> Result<HashMap<String, String>> {
Ok(map) Ok(map)
} }
pub fn kv2line(map: HashMap<String, String>) -> String {
map.iter()
.map(|(k, v)| format!("{}=\"{}\"", k, v.replace('"', "\\\"")))
.collect::<Vec<_>>()
.join(" ")
}

View File

@ -100,12 +100,12 @@ impl ControlService for RuntimeControlService {
guest: Some(Guest { guest: Some(Guest {
id: uuid.to_string(), id: uuid.to_string(),
state: Some(GuestState { state: Some(GuestState {
status: GuestStatus::Start.into(), status: GuestStatus::Starting.into(),
network: None,
exit_info: None, exit_info: None,
error_info: None, error_info: None,
}), }),
spec: Some(spec), spec: Some(spec),
network: None,
}), }),
}, },
) )
@ -152,7 +152,7 @@ impl ControlService for RuntimeControlService {
.into()); .into());
} }
guest.state.as_mut().unwrap().status = GuestStatus::Destroy.into(); guest.state.as_mut().unwrap().status = GuestStatus::Destroying.into();
self.guests self.guests
.update(uuid, entry) .update(uuid, entry)
.await .await

View File

@ -121,6 +121,7 @@ impl DaemonEventGenerator {
guest.state = Some(GuestState { guest.state = Some(GuestState {
status: GuestStatus::Exited.into(), status: GuestStatus::Exited.into(),
network: guest.state.clone().unwrap_or_default().network,
exit_info: Some(GuestExitInfo { code }), exit_info: Some(GuestExitInfo { code }),
error_info: None, error_info: None,
}); });

View File

@ -53,7 +53,7 @@ impl GuestReconciler {
} }
}, },
_ = sleep(Duration::from_secs(30)) => { _ = sleep(Duration::from_secs(5)) => {
if let Err(error) = self.reconcile_runtime(false).await { if let Err(error) = self.reconcile_runtime(false).await {
error!("runtime reconciler failed: {}", error); error!("runtime reconciler failed: {}", error);
} }
@ -79,10 +79,9 @@ impl GuestReconciler {
None => { None => {
let mut state = stored_guest.state.as_mut().cloned().unwrap_or_default(); let mut state = stored_guest.state.as_mut().cloned().unwrap_or_default();
if state.status() == GuestStatus::Started { if state.status() == GuestStatus::Started {
state.status = GuestStatus::Start.into(); state.status = GuestStatus::Starting.into();
} }
stored_guest.state = Some(state); stored_guest.state = Some(state);
stored_guest.network = None;
} }
Some(runtime) => { Some(runtime) => {
@ -93,18 +92,18 @@ impl GuestReconciler {
} else { } else {
state.status = GuestStatus::Started.into(); state.status = GuestStatus::Started.into();
} }
stored_guest.state = Some(state); state.network = Some(GuestNetworkState {
stored_guest.network = Some(GuestNetworkState {
ipv4: runtime.ipv4.map(|x| x.ip().to_string()).unwrap_or_default(), ipv4: runtime.ipv4.map(|x| x.ip().to_string()).unwrap_or_default(),
ipv6: runtime.ipv6.map(|x| x.ip().to_string()).unwrap_or_default(), ipv6: runtime.ipv6.map(|x| x.ip().to_string()).unwrap_or_default(),
}); });
stored_guest.state = Some(state);
} }
} }
let changed = *stored_guest != previous_guest; let changed = *stored_guest != previous_guest;
self.guests.update(uuid, stored_guest_entry).await?;
if changed || initial { if changed || initial {
self.guests.update(uuid, stored_guest_entry).await?;
if let Err(error) = self.reconcile(uuid).await { if let Err(error) = self.reconcile(uuid).await {
error!("failed to reconcile guest {}: {}", uuid, error); error!("failed to reconcile guest {}: {}", uuid, error);
} }
@ -134,8 +133,8 @@ impl GuestReconciler {
}))?; }))?;
let result = match guest.state.as_ref().map(|x| x.status()).unwrap_or_default() { let result = match guest.state.as_ref().map(|x| x.status()).unwrap_or_default() {
GuestStatus::Start => self.start(uuid, guest).await, GuestStatus::Starting => self.start(uuid, guest).await,
GuestStatus::Destroy | GuestStatus::Exited => self.destroy(uuid, guest).await, GuestStatus::Destroying | GuestStatus::Exited => self.destroy(uuid, guest).await,
_ => Ok(false), _ => Ok(false),
}; };
@ -143,6 +142,7 @@ impl GuestReconciler {
Ok(changed) => changed, Ok(changed) => changed,
Err(error) => { Err(error) => {
guest.state = Some(guest.state.as_mut().cloned().unwrap_or_default()); guest.state = Some(guest.state.as_mut().cloned().unwrap_or_default());
guest.state.as_mut().unwrap().status = GuestStatus::Failed.into();
guest.state.as_mut().unwrap().error_info = Some(GuestErrorInfo { guest.state.as_mut().unwrap().error_info = Some(GuestErrorInfo {
message: error.to_string(), message: error.to_string(),
}); });
@ -152,8 +152,8 @@ impl GuestReconciler {
info!("reconciled guest {}", uuid); info!("reconciled guest {}", uuid);
let destroyed = let status = guest.state.as_ref().map(|x| x.status()).unwrap_or_default();
guest.state.as_ref().map(|x| x.status()).unwrap_or_default() == GuestStatus::Destroyed; let destroyed = status == GuestStatus::Destroyed || status == GuestStatus::Failed;
if changed { if changed {
let event = DaemonEvent::GuestChanged(GuestChangedEvent { let event = DaemonEvent::GuestChanged(GuestChangedEvent {
@ -205,12 +205,12 @@ impl GuestReconciler {
}) })
.await?; .await?;
info!("started guest {}", uuid); info!("started guest {}", uuid);
guest.network = Some(GuestNetworkState {
ipv4: info.ipv4.map(|x| x.ip().to_string()).unwrap_or_default(),
ipv6: info.ipv6.map(|x| x.ip().to_string()).unwrap_or_default(),
});
guest.state = Some(GuestState { guest.state = Some(GuestState {
status: GuestStatus::Started.into(), status: GuestStatus::Started.into(),
network: Some(GuestNetworkState {
ipv4: info.ipv4.map(|x| x.ip().to_string()).unwrap_or_default(),
ipv6: info.ipv6.map(|x| x.ip().to_string()).unwrap_or_default(),
}),
exit_info: None, exit_info: None,
error_info: None, error_info: None,
}); });
@ -219,13 +219,13 @@ impl GuestReconciler {
async fn destroy(&self, uuid: Uuid, guest: &mut Guest) -> Result<bool> { async fn destroy(&self, uuid: Uuid, guest: &mut Guest) -> Result<bool> {
if let Err(error) = self.runtime.destroy(uuid).await { if let Err(error) = self.runtime.destroy(uuid).await {
warn!("failed to destroy runtime guest {}: {}", uuid, error); trace!("failed to destroy runtime guest {}: {}", uuid, error);
} }
info!("destroyed guest {}", uuid); info!("destroyed guest {}", uuid);
guest.network = None;
guest.state = Some(GuestState { guest.state = Some(GuestState {
status: GuestStatus::Destroyed.into(), status: GuestStatus::Destroyed.into(),
network: None,
exit_info: None, exit_info: None,
error_info: None, error_info: None,
}); });

View File

@ -110,7 +110,7 @@ impl OciRegistryClient {
if !response.status().is_success() { if !response.status().is_success() {
return Err(anyhow!( return Err(anyhow!(
"failed to send request to {}: status {}", "request to {} failed: status {}",
req.build()?.url(), req.build()?.url(),
response.status() response.status()
)); ));

View File

@ -15,15 +15,14 @@ TARGET_OS_DIR="${TARGET_DIR}/os"
mkdir -p "${TARGET_OS_DIR}" mkdir -p "${TARGET_OS_DIR}"
cp "${TARGET_DIR}/dist/krata_${KRATA_VERSION}_${TARGET_ARCH}.apk" "${TARGET_OS_DIR}/krata-${TARGET_ARCH}.apk" cp "${TARGET_DIR}/dist/krata_${KRATA_VERSION}_${TARGET_ARCH}.apk" "${TARGET_OS_DIR}/krata-${TARGET_ARCH}.apk"
DOCKER_FLAGS="" DOCKER_FLAGS="--platform linux/${TARGET_ARCH_ALT}"
if [ -t 0 ] if [ -t 0 ]
then then
DOCKER_FLAGS="-it" DOCKER_FLAGS="${DOCKER_FLAGS} -it"
fi fi
if [ "${CROSS_COMPILE}" = "1" ] if [ "${CROSS_COMPILE}" = "1" ]
then then
DOCKER_FLAGS="${DOCKER_FLAGS} --platform linux/${TARGET_ARCH_ALT}"
docker run --privileged --rm tonistiigi/binfmt --install all docker run --privileged --rm tonistiigi/binfmt --install all
fi fi