use std::collections::{BTreeMap, BTreeSet};
use super::action::TargetKey;
use super::values::{PathValue, Scope, derived_symbol};
pub const DEFAULT_HOST_CAPACITY: u16 = 1;
pub const BUDGET_ALLOWANCE_PER_HOUR: u32 = 2_500;
pub const REFRESHES_PER_HOUR: u32 = 60;
pub const INVENTORY_REQUESTS_PER_TARGET_REFRESH: u32 = 1;
pub const ADMISSION_REQUESTS_PER_REPOSITORY_REFRESH: u32 = 3;
pub const PROJECTION_REQUESTS_PER_POLICY_REFRESH: u32 = 6;
#[must_use]
pub const fn repository_admission_cost() -> u32 {
(INVENTORY_REQUESTS_PER_TARGET_REFRESH + ADMISSION_REQUESTS_PER_REPOSITORY_REFRESH)
* REFRESHES_PER_HOUR
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Installation {
None,
Standard,
Wide,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstallationSpec {
pub id: u64,
pub account: &'static str,
pub repositories: Vec<String>,
}
impl Installation {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Installation::None => "no-installation",
Installation::Standard => "standard",
Installation::Wide => "wide",
}
}
#[must_use]
pub fn specs(self) -> Vec<InstallationSpec> {
match self {
Installation::None => Vec::new(),
Installation::Standard => vec![
InstallationSpec {
id: 101,
account: "acme",
repositories: vec!["acme/widgets".to_string(), "acme/gadgets".to_string()],
},
InstallationSpec {
id: 202,
account: "globex",
repositories: vec!["globex/portal".to_string()],
},
],
Installation::Wide => {
let mut repositories = vec!["acme/widgets".to_string(), "acme/gadgets".to_string()];
repositories.extend((1..=11).map(|n| format!("acme/fleet-{n:02}")));
vec![InstallationSpec {
id: 303,
account: "acme",
repositories,
}]
}
}
}
#[must_use]
pub fn reaches(self, key: &TargetKey) -> bool {
self.specs().iter().any(|spec| match key.scope {
Scope::Repository => spec.repositories.contains(&key.slug),
Scope::Organization => spec.account == key.slug,
})
}
#[must_use]
pub fn repositories_of(self, organization: &str) -> u32 {
self.specs()
.iter()
.find(|spec| spec.account == organization)
.map_or(0, |spec| {
u32::try_from(spec.repositories.len()).unwrap_or(u32::MAX)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PolicyState {
Pending,
Active,
Draining,
Disabled,
RepairRequired,
}
impl PolicyState {
#[must_use]
pub const fn token(self) -> &'static str {
match self {
PolicyState::Pending => "pending",
PolicyState::Active => "active",
PolicyState::Draining => "draining",
PolicyState::Disabled => "disabled",
PolicyState::RepairRequired => "repair_required",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Mode {
MonitorOnly,
Autoscale {
max_capacity: u16,
extra_labels: BTreeSet<String>,
},
}
impl Mode {
#[must_use]
pub const fn token(&self) -> &'static str {
match self {
Mode::MonitorOnly => "monitor_only",
Mode::Autoscale { .. } => "autoscale",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Tally {
pub active: u16,
pub awaiting_cleanup: u16,
pub cleaned: u16,
}
impl Tally {
#[must_use]
pub const fn is_empty(self) -> bool {
self.active == 0 && self.awaiting_cleanup == 0 && self.cleaned == 0
}
#[must_use]
pub const fn uncleaned(self) -> u16 {
self.active.saturating_add(self.awaiting_cleanup)
}
#[must_use]
pub const fn plus(self, other: Tally) -> Tally {
Tally {
active: self.active.saturating_add(other.active),
awaiting_cleanup: self.awaiting_cleanup.saturating_add(other.awaiting_cleanup),
cleaned: self.cleaned.saturating_add(other.cleaned),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Policy {
pub display: String,
pub host_label: String,
pub mode: Mode,
pub enabled: bool,
pub state: PolicyState,
pub workspace: Option<PathValue>,
pub attempts: Tally,
}
impl Policy {
#[must_use]
pub fn derived_label(&self) -> String {
derived_symbol(&self.host_label)
}
#[must_use]
pub fn routing_labels(&self) -> Option<Vec<String>> {
match &self.mode {
Mode::MonitorOnly => None,
Mode::Autoscale { extra_labels, .. } => {
let mut labels = vec![self.derived_label()];
labels.extend(extra_labels.iter().cloned());
Some(labels)
}
}
}
#[must_use]
pub const fn max_capacity(&self) -> Option<u16> {
match &self.mode {
Mode::MonitorOnly => None,
Mode::Autoscale { max_capacity, .. } => Some(*max_capacity),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct HostModel {
pub capacity: u16,
pub runner_root: Option<PathValue>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Model {
pub installation: Installation,
pub credential: bool,
pub host: Option<HostModel>,
pub policies: BTreeMap<TargetKey, Policy>,
pub retained: BTreeMap<TargetKey, Tally>,
pub directories: BTreeSet<PathValue>,
pub package_cache: bool,
}
impl Model {
#[must_use]
pub fn fresh(installation: Installation) -> Self {
Self {
installation,
credential: false,
host: None,
policies: BTreeMap::new(),
retained: BTreeMap::new(),
directories: BTreeSet::new(),
package_cache: false,
}
}
#[must_use]
pub fn host_capacity(&self) -> u16 {
self.host
.map_or(DEFAULT_HOST_CAPACITY, |host| host.capacity)
}
#[must_use]
pub fn runner_root(&self) -> Option<PathValue> {
self.host.and_then(|host| host.runner_root)
}
#[must_use]
pub fn journal(&self) -> Tally {
self.policies
.values()
.map(|policy| policy.attempts)
.chain(self.retained.values().copied())
.fold(Tally::default(), Tally::plus)
}
#[must_use]
pub fn retained_total(&self) -> Tally {
self.retained
.values()
.copied()
.fold(Tally::default(), Tally::plus)
}
pub fn validate(&self) -> Result<(), String> {
if let Some(host) = self.host
&& host.capacity == 0
{
return Err("a host record never holds a zero capacity".to_string());
}
if let Some(root) = self.runner_root() {
if root.location().is_none() || root == PathValue::InsideAppState {
return Err(format!(
"{root:?} can never be a configured host runner root"
));
}
if root.is_creatable_leaf() && !self.directories.contains(&root) {
return Err(format!("the configured host root {root:?} must exist"));
}
}
for path in &self.directories {
if !path.is_creatable_leaf() {
return Err(format!("{path:?} is not a directory a command can create"));
}
if let Some(parent) = path.creatable_parent()
&& !self.directories.contains(&parent)
{
return Err(format!("{path:?} exists without its parent {parent:?}"));
}
}
let roots: Vec<(&TargetKey, PathValue)> = self
.policies
.iter()
.filter_map(|(key, policy)| policy.workspace.map(|root| (key, root)))
.collect();
for (index, (key, root)) in roots.iter().enumerate() {
if key.scope != Scope::Repository {
return Err(format!("{key} is an organization and cannot be persistent"));
}
if !self.directory_exists(*root) {
return Err(format!("{key}'s persistent root {root:?} must exist"));
}
if let Some(host) = self.runner_root()
&& root.relation(host) != super::values::Relation::Disjoint
{
return Err(format!(
"{key}'s root {root:?} overlaps the host root {host:?}"
));
}
for (other_key, other) in &roots[index + 1..] {
if root.relation(*other) != super::values::Relation::Disjoint {
return Err(format!(
"{key}'s root {root:?} overlaps {other_key}'s root {other:?}"
));
}
}
}
for (key, policy) in &self.policies {
if policy.enabled != (policy.state == PolicyState::Active) {
return Err(format!(
"{key}: enabled={} with state {}; only an active policy is enabled",
policy.enabled,
policy.state.token()
));
}
match &policy.mode {
Mode::MonitorOnly => {
if !matches!(
policy.state,
PolicyState::Pending | PolicyState::RepairRequired
) {
return Err(format!(
"{key}: a monitor-only policy is never armed, so it cannot be {}",
policy.state.token()
));
}
}
Mode::Autoscale {
max_capacity,
extra_labels,
} => {
if *max_capacity == 0 {
return Err(format!("{key}: an autoscale ceiling is at least one"));
}
if extra_labels.contains(&policy.derived_label()) {
return Err(format!(
"{key}: the derived host label is never an optional label"
));
}
}
}
}
Ok(())
}
#[must_use]
pub fn in_use(&self) -> u16 {
self.journal().active
}
#[must_use]
pub fn directory_exists(&self, path: PathValue) -> bool {
path == PathValue::RootsDir || self.directories.contains(&path)
}
#[must_use]
pub fn admission_cost(&self, key: &TargetKey) -> u32 {
let repositories = match key.scope {
Scope::Repository => 1,
Scope::Organization => self.installation.repositories_of(&key.slug),
};
(INVENTORY_REQUESTS_PER_TARGET_REFRESH
+ repositories * ADMISSION_REQUESTS_PER_REPOSITORY_REFRESH)
* REFRESHES_PER_HOUR
}
#[must_use]
pub fn admitted_cost(&self) -> u32 {
self.policies
.keys()
.map(|key| self.admission_cost(key))
.sum()
}
#[must_use]
pub fn has_scope(&self, scope: Scope) -> bool {
self.policies.keys().any(|key| key.scope == scope)
}
#[must_use]
pub fn status(&self) -> StatusProjection {
let journal = self.journal();
let capacity = self.host_capacity();
let count = u32::try_from(self.policies.len()).unwrap_or(u32::MAX);
StatusProjection {
credential_present: self.credential,
host_configured: self.host.is_some(),
capacity,
in_use: journal.active,
headroom: capacity.saturating_sub(journal.active),
runner_root_source: if self.runner_root().is_some() {
"configured"
} else {
"platform_default"
},
configured_runner_root: self.runner_root(),
active_ephemeral_attempts: journal.active,
cleanup_blocked_ephemeral_attempts: journal.awaiting_cleanup,
projected_requests_per_hour: count
* PROJECTION_REQUESTS_PER_POLICY_REFRESH
* REFRESHES_PER_HOUR,
projection_is_floor: self.has_scope(Scope::Organization),
policies: self
.policies
.iter()
.map(|(key, policy)| PolicyProjection {
target: policy.display.clone(),
scope: key.scope.token(),
mode: policy.mode.token(),
state: policy.state.token(),
enabled: policy.enabled,
min_capacity: 0,
max_capacity: policy.max_capacity(),
routing_labels: policy.routing_labels().unwrap_or_default(),
active_attempts: policy.attempts.active,
cleanup_blocked_attempts: policy.attempts.awaiting_cleanup,
workspace_mode: if policy.workspace.is_some() {
"persistent"
} else {
"ephemeral"
},
workspace_root: policy.workspace,
workspace_root_source: match (policy.workspace, self.runner_root()) {
(Some(_), _) => "repository",
(None, Some(_)) => "configured",
(None, None) => "platform_default",
},
})
.collect(),
}
}
#[must_use]
pub fn list_lines(&self, scope: Scope) -> Vec<String> {
self.policies
.iter()
.filter(|(key, _)| key.scope == scope)
.map(|(_, policy)| {
format!(
"{}\t{}\t{}\tenabled={}\tmax={}\tworkspace={}",
policy.display,
policy.mode.token(),
policy.state.token(),
policy.enabled,
policy
.max_capacity()
.map_or_else(|| "-".to_string(), |n| n.to_string()),
if policy.workspace.is_some() {
"persistent"
} else {
"ephemeral"
}
)
})
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatusProjection {
pub credential_present: bool,
pub host_configured: bool,
pub capacity: u16,
pub in_use: u16,
pub headroom: u16,
pub runner_root_source: &'static str,
pub configured_runner_root: Option<PathValue>,
pub active_ephemeral_attempts: u16,
pub cleanup_blocked_ephemeral_attempts: u16,
pub projected_requests_per_hour: u32,
pub projection_is_floor: bool,
pub policies: Vec<PolicyProjection>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PolicyProjection {
pub target: String,
pub scope: &'static str,
pub mode: &'static str,
pub state: &'static str,
pub enabled: bool,
pub min_capacity: u16,
pub max_capacity: Option<u16>,
pub routing_labels: Vec<String>,
pub active_attempts: u16,
pub cleanup_blocked_attempts: u16,
pub workspace_mode: &'static str,
pub workspace_root: Option<PathValue>,
pub workspace_root_source: &'static str,
}