1use core::time::Duration;
4use std::time::SystemTime;
5
6use crate::model::{Confidence, MetricState};
7
8#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize))]
16#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
17pub enum EnvironmentKind {
18 #[default]
20 NoEvidenceFound,
21 Container,
23 VirtualMachine,
25}
26
27impl EnvironmentKind {
28 #[must_use]
30 pub const fn label(self) -> &'static str {
31 match self {
32 Self::NoEvidenceFound => "no container/VM evidence",
33 Self::Container => "container",
34 Self::VirtualMachine => "virtual machine",
35 }
36 }
37}
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize))]
44#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
45pub enum ContainerRuntime {
46 Docker,
48 Containerd,
50 CriO,
52 Podman,
54 Lxc,
56 SystemdMachine,
58 Unknown,
60}
61
62impl ContainerRuntime {
63 #[must_use]
65 pub const fn label(self) -> &'static str {
66 match self {
67 Self::Docker => "docker",
68 Self::Containerd => "containerd",
69 Self::CriO => "cri-o",
70 Self::Podman => "podman",
71 Self::Lxc => "lxc",
72 Self::SystemdMachine => "systemd-machine",
73 Self::Unknown => "container",
74 }
75 }
76}
77
78#[derive(Clone, Debug, Eq, PartialEq)]
80#[cfg_attr(feature = "serde", derive(serde::Serialize))]
81pub struct ContainerIdentity {
82 pub runtime: ContainerRuntime,
84 pub id: Box<str>,
86 pub kubernetes: bool,
88}
89
90impl ContainerIdentity {
91 #[must_use]
96 pub fn short_id(&self) -> &str {
97 let cut = self
98 .id
99 .char_indices()
100 .nth(12)
101 .map_or(self.id.len(), |(index, _)| index);
102 self.id.get(..cut).unwrap_or(&self.id)
103 }
104
105 #[must_use]
107 pub fn label(&self) -> String {
108 if self.kubernetes {
109 format!("kubernetes/{} {}", self.runtime.label(), self.short_id())
110 } else {
111 format!("{} {}", self.runtime.label(), self.short_id())
112 }
113 }
114}
115
116#[derive(Clone, Debug, PartialEq)]
118#[cfg_attr(feature = "serde", derive(serde::Serialize))]
119pub struct HostEnvironment {
120 pub kind: EnvironmentKind,
122 pub evidence: Box<str>,
126 pub confidence: Confidence,
128 pub container: Option<ContainerIdentity>,
136}
137
138#[derive(Clone, Debug, PartialEq)]
140#[cfg_attr(feature = "serde", derive(serde::Serialize))]
141pub struct HostSnapshot {
142 pub hostname: MetricState<Box<str>>,
144 pub os_name: MetricState<Box<str>>,
146 pub os_version: MetricState<Box<str>>,
148 pub kernel_version: MetricState<Box<str>>,
150 pub arch: &'static str,
152 pub cpu_brand: MetricState<Box<str>>,
154 pub uptime: MetricState<Duration>,
156 pub boot_time: MetricState<SystemTime>,
158 pub environment: MetricState<HostEnvironment>,
160}
161
162impl HostSnapshot {
163 #[must_use]
165 pub const fn warming_up() -> Self {
166 Self {
167 hostname: MetricState::WarmingUp,
168 os_name: MetricState::WarmingUp,
169 os_version: MetricState::WarmingUp,
170 kernel_version: MetricState::WarmingUp,
171 arch: std::env::consts::ARCH,
172 cpu_brand: MetricState::WarmingUp,
173 uptime: MetricState::WarmingUp,
174 boot_time: MetricState::WarmingUp,
175 environment: MetricState::WarmingUp,
176 }
177 }
178
179 #[must_use]
184 pub fn display_hostname(&self) -> &str {
185 self.hostname
186 .displayable()
187 .map_or("unknown", |(name, _)| name)
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn arch_is_known_at_compile_time_and_never_unavailable() {
197 let host = HostSnapshot::warming_up();
198 assert!(!host.arch.is_empty());
199 assert!(
200 [
201 "aarch64",
202 "x86_64",
203 "arm",
204 "x86",
205 "powerpc64",
206 "riscv64",
207 "s390x",
208 "loongarch64"
209 ]
210 .contains(&host.arch),
211 "unexpected arch {}",
212 host.arch
213 );
214 }
215
216 #[test]
217 fn an_unresolved_hostname_renders_as_unknown_not_as_an_empty_title() {
218 let host = HostSnapshot::warming_up();
219 assert_eq!(host.display_hostname(), "unknown");
220 }
221
222 #[test]
223 fn a_stale_hostname_is_still_displayable() {
224 let mut host = HostSnapshot::warming_up();
225 host.hostname = MetricState::Available("dev-mbp".into()).into_stale(Duration::from_secs(5));
226 assert_eq!(host.display_hostname(), "dev-mbp");
227 }
228
229 #[test]
230 fn absence_of_evidence_is_not_reported_as_bare_metal() {
231 assert_eq!(EnvironmentKind::default(), EnvironmentKind::NoEvidenceFound);
232 assert!(
233 EnvironmentKind::NoEvidenceFound
234 .label()
235 .contains("evidence")
236 );
237 }
238
239 #[test]
240 fn an_environment_classification_carries_its_evidence_and_confidence() {
241 let env = HostEnvironment {
242 kind: EnvironmentKind::Container,
243 evidence: "/proc/1/cgroup names docker".into(),
244 confidence: Confidence::High,
245 container: None,
246 };
247 assert!(!env.evidence.is_empty());
248 assert_eq!(env.confidence, Confidence::High);
249 }
250}