mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
* Multiple (#3111) * fix alerts i18n, fix status display, better, remove usb media, hide shutdown for install complete * trigger chnage detection for localize pipe and round out implementing localize pipe for consistency even though not needed * Fix PackageInfoShort to handle LocaleString on releaseNotes (#3112) * Fix PackageInfoShort to handle LocaleString on releaseNotes * fix: filter by target_version in get_matching_models and pass otherVersions from install * chore: add exver documentation for ai agents * frontend plus some be types --------- Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
540 lines
18 KiB
Rust
540 lines
18 KiB
Rust
use std::collections::{BTreeMap, BTreeSet};
|
|
use std::path::PathBuf;
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use exver::VersionRange;
|
|
use imbl_value::InternedString;
|
|
use patch_db::HasModel;
|
|
use patch_db::json_ptr::JsonPointer;
|
|
use reqwest::Url;
|
|
use serde::{Deserialize, Serialize};
|
|
use ts_rs::TS;
|
|
|
|
use crate::net::host::Hosts;
|
|
use crate::net::service_interface::ServiceInterface;
|
|
use crate::prelude::*;
|
|
use crate::progress::FullProgress;
|
|
use crate::s9pk::manifest::{LocaleString, Manifest};
|
|
use crate::status::StatusInfo;
|
|
use crate::util::DataUrl;
|
|
use crate::util::serde::{Pem, is_partial_of};
|
|
use crate::{ActionId, GatewayId, HealthCheckId, HostId, PackageId, ReplayId, ServiceInterfaceId};
|
|
|
|
#[derive(Debug, Default, Deserialize, Serialize, TS)]
|
|
#[ts(export)]
|
|
pub struct AllPackageData(pub BTreeMap<PackageId, PackageDataEntry>);
|
|
impl Map for AllPackageData {
|
|
type Key = PackageId;
|
|
type Value = PackageDataEntry;
|
|
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
|
|
Ok(key)
|
|
}
|
|
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
|
|
Ok(key.clone().into())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub enum ManifestPreference {
|
|
Old,
|
|
New,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[serde(tag = "state")]
|
|
#[model = "Model<Self>"]
|
|
#[ts(export)]
|
|
pub enum PackageState {
|
|
Installing(InstallingState),
|
|
Restoring(InstallingState),
|
|
Updating(UpdatingState),
|
|
Installed(InstalledState),
|
|
Removing(InstalledState),
|
|
}
|
|
impl PackageState {
|
|
pub fn expect_installed(&self) -> Result<&InstalledState, Error> {
|
|
match self {
|
|
Self::Installed(a) => Ok(a),
|
|
_ => Err(Error::new(
|
|
eyre!(
|
|
"Package {} is not in installed state",
|
|
self.as_manifest(ManifestPreference::Old).id
|
|
),
|
|
ErrorKind::InvalidRequest,
|
|
)),
|
|
}
|
|
}
|
|
pub fn expect_removing(&self) -> Result<&InstalledState, Error> {
|
|
match self {
|
|
Self::Removing(a) => Ok(a),
|
|
_ => Err(Error::new(
|
|
eyre!(
|
|
"Package {} is not in removing state",
|
|
self.as_manifest(ManifestPreference::Old).id
|
|
),
|
|
ErrorKind::InvalidRequest,
|
|
)),
|
|
}
|
|
}
|
|
pub fn into_installing_info(self) -> Option<InstallingInfo> {
|
|
match self {
|
|
Self::Installing(InstallingState { installing_info })
|
|
| Self::Restoring(InstallingState { installing_info }) => Some(installing_info),
|
|
Self::Updating(UpdatingState {
|
|
installing_info, ..
|
|
}) => Some(installing_info),
|
|
Self::Installed(_) | Self::Removing(_) => None,
|
|
}
|
|
}
|
|
pub fn as_installing_info(&self) -> Option<&InstallingInfo> {
|
|
match self {
|
|
Self::Installing(InstallingState { installing_info })
|
|
| Self::Restoring(InstallingState { installing_info }) => Some(installing_info),
|
|
Self::Updating(UpdatingState {
|
|
installing_info, ..
|
|
}) => Some(installing_info),
|
|
Self::Installed(_) | Self::Removing(_) => None,
|
|
}
|
|
}
|
|
pub fn as_installing_info_mut(&mut self) -> Option<&mut InstallingInfo> {
|
|
match self {
|
|
Self::Installing(InstallingState { installing_info })
|
|
| Self::Restoring(InstallingState { installing_info }) => Some(installing_info),
|
|
Self::Updating(UpdatingState {
|
|
installing_info, ..
|
|
}) => Some(installing_info),
|
|
Self::Installed(_) | Self::Removing(_) => None,
|
|
}
|
|
}
|
|
pub fn into_manifest(self, preference: ManifestPreference) -> Manifest {
|
|
match self {
|
|
Self::Installing(InstallingState {
|
|
installing_info: InstallingInfo { new_manifest, .. },
|
|
})
|
|
| Self::Restoring(InstallingState {
|
|
installing_info: InstallingInfo { new_manifest, .. },
|
|
}) => new_manifest,
|
|
Self::Updating(UpdatingState { manifest, .. })
|
|
if preference == ManifestPreference::Old =>
|
|
{
|
|
manifest
|
|
}
|
|
Self::Updating(UpdatingState {
|
|
installing_info: InstallingInfo { new_manifest, .. },
|
|
..
|
|
}) => new_manifest,
|
|
Self::Installed(InstalledState { manifest })
|
|
| Self::Removing(InstalledState { manifest }) => manifest,
|
|
}
|
|
}
|
|
pub fn as_manifest(&self, preference: ManifestPreference) -> &Manifest {
|
|
match self {
|
|
Self::Installing(InstallingState {
|
|
installing_info: InstallingInfo { new_manifest, .. },
|
|
})
|
|
| Self::Restoring(InstallingState {
|
|
installing_info: InstallingInfo { new_manifest, .. },
|
|
}) => new_manifest,
|
|
Self::Updating(UpdatingState { manifest, .. })
|
|
if preference == ManifestPreference::Old =>
|
|
{
|
|
manifest
|
|
}
|
|
Self::Updating(UpdatingState {
|
|
installing_info: InstallingInfo { new_manifest, .. },
|
|
..
|
|
}) => new_manifest,
|
|
Self::Installed(InstalledState { manifest })
|
|
| Self::Removing(InstalledState { manifest }) => manifest,
|
|
}
|
|
}
|
|
pub fn as_manifest_mut(&mut self, preference: ManifestPreference) -> &mut Manifest {
|
|
match self {
|
|
Self::Installing(InstallingState {
|
|
installing_info: InstallingInfo { new_manifest, .. },
|
|
})
|
|
| Self::Restoring(InstallingState {
|
|
installing_info: InstallingInfo { new_manifest, .. },
|
|
}) => new_manifest,
|
|
Self::Updating(UpdatingState { manifest, .. })
|
|
if preference == ManifestPreference::Old =>
|
|
{
|
|
manifest
|
|
}
|
|
Self::Updating(UpdatingState {
|
|
installing_info: InstallingInfo { new_manifest, .. },
|
|
..
|
|
}) => new_manifest,
|
|
Self::Installed(InstalledState { manifest })
|
|
| Self::Removing(InstalledState { manifest }) => manifest,
|
|
}
|
|
}
|
|
}
|
|
impl Model<PackageState> {
|
|
pub fn expect_installed(&self) -> Result<&Model<InstalledState>, Error> {
|
|
match self.as_match() {
|
|
PackageStateMatchModelRef::Installed(a) => Ok(a),
|
|
_ => Err(Error::new(
|
|
eyre!(
|
|
"Package {} is not in installed state",
|
|
self.as_manifest(ManifestPreference::Old).as_id().de()?
|
|
),
|
|
ErrorKind::InvalidRequest,
|
|
)),
|
|
}
|
|
}
|
|
pub fn into_installing_info(self) -> Option<Model<InstallingInfo>> {
|
|
match self.into_match() {
|
|
PackageStateMatchModel::Installing(s) | PackageStateMatchModel::Restoring(s) => {
|
|
Some(s.into_installing_info())
|
|
}
|
|
PackageStateMatchModel::Updating(s) => Some(s.into_installing_info()),
|
|
PackageStateMatchModel::Installed(_) | PackageStateMatchModel::Removing(_) => None,
|
|
PackageStateMatchModel::Error(_) => None,
|
|
}
|
|
}
|
|
pub fn as_installing_info(&self) -> Option<&Model<InstallingInfo>> {
|
|
match self.as_match() {
|
|
PackageStateMatchModelRef::Installing(s) | PackageStateMatchModelRef::Restoring(s) => {
|
|
Some(s.as_installing_info())
|
|
}
|
|
PackageStateMatchModelRef::Updating(s) => Some(s.as_installing_info()),
|
|
PackageStateMatchModelRef::Installed(_) | PackageStateMatchModelRef::Removing(_) => {
|
|
None
|
|
}
|
|
PackageStateMatchModelRef::Error(_) => None,
|
|
}
|
|
}
|
|
pub fn as_installing_info_mut(&mut self) -> Option<&mut Model<InstallingInfo>> {
|
|
match self.as_match_mut() {
|
|
PackageStateMatchModelMut::Installing(s) | PackageStateMatchModelMut::Restoring(s) => {
|
|
Some(s.as_installing_info_mut())
|
|
}
|
|
PackageStateMatchModelMut::Updating(s) => Some(s.as_installing_info_mut()),
|
|
PackageStateMatchModelMut::Installed(_) | PackageStateMatchModelMut::Removing(_) => {
|
|
None
|
|
}
|
|
PackageStateMatchModelMut::Error(_) => None,
|
|
}
|
|
}
|
|
pub fn into_manifest(self, preference: ManifestPreference) -> Model<Manifest> {
|
|
match self.into_match() {
|
|
PackageStateMatchModel::Installing(s) | PackageStateMatchModel::Restoring(s) => {
|
|
s.into_installing_info().into_new_manifest()
|
|
}
|
|
PackageStateMatchModel::Updating(s) if preference == ManifestPreference::Old => {
|
|
s.into_manifest()
|
|
}
|
|
PackageStateMatchModel::Updating(s) => s.into_installing_info().into_new_manifest(),
|
|
PackageStateMatchModel::Installed(s) | PackageStateMatchModel::Removing(s) => {
|
|
s.into_manifest()
|
|
}
|
|
PackageStateMatchModel::Error(_) => Value::Null.into(),
|
|
}
|
|
}
|
|
pub fn as_manifest(&self, preference: ManifestPreference) -> &Model<Manifest> {
|
|
match self.as_match() {
|
|
PackageStateMatchModelRef::Installing(s) | PackageStateMatchModelRef::Restoring(s) => {
|
|
s.as_installing_info().as_new_manifest()
|
|
}
|
|
PackageStateMatchModelRef::Updating(s) if preference == ManifestPreference::Old => {
|
|
s.as_manifest()
|
|
}
|
|
PackageStateMatchModelRef::Updating(s) => s.as_installing_info().as_new_manifest(),
|
|
PackageStateMatchModelRef::Installed(s) | PackageStateMatchModelRef::Removing(s) => {
|
|
s.as_manifest()
|
|
}
|
|
PackageStateMatchModelRef::Error(_) => (&Value::Null).into(),
|
|
}
|
|
}
|
|
pub fn as_manifest_mut(
|
|
&mut self,
|
|
preference: ManifestPreference,
|
|
) -> Result<&mut Model<Manifest>, Error> {
|
|
Ok(match self.as_match_mut() {
|
|
PackageStateMatchModelMut::Installing(s) | PackageStateMatchModelMut::Restoring(s) => {
|
|
s.as_installing_info_mut().as_new_manifest_mut()
|
|
}
|
|
PackageStateMatchModelMut::Updating(s) if preference == ManifestPreference::Old => {
|
|
s.as_manifest_mut()
|
|
}
|
|
PackageStateMatchModelMut::Updating(s) => {
|
|
s.as_installing_info_mut().as_new_manifest_mut()
|
|
}
|
|
PackageStateMatchModelMut::Installed(s) | PackageStateMatchModelMut::Removing(s) => {
|
|
s.as_manifest_mut()
|
|
}
|
|
PackageStateMatchModelMut::Error(_) => {
|
|
return Err(Error::new(
|
|
eyre!("could not determine package state to get manifest"),
|
|
ErrorKind::Database,
|
|
));
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[model = "Model<Self>"]
|
|
#[ts(export)]
|
|
pub struct InstallingState {
|
|
pub installing_info: InstallingInfo,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[model = "Model<Self>"]
|
|
#[ts(export)]
|
|
pub struct UpdatingState {
|
|
pub manifest: Manifest,
|
|
pub s9pk: PathBuf,
|
|
pub installing_info: InstallingInfo,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[model = "Model<Self>"]
|
|
#[ts(export)]
|
|
pub struct InstalledState {
|
|
pub manifest: Manifest,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[model = "Model<Self>"]
|
|
#[ts(export)]
|
|
pub struct InstallingInfo {
|
|
pub new_manifest: Manifest,
|
|
pub progress: FullProgress,
|
|
}
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
|
|
#[ts(export)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub enum AllowedStatuses {
|
|
OnlyRunning,
|
|
OnlyStopped,
|
|
Any,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[model = "Model<Self>"]
|
|
pub struct ActionMetadata {
|
|
/// A human-readable name
|
|
pub name: String,
|
|
/// A detailed description of what the action will do
|
|
pub description: String,
|
|
/// Presents as an alert prior to executing the action. Should be used sparingly but important if the action could have harmful, unintended consequences
|
|
pub warning: Option<String>,
|
|
#[serde(default)]
|
|
/// One of: "enabled", "hidden", or { disabled: "" }
|
|
/// - "enabled" - the action is available be run
|
|
/// - "hidden" - the action cannot be seen or run
|
|
/// - { disabled: "example explanation" } means the action is visible but cannot be run. Replace "example explanation" with a reason why the action is disable to prevent user confusion.
|
|
pub visibility: ActionVisibility,
|
|
/// One of: "only-stopped", "only-running", "all"
|
|
/// - "only-stopped" - the action can only be run when the service is stopped
|
|
/// - "only-running" - the action can only be run when the service is running
|
|
/// - "any" - the action can only be run regardless of the service's status
|
|
pub allowed_statuses: AllowedStatuses,
|
|
pub has_input: bool,
|
|
/// If provided, this action will be nested under a header of this value, along with other actions of the same group
|
|
pub group: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
|
|
#[ts(export)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
#[serde(rename_all_fields = "camelCase")]
|
|
pub enum ActionVisibility {
|
|
Hidden,
|
|
Disabled(String),
|
|
Enabled,
|
|
}
|
|
impl Default for ActionVisibility {
|
|
fn default() -> Self {
|
|
Self::Enabled
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[model = "Model<Self>"]
|
|
#[ts(export)]
|
|
pub struct PackageDataEntry {
|
|
pub state_info: PackageState,
|
|
pub s9pk: PathBuf,
|
|
pub status_info: StatusInfo,
|
|
#[ts(type = "string | null")]
|
|
pub registry: Option<Url>,
|
|
#[ts(type = "string")]
|
|
pub developer_key: Pem<ed25519_dalek::VerifyingKey>,
|
|
pub icon: DataUrl<'static>,
|
|
#[ts(type = "string | null")]
|
|
pub last_backup: Option<DateTime<Utc>>,
|
|
pub current_dependencies: CurrentDependencies,
|
|
pub actions: BTreeMap<ActionId, ActionMetadata>,
|
|
pub tasks: BTreeMap<ReplayId, TaskEntry>,
|
|
pub service_interfaces: BTreeMap<ServiceInterfaceId, ServiceInterface>,
|
|
pub hosts: Hosts,
|
|
#[ts(type = "string[]")]
|
|
pub store_exposed_dependents: Vec<JsonPointer>,
|
|
#[serde(default)]
|
|
#[ts(type = "string | null")]
|
|
pub outbound_gateway: Option<GatewayId>,
|
|
}
|
|
impl AsRef<PackageDataEntry> for PackageDataEntry {
|
|
fn as_ref(&self) -> &PackageDataEntry {
|
|
self
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, Deserialize, Serialize, TS)]
|
|
#[ts(export)]
|
|
pub struct CurrentDependencies(pub BTreeMap<PackageId, CurrentDependencyInfo>);
|
|
impl CurrentDependencies {
|
|
pub fn map(
|
|
mut self,
|
|
transform: impl Fn(
|
|
BTreeMap<PackageId, CurrentDependencyInfo>,
|
|
) -> BTreeMap<PackageId, CurrentDependencyInfo>,
|
|
) -> Self {
|
|
self.0 = transform(self.0);
|
|
self
|
|
}
|
|
}
|
|
impl Map for CurrentDependencies {
|
|
type Key = PackageId;
|
|
type Value = CurrentDependencyInfo;
|
|
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
|
|
Ok(key)
|
|
}
|
|
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
|
|
Ok(key.clone().into())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize, TS, HasModel)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[model = "Model<Self>"]
|
|
pub struct CurrentDependencyInfo {
|
|
pub title: Option<LocaleString>,
|
|
pub icon: Option<DataUrl<'static>>,
|
|
#[serde(flatten)]
|
|
pub kind: CurrentDependencyKind,
|
|
#[ts(type = "string")]
|
|
pub version_range: VersionRange,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
#[serde(tag = "kind")]
|
|
pub enum CurrentDependencyKind {
|
|
Exists,
|
|
#[serde(rename_all = "camelCase")]
|
|
Running {
|
|
#[serde(default)]
|
|
#[ts(type = "string[]")]
|
|
health_checks: BTreeSet<HealthCheckId>,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize, TS, HasModel)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export)]
|
|
#[model = "Model<Self>"]
|
|
pub struct TaskEntry {
|
|
pub task: Task,
|
|
pub active: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize, TS, HasModel)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export)]
|
|
#[model = "Model<Self>"]
|
|
pub struct Task {
|
|
pub package_id: PackageId,
|
|
pub action_id: ActionId,
|
|
#[serde(default)]
|
|
pub severity: TaskSeverity,
|
|
#[ts(optional)]
|
|
pub reason: Option<String>,
|
|
#[ts(optional)]
|
|
pub when: Option<TaskTrigger>,
|
|
#[ts(optional)]
|
|
pub input: Option<TaskInput>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq, Eq, PartialOrd, Ord)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
#[ts(export)]
|
|
pub enum TaskSeverity {
|
|
Optional,
|
|
Important,
|
|
Critical,
|
|
}
|
|
impl Default for TaskSeverity {
|
|
fn default() -> Self {
|
|
TaskSeverity::Important
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export)]
|
|
pub struct TaskTrigger {
|
|
#[serde(default)]
|
|
pub once: bool,
|
|
pub condition: TaskCondition,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
#[ts(export)]
|
|
pub enum TaskCondition {
|
|
InputNotMatches,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
#[serde(tag = "kind")]
|
|
pub enum TaskInput {
|
|
Partial {
|
|
#[ts(type = "Record<string, unknown>")]
|
|
value: Value,
|
|
},
|
|
}
|
|
impl TaskInput {
|
|
pub fn matches(&self, input: Option<&Value>) -> bool {
|
|
match self {
|
|
Self::Partial { value } => match input {
|
|
None => false,
|
|
Some(full) => is_partial_of(value, full),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize, Serialize)]
|
|
pub struct InterfaceAddressMap(pub BTreeMap<HostId, InterfaceAddresses>);
|
|
impl Map for InterfaceAddressMap {
|
|
type Key = HostId;
|
|
type Value = InterfaceAddresses;
|
|
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
|
|
Ok(key)
|
|
}
|
|
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
|
|
Ok(key.clone().into())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[model = "Model<Self>"]
|
|
pub struct InterfaceAddresses {
|
|
pub tor_address: Option<String>,
|
|
pub lan_address: Option<String>,
|
|
}
|