use serde::Deserialize;
use serde_json::Value;
use std::collections::BTreeMap;
pub type Extra = serde_json::Map<String, Value>;
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct DeploymentStatus {
pub rollout_revision: String,
pub spec: DeploymentSpec,
pub kind: String,
pub desired_replicas: u32,
pub ready: usize,
pub pending: usize,
pub total_in_flight: usize,
pub vms: Vec<VmStatus>,
pub workspace: Option<WorkspaceStatus>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct WorkspaceSpec {
pub path: Option<String>,
pub store: String,
#[serde(rename = "ref")]
pub artifact_ref: Option<String>,
pub auth: Option<SecretRef>,
pub snapshot_interval_secs: Option<u64>,
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct WorkspaceStatus {
pub path: String,
pub store: String,
pub digest: Option<String>,
pub captured_at: Option<u64>,
pub captured_from: Option<String>,
pub files: u64,
pub bytes: u64,
pub pushed: Option<String>,
pub pushed_at: Option<u64>,
pub push_pending: bool,
pub phase: String,
pub blocked: Option<String>,
pub pending: Vec<PendingCapture>,
pub last_error: Option<String>,
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct PendingCapture {
pub sandbox_id: String,
pub then: String,
pub queued_at: u64,
pub attempts: u32,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct VmStatus {
pub sandbox_id: String,
pub addr: String,
pub in_flight: usize,
pub healthy: bool,
pub draining: bool,
#[serde(flatten)]
pub extra: Extra,
}
impl VmStatus {
pub fn status(&self) -> &'static str {
if self.draining {
"Draining"
} else if self.healthy {
"Ready"
} else {
"NotReady"
}
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct DeploymentSpec {
pub id: String,
pub namespace: String,
pub account_id: Option<String>,
pub user_id: Option<String>,
pub routes: Vec<RouteRule>,
pub maintenance: bool,
pub vm: Option<VmSpec>,
pub scaling: ScalingPolicy,
pub health: HealthCheck,
pub upstreams: Vec<String>,
pub discovery: Option<DiscoverySpec>,
pub build: Option<BuildSpec>,
pub artifact: Option<ArtifactSpec>,
pub update: Option<UpdateSpec>,
pub auth: Option<AuthGate>,
pub site: Option<SiteSpec>,
}
impl DeploymentSpec {
pub fn namespace(&self) -> &str {
if self.namespace.is_empty() {
crate::DEFAULT_NAMESPACE
} else {
&self.namespace
}
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct DiscoverySpec {
pub service_id: String,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct SiteSpec {
pub root: String,
pub index: String,
pub not_found: Option<String>,
pub spa: bool,
pub cache_control: String,
}
impl DeploymentSpec {
pub fn is_static(&self) -> bool {
!self.upstreams.is_empty() || self.discovery.is_some()
}
pub fn is_site(&self) -> bool {
self.site.is_some()
}
pub fn routes_summary(&self) -> String {
if self.routes.is_empty() {
return "<none>".into();
}
let shown: Vec<String> = self.routes.iter().take(3).map(RouteRule::render).collect();
let extra = self.routes.len().saturating_sub(shown.len());
if extra > 0 {
format!("{} +{extra} more", shown.join(","))
} else {
shown.join(",")
}
}
pub fn backend_summary(&self) -> String {
if let Some(site) = &self.site {
return site.root.clone();
}
if self.is_static() {
if let Some(discovery) = &self.discovery {
let upstreams = self.upstreams.join(",");
return if upstreams.is_empty() {
format!("discovery:{}", discovery.service_id)
} else {
format!("discovery:{} ({upstreams})", discovery.service_id)
};
}
return self.upstreams.join(",");
}
match &self.vm {
Some(vm) => {
let image = vm.image.as_deref().unwrap_or("ubuntu:24.04 (default)");
format!("{image}:{}", vm.port)
}
None => "<none>".into(),
}
}
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct RouteRule {
pub host: Option<String>,
pub host_suffix: Option<String>,
pub path_prefix: Option<String>,
pub strip_prefix: bool,
}
impl RouteRule {
pub fn render(&self) -> String {
let mut s = String::new();
if let Some(h) = &self.host {
s.push_str(h);
}
if let Some(suffix) = &self.host_suffix {
if !s.is_empty() {
s.push('+');
}
s.push_str("*.");
s.push_str(suffix.trim_start_matches('.'));
}
if let Some(p) = &self.path_prefix {
if s.is_empty() {
s.push('*');
}
s.push_str(p);
}
if s.is_empty() { "<empty>".into() } else { s }
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct VmSpec {
pub driver: String,
pub image: Option<String>,
pub port: u16,
pub start_command: Option<String>,
pub size_class: Option<String>,
pub disk_size_gb: Option<u32>,
pub working_directory: Option<String>,
pub env_vars: Option<BTreeMap<String, String>>,
pub setup_hooks: Option<Vec<String>>,
pub open_ports: Vec<u16>,
pub mounts: Vec<MountSpec>,
pub workspace: Option<WorkspaceSpec>,
pub ttl_seconds: u64,
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct MountSpec {
pub path: String,
pub store: String,
#[serde(rename = "ref")]
pub artifact_ref: String,
pub auth: Option<SecretRef>,
pub strip_components: Option<usize>,
pub read_only: bool,
pub digest: Option<String>,
}
impl MountSpec {
pub fn summary(&self) -> String {
let store = self
.store
.trim_end_matches('/')
.trim_start_matches("https://")
.trim_start_matches("http://");
format!(
"{} <- {store}/{} ({})",
self.path,
self.artifact_ref,
if self.read_only { "ro" } else { "rw" },
)
}
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct BuildSpec {
pub repo: String,
pub store: String,
#[serde(rename = "ref")]
pub git_ref: Option<String>,
pub dockerfile: Option<String>,
pub context: Option<String>,
pub image_name: Option<String>,
pub image_size_mb: Option<u64>,
pub auth: Option<SecretRef>,
}
impl BuildSpec {
pub fn summary(&self) -> String {
if !self.store.is_empty() {
let store = self
.store
.trim_end_matches('/')
.trim_start_matches("https://")
.trim_start_matches("http://");
return match &self.git_ref {
Some(r) => format!("{store}/{r}"),
None => format!("{store}/(no ref)"),
};
}
let repo = self
.repo
.trim_end_matches(".git")
.trim_start_matches("https://")
.trim_start_matches("http://")
.trim_start_matches("ssh://");
match &self.git_ref {
Some(r) => format!("{repo}@{r}"),
None => format!("{repo}@(default branch)"),
}
}
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct ArtifactSpec {
pub store: String,
#[serde(rename = "ref")]
pub artifact_ref: String,
pub image_name: Option<String>,
pub grow_gb: Option<u64>,
pub auth: Option<SecretRef>,
pub strip_components: Option<usize>,
}
impl ArtifactSpec {
pub fn summary(&self) -> String {
let store = self
.store
.trim_end_matches('/')
.trim_start_matches("https://")
.trim_start_matches("http://");
format!("{store}/{}", self.artifact_ref)
}
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct MountOutcome {
pub path: String,
pub store: String,
#[serde(rename = "ref")]
pub artifact_ref: String,
pub digest: Option<String>,
pub tree: Option<String>,
pub files: Option<usize>,
pub bytes: Option<u64>,
pub unpacked: Option<u64>,
pub reused: bool,
pub changed: bool,
}
impl MountOutcome {
pub fn summary(&self) -> String {
let store = self
.store
.trim_end_matches('/')
.trim_start_matches("https://")
.trim_start_matches("http://");
format!("{store}/{}", self.artifact_ref)
}
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct SecretRef {
pub secret: String,
pub key: String,
pub username: Option<String>,
pub namespace: Option<String>,
}
impl SecretRef {
pub fn render(&self) -> String {
format!("{}/{}", self.secret, self.key)
}
pub fn render_in(&self, ns: &str) -> String {
match self.namespace.as_deref() {
Some(other) if other != ns => format!("{}/{} in namespace {other}", self.secret, self.key),
_ => self.render(),
}
}
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct UpdateSpec {
pub working_dir: String,
pub commands: Vec<String>,
pub env: Option<BTreeMap<String, String>>,
pub env_from: Vec<SecretEnv>,
pub auth: Option<SecretRef>,
pub timeout_secs: Option<u64>,
pub verify_timeout_secs: Option<u64>,
}
impl UpdateSpec {
pub fn summary(&self) -> String {
format!(
"{} ({} command{})",
self.working_dir,
self.commands.len(),
if self.commands.len() == 1 { "" } else { "s" }
)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct PublicPath {
pub path: String,
pub scope: String,
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct AuthGate {
pub provider: Value,
pub client_id: Option<String>,
pub client_secret: Option<SecretRef>,
pub allowed_domains: Vec<String>,
pub allowed_emails: Vec<String>,
pub public_paths: Vec<PublicPath>,
pub session_scope: Option<String>,
pub base_path: String,
pub session_ttl_secs: u64,
pub cookie_name: String,
pub cookie_domain: Option<String>,
pub redirect_url: Option<String>,
pub forward_identity: bool,
pub jwt: Option<JwtSpec>,
pub provider_ref: Option<String>,
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct JwtSpec {
pub secret: Option<SecretRef>,
pub public_key: Option<String>,
pub jwks_url: Option<String>,
pub algorithms: Vec<String>,
pub issuer: String,
pub audience: Option<String>,
pub require: BTreeMap<String, Value>,
pub subject_claim: String,
pub email_claim: String,
pub name_claim: String,
pub leeway_secs: Option<u64>,
pub cookie: Option<String>,
pub login_url: Option<String>,
pub login_redirect_param: Option<String>,
}
impl JwtSpec {
pub fn key_summary(&self) -> String {
match (&self.secret, &self.public_key, &self.jwks_url) {
(Some(r), _, _) => format!("shared secret {}", r.render()),
(_, Some(_), _) => "an inline public key".to_string(),
(_, _, Some(url)) => format!("the key set at {url}"),
_ => "<none>".to_string(),
}
}
pub fn require_summary(&self) -> String {
if self.require.is_empty() {
return "any token this issuer signed".to_string();
}
self.require
.iter()
.map(|(claim, wanted)| {
let rendered = match wanted {
Value::Array(vs) => vs
.iter()
.map(render_claim_value)
.collect::<Vec<_>>()
.join("|"),
single => render_claim_value(single),
};
format!("{claim}={rendered}")
})
.collect::<Vec<_>>()
.join(", ")
}
}
fn render_claim_value(v: &Value) -> String {
match v {
Value::String(s) => s.clone(),
other => other.to_string(),
}
}
impl AuthGate {
pub fn allow_summary(&self) -> String {
let mut parts: Vec<String> = Vec::new();
if self.allowed_domains.iter().any(|d| d == "*") {
parts.push("any Google account".into());
} else {
parts.extend(self.allowed_domains.iter().map(|d| format!("@{d}")));
}
parts.extend(self.allowed_emails.iter().cloned());
if parts.is_empty() {
"<nobody>".into()
} else {
parts.join(", ")
}
}
pub fn providers(&self) -> Vec<String> {
match &self.provider {
Value::String(s) if !s.is_empty() => vec![s.clone()],
Value::Array(items) => items
.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect(),
_ => vec!["google".to_string()],
}
}
pub fn accepts_app_token(&self) -> bool {
self.providers().iter().any(|p| p == "app-token")
}
pub fn accepts_jwt(&self) -> bool {
self.providers().iter().any(|p| p == "jwt")
}
pub fn callback_url(&self, host: &str) -> String {
format!(
"https://{host}{}/callback",
self.base_path.trim_end_matches('/')
)
}
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct AuthProviderView {
pub name: String,
pub namespace: String,
pub description: Option<String>,
pub created_at: u64,
pub provider: Value,
pub client_id: Option<String>,
pub client_secret: Option<SecretRef>,
pub allowed_domains: Vec<String>,
pub allowed_emails: Vec<String>,
pub jwt: Option<JwtSpec>,
pub cookie_domain: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
impl AuthProviderView {
pub fn providers(&self) -> Vec<String> {
match &self.provider {
Value::String(s) if !s.is_empty() => vec![s.clone()],
Value::Array(items) => items
.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect(),
_ => vec!["google".to_string()],
}
}
pub fn admits(&self) -> String {
let mut parts: Vec<String> = Vec::new();
if !self.allowed_domains.is_empty() || !self.allowed_emails.is_empty() {
if self.allowed_domains.iter().any(|d| d == "*") {
parts.push("any Google account".into());
} else {
parts.extend(self.allowed_domains.iter().map(|d| format!("@{d}")));
}
parts.extend(self.allowed_emails.iter().cloned());
}
if let Some(jwt) = &self.jwt {
parts.push(jwt.require_summary());
}
if parts.is_empty() && self.providers().iter().any(|p| p == "app-token") {
parts.push("whatever the app-token is scoped to".into());
}
if parts.is_empty() {
"<nobody>".into()
} else {
parts.join(", ")
}
}
pub fn trust_summary(&self) -> String {
match (&self.jwt, &self.client_id) {
(Some(jwt), _) => jwt.issuer.clone(),
(None, Some(id)) => id.clone(),
(None, None) => "—".into(),
}
}
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct SecretEnv {
pub secret: String,
pub key: String,
#[serde(rename = "as")]
pub env: Option<String>,
}
impl SecretEnv {
pub fn env_name(&self) -> String {
self.env
.clone()
.unwrap_or_else(|| self.key.to_ascii_uppercase())
}
pub fn render(&self) -> String {
format!("{}={}/{}", self.env_name(), self.secret, self.key)
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct SecretSummary {
pub id: String,
pub description: Option<String>,
pub keys: Vec<String>,
pub updated_at: u64,
pub encrypted_at_rest: bool,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct WorkflowView {
pub id: String,
pub repo: String,
#[serde(rename = "ref")]
pub git_ref: String,
pub path: String,
pub network: String,
pub auth: Option<Value>,
pub secrets_prefix: Option<String>,
pub enabled: bool,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct WorkflowList {
pub workflows: Vec<WorkflowView>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct JobRecord {
pub id: String,
pub deployment: String,
pub kind: String,
pub status: String,
pub started_at: u64,
pub finished_at: Option<u64>,
pub repo: String,
#[serde(rename = "ref")]
pub git_ref: Option<String>,
pub commit: Option<String>,
pub dockerfile: Option<String>,
pub image: Option<String>,
pub rolled_out: bool,
pub store: Option<String>,
#[serde(rename = "artifact")]
pub artifact_ref: Option<String>,
pub digest: Option<String>,
pub bytes: Option<u64>,
pub reused: bool,
pub site_root: Option<String>,
pub files: Option<usize>,
pub mounts: Vec<MountOutcome>,
pub working_dir: Option<String>,
pub commands_total: Option<usize>,
pub commands_run: Option<usize>,
pub verified: Option<bool>,
pub error: Option<String>,
pub log: Vec<String>,
#[serde(flatten)]
pub extra: Extra,
}
impl JobRecord {
pub fn is_running(&self) -> bool {
self.status == "running"
}
pub fn succeeded(&self) -> bool {
self.status == "succeeded"
}
pub fn is_update(&self) -> bool {
self.kind == "host-update"
}
pub fn is_pull(&self) -> bool {
self.kind == "artifact-pull"
}
pub fn is_mount_pull(&self) -> bool {
self.kind == "mount-pull"
}
pub fn short_commit(&self) -> String {
match &self.commit {
Some(c) => c.chars().take(12).collect(),
None => "—".into(),
}
}
pub fn short_digest(&self) -> String {
match &self.digest {
Some(d) => d.chars().take(12).collect(),
None => "—".into(),
}
}
pub fn result_summary(&self) -> String {
if self.is_update() {
return match (self.commands_run, self.commands_total) {
(Some(run), Some(total)) => format!("{run}/{total} commands"),
_ => "—".into(),
};
}
if self.is_mount_pull() {
let changed = self.mounts.iter().filter(|m| m.changed).count();
return match (self.mounts.len(), changed) {
(0, _) => "—".into(),
(total, 0) => format!("{total} unchanged"),
(total, n) => format!("{n}/{total} updated"),
};
}
self.image.clone().unwrap_or_else(|| "—".into())
}
pub fn target_summary(&self) -> String {
if self.is_update() {
return self.working_dir.clone().unwrap_or_else(|| "—".into());
}
if self.is_pull() {
return self.artifact_ref.clone().unwrap_or_else(|| "—".into());
}
if self.is_mount_pull() {
let paths: Vec<&str> = self.mounts.iter().map(|m| m.path.as_str()).take(2).collect();
return match (paths.is_empty(), self.mounts.len().saturating_sub(paths.len())) {
(true, _) => "—".into(),
(false, 0) => paths.join(","),
(false, more) => format!("{} +{more} more", paths.join(",")),
};
}
self.git_ref.clone().unwrap_or_else(|| "(default)".into())
}
pub fn elapsed_secs(&self, now: u64) -> u64 {
self.finished_at
.unwrap_or(now)
.saturating_sub(self.started_at)
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct ScalingPolicy {
pub min_replicas: u32,
pub max_replicas: u32,
pub warm_pool: u32,
pub target_concurrency: u32,
pub scale_to_zero_after_secs: u64,
pub cold_start_timeout_secs: u64,
pub drain_timeout_secs: u64,
pub boot_timeout_secs: u64,
pub idle_action: String,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct HealthCheck {
pub expected_header: Option<ExpectedHeader>,
pub path: Option<String>,
pub port: Option<u16>,
pub timeout_secs: u64,
}
#[derive(Debug, Default, Deserialize)]
pub struct ExpectedHeader {
pub name: String,
pub value: String,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct MetricsResponse {
pub generated_at: u64,
pub uptime_secs: u64,
pub host: HostUsage,
pub fleet: FleetPool,
pub global: DeploymentMetrics,
pub obs: Option<ObsStats>,
pub security: Option<SecuritySummary>,
pub daemon: DaemonStatus,
pub deployments: Vec<DeploymentView>,
pub matched: usize,
pub tracked_deployments: usize,
#[serde(default)]
pub host_sandboxes: Vec<HostSandboxView>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct HostSandboxView {
pub sandbox_id: String,
pub name: String,
pub status: String,
pub image: String,
pub size_class: Option<String>,
pub guest_ip: Option<String>,
pub uptime_secs: u64,
pub cpu_percent: Option<f64>,
pub memory_bytes: Option<u64>,
pub account_id: Option<String>,
pub created_at: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct SecuritySummary {
pub open: usize,
pub urgent: u64,
pub dropped: u64,
pub clients_at_capacity: bool,
pub rules: usize,
pub blocked: u64,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct ObsStats {
pub queued: u64,
pub dropped: u64,
pub shipped: u64,
pub failed: u64,
pub healthy: bool,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct HostUsage {
pub available: bool,
pub cpu_count: u64,
pub cpu_percent: f64,
pub memory_total_bytes: u64,
pub memory_used_bytes: u64,
pub sampled_at_ms: u64,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct FleetPool {
pub deployments: usize,
pub ready: usize,
pub draining: usize,
pub pending: usize,
pub total_in_flight: usize,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct DeploymentView {
pub id: String,
pub namespace: String,
pub kind: String,
pub upstreams: Vec<String>,
pub routed: bool,
pub hosts: Vec<String>,
pub urls: Vec<String>,
pub site_root: Option<String>,
pub site_spa: bool,
pub job_kind: Option<String>,
pub pool: PoolStatus,
pub vms: Vec<VmView>,
pub pending_vms: Vec<PendingVmView>,
pub metrics: DeploymentMetrics,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct PendingVmView {
pub sandbox_id: String,
pub age_secs: u64,
pub status: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct PoolStatus {
pub desired_replicas: u32,
pub ready: usize,
pub draining: usize,
pub pending: usize,
pub total_in_flight: usize,
pub target_concurrency: u32,
pub min_replicas: u32,
pub max_replicas: u32,
pub warm_pool: u32,
pub utilization: Option<f64>,
pub cpu_percent: Option<f64>,
pub memory_bytes: Option<u64>,
pub boot_timeout_secs: u64,
pub cold_start_timeout_secs: u64,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct VmView {
pub sandbox_id: String,
pub addr: String,
pub in_flight: usize,
pub healthy: bool,
pub draining: bool,
pub uptime_secs: u64,
pub cpu_percent: Option<f64>,
pub memory_bytes: Option<u64>,
#[serde(flatten)]
pub extra: Extra,
}
impl VmView {
pub fn status(&self) -> &'static str {
if self.draining {
"Draining"
} else if self.healthy {
"Ready"
} else {
"NotReady"
}
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct DeploymentMetrics {
pub requests: StatusCounts,
pub latency_ms: Histogram,
pub cold_start_s: Histogram,
pub autoscale: AutoscaleCounts,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct StatusCounts {
pub total: u64,
pub c2xx: u64,
pub c3xx: u64,
pub c4xx: u64,
pub c5xx: u64,
pub errors: u64,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct Histogram {
pub count: u64,
pub sum: u64,
pub mean: f64,
pub p50: f64,
pub p90: f64,
pub p99: f64,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct AutoscaleCounts {
pub vms_created: u64,
pub vms_drained: u64,
pub vms_reaped: u64,
pub scale_up_events: u64,
pub scale_down_events: u64,
pub cold_start_waits: u64,
pub cold_start_hits: u64,
pub cold_start_timeouts: u64,
pub boot_timeouts: u64,
pub create_failures: u64,
pub last_create_error: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct DaemonStatus {
pub reachable: bool,
pub last_error: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct CertStatus {
pub host: String,
pub not_after: String,
pub issuer: String,
pub needs_renewal: bool,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DiskState {
Running,
Stopped,
Orphan,
#[default]
#[serde(other)]
Unknown,
}
impl DiskState {
pub fn label(self) -> &'static str {
match self {
Self::Running => "running",
Self::Stopped => "stopped",
Self::Orphan => "orphan",
Self::Unknown => "unknown",
}
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct DiskPart {
pub kind: String,
pub path: String,
pub bytes: u64,
pub apparent_bytes: u64,
pub modified_at: u64,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct DiskArchiveRecord {
pub uri: String,
pub at: u64,
pub bytes: u64,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct DiskArchiveView {
pub id: String,
pub sandbox_id: String,
pub uri: String,
pub started_at: u64,
pub finished_at: Option<u64>,
pub status: String,
pub bytes: u64,
pub expected_bytes: u64,
pub error: Option<String>,
pub purged: bool,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct DiskInfo {
pub sandbox_id: String,
pub name: Option<String>,
pub deployment: Option<String>,
pub state: DiskState,
pub claimed: bool,
pub retain: bool,
pub note: Option<String>,
pub bytes: u64,
pub apparent_bytes: u64,
pub modified_at: u64,
pub expires_at: Option<u64>,
pub held_by: Option<String>,
pub archived: Option<DiskArchiveRecord>,
pub parts: Vec<DiskPart>,
pub roots: Vec<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct DiskTotals {
pub disks: usize,
pub bytes: u64,
pub apparent_bytes: u64,
pub running: usize,
pub stopped: usize,
pub orphan: usize,
pub retained: usize,
pub expiring_now: usize,
pub reclaimable_bytes: u64,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct DiskInventory {
pub complete: bool,
pub incomplete_reason: Option<String>,
pub data_dir: String,
pub tmp_dir: String,
pub ttl_secs: u64,
pub sweep_secs: u64,
pub archive_enabled: bool,
pub archive_on_expire: bool,
pub archive_target: Option<String>,
pub free_bytes: Option<u64>,
pub filesystem_bytes: Option<u64>,
pub orphan_ttl_secs: u64,
pub totals: DiskTotals,
pub disks: Vec<DiskInfo>,
pub archives: Vec<DiskArchiveView>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct PluginView {
pub id: String,
pub name: String,
pub description: String,
pub config_schema: Value,
pub enabled: bool,
pub config: Value,
pub updated_at: u64,
pub last_error: Option<String>,
pub status: Value,
#[serde(flatten)]
pub extra: Extra,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_deployment_parses_from_a_partial_body() {
let d: DeploymentStatus = serde_json::from_str(r#"{"spec":{"id":"web"},"ready":2}"#).unwrap();
assert_eq!(d.spec.id, "web");
assert_eq!(d.ready, 2);
assert_eq!(d.desired_replicas, 0);
}
#[test]
fn routes_render_with_their_matching_semantics() {
let exact = RouteRule {
host: Some("secrets.local".into()),
..Default::default()
};
assert_eq!(exact.render(), "secrets.local");
let wild = RouteRule {
host_suffix: Some(".apps.example.com".into()),
path_prefix: Some("/api".into()),
..Default::default()
};
assert_eq!(wild.render(), "*.apps.example.com/api");
let path_only = RouteRule {
path_prefix: Some("/legacy".into()),
..Default::default()
};
assert_eq!(path_only.render(), "*/legacy");
assert_eq!(RouteRule::default().render(), "<empty>");
}
#[test]
fn many_routes_collapse_to_a_summary() {
let spec = DeploymentSpec {
routes: (0..5)
.map(|i| RouteRule {
host: Some(format!("h{i}.local")),
..Default::default()
})
.collect(),
..Default::default()
};
assert_eq!(spec.routes_summary(), "h0.local,h1.local,h2.local +2 more");
}
#[test]
fn a_build_source_renders_as_one_column() {
let b = BuildSpec {
repo: "https://github.com/acme/web.git".into(),
git_ref: Some("main".into()),
..Default::default()
};
assert_eq!(b.summary(), "github.com/acme/web@main");
let default_branch = BuildSpec {
repo: "git@github.com:acme/web.git".into(),
..Default::default()
};
assert_eq!(
default_branch.summary(),
"git@github.com:acme/web@(default branch)"
);
let from_store = BuildSpec {
store: "http://art.internal:8080".into(),
git_ref: Some("web-rootfs".into()),
..Default::default()
};
assert_eq!(from_store.summary(), "art.internal:8080/web-rootfs");
let parsed: BuildSpec =
serde_json::from_str(r#"{"store":"/srv/artifacts","ref":"web"}"#).unwrap();
assert_eq!(parsed.summary(), "/srv/artifacts/web");
}
#[test]
fn a_job_record_parses_from_a_partial_body() {
let r: JobRecord =
serde_json::from_str(r#"{"id":"job-1","status":"running","started_at":100}"#).unwrap();
assert!(r.is_running());
assert!(!r.succeeded());
assert_eq!(r.short_commit(), "—");
assert_eq!(r.elapsed_secs(130), 30, "a running job measures against now");
let done: JobRecord = serde_json::from_str(
r#"{"id":"job-1","kind":"image-build","status":"succeeded","started_at":100,
"finished_at":160,"commit":"0123456789abcdef","ref":"main","image":"web-0123"}"#,
)
.unwrap();
assert!(done.succeeded());
assert_eq!(done.short_commit(), "0123456789ab");
assert_eq!(done.elapsed_secs(999), 60, "a finished one measures its own span");
assert_eq!(done.target_summary(), "main");
assert_eq!(done.result_summary(), "web-0123");
}
#[test]
fn an_update_record_summarizes_its_own_fields() {
let update: JobRecord = serde_json::from_str(
r#"{"id":"job-2","kind":"host-update","status":"failed","started_at":100,
"working_dir":"/srv/app-obs","commands_total":3,"commands_run":1,
"verified":false}"#,
)
.unwrap();
assert!(update.is_update());
assert_eq!(update.target_summary(), "/srv/app-obs");
assert_eq!(update.result_summary(), "1/3 commands");
assert_eq!(update.verified, Some(false));
}
#[test]
fn an_artifact_source_renders_as_one_column_shaped_like_a_build_one() {
let a = ArtifactSpec {
store: "http://10.0.0.4:8080/".into(),
artifact_ref: "web-v2".into(),
..Default::default()
};
assert_eq!(a.summary(), "10.0.0.4:8080/web-v2");
let local = ArtifactSpec {
store: "/srv/artifacts".into(),
artifact_ref: "debian-hermes".into(),
..Default::default()
};
assert_eq!(local.summary(), "/srv/artifacts/debian-hermes");
}
#[test]
fn a_pull_record_summarizes_the_store_side_fields_not_the_git_ones() {
let pull: JobRecord = serde_json::from_str(
r#"{"id":"job-3","kind":"artifact-pull","status":"succeeded","started_at":100,
"store":"http://127.0.0.1:8080","artifact":"debian-hermes",
"digest":"c74abee2ce8409f1aaaa","image":"web-c74abee2ce84",
"bytes":609222656,"rolled_out":true}"#,
)
.unwrap();
assert!(pull.is_pull());
assert!(!pull.is_update());
assert_eq!(pull.target_summary(), "debian-hermes");
assert_eq!(pull.result_summary(), "web-c74abee2ce84");
assert_eq!(pull.short_digest(), "c74abee2ce84");
assert_eq!(pull.short_commit(), "—", "a pull has no commit");
assert!(!pull.reused);
}
#[test]
fn a_reused_image_is_distinguishable_from_a_pull_that_never_ran() {
let reused: JobRecord = serde_json::from_str(
r#"{"id":"job-4","kind":"artifact-pull","status":"succeeded","started_at":1,
"bytes":0,"reused":true}"#,
)
.unwrap();
assert!(reused.reused);
assert_eq!(reused.bytes, Some(0));
let failed: JobRecord = serde_json::from_str(
r#"{"id":"job-5","kind":"artifact-pull","status":"failed","started_at":1}"#,
)
.unwrap();
assert!(!failed.reused);
assert_eq!(failed.bytes, None);
}
#[test]
fn an_update_source_renders_its_directory_and_command_count() {
let u = UpdateSpec {
working_dir: "/srv/app-obs".into(),
commands: vec!["git pull".into(), "cargo build --release".into()],
..Default::default()
};
assert_eq!(u.summary(), "/srv/app-obs (2 commands)");
let one = UpdateSpec {
working_dir: "/srv/x".into(),
commands: vec!["make deploy".into()],
..Default::default()
};
assert_eq!(one.summary(), "/srv/x (1 command)");
}
#[test]
fn a_gate_says_who_may_enter_and_where_the_provider_redirects() {
let g = AuthGate {
allowed_domains: vec!["example.com".into()],
allowed_emails: vec!["contractor@gmail.com".into()],
base_path: "/__applb/auth".into(),
..Default::default()
};
assert_eq!(g.allow_summary(), "@example.com, contractor@gmail.com");
assert_eq!(
g.callback_url("app.example.com"),
"https://app.example.com/__applb/auth/callback"
);
let any = AuthGate {
allowed_domains: vec!["*".into()],
..Default::default()
};
assert_eq!(any.allow_summary(), "any Google account");
let unknown: AuthGate = serde_json::from_str(r#"{"provider":"okta","client_id":"x"}"#).unwrap();
assert_eq!(unknown.provider, "okta");
assert_eq!(unknown.allow_summary(), "<nobody>");
}
#[test]
fn a_secret_env_defaults_to_the_upper_cased_key() {
let e = SecretEnv {
secret: "obs".into(),
key: "ingest_token".into(),
env: None,
};
assert_eq!(e.env_name(), "INGEST_TOKEN");
assert_eq!(e.render(), "INGEST_TOKEN=obs/ingest_token");
let renamed = SecretEnv {
env: Some("APP_OBS_INGEST_TOKEN".into()),
..e
};
assert_eq!(renamed.render(), "APP_OBS_INGEST_TOKEN=obs/ingest_token");
}
#[test]
fn vm_status_puts_draining_ahead_of_health() {
let vm = VmStatus {
healthy: true,
draining: true,
..Default::default()
};
assert_eq!(vm.status(), "Draining");
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct ExecOutput {
pub sandbox_id: String,
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub output: String,
#[serde(flatten)]
pub extra: Extra,
}
impl ExecOutput {
pub fn ok(&self) -> bool {
self.exit_code == 0
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct EvictOutcome {
pub sandbox_id: String,
pub outcome: String,
#[serde(flatten)]
pub extra: Extra,
}
impl EvictOutcome {
pub fn is_draining(&self) -> bool {
self.outcome == "draining"
}
}
#[derive(Debug, Default, Deserialize, serde::Serialize)]
#[serde(default)]
pub struct UpstreamTrafficStatus {
pub deployment_id: String,
pub upstream: String,
pub state: String,
pub healthy: bool,
pub in_flight: usize,
pub reason: Option<String>,
pub started_at: Option<u64>,
#[serde(flatten)]
pub extra: Extra,
}
impl UpstreamTrafficStatus {
pub fn is_drained(&self) -> bool {
self.state == "drained" && self.in_flight == 0
}
pub fn is_accepting(&self) -> bool {
self.state == "accepting"
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, serde::Serialize)]
#[serde(rename_all = "lowercase")]
pub enum AdminScope {
#[default]
None,
View,
Admin,
}
impl std::fmt::Display for AdminScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::None => "none",
Self::View => "view",
Self::Admin => "admin",
})
}
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct TokenSummary {
pub id: String,
pub name: String,
pub admin: AdminScope,
pub namespace: Option<String>,
pub deployments: Vec<String>,
pub created_at: u64,
pub expires_at: Option<u64>,
pub last_used_at: Option<u64>,
#[serde(flatten)]
pub extra: Extra,
}
impl TokenSummary {
pub fn covers_fleet(&self) -> bool {
self.namespace.is_none() && self.deployments.iter().any(|d| d == "*")
}
pub fn allows(&self, deployment: &str) -> bool {
self.deployments.iter().any(|d| d == "*" || d == deployment)
}
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct NamespaceEntry {
pub namespace: String,
pub deployments: u64,
pub declared: bool,
pub description: Option<String>,
pub created_at: Option<u64>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct FeedIndexEntry {
pub namespace: String,
pub events: u64,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default)]
pub struct FeedEvent {
pub id: u64,
pub ts: u64,
pub last_ts: u64,
pub count: u64,
pub namespace: String,
pub deployment: String,
pub kind: String,
pub title: String,
pub detail: String,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Default, Clone, Deserialize)]
pub struct MintedToken {
#[serde(flatten)]
pub summary: TokenSummary,
pub token: String,
}