use serde::{Deserialize, Deserializer};
use std::collections::BTreeMap;
pub const COMPOSE_PROJECT_LABEL: &str = "com.docker.compose.project";
#[derive(Debug, Clone, Deserialize)]
pub struct Container {
#[serde(rename = "ID")]
pub id: String,
#[serde(rename = "Names")]
pub name: String,
#[serde(rename = "Image")]
pub image: String,
#[serde(rename = "State")]
pub state: String,
#[serde(rename = "Status", default)]
pub status: String,
#[serde(rename = "Ports", default)]
pub ports: String,
#[serde(rename = "Labels", default, deserialize_with = "labels_from_string")]
pub labels: BTreeMap<String, String>,
}
impl Container {
pub fn state_kind(&self) -> ContainerState {
ContainerState::parse(&self.state)
}
pub fn compose_project(&self) -> Option<&str> {
self.labels.get(COMPOSE_PROJECT_LABEL).map(String::as_str)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ContainerState {
Running,
Restarting,
Paused,
Created,
Exited,
Dead,
Other,
}
impl ContainerState {
pub fn parse(s: &str) -> Self {
match s {
"running" => Self::Running,
"restarting" => Self::Restarting,
"paused" => Self::Paused,
"created" => Self::Created,
"exited" | "removing" => Self::Exited,
"dead" => Self::Dead,
_ => Self::Other,
}
}
pub fn icon(self) -> &'static str {
match self {
Self::Running => "●",
Self::Restarting => "↻",
Self::Paused => "‖",
Self::Created => "○",
Self::Exited => "■",
Self::Dead => "✗",
Self::Other => "?",
}
}
pub fn sort_rank(self) -> u8 {
match self {
Self::Running => 0,
Self::Restarting => 1,
Self::Paused => 2,
Self::Created => 3,
Self::Exited => 4,
Self::Dead => 5,
Self::Other => 6,
}
}
}
fn labels_from_string<'de, D: Deserializer<'de>>(
d: D,
) -> Result<BTreeMap<String, String>, D::Error> {
let raw = String::deserialize(d)?;
Ok(parse_label_string(&raw))
}
pub fn parse_label_string(raw: &str) -> BTreeMap<String, String> {
let mut labels = BTreeMap::new();
let mut last_key: Option<String> = None;
for seg in raw.split(',') {
match seg.split_once('=') {
Some((k, v)) if !k.is_empty() && !k.contains(' ') => {
labels.insert(k.to_string(), v.to_string());
last_key = Some(k.to_string());
}
_ => {
if let Some(k) = &last_key {
if let Some(v) = labels.get_mut(k) {
v.push(',');
v.push_str(seg);
}
}
}
}
}
labels
}
#[derive(Debug, Clone, Deserialize)]
pub struct Image {
#[serde(rename = "ID")]
pub id: String,
#[serde(rename = "Repository")]
pub repository: String,
#[serde(rename = "Tag")]
pub tag: String,
#[serde(rename = "Size", default)]
pub size: String,
#[serde(rename = "CreatedSince", default)]
pub created_since: String,
}
impl Image {
pub fn is_dangling(&self) -> bool {
self.repository == "<none>" || self.tag == "<none>"
}
pub fn name(&self) -> String {
format!("{}:{}", self.repository, self.tag)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ContainerInspect {
#[serde(rename = "Id")]
pub id: String,
#[serde(rename = "Name", default)]
pub name: String,
#[serde(rename = "Created", default)]
pub created: String,
#[serde(rename = "State", default)]
pub state: InspectState,
#[serde(rename = "HostConfig", default)]
pub host_config: HostConfig,
#[serde(rename = "Mounts", default)]
pub mounts: Vec<Mount>,
#[serde(rename = "Config", default)]
pub config: ContainerConfig,
#[serde(rename = "NetworkSettings", default)]
pub network_settings: NetworkSettings,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct InspectState {
#[serde(rename = "Status", default)]
pub status: String,
#[serde(rename = "ExitCode", default)]
pub exit_code: i64,
#[serde(rename = "Error", default)]
pub error: String,
#[serde(rename = "StartedAt", default)]
pub started_at: String,
#[serde(rename = "FinishedAt", default)]
pub finished_at: String,
#[serde(rename = "Health", default)]
pub health: Option<Health>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Health {
#[serde(rename = "Status", default)]
pub status: String,
#[serde(rename = "FailingStreak", default)]
pub failing_streak: u64,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct HostConfig {
#[serde(rename = "RestartPolicy", default)]
pub restart_policy: RestartPolicy,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct RestartPolicy {
#[serde(rename = "Name", default)]
pub name: String,
#[serde(rename = "MaximumRetryCount", default)]
pub maximum_retry_count: u64,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Mount {
#[serde(rename = "Type", default)]
pub kind: String,
#[serde(rename = "Name", default)]
pub name: Option<String>,
#[serde(rename = "Source", default)]
pub source: String,
#[serde(rename = "Destination", default)]
pub destination: String,
#[serde(rename = "Mode", default)]
pub mode: String,
#[serde(rename = "RW", default)]
pub rw: bool,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct ContainerConfig {
#[serde(rename = "Image", default)]
pub image: String,
#[serde(rename = "Cmd", default)]
pub cmd: Option<Vec<String>>,
#[serde(rename = "Entrypoint", default)]
pub entrypoint: Option<Vec<String>>,
#[serde(rename = "Labels", default)]
pub labels: Option<BTreeMap<String, String>>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct NetworkSettings {
#[serde(rename = "Ports", default)]
pub ports: Option<BTreeMap<String, Option<Vec<PortBinding>>>>,
#[serde(rename = "Networks", default)]
pub networks: Option<BTreeMap<String, Network>>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct PortBinding {
#[serde(rename = "HostIp", default)]
pub host_ip: String,
#[serde(rename = "HostPort", default)]
pub host_port: String,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Network {
#[serde(rename = "IPAddress", default)]
pub ip_address: String,
#[serde(rename = "Gateway", default)]
pub gateway: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ImageInspect {
#[serde(rename = "Id")]
pub id: String,
#[serde(rename = "RepoTags", default)]
pub repo_tags: Option<Vec<String>>,
#[serde(rename = "RepoDigests", default)]
pub repo_digests: Option<Vec<String>>,
#[serde(rename = "Created", default)]
pub created: String,
#[serde(rename = "Size", default)]
pub size: u64,
#[serde(rename = "Architecture", default)]
pub architecture: String,
#[serde(rename = "Os", default)]
pub os: String,
#[serde(rename = "Config", default)]
pub config: ImageConfig,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct ImageConfig {
#[serde(rename = "Cmd", default)]
pub cmd: Option<Vec<String>>,
#[serde(rename = "Entrypoint", default)]
pub entrypoint: Option<Vec<String>>,
#[serde(rename = "WorkingDir", default)]
pub working_dir: String,
#[serde(rename = "ExposedPorts", default)]
pub exposed_ports: Option<BTreeMap<String, serde_json::Value>>,
#[serde(rename = "Labels", default)]
pub labels: Option<BTreeMap<String, String>>,
}
#[derive(Debug, Clone)]
pub enum InspectSummary {
Container(Box<ContainerInspect>),
Image(Box<ImageInspect>),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn label_string_keeps_commas_inside_values() {
let labels = parse_label_string(
"com.docker.compose.project=demo,org.opencontainers.image.description=One Postgres, many uses,com.docker.compose.service=db",
);
assert_eq!(labels[COMPOSE_PROJECT_LABEL], "demo");
assert_eq!(labels["com.docker.compose.service"], "db");
assert_eq!(
labels["org.opencontainers.image.description"],
"One Postgres, many uses"
);
assert!(parse_label_string("").is_empty());
}
#[test]
fn container_state_parses_and_ranks() {
assert_eq!(ContainerState::parse("running"), ContainerState::Running);
assert_eq!(ContainerState::parse("exited"), ContainerState::Exited);
assert_eq!(ContainerState::parse("weird"), ContainerState::Other);
assert!(ContainerState::Running.sort_rank() < ContainerState::Exited.sort_rank());
}
#[test]
fn image_dangling_and_name() {
let img = Image {
id: "abc".into(),
repository: "<none>".into(),
tag: "<none>".into(),
size: "1MB".into(),
created_since: String::new(),
};
assert!(img.is_dangling());
let img = Image {
repository: "nginx".into(),
tag: "alpine".into(),
..img
};
assert!(!img.is_dangling());
assert_eq!(img.name(), "nginx:alpine");
}
}