Skip to content

Commit

Permalink
feat: container inspect command
Browse files Browse the repository at this point in the history
  • Loading branch information
pxseu committed Jun 8, 2023
1 parent f6e33f2 commit 928cbbc
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 14 deletions.
121 changes: 121 additions & 0 deletions src/commands/containers/inspect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use std::io::Write;

use anyhow::{ensure, Result};
use clap::Parser;
use tabwriter::TabWriter;

use super::utils::{format_containers, get_all_containers, get_container, UNAVAILABLE_ELEMENT};
use crate::commands::ignite::utils::{format_deployments, get_all_deployments, get_deployment};
use crate::state::State;
use crate::utils::relative_time;
use crate::utils::size::{parse_size, user_friendly_size};

#[derive(Debug, Parser)]
#[clap(about = "Inspect a container")]
pub struct Options {
#[clap(help = "ID of the container")]
pub container: Option<String>,
}

pub async fn handle(options: Options, state: State) -> Result<()> {
let (container, deployment) = if let Some(container_id) = options.container {
let container = get_container(&state.http, &container_id).await?;
let deployment = get_deployment(&state.http, &container.deployment_id).await?;

(container, deployment)
} else {
let project_id = state.ctx.current_project_error()?.id;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

let deployment = deployments[idx].to_owned();

let containers = get_all_containers(&state.http, &deployment.id).await?;
ensure!(!containers.is_empty(), "No containers found");
let containers_fmt = format_containers(&containers, false);

let idx = dialoguer::Select::new()
.with_prompt("Select container")
.items(&containers_fmt)
.default(0)
.interact()?;

(containers[idx].to_owned(), deployment)
};

let mut tw = TabWriter::new(vec![]);

writeln!(tw, "{}", container.id)?;
writeln!(tw, " Metadata")?;
writeln!(tw, "\tDeployment: {} ({})", deployment.name, deployment.id)?;
writeln!(tw, "\tCreated: {} ago", relative_time(container.created_at))?;
writeln!(tw, "\tState: {}", container.state)?;
writeln!(
tw,
"\tUptime: {}",
container
.uptime
.as_ref()
.map(|u| {
u.last_start
.map(relative_time)
.unwrap_or_else(|| UNAVAILABLE_ELEMENT.to_string())
})
.unwrap_or_else(|| UNAVAILABLE_ELEMENT.to_string())
)?;
writeln!(
tw,
"\tInternal IP: {}",
container
.internal_ip
.unwrap_or_else(|| UNAVAILABLE_ELEMENT.to_string())
)?;
writeln!(tw, "\tRegion: {}", container.region)?;
writeln!(tw, "\tType: {}", container.type_)?;
writeln!(tw, " Metrics")?;
writeln!(
tw,
"\tCPU: {}",
container
.metrics
.clone()
.map(|m| format!(
"{:.2}%/{} vcpu",
m.cpu_usage_percent(deployment.config.resources.vcpu),
deployment.config.resources.vcpu
))
.unwrap_or_else(|| UNAVAILABLE_ELEMENT.to_string())
)?;
writeln!(
tw,
"\tMemory: {}",
container
.metrics
.map(|m| -> Result<String> {
let ram = parse_size(&deployment.config.resources.ram)?;

Ok(format!(
"{:.2}% {}/{}",
m.memory_usage_percent(ram),
user_friendly_size(m.memory_usage_bytes)?,
user_friendly_size(ram)?
))
})
.transpose()?
.unwrap_or_else(|| UNAVAILABLE_ELEMENT.to_string())
)?;

tw.flush()?;

print!("{}", String::from_utf8(tw.into_inner()?)?);

Ok(())
}
4 changes: 4 additions & 0 deletions src/commands/containers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod create;
mod delete;
mod inspect;
mod list;
mod logs;
mod recreate;
Expand All @@ -21,6 +22,8 @@ pub enum Commands {
Recreate(recreate::Options),
#[clap(name = "ls", alias = "list")]
List(list::Options),
#[clap(alias = "info")]
Inspect(inspect::Options),

#[clap(name = "logs", alias = "log")]
Log(logs::Options),
Expand All @@ -40,5 +43,6 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Commands::List(options) => list::handle(options, state).await,
Commands::Log(options) => logs::handle(options, state).await,
Commands::Recreate(options) => recreate::handle(options, state).await,
Commands::Inspect(options) => inspect::handle(options, state).await,
}
}
27 changes: 24 additions & 3 deletions src/commands/containers/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,16 @@ impl Display for ContainerState {
}
}

#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct Uptime {
pub last_start: Option<DateTime<Utc>>,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct Container {
pub id: String,
pub created_at: String,
pub created_at: DateTime<Utc>,
pub state: ContainerState,
pub metrics: Option<Metrics>,
pub deployment_id: String,
pub internal_ip: Option<String>,
pub region: String,
Expand Down Expand Up @@ -118,3 +119,23 @@ pub struct LogsResponse {
pub struct SingleContainer {
pub container: Container,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Metrics {
pub cpu_usage_percent: f64,
pub memory_usage_bytes: u64,
}

/// Reusable metrics functions
impl Metrics {
/// Normalize the metrics to the number of vcpus
pub fn cpu_usage_percent(&self, cpu_count: f64) -> f64 {
// 100% = 4vcpu
self.cpu_usage_percent * (cpu_count / 4.0)
}

/// Normalize the metrics to the amount of memory
pub fn memory_usage_percent(&self, memory: u64) -> f64 {
self.memory_usage_bytes as f64 / (memory as f64) * 100.0
}
}
10 changes: 9 additions & 1 deletion src/commands/containers/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ pub async fn delete_container(
.map(|response| response.container))
}

pub async fn get_container(http: &HttpClient, container_id: &str) -> Result<Container> {
Ok(http
.request::<SingleContainer>("GET", &format!("/ignite/containers/{container_id}"), None)
.await?
.ok_or_else(|| anyhow!("Error while parsing response"))?
.container)
}

pub async fn get_all_containers(http: &HttpClient, deployment_id: &str) -> Result<Vec<Container>> {
let response = http
.request::<MultipleContainersResponse>(
Expand Down Expand Up @@ -82,7 +90,7 @@ pub async fn get_container_logs(
Ok(response.logs)
}

const UNAVAILABLE_ELEMENT: &str = "-";
pub const UNAVAILABLE_ELEMENT: &str = "-";

pub fn format_containers(containers: &Vec<Container>, title: bool) -> Vec<String> {
let mut tw = TabWriter::new(vec![]);
Expand Down
10 changes: 6 additions & 4 deletions src/commands/ignite/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};

use crate::commands::containers::types::ContainerType;
use crate::utils::parse_key_val;
use crate::utils::size::{parse_size, unit_multiplier};
use crate::utils::size::{parse_size, unit_multiplier, user_friendly_size};

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct Vgpu {
Expand Down Expand Up @@ -88,13 +88,15 @@ impl Resources {
for tier in tiers {
if tier.resources.cpu == self.vcpu && tier.resources.memory == parse_size(&self.ram)? {
return Ok(format!(
"{} - {}vcpu {}B",
tier.name, tier.resources.cpu, tier.resources.memory
"{} - {} vcpu {}",
tier.name,
tier.resources.cpu,
user_friendly_size(tier.resources.memory)?
));
}
}

Ok(format!("{}vcpu {}", self.vcpu, self.ram))
Ok(format!("{} vcpu {}", self.vcpu, self.ram))
}
}

Expand Down
7 changes: 3 additions & 4 deletions src/state/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,9 @@ impl HttpClient {
where
T: serde::de::DeserializeOwned,
{
let mut request = self.client.request(
method.parse().unwrap(),
format!("{}{}", self.base_url, path),
);
let mut request = self
.client
.request(method.parse()?, format!("{}{path}", self.base_url));

log::debug!("request: {} {} {:?}", method, path, data);

Expand Down
3 changes: 1 addition & 2 deletions src/utils/deser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ where
T: Default + Deserialize<'de>,
D: Deserializer<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
Option::deserialize(deserializer).map(|opt| opt.unwrap_or_default())
}

pub fn deserialize_string_to_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
Expand Down

0 comments on commit 928cbbc

Please sign in to comment.