use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::Serialize;
use super::diff::ScopeView;
use super::error::AdminError;
use crate::convergence::RevisionReport;
use crate::desired_state::{
Actor, AuditEvent, DesiredState, LoadedRevision, RevisionId, RevisionManifest,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HistoryLimit(u32);
impl HistoryLimit {
pub const MAX: u32 = 100;
pub const DEFAULT: Self = Self(20);
pub const fn parse(requested: u32) -> Result<Self, AdminError> {
if requested == 0 || requested > Self::MAX {
return Err(AdminError::HistoryLimitInvalid { max: Self::MAX });
}
Ok(Self(requested))
}
pub const fn get(self) -> u32 {
self.0
}
}
impl Default for HistoryLimit {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct HistoryRequest {
pub limit: HistoryLimit,
pub start: Option<RevisionId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RevisionRecord {
pub revision: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
pub created_at_ms: u64,
pub mutation: String,
pub checksum: String,
pub resources: usize,
pub blobs: usize,
}
impl RevisionRecord {
pub fn of(manifest: &RevisionManifest) -> Self {
Self {
revision: manifest.id.to_string(),
parent: manifest.parent.map(|parent| parent.to_string()),
created_at_ms: millis(manifest.created_at),
mutation: manifest.mutation.to_string(),
checksum: manifest.checksum.to_string(),
resources: manifest.entries.len(),
blobs: manifest.blobs.len(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RevisionPage {
pub revisions: Vec<RevisionRecord>,
pub limit: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_start: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ActorView {
pub kind: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub issuer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub component: Option<String>,
}
impl ActorView {
pub fn of(actor: &Actor) -> Self {
match actor {
Actor::Human { issuer, subject } => Self {
kind: "human",
issuer: Some(issuer.clone()),
subject: Some(subject.clone()),
component: None,
},
Actor::Breakglass => Self {
kind: "breakglass",
issuer: None,
subject: None,
component: None,
},
Actor::System { component } => Self {
kind: "system",
issuer: None,
subject: None,
component: Some(component.clone()),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AuditRecord {
pub event: String,
pub mutation: String,
pub actor: ActorView,
pub kind: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
pub summary: String,
pub recorded_at_ms: u64,
}
impl AuditRecord {
pub fn of(event: &AuditEvent) -> Self {
Self {
event: event.id.to_string(),
mutation: event.mutation.to_string(),
actor: ActorView::of(&event.actor),
kind: event.kind.as_str(),
target: event.target.map(|target| target.to_string()),
summary: event.summary.clone(),
recorded_at_ms: millis(event.recorded_at),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AuditPage {
pub revision: String,
pub events: Vec<AuditRecord>,
pub truncated: bool,
}
impl AuditPage {
pub const MAX_EVENTS: usize = 100;
pub fn of(revision: RevisionId, events: &[AuditEvent]) -> Self {
Self {
revision: revision.to_string(),
events: events
.iter()
.take(Self::MAX_EVENTS)
.map(AuditRecord::of)
.collect(),
truncated: events.len() > Self::MAX_EVENTS,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ResourceRecord {
pub kind: &'static str,
pub resource: String,
pub version: u64,
pub scope: ScopeView,
pub slug: String,
pub content: String,
pub depends_on: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct BlobRecord {
pub kind: &'static str,
pub digest: String,
pub size_bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct StateView {
#[serde(skip_serializing_if = "Option::is_none")]
pub revision: Option<String>,
pub resources: Vec<ResourceRecord>,
pub blobs: Vec<BlobRecord>,
}
impl StateView {
pub fn of(revision: Option<&LoadedRevision>) -> Result<Self, AdminError> {
let empty = DesiredState::new();
let state = revision.map_or(&empty, LoadedRevision::state);
let mut resources = Vec::with_capacity(state.resources().len());
for resource in state.resources() {
resources.push(ResourceRecord {
kind: resource.reference.kind.as_str(),
resource: resource.reference.id.to_string(),
version: resource.reference.version.get(),
scope: ScopeView::of(&resource.scope),
slug: resource.slug.as_str().to_owned(),
content: resource.content_checksum()?.to_string(),
depends_on: resource
.depends_on
.iter()
.map(ToString::to_string)
.collect(),
});
}
Ok(Self {
revision: revision.map(|revision| revision.id().to_string()),
resources,
blobs: state
.blobs()
.map(|blob| BlobRecord {
kind: blob.kind.as_str(),
digest: blob.digest.to_string(),
size_bytes: blob.size_bytes,
})
.collect(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ConvergenceResult {
pub converged: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub desired: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub loaded: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<&'static str>,
pub generation: u64,
pub lag_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_convergence_ms: Option<u64>,
pub consecutive_failures: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_rejection: Option<&'static str>,
}
impl ConvergenceResult {
pub fn of(report: &RevisionReport) -> Self {
Self {
converged: report.converged(),
desired: report.desired.map(|id| id.to_string()),
loaded: report.loaded.map(|id| id.to_string()),
active: report.active.map(|id| id.to_string()),
source: report.source.map(|source| source.as_str()),
generation: report.generation,
lag_ms: duration_ms(report.lag),
last_convergence_ms: report.last_convergence.map(duration_ms),
consecutive_failures: report.consecutive_failures,
last_rejection: report
.last_rejection
.as_ref()
.map(|rejection| rejection.reason),
}
}
}
fn millis(at: SystemTime) -> u64 {
at.duration_since(UNIX_EPOCH).map_or(0, |since| {
u64::try_from(since.as_millis()).unwrap_or(u64::MAX)
})
}
fn duration_ms(duration: Duration) -> u64 {
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
}