diff --git a/Makefile b/Makefile index bd3c14cc2..48a28c388 100644 --- a/Makefile +++ b/Makefile @@ -173,7 +173,7 @@ container-runtime/node_modules: container-runtime/package.json container-runtime npm --prefix container-runtime ci touch container-runtime/node_modules -core/startos/bindings: $(CORE_SRC) $(ENVIRONMENT_FILE) $(PLATFORM_FILE) +core/startos/bindings: $(shell git ls-files core) $(ENVIRONMENT_FILE) $(PLATFORM_FILE) (cd core/ && cargo test) touch core/startos/bindings diff --git a/container-runtime/src/Adapters/HostSystemStartOs.ts b/container-runtime/src/Adapters/HostSystemStartOs.ts index ecd490ea7..f173e4e62 100644 --- a/container-runtime/src/Adapters/HostSystemStartOs.ts +++ b/container-runtime/src/Adapters/HostSystemStartOs.ts @@ -117,10 +117,7 @@ export class HostSystemStartOs implements Effects { T.Effects["createOverlayedImage"] > } - destroyOverlayedImage(options: { - imageId: string - guid: string - }): Promise { + destroyOverlayedImage(options: { guid: string }): Promise { return this.rpcRound("destroyOverlayedImage", options) as ReturnType< T.Effects["destroyOverlayedImage"] > diff --git a/core/models/src/id/health_check.rs b/core/models/src/id/health_check.rs index 72c2947e7..c416ab1e6 100644 --- a/core/models/src/id/health_check.rs +++ b/core/models/src/id/health_check.rs @@ -1,8 +1,9 @@ use std::path::Path; +use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize}; -use crate::Id; +use crate::{Id, InvalidId}; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, ts_rs::TS)] pub struct HealthCheckId(Id); @@ -11,6 +12,12 @@ impl std::fmt::Display for HealthCheckId { write!(f, "{}", &self.0) } } +impl FromStr for HealthCheckId { + type Err = InvalidId; + fn from_str(s: &str) -> Result { + Id::from_str(s).map(HealthCheckId) + } +} impl AsRef for HealthCheckId { fn as_ref(&self) -> &str { self.0.as_ref() diff --git a/core/models/src/id/mod.rs b/core/models/src/id/mod.rs index ef38ece9d..29825f628 100644 --- a/core/models/src/id/mod.rs +++ b/core/models/src/id/mod.rs @@ -1,4 +1,5 @@ use std::borrow::Borrow; +use std::str::FromStr; use regex::Regex; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -59,6 +60,12 @@ impl TryFrom<&str> for Id { } } } +impl FromStr for Id { + type Err = InvalidId; + fn from_str(s: &str) -> Result { + Self::try_from(s) + } +} impl From for InternedString { fn from(value: Id) -> Self { value.0 diff --git a/core/startos/bindings/DependencyKind.ts b/core/startos/bindings/DependencyKind.ts new file mode 100644 index 000000000..018562971 --- /dev/null +++ b/core/startos/bindings/DependencyKind.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DependencyKind = "exists" | "running"; \ No newline at end of file diff --git a/core/startos/bindings/DependencyRequirement.ts b/core/startos/bindings/DependencyRequirement.ts new file mode 100644 index 000000000..9ece754c1 --- /dev/null +++ b/core/startos/bindings/DependencyRequirement.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DependencyKind } from "./DependencyKind"; +import type { HealthCheckId } from "./HealthCheckId"; +import type { PackageId } from "./PackageId"; + +export interface DependencyRequirement { id: PackageId, kind: DependencyKind, healthChecks: Array, } \ No newline at end of file diff --git a/core/startos/bindings/DestroyOverlayedImageParams.ts b/core/startos/bindings/DestroyOverlayedImageParams.ts index 15dd2bf53..b875e45bd 100644 --- a/core/startos/bindings/DestroyOverlayedImageParams.ts +++ b/core/startos/bindings/DestroyOverlayedImageParams.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export interface DestroyOverlayedImageParams { imageId: string , guid: string, } \ No newline at end of file +export interface DestroyOverlayedImageParams { guid: string, } \ No newline at end of file diff --git a/core/startos/bindings/SetDependenciesParams.ts b/core/startos/bindings/SetDependenciesParams.ts new file mode 100644 index 000000000..7741cddde --- /dev/null +++ b/core/startos/bindings/SetDependenciesParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DependencyRequirement } from "./DependencyRequirement"; + +export interface SetDependenciesParams { dependencies: Array, } \ No newline at end of file diff --git a/core/startos/src/backup/backup_bulk.rs b/core/startos/src/backup/backup_bulk.rs index 4f633d7ae..8c609f672 100644 --- a/core/startos/src/backup/backup_bulk.rs +++ b/core/startos/src/backup/backup_bulk.rs @@ -18,7 +18,8 @@ use crate::auth::check_password_against_db; use crate::backup::os::OsBackup; use crate::backup::{BackupReport, ServerBackupReport}; use crate::context::RpcContext; -use crate::db::model::{BackupProgress, DatabaseModel}; +use crate::db::model::public::BackupProgress; +use crate::db::model::DatabaseModel; use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; @@ -174,7 +175,7 @@ pub async fn backup_all( .as_package_data() .as_entries()? .into_iter() - .filter(|(_, m)| m.expect_as_installed().is_ok()) + .filter(|(_, m)| m.as_state_info().expect_installed().is_ok()) .map(|(id, _)| id) .collect() }; diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index 0f11bf63f..f987ffbf4 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -19,7 +19,7 @@ use super::setup::CURRENT_SECRET; use crate::account::AccountInfo; use crate::context::config::ServerConfig; use crate::core::rpc_continuations::{RequestGuid, RestHandler, RpcContinuation, WebSocketHandler}; -use crate::db::model::CurrentDependents; +use crate::db::model::package::CurrentDependents; use crate::db::prelude::PatchDbExt; use crate::dependencies::compute_dependency_config_errs; use crate::disk::OsPartitionInfo; @@ -219,12 +219,7 @@ impl RpcContext { for (package_id, package) in f.as_public_mut().as_package_data_mut().as_entries_mut()? { - for (k, v) in package - .as_installed_mut() - .into_iter() - .flat_map(|i| i.clone().into_current_dependencies().into_entries()) - .flatten() - { + for (k, v) in package.clone().into_current_dependencies().into_entries()? { let mut entry: BTreeMap<_, _> = current_dependents.remove(&k).unwrap_or_default(); entry.insert(package_id.clone(), v.de()?); @@ -236,16 +231,7 @@ impl RpcContext { .as_public_mut() .as_package_data_mut() .as_idx_mut(&package_id) - .and_then(|pde| pde.expect_as_installed_mut().ok()) - .map(|i| i.as_installed_mut().as_current_dependents_mut()) - { - deps.ser(&CurrentDependents(current_dependents))?; - } else if let Some(deps) = f - .as_public_mut() - .as_package_data_mut() - .as_idx_mut(&package_id) - .and_then(|pde| pde.expect_as_removing_mut().ok()) - .map(|i| i.as_removing_mut().as_current_dependents_mut()) + .map(|i| i.as_current_dependents_mut()) { deps.ser(&CurrentDependents(current_dependents))?; } @@ -261,23 +247,18 @@ impl RpcContext { let peek = self.db.peek().await; for (package_id, package) in peek.as_public().as_package_data().as_entries()?.into_iter() { let package = package.clone(); - if let Some(current_dependencies) = package - .as_installed() - .and_then(|x| x.as_current_dependencies().de().ok()) - { - let manifest = package.as_manifest().de()?; - all_dependency_config_errs.insert( - package_id.clone(), - compute_dependency_config_errs( - self, - &peek, - &manifest, - ¤t_dependencies, - &Default::default(), - ) - .await?, - ); - } + let current_dependencies = package.as_current_dependencies().de()?; + all_dependency_config_errs.insert( + package_id.clone(), + compute_dependency_config_errs( + self, + &peek, + &package_id, + ¤t_dependencies, + &Default::default(), + ) + .await?, + ); } self.db .mutate(|v| { @@ -286,7 +267,6 @@ impl RpcContext { .as_public_mut() .as_package_data_mut() .as_idx_mut(&package_id) - .and_then(|pde| pde.as_installed_mut()) .map(|i| i.as_status_mut().as_dependency_config_errors_mut()) { config_errors.ser(&errs)?; diff --git a/core/startos/src/db/model.rs b/core/startos/src/db/model.rs deleted file mode 100644 index b79d9d417..000000000 --- a/core/startos/src/db/model.rs +++ /dev/null @@ -1,627 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::net::{Ipv4Addr, Ipv6Addr}; - -use chrono::{DateTime, Utc}; -use emver::VersionRange; -use imbl_value::InternedString; -use ipnet::{Ipv4Net, Ipv6Net}; -use isocountry::CountryCode; -use itertools::Itertools; -use models::{DataUrl, HealthCheckId, HostId, PackageId}; -use openssl::hash::MessageDigest; -use patch_db::json_ptr::JsonPointer; -use patch_db::{HasModel, Value}; -use reqwest::Url; -use serde::{Deserialize, Serialize}; -use torut::onion::OnionAddressV3; - -use crate::account::AccountInfo; -use crate::auth::Sessions; -use crate::backup::target::cifs::CifsTargets; -use crate::net::forward::AvailablePorts; -use crate::net::host::HostInfo; -use crate::net::keys::KeyStore; -use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr}; -use crate::notifications::Notifications; -use crate::prelude::*; -use crate::progress::FullProgress; -use crate::s9pk::manifest::Manifest; -use crate::ssh::SshKeys; -use crate::status::Status; -use crate::util::cpupower::Governor; -use crate::util::serde::Pem; -use crate::util::Version; -use crate::version::{Current, VersionT}; -use crate::{ARCH, PLATFORM}; - -fn get_arch() -> InternedString { - (*ARCH).into() -} - -fn get_platform() -> InternedString { - (&*PLATFORM).into() -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct Database { - pub public: Public, - pub private: Private, -} -impl Database { - pub fn init(account: &AccountInfo) -> Result { - let lan_address = account.hostname.lan_address().parse().unwrap(); - Ok(Database { - public: Public { - server_info: ServerInfo { - arch: get_arch(), - platform: get_platform(), - id: account.server_id.clone(), - version: Current::new().semver().into(), - hostname: account.hostname.no_dot_host_name(), - last_backup: None, - last_wifi_region: None, - eos_version_compat: Current::new().compat().clone(), - lan_address, - onion_address: account.tor_key.public().get_onion_address(), - tor_address: format!( - "https://{}", - account.tor_key.public().get_onion_address() - ) - .parse() - .unwrap(), - ip_info: BTreeMap::new(), - status_info: ServerStatus { - backup_progress: None, - updated: false, - update_progress: None, - shutting_down: false, - restarting: false, - }, - wifi: WifiInfo { - ssids: Vec::new(), - connected: None, - selected: None, - }, - unread_notification_count: 0, - connection_addresses: ConnectionAddresses { - tor: Vec::new(), - clearnet: Vec::new(), - }, - password_hash: account.password.clone(), - pubkey: ssh_key::PublicKey::from(&account.ssh_key) - .to_openssh() - .unwrap(), - ca_fingerprint: account - .root_ca_cert - .digest(MessageDigest::sha256()) - .unwrap() - .iter() - .map(|x| format!("{x:X}")) - .join(":"), - ntp_synced: false, - zram: true, - governor: None, - }, - package_data: AllPackageData::default(), - ui: serde_json::from_str(include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../web/patchdb-ui-seed.json" - ))) - .unwrap(), - }, - private: Private { - key_store: KeyStore::new(account)?, - password: account.password.clone(), - ssh_privkey: Pem(account.ssh_key.clone()), - ssh_pubkeys: SshKeys::new(), - available_ports: AvailablePorts::new(), - sessions: Sessions::new(), - notifications: Notifications::new(), - cifs: CifsTargets::new(), - package_stores: BTreeMap::new(), - }, // TODO - }) - } -} - -pub type DatabaseModel = Model; - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -// #[macro_debug] -pub struct Public { - pub server_info: ServerInfo, - pub package_data: AllPackageData, - pub ui: Value, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct Private { - pub key_store: KeyStore, - pub password: String, // argon2 hash - pub ssh_privkey: Pem, - pub ssh_pubkeys: SshKeys, - pub available_ports: AvailablePorts, - pub sessions: Sessions, - pub notifications: Notifications, - pub cifs: CifsTargets, - #[serde(default)] - pub package_stores: BTreeMap, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct ServerInfo { - #[serde(default = "get_arch")] - pub arch: InternedString, - #[serde(default = "get_platform")] - pub platform: InternedString, - pub id: String, - pub hostname: String, - pub version: Version, - pub last_backup: Option>, - /// Used in the wifi to determine the region to set the system to - pub last_wifi_region: Option, - pub eos_version_compat: VersionRange, - pub lan_address: Url, - pub onion_address: OnionAddressV3, - /// for backwards compatibility - pub tor_address: Url, - pub ip_info: BTreeMap, - #[serde(default)] - pub status_info: ServerStatus, - pub wifi: WifiInfo, - pub unread_notification_count: u64, - pub connection_addresses: ConnectionAddresses, - pub password_hash: String, - pub pubkey: String, - pub ca_fingerprint: String, - #[serde(default)] - pub ntp_synced: bool, - #[serde(default)] - pub zram: bool, - pub governor: Option, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct IpInfo { - pub ipv4_range: Option, - pub ipv4: Option, - pub ipv6_range: Option, - pub ipv6: Option, -} -impl IpInfo { - pub async fn for_interface(iface: &str) -> Result { - let (ipv4, ipv4_range) = get_iface_ipv4_addr(iface).await?.unzip(); - let (ipv6, ipv6_range) = get_iface_ipv6_addr(iface).await?.unzip(); - Ok(Self { - ipv4_range, - ipv4, - ipv6_range, - ipv6, - }) - } -} - -#[derive(Debug, Default, Deserialize, Serialize, HasModel)] -#[model = "Model"] -pub struct BackupProgress { - pub complete: bool, -} - -#[derive(Debug, Default, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct ServerStatus { - pub backup_progress: Option>, - pub updated: bool, - pub update_progress: Option, - #[serde(default)] - pub shutting_down: bool, - #[serde(default)] - pub restarting: bool, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct UpdateProgress { - pub size: Option, - pub downloaded: u64, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct WifiInfo { - pub ssids: Vec, - pub selected: Option, - pub connected: Option, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct ServerSpecs { - pub cpu: String, - pub disk: String, - pub memory: String, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct ConnectionAddresses { - pub tor: Vec, - pub clearnet: Vec, -} - -#[derive(Debug, Default, Deserialize, Serialize)] -pub struct AllPackageData(pub BTreeMap); -impl Map for AllPackageData { - type Key = PackageId; - type Value = PackageDataEntry; - fn key_str(key: &Self::Key) -> Result, Error> { - Ok(key) - } - fn key_string(key: &Self::Key) -> Result { - Ok(key.clone().into()) - } -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct StaticFiles { - license: String, - instructions: String, - icon: DataUrl<'static>, -} -impl StaticFiles { - pub fn local(id: &PackageId, version: &Version, icon: DataUrl<'static>) -> Self { - StaticFiles { - license: format!("/public/package-data/{}/{}/LICENSE.md", id, version), - instructions: format!("/public/package-data/{}/{}/INSTRUCTIONS.md", id, version), - icon, - } - } -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct PackageDataEntryInstalling { - pub static_files: StaticFiles, - pub manifest: Manifest, - pub install_progress: FullProgress, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct PackageDataEntryUpdating { - pub static_files: StaticFiles, - pub manifest: Manifest, - pub installed: InstalledPackageInfo, - pub install_progress: FullProgress, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct PackageDataEntryRestoring { - pub static_files: StaticFiles, - pub manifest: Manifest, - pub install_progress: FullProgress, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct PackageDataEntryRemoving { - pub static_files: StaticFiles, - pub manifest: Manifest, - pub removing: InstalledPackageInfo, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct PackageDataEntryInstalled { - pub static_files: StaticFiles, - pub manifest: Manifest, - pub installed: InstalledPackageInfo, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(tag = "state")] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -// #[macro_debug] -pub enum PackageDataEntry { - Installing(PackageDataEntryInstalling), - Updating(PackageDataEntryUpdating), - Restoring(PackageDataEntryRestoring), - Removing(PackageDataEntryRemoving), - Installed(PackageDataEntryInstalled), -} -impl Model { - pub fn expect_into_installed(self) -> Result, Error> { - if let PackageDataEntryMatchModel::Installed(a) = self.into_match() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in installed state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_as_installed(&self) -> Result<&Model, Error> { - if let PackageDataEntryMatchModelRef::Installed(a) = self.as_match() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in installed state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_as_installed_mut( - &mut self, - ) -> Result<&mut Model, Error> { - if let PackageDataEntryMatchModelMut::Installed(a) = self.as_match_mut() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in installed state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_into_removing(self) -> Result, Error> { - if let PackageDataEntryMatchModel::Removing(a) = self.into_match() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in removing state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_as_removing(&self) -> Result<&Model, Error> { - if let PackageDataEntryMatchModelRef::Removing(a) = self.as_match() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in removing state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_as_removing_mut( - &mut self, - ) -> Result<&mut Model, Error> { - if let PackageDataEntryMatchModelMut::Removing(a) = self.as_match_mut() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in removing state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_as_installing_mut( - &mut self, - ) -> Result<&mut Model, Error> { - if let PackageDataEntryMatchModelMut::Installing(a) = self.as_match_mut() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in installing state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn into_manifest(self) -> Model { - match self.into_match() { - PackageDataEntryMatchModel::Installing(a) => a.into_manifest(), - PackageDataEntryMatchModel::Updating(a) => a.into_installed().into_manifest(), - PackageDataEntryMatchModel::Restoring(a) => a.into_manifest(), - PackageDataEntryMatchModel::Removing(a) => a.into_manifest(), - PackageDataEntryMatchModel::Installed(a) => a.into_manifest(), - PackageDataEntryMatchModel::Error(_) => Model::from(Value::Null), - } - } - pub fn as_manifest(&self) -> &Model { - match self.as_match() { - PackageDataEntryMatchModelRef::Installing(a) => a.as_manifest(), - PackageDataEntryMatchModelRef::Updating(a) => a.as_installed().as_manifest(), - PackageDataEntryMatchModelRef::Restoring(a) => a.as_manifest(), - PackageDataEntryMatchModelRef::Removing(a) => a.as_manifest(), - PackageDataEntryMatchModelRef::Installed(a) => a.as_manifest(), - PackageDataEntryMatchModelRef::Error(_) => (&Value::Null).into(), - } - } - pub fn into_installed(self) -> Option> { - match self.into_match() { - PackageDataEntryMatchModel::Installing(_) => None, - PackageDataEntryMatchModel::Updating(a) => Some(a.into_installed()), - PackageDataEntryMatchModel::Restoring(_) => None, - PackageDataEntryMatchModel::Removing(_) => None, - PackageDataEntryMatchModel::Installed(a) => Some(a.into_installed()), - PackageDataEntryMatchModel::Error(_) => None, - } - } - pub fn as_installed(&self) -> Option<&Model> { - match self.as_match() { - PackageDataEntryMatchModelRef::Installing(_) => None, - PackageDataEntryMatchModelRef::Updating(a) => Some(a.as_installed()), - PackageDataEntryMatchModelRef::Restoring(_) => None, - PackageDataEntryMatchModelRef::Removing(_) => None, - PackageDataEntryMatchModelRef::Installed(a) => Some(a.as_installed()), - PackageDataEntryMatchModelRef::Error(_) => None, - } - } - pub fn as_installed_mut(&mut self) -> Option<&mut Model> { - match self.as_match_mut() { - PackageDataEntryMatchModelMut::Installing(_) => None, - PackageDataEntryMatchModelMut::Updating(a) => Some(a.as_installed_mut()), - PackageDataEntryMatchModelMut::Restoring(_) => None, - PackageDataEntryMatchModelMut::Removing(_) => None, - PackageDataEntryMatchModelMut::Installed(a) => Some(a.as_installed_mut()), - PackageDataEntryMatchModelMut::Error(_) => None, - } - } - pub fn as_install_progress(&self) -> Option<&Model> { - match self.as_match() { - PackageDataEntryMatchModelRef::Installing(a) => Some(a.as_install_progress()), - PackageDataEntryMatchModelRef::Updating(a) => Some(a.as_install_progress()), - PackageDataEntryMatchModelRef::Restoring(a) => Some(a.as_install_progress()), - PackageDataEntryMatchModelRef::Removing(_) => None, - PackageDataEntryMatchModelRef::Installed(_) => None, - PackageDataEntryMatchModelRef::Error(_) => None, - } - } - pub fn as_install_progress_mut(&mut self) -> Option<&mut Model> { - match self.as_match_mut() { - PackageDataEntryMatchModelMut::Installing(a) => Some(a.as_install_progress_mut()), - PackageDataEntryMatchModelMut::Updating(a) => Some(a.as_install_progress_mut()), - PackageDataEntryMatchModelMut::Restoring(a) => Some(a.as_install_progress_mut()), - PackageDataEntryMatchModelMut::Removing(_) => None, - PackageDataEntryMatchModelMut::Installed(_) => None, - PackageDataEntryMatchModelMut::Error(_) => None, - } - } -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct InstalledPackageInfo { - pub status: Status, - pub marketplace_url: Option, - #[serde(default)] - #[serde(with = "crate::util::serde::ed25519_pubkey")] - pub developer_key: ed25519_dalek::VerifyingKey, - pub manifest: Manifest, - pub last_backup: Option>, - pub dependency_info: BTreeMap, - pub current_dependents: CurrentDependents, - pub current_dependencies: CurrentDependencies, - pub interface_addresses: InterfaceAddressMap, - pub hosts: HostInfo, - pub store_exposed_ui: Vec, - pub store_exposed_dependents: Vec, -} -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[model = "Model"] -pub struct ExposedDependent { - path: String, - title: String, - description: Option, - masked: Option, - copyable: Option, - qr: Option, -} -#[derive(Clone, Debug, Deserialize, Serialize, HasModel, ts_rs::TS)] -#[model = "Model"] -#[ts(export)] -pub struct ExposedUI { - #[ts(type = "string")] - pub path: JsonPointer, - pub title: String, - pub description: Option, - pub masked: Option, - pub copyable: Option, - pub qr: Option, -} - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -pub struct CurrentDependents(pub BTreeMap); -impl CurrentDependents { - pub fn map( - mut self, - transform: impl Fn( - BTreeMap, - ) -> BTreeMap, - ) -> Self { - self.0 = transform(self.0); - self - } -} -impl Map for CurrentDependents { - type Key = PackageId; - type Value = CurrentDependencyInfo; - fn key_str(key: &Self::Key) -> Result, Error> { - Ok(key) - } - fn key_string(key: &Self::Key) -> Result { - Ok(key.clone().into()) - } -} -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -pub struct CurrentDependencies(pub BTreeMap); -impl CurrentDependencies { - pub fn map( - mut self, - transform: impl Fn( - BTreeMap, - ) -> BTreeMap, - ) -> Self { - self.0 = transform(self.0); - self - } -} -impl Map for CurrentDependencies { - type Key = PackageId; - type Value = CurrentDependencyInfo; - fn key_str(key: &Self::Key) -> Result, Error> { - Ok(key) - } - fn key_string(key: &Self::Key) -> Result { - Ok(key.clone().into()) - } -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct StaticDependencyInfo { - pub title: String, - pub icon: DataUrl<'static>, -} - -#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct CurrentDependencyInfo { - #[serde(default)] - pub health_checks: BTreeSet, -} - -#[derive(Debug, Default, Deserialize, Serialize)] -pub struct InterfaceAddressMap(pub BTreeMap); -impl Map for InterfaceAddressMap { - type Key = HostId; - type Value = InterfaceAddresses; - fn key_str(key: &Self::Key) -> Result, Error> { - Ok(key) - } - fn key_string(key: &Self::Key) -> Result { - Ok(key.clone().into()) - } -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct InterfaceAddresses { - pub tor_address: Option, - pub lan_address: Option, -} diff --git a/core/startos/src/db/model/mod.rs b/core/startos/src/db/model/mod.rs new file mode 100644 index 000000000..24497ed4e --- /dev/null +++ b/core/startos/src/db/model/mod.rs @@ -0,0 +1,48 @@ +use std::collections::BTreeMap; + +use patch_db::HasModel; +use serde::{Deserialize, Serialize}; + +use crate::account::AccountInfo; +use crate::auth::Sessions; +use crate::backup::target::cifs::CifsTargets; +use crate::db::model::private::Private; +use crate::db::model::public::Public; +use crate::net::forward::AvailablePorts; +use crate::net::keys::KeyStore; +use crate::notifications::Notifications; +use crate::prelude::*; +use crate::ssh::SshKeys; +use crate::util::serde::Pem; + +pub mod package; +pub mod private; +pub mod public; + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct Database { + pub public: Public, + pub private: Private, +} +impl Database { + pub fn init(account: &AccountInfo) -> Result { + Ok(Self { + public: Public::init(account)?, + private: Private { + key_store: KeyStore::new(account)?, + password: account.password.clone(), + ssh_privkey: Pem(account.ssh_key.clone()), + ssh_pubkeys: SshKeys::new(), + available_ports: AvailablePorts::new(), + sessions: Sessions::new(), + notifications: Notifications::new(), + cifs: CifsTargets::new(), + package_stores: BTreeMap::new(), + }, // TODO + }) + } +} + +pub type DatabaseModel = Model; diff --git a/core/startos/src/db/model/package.rs b/core/startos/src/db/model/package.rs new file mode 100644 index 000000000..cb4e3a255 --- /dev/null +++ b/core/startos/src/db/model/package.rs @@ -0,0 +1,424 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use chrono::{DateTime, Utc}; +use imbl_value::InternedString; +use models::{DataUrl, HealthCheckId, HostId, PackageId}; +use patch_db::json_ptr::JsonPointer; +use patch_db::HasModel; +use reqwest::Url; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::net::host::HostInfo; +use crate::prelude::*; +use crate::progress::FullProgress; +use crate::s9pk::manifest::Manifest; +use crate::status::Status; + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct AllPackageData(pub BTreeMap); +impl Map for AllPackageData { + type Key = PackageId; + type Value = PackageDataEntry; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(key.clone().into()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum ManifestPreference { + Old, + New, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[serde(tag = "state")] +#[model = "Model"] +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), + a => Err(Error::new( + eyre!( + "Package {} is not in installed state", + self.as_manifest(ManifestPreference::Old).id + ), + ErrorKind::InvalidRequest, + )), + } + } + pub fn into_installing_info(self) -> Option { + 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 { + pub fn expect_installed(&self) -> Result<&Model, Error> { + match self.as_match() { + PackageStateMatchModelRef::Installed(a) => Ok(a), + 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> { + 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> { + 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> { + 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 { + 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 { + 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, 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(s) => { + return Err(Error::new( + eyre!("could not determine package state to get manifest"), + ErrorKind::Database, + )) + } + }) + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct InstallingState { + pub installing_info: InstallingInfo, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct UpdatingState { + pub manifest: Manifest, + pub installing_info: InstallingInfo, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct InstalledState { + pub manifest: Manifest, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct InstallingInfo { + pub new_manifest: Manifest, + pub progress: FullProgress, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct PackageDataEntry { + pub state_info: PackageState, + pub status: Status, + pub marketplace_url: Option, + #[serde(default)] + #[serde(with = "crate::util::serde::ed25519_pubkey")] + pub developer_key: ed25519_dalek::VerifyingKey, + pub icon: DataUrl<'static>, + pub last_backup: Option>, + pub dependency_info: BTreeMap, + pub current_dependents: CurrentDependents, + pub current_dependencies: CurrentDependencies, + pub interface_addresses: InterfaceAddressMap, + pub hosts: HostInfo, + pub store_exposed_ui: Vec, + pub store_exposed_dependents: Vec, +} +impl AsRef for PackageDataEntry { + fn as_ref(&self) -> &PackageDataEntry { + self + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[model = "Model"] +pub struct ExposedDependent { + path: String, + title: String, + description: Option, + masked: Option, + copyable: Option, + qr: Option, +} +#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)] +#[model = "Model"] +#[ts(export)] +pub struct ExposedUI { + #[ts(type = "string")] + pub path: JsonPointer, + pub title: String, + pub description: Option, + pub masked: Option, + pub copyable: Option, + pub qr: Option, +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct CurrentDependents(pub BTreeMap); +impl CurrentDependents { + pub fn map( + mut self, + transform: impl Fn( + BTreeMap, + ) -> BTreeMap, + ) -> Self { + self.0 = transform(self.0); + self + } +} +impl Map for CurrentDependents { + type Key = PackageId; + type Value = CurrentDependencyInfo; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(key.clone().into()) + } +} +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct CurrentDependencies(pub BTreeMap); +impl CurrentDependencies { + pub fn map( + mut self, + transform: impl Fn( + BTreeMap, + ) -> BTreeMap, + ) -> Self { + self.0 = transform(self.0); + self + } +} +impl Map for CurrentDependencies { + type Key = PackageId; + type Value = CurrentDependencyInfo; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(key.clone().into()) + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct StaticDependencyInfo { + pub title: String, + pub icon: DataUrl<'static>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +#[serde(tag = "kind")] +pub enum CurrentDependencyInfo { + Exists, + #[serde(rename_all = "kebab-case")] + Running { + #[serde(default)] + health_checks: BTreeSet, + }, +} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct InterfaceAddressMap(pub BTreeMap); +impl Map for InterfaceAddressMap { + type Key = HostId; + type Value = InterfaceAddresses; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(key.clone().into()) + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct InterfaceAddresses { + pub tor_address: Option, + pub lan_address: Option, +} diff --git a/core/startos/src/db/model/private.rs b/core/startos/src/db/model/private.rs new file mode 100644 index 000000000..b9bc51e63 --- /dev/null +++ b/core/startos/src/db/model/private.rs @@ -0,0 +1,30 @@ +use std::collections::BTreeMap; + +use models::PackageId; +use patch_db::{HasModel, Value}; +use serde::{Deserialize, Serialize}; + +use crate::auth::Sessions; +use crate::backup::target::cifs::CifsTargets; +use crate::net::forward::AvailablePorts; +use crate::net::keys::KeyStore; +use crate::notifications::Notifications; +use crate::prelude::*; +use crate::ssh::SshKeys; +use crate::util::serde::Pem; + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct Private { + pub key_store: KeyStore, + pub password: String, // argon2 hash + pub ssh_privkey: Pem, + pub ssh_pubkeys: SshKeys, + pub available_ports: AvailablePorts, + pub sessions: Sessions, + pub notifications: Notifications, + pub cifs: CifsTargets, + #[serde(default)] + pub package_stores: BTreeMap, +} diff --git a/core/startos/src/db/model/public.rs b/core/startos/src/db/model/public.rs new file mode 100644 index 000000000..a9cf8d7e2 --- /dev/null +++ b/core/startos/src/db/model/public.rs @@ -0,0 +1,210 @@ +use std::collections::BTreeMap; +use std::net::{Ipv4Addr, Ipv6Addr}; + +use chrono::{DateTime, Utc}; +use emver::VersionRange; +use imbl_value::InternedString; +use ipnet::{Ipv4Net, Ipv6Net}; +use isocountry::CountryCode; +use itertools::Itertools; +use models::PackageId; +use openssl::hash::MessageDigest; +use patch_db::{HasModel, Value}; +use reqwest::Url; +use serde::{Deserialize, Serialize}; +use torut::onion::OnionAddressV3; + +use crate::account::AccountInfo; +use crate::db::model::package::AllPackageData; +use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr}; +use crate::prelude::*; +use crate::util::cpupower::Governor; +use crate::util::Version; +use crate::version::{Current, VersionT}; +use crate::{ARCH, PLATFORM}; + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +// #[macro_debug] +pub struct Public { + pub server_info: ServerInfo, + pub package_data: AllPackageData, + pub ui: Value, +} +impl Public { + pub fn init(account: &AccountInfo) -> Result { + let lan_address = account.hostname.lan_address().parse().unwrap(); + Ok(Self { + server_info: ServerInfo { + arch: get_arch(), + platform: get_platform(), + id: account.server_id.clone(), + version: Current::new().semver().into(), + hostname: account.hostname.no_dot_host_name(), + last_backup: None, + last_wifi_region: None, + eos_version_compat: Current::new().compat().clone(), + lan_address, + onion_address: account.tor_key.public().get_onion_address(), + tor_address: format!("https://{}", account.tor_key.public().get_onion_address()) + .parse() + .unwrap(), + ip_info: BTreeMap::new(), + status_info: ServerStatus { + backup_progress: None, + updated: false, + update_progress: None, + shutting_down: false, + restarting: false, + }, + wifi: WifiInfo { + ssids: Vec::new(), + connected: None, + selected: None, + }, + unread_notification_count: 0, + connection_addresses: ConnectionAddresses { + tor: Vec::new(), + clearnet: Vec::new(), + }, + password_hash: account.password.clone(), + pubkey: ssh_key::PublicKey::from(&account.ssh_key) + .to_openssh() + .unwrap(), + ca_fingerprint: account + .root_ca_cert + .digest(MessageDigest::sha256()) + .unwrap() + .iter() + .map(|x| format!("{x:X}")) + .join(":"), + ntp_synced: false, + zram: true, + governor: None, + }, + package_data: AllPackageData::default(), + ui: serde_json::from_str(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../web/patchdb-ui-seed.json" + ))) + .with_kind(ErrorKind::Deserialization)?, + }) + } +} + +fn get_arch() -> InternedString { + (*ARCH).into() +} + +fn get_platform() -> InternedString { + (&*PLATFORM).into() +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct ServerInfo { + #[serde(default = "get_arch")] + pub arch: InternedString, + #[serde(default = "get_platform")] + pub platform: InternedString, + pub id: String, + pub hostname: String, + pub version: Version, + pub last_backup: Option>, + /// Used in the wifi to determine the region to set the system to + pub last_wifi_region: Option, + pub eos_version_compat: VersionRange, + pub lan_address: Url, + pub onion_address: OnionAddressV3, + /// for backwards compatibility + pub tor_address: Url, + pub ip_info: BTreeMap, + #[serde(default)] + pub status_info: ServerStatus, + pub wifi: WifiInfo, + pub unread_notification_count: u64, + pub connection_addresses: ConnectionAddresses, + pub password_hash: String, + pub pubkey: String, + pub ca_fingerprint: String, + #[serde(default)] + pub ntp_synced: bool, + #[serde(default)] + pub zram: bool, + pub governor: Option, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct IpInfo { + pub ipv4_range: Option, + pub ipv4: Option, + pub ipv6_range: Option, + pub ipv6: Option, +} +impl IpInfo { + pub async fn for_interface(iface: &str) -> Result { + let (ipv4, ipv4_range) = get_iface_ipv4_addr(iface).await?.unzip(); + let (ipv6, ipv6_range) = get_iface_ipv6_addr(iface).await?.unzip(); + Ok(Self { + ipv4_range, + ipv4, + ipv6_range, + ipv6, + }) + } +} + +#[derive(Debug, Default, Deserialize, Serialize, HasModel)] +#[model = "Model"] +pub struct BackupProgress { + pub complete: bool, +} + +#[derive(Debug, Default, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct ServerStatus { + pub backup_progress: Option>, + pub updated: bool, + pub update_progress: Option, + #[serde(default)] + pub shutting_down: bool, + #[serde(default)] + pub restarting: bool, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct UpdateProgress { + pub size: Option, + pub downloaded: u64, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct WifiInfo { + pub ssids: Vec, + pub selected: Option, + pub connected: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct ServerSpecs { + pub cpu: String, + pub disk: String, + pub memory: String, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct ConnectionAddresses { + pub tor: Vec, + pub clearnet: Vec, +} diff --git a/core/startos/src/db/prelude.rs b/core/startos/src/db/prelude.rs index 0e90e2c88..43dd59002 100644 --- a/core/startos/src/db/prelude.rs +++ b/core/startos/src/db/prelude.rs @@ -124,6 +124,12 @@ impl Model { self.ser(&orig)?; Ok(res) } + pub fn map_mutate(&mut self, f: impl FnOnce(T) -> Result) -> Result { + let mut orig = self.de()?; + let res = f(orig)?; + self.ser(&res)?; + Ok(res) + } } impl Clone for Model { fn clone(&self) -> Self { diff --git a/core/startos/src/dependencies.rs b/core/startos/src/dependencies.rs index 4e96a3db3..a22411244 100644 --- a/core/startos/src/dependencies.rs +++ b/core/startos/src/dependencies.rs @@ -10,7 +10,8 @@ use tracing::instrument; use crate::config::{Config, ConfigSpec, ConfigureContext}; use crate::context::RpcContext; -use crate::db::model::{CurrentDependencies, Database}; +use crate::db::model::package::CurrentDependencies; +use crate::db::model::Database; use crate::prelude::*; use crate::s9pk::manifest::Manifest; use crate::status::DependencyConfigErrors; @@ -195,52 +196,19 @@ pub async fn configure_logic( todo!() } -#[instrument(skip_all)] -pub fn add_dependent_to_current_dependents_lists( - db: &mut Model, - dependent_id: &PackageId, - current_dependencies: &CurrentDependencies, -) -> Result<(), Error> { - for (dependency, dep_info) in ¤t_dependencies.0 { - if let Some(dependency_dependents) = db - .as_public_mut() - .as_package_data_mut() - .as_idx_mut(dependency) - .and_then(|pde| pde.as_installed_mut()) - .map(|i| i.as_current_dependents_mut()) - { - dependency_dependents.insert(dependent_id, dep_info)?; - } - } - Ok(()) -} - #[instrument(skip_all)] pub async fn compute_dependency_config_errs( ctx: &RpcContext, db: &Peeked, - manifest: &Manifest, + id: &PackageId, current_dependencies: &CurrentDependencies, dependency_config: &BTreeMap, ) -> Result { let mut dependency_config_errs = BTreeMap::new(); - for (dependency, _dep_info) in current_dependencies - .0 - .iter() - .filter(|(dep_id, _)| dep_id != &&manifest.id) - { + for (dependency, _dep_info) in current_dependencies.0.iter() { // check if config passes dependency check - if let Some(cfg) = &manifest - .dependencies - .0 - .get(dependency) - .or_not_found(dependency)? - .config - { - let error = todo!(); - { - dependency_config_errs.insert(dependency.clone(), error); - } + if let Some(error) = todo!() { + dependency_config_errs.insert(dependency.clone(), error); } } Ok(DependencyConfigErrors(dependency_config_errs)) diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index 62bbdc58b..d1d5a9943 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -11,7 +11,7 @@ use tracing::instrument; use crate::account::AccountInfo; use crate::context::config::ServerConfig; -use crate::db::model::ServerStatus; +use crate::db::model::public::ServerStatus; use crate::disk::mount::util::unmount; use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH; use crate::prelude::*; diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index 6ea4a7129..f4c415915 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -18,10 +18,7 @@ use tracing::instrument; use crate::context::{CliContext, RpcContext}; use crate::core::rpc_continuations::{RequestGuid, RpcContinuation}; -use crate::db::model::{ - PackageDataEntry, PackageDataEntryInstalled, PackageDataEntryMatchModelRef, - PackageDataEntryRemoving, -}; +use crate::db::model::package::{ManifestPreference, PackageState, PackageStateMatchModelRef}; use crate::prelude::*; use crate::progress::{FullProgress, PhasedProgressBar}; use crate::s9pk::manifest::PackageId; @@ -40,27 +37,27 @@ pub async fn list(ctx: RpcContext) -> Result { Ok(ctx.db.peek().await.as_public().as_package_data().as_entries()? .iter() .filter_map(|(id, pde)| { - let status = match pde.as_match() { - PackageDataEntryMatchModelRef::Installed(_) => { + let status = match pde.as_state_info().as_match() { + PackageStateMatchModelRef::Installed(_) => { "installed" } - PackageDataEntryMatchModelRef::Installing(_) => { + PackageStateMatchModelRef::Installing(_) => { "installing" } - PackageDataEntryMatchModelRef::Updating(_) => { + PackageStateMatchModelRef::Updating(_) => { "updating" } - PackageDataEntryMatchModelRef::Restoring(_) => { + PackageStateMatchModelRef::Restoring(_) => { "restoring" } - PackageDataEntryMatchModelRef::Removing(_) => { + PackageStateMatchModelRef::Removing(_) => { "removing" } - PackageDataEntryMatchModelRef::Error(_) => { + PackageStateMatchModelRef::Error(_) => { "error" } }; - serde_json::to_value(json!({ "status":status, "id": id.clone(), "version": pde.as_manifest().as_version().de().ok()?})) + serde_json::to_value(json!({ "status": status, "id": id.clone(), "version": pde.as_state_info().as_manifest(ManifestPreference::Old).as_version().de().ok()?})) .ok() }) .collect()) @@ -212,7 +209,7 @@ pub async fn sideload(ctx: RpcContext) -> Result { .as_public() .as_package_data() .as_idx(&id) - .and_then(|e| e.as_install_progress()) + .and_then(|e| e.as_state_info().as_installing_info()).map(|i| i.as_progress()) { Ok::<_, ()>(p.de()?) } else { @@ -407,31 +404,18 @@ pub async fn uninstall( ) -> Result { ctx.db .mutate(|db| { - let (manifest, static_files, installed) = match db - .as_public() - .as_package_data() - .as_idx(&id) - .or_not_found(&id)? - .de()? - { - PackageDataEntry::Installed(PackageDataEntryInstalled { - manifest, - static_files, - installed, - }) => (manifest, static_files, installed), - _ => { - return Err(Error::new( - eyre!("Package is not installed."), - crate::ErrorKind::NotFound, - )); - } - }; - let pde = PackageDataEntry::Removing(PackageDataEntryRemoving { - manifest, - static_files, - removing: installed, - }); - db.as_public_mut().as_package_data_mut().insert(&id, &pde) + let entry = db + .as_public_mut() + .as_package_data_mut() + .as_idx_mut(&id) + .or_not_found(&id)?; + entry.as_state_info_mut().map_mutate(|s| match s { + PackageState::Installed(s) => Ok(PackageState::Removing(s)), + _ => Err(Error::new( + eyre!("Package {id} is not installed."), + crate::ErrorKind::NotFound, + )), + }) }) .await?; diff --git a/core/startos/src/net/dhcp.rs b/core/startos/src/net/dhcp.rs index a8dbcabb0..d66448406 100644 --- a/core/startos/src/net/dhcp.rs +++ b/core/startos/src/net/dhcp.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use crate::context::{CliContext, RpcContext}; -use crate::db::model::IpInfo; +use crate::db::model::public::IpInfo; use crate::net::utils::{iface_is_physical, list_interfaces}; use crate::prelude::*; use crate::Error; diff --git a/core/startos/src/net/net_controller.rs b/core/startos/src/net/net_controller.rs index d9d7a5d76..5fa2a30ec 100644 --- a/core/startos/src/net/net_controller.rs +++ b/core/startos/src/net/net_controller.rs @@ -205,8 +205,6 @@ impl NetService { .as_package_data_mut() .as_idx_mut(pkg_id) .or_not_found(pkg_id)? - .as_installed_mut() - .or_not_found(pkg_id)? .as_hosts_mut(); hosts.add_binding(&mut ports, kind, &id, internal_port, options)?; let host = hosts diff --git a/core/startos/src/properties.rs b/core/startos/src/properties.rs index 1a8315cc3..4f59d4303 100644 --- a/core/startos/src/properties.rs +++ b/core/startos/src/properties.rs @@ -4,9 +4,10 @@ use models::PackageId; use rpc_toolkit::command; use serde::{Deserialize, Serialize}; +use crate::context::RpcContext; +use crate::db::model::package::ExposedUI; use crate::prelude::*; use crate::Error; -use crate::{context::RpcContext, db::model::ExposedUI}; pub fn display_properties(response: Value) { println!("{}", response); @@ -59,8 +60,6 @@ pub async fn properties( .as_package_data() .as_idx(&id) .or_not_found(&id)? - .as_installed() - .or_not_found(&id)? .as_store_exposed_ui() .de()? .into_properties(&data)) diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index 83f03fc4b..fb521ca34 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -16,9 +16,8 @@ use crate::action::ActionResult; use crate::config::action::ConfigRes; use crate::context::{CliContext, RpcContext}; use crate::core::rpc_continuations::RequestGuid; -use crate::db::model::{ - InstalledPackageInfo, PackageDataEntry, PackageDataEntryInstalled, PackageDataEntryMatchModel, - StaticFiles, +use crate::db::model::package::{ + InstalledState, PackageDataEntry, PackageState, PackageStateMatchModelRef, UpdatingState, }; use crate::disk::mount::guard::GenericMountGuard; use crate::install::PKG_ARCHIVE_DIR; @@ -28,7 +27,7 @@ use crate::s9pk::S9pk; use crate::service::service_map::InstallProgressHandles; use crate::service::transition::TransitionKind; use crate::status::health_check::HealthCheckResult; -use crate::status::{MainStatus, Status}; +use crate::status::MainStatus; use crate::util::actor::{Actor, BackgroundJobs, SimpleActor}; use crate::volume::data_dir; @@ -100,7 +99,7 @@ impl Service { ) -> Result, Error> { let handle_installed = { let ctx = ctx.clone(); - move |s9pk: S9pk, i: Model| async move { + move |s9pk: S9pk, i: Model| async move { for volume_id in &s9pk.as_manifest().volumes { let tmp_path = data_dir(&ctx.datadir, &s9pk.as_manifest().id.clone(), volume_id); @@ -118,16 +117,18 @@ impl Service { }; let s9pk_dir = ctx.datadir.join(PKG_ARCHIVE_DIR).join("installed"); // TODO: make this based on hash let s9pk_path = s9pk_dir.join(id).with_extension("s9pk"); - match ctx + let Some(entry) = ctx .db .peek() .await .into_public() .into_package_data() .into_idx(id) - .map(|pde| pde.into_match()) - { - Some(PackageDataEntryMatchModel::Installing(_)) => { + else { + return Ok(None); + }; + match entry.as_state_info().as_match() { + PackageStateMatchModelRef::Installing(_) => { if disposition == LoadDisposition::Retry { if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| { tracing::error!("Error opening s9pk for install: {e}"); @@ -150,14 +151,17 @@ impl Service { .await?; Ok(None) } - Some(PackageDataEntryMatchModel::Updating(e)) => { + PackageStateMatchModelRef::Updating(s) => { if disposition == LoadDisposition::Retry - && e.as_install_progress().de()?.phases.iter().any( - |NamedProgress { name, progress }| { + && s.as_installing_info() + .as_progress() + .de()? + .phases + .iter() + .any(|NamedProgress { name, progress }| { name.eq_ignore_ascii_case("download") && progress == &Progress::Complete(true) - }, - ) + }) { if let Ok(s9pk) = S9pk::open(&s9pk_path, Some(id)).await.map_err(|e| { tracing::error!("Error opening s9pk for update: {e}"); @@ -166,7 +170,7 @@ impl Service { if let Ok(service) = Self::install( ctx.clone(), s9pk, - Some(e.as_installed().as_manifest().as_version().de()?), + Some(s.as_manifest().as_version().de()?), None, ) .await @@ -181,24 +185,28 @@ impl Service { let s9pk = S9pk::open(s9pk_path, Some(id)).await?; ctx.db .mutate({ - let manifest = s9pk.as_manifest().clone(); |db| { db.as_public_mut() .as_package_data_mut() - .as_idx_mut(&manifest.id) - .or_not_found(&manifest.id)? - .ser(&PackageDataEntry::Installed(PackageDataEntryInstalled { - static_files: e.as_static_files().de()?, - manifest, - installed: e.as_installed().de()?, - })) + .as_idx_mut(&id) + .or_not_found(&id)? + .as_state_info_mut() + .map_mutate(|s| { + if let PackageState::Updating(UpdatingState { + manifest, .. + }) = s + { + Ok(PackageState::Installed(InstalledState { manifest })) + } else { + Err(Error::new(eyre!("Race condition detected - package state changed during load"), ErrorKind::Database)) + } + }) } }) .await?; - handle_installed(s9pk, e.as_installed().clone()).await + handle_installed(s9pk, entry).await } - Some(PackageDataEntryMatchModel::Removing(_)) - | Some(PackageDataEntryMatchModel::Restoring(_)) => { + PackageStateMatchModelRef::Removing(_) | PackageStateMatchModelRef::Restoring(_) => { if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| { tracing::error!("Error opening s9pk for removal: {e}"); tracing::debug!("{e:?}") @@ -230,18 +238,13 @@ impl Service { Ok(None) } - Some(PackageDataEntryMatchModel::Installed(i)) => { - handle_installed( - S9pk::open(s9pk_path, Some(id)).await?, - i.as_installed().clone(), - ) - .await + PackageStateMatchModelRef::Installed(_) => { + handle_installed(S9pk::open(s9pk_path, Some(id)).await?, entry).await } - Some(PackageDataEntryMatchModel::Error(e)) => Err(Error::new( + PackageStateMatchModelRef::Error(e) => Err(Error::new( eyre!("Failed to parse PackageDataEntry, found {e:?}"), ErrorKind::Deserialization, )), - None => Ok(None), } } @@ -255,7 +258,6 @@ impl Service { let manifest = s9pk.as_manifest().clone(); let developer_key = s9pk.as_archive().signer(); let icon = s9pk.icon_data_url().await?; - let static_files = StaticFiles::local(&manifest.id, &manifest.version, icon); let service = Self::new(ctx.clone(), s9pk, StartStop::Stop).await?; service .seed @@ -270,32 +272,19 @@ impl Service { } ctx.db .mutate(|d| { - d.as_public_mut() + let entry = d + .as_public_mut() .as_package_data_mut() .as_idx_mut(&manifest.id) - .or_not_found(&manifest.id)? - .ser(&PackageDataEntry::Installed(PackageDataEntryInstalled { - installed: InstalledPackageInfo { - current_dependencies: Default::default(), // TODO - current_dependents: Default::default(), // TODO - dependency_info: Default::default(), // TODO - developer_key, - status: Status { - configured: false, // TODO - main: MainStatus::Stopped, // TODO - dependency_config_errors: Default::default(), // TODO - }, - interface_addresses: Default::default(), // TODO - marketplace_url: None, // TODO - manifest: manifest.clone(), - last_backup: None, // TODO - hosts: Default::default(), // TODO - store_exposed_dependents: Default::default(), // TODO - store_exposed_ui: Default::default(), // TODO - }, - manifest, - static_files, - })) + .or_not_found(&manifest.id)?; + entry + .as_state_info_mut() + .ser(&PackageState::Installed(InstalledState { manifest }))?; + entry.as_developer_key_mut().ser(&developer_key)?; + entry.as_icon_mut().ser(&icon)?; + // TODO: marketplace url + // TODO: dependency info + Ok(()) }) .await?; Ok(service) @@ -466,11 +455,7 @@ impl Actor for ServiceActor { seed.ctx .db .mutate(|d| { - if let Some(i) = d - .as_public_mut() - .as_package_data_mut() - .as_idx_mut(&id) - .and_then(|p| p.as_installed_mut()) + if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id) { i.as_status_mut().as_main_mut().ser(&main_status)?; } diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index 4ed160eb7..fa5df79de 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeSet; use std::ffi::OsString; use std::os::unix::process::CommandExt; use std::path::{Path, PathBuf}; @@ -7,13 +8,14 @@ use std::sync::{Arc, Weak}; use clap::builder::ValueParserFactory; use clap::Parser; use imbl_value::{json, InternedString}; -use models::{ActionId, HealthCheckId, ImageId, PackageId}; +use models::{ActionId, HealthCheckId, ImageId, InvalidId, PackageId}; use patch_db::json_ptr::JsonPointer; use rpc_toolkit::{from_fn, from_fn_async, AnyContext, Context, Empty, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; use tokio::process::Command; use ts_rs::TS; -use crate::db::model::ExposedUI; +use crate::db::model::package::{CurrentDependencies, CurrentDependencyInfo, ExposedUI}; use crate::disk::mount::filesystem::idmapped::IdMapped; use crate::disk::mount::filesystem::loop_dev::LoopDev; use crate::disk::mount::filesystem::overlayfs::OverlayGuard; @@ -131,8 +133,13 @@ pub fn service_effect_handler() -> ParentHandler { .subcommand("clearBindings", from_fn_async(clear_bindings).no_cli()) .subcommand("bind", from_fn_async(bind).no_cli()) .subcommand("getHostInfo", from_fn_async(get_host_info).no_cli()) + .subcommand( + "setDependencies", + from_fn_async(set_dependencies) + .no_display() + .with_remote_cli::(), + ) // TODO @DrBonez when we get the new api for 4.0 - // .subcommand("setDependencies",from_fn_async(set_dependencies).no_cli()) // .subcommand("embassyGetInterface",from_fn_async(embassy_get_interface).no_cli()) // .subcommand("mount",from_fn_async(mount).no_cli()) // .subcommand("removeAction",from_fn_async(remove_action).no_cli()) @@ -459,8 +466,6 @@ async fn expose_for_dependents( .as_package_data_mut() .as_idx_mut(&package_id) .or_not_found(&package_id)? - .as_installed_mut() - .or_not_found(&package_id)? .as_store_exposed_dependents_mut() .ser(&paths) }) @@ -488,8 +493,6 @@ async fn expose_ui( .as_package_data_mut() .as_idx_mut(&package_id) .or_not_found(&package_id)? - .as_installed_mut() - .or_not_found(&package_id)? .as_store_exposed_ui_mut() .ser(&paths) }) @@ -566,8 +569,6 @@ async fn get_configured(context: EffectContext, _: Empty) -> Result Result .as_package_data() .as_idx(&package_id) .or_not_found(&package_id)? - .as_installed() - .or_not_found(&package_id)? .as_status() .as_main() .de()?; @@ -600,8 +599,6 @@ async fn running(context: EffectContext, params: ParamsPackageId) -> Result Result .as_package_data_mut() .as_idx_mut(package_id) .or_not_found(package_id)? - .as_installed_mut() - .or_not_found(package_id)? .as_status_mut() .as_configured_mut() .ser(¶ms.configured) @@ -733,8 +728,6 @@ async fn set_health( .as_package_data() .as_idx(package_id) .or_not_found(package_id)? - .as_installed() - .or_not_found(package_id)? .as_status() .as_main() .de()?; @@ -764,8 +757,6 @@ async fn set_health( .as_package_data_mut() .as_idx_mut(package_id) .or_not_found(package_id)? - .as_installed_mut() - .or_not_found(package_id)? .as_status_mut() .as_main_mut() .ser(&main) @@ -778,8 +769,6 @@ async fn set_health( #[command(rename_all = "camelCase")] #[ts(export)] pub struct DestroyOverlayedImageParams { - #[ts(type = "string ")] - image_id: ImageId, #[ts(type = "string")] guid: InternedString, } @@ -787,7 +776,7 @@ pub struct DestroyOverlayedImageParams { #[instrument(skip_all)] pub async fn destroy_overlayed_image( ctx: EffectContext, - DestroyOverlayedImageParams { image_id, guid }: DestroyOverlayedImageParams, + DestroyOverlayedImageParams { guid }: DestroyOverlayedImageParams, ) -> Result<(), Error> { let ctx = ctx.deref()?; if ctx @@ -873,3 +862,125 @@ pub async fn create_overlayed_image( )) } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +enum DependencyKind { + Exists, + Running, +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +struct DependencyRequirement { + id: PackageId, + kind: DependencyKind, + #[serde(default)] + health_checks: BTreeSet, +} +// filebrowser:exists,bitcoind:running:foo+bar+baz +impl FromStr for DependencyRequirement { + type Err = Error; + fn from_str(s: &str) -> Result { + match s.split_once(":") { + Some((id, "e")) | Some((id, "exists")) => Ok(Self { + id: id.parse()?, + kind: DependencyKind::Exists, + health_checks: BTreeSet::new(), + }), + Some((id, rest)) => { + let health_checks = match rest.split_once(":") { + Some(("r", rest)) | Some(("running", rest)) => rest + .split("+") + .map(|id| id.parse().map_err(Error::from)) + .collect(), + Some((kind, _)) => Err(Error::new( + eyre!("unknown dependency kind {kind}"), + ErrorKind::InvalidRequest, + )), + None => match rest { + "r" | "running" => Ok(BTreeSet::new()), + kind => Err(Error::new( + eyre!("unknown dependency kind {kind}"), + ErrorKind::InvalidRequest, + )), + }, + }?; + Ok(Self { + id: id.parse()?, + kind: DependencyKind::Running, + health_checks, + }) + } + None => Ok(Self { + id: s.parse()?, + kind: DependencyKind::Running, + health_checks: BTreeSet::new(), + }), + } + } +} +impl ValueParserFactory for DependencyRequirement { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "camelCase")] +#[ts(export)] +pub struct SetDependenciesParams { + dependencies: Vec, +} + +pub async fn set_dependencies( + ctx: EffectContext, + SetDependenciesParams { dependencies }: SetDependenciesParams, +) -> Result<(), Error> { + let ctx = ctx.deref()?; + let id = &ctx.id; + ctx.ctx + .db + .mutate(|db| { + let dependencies = CurrentDependencies( + dependencies + .into_iter() + .map( + |DependencyRequirement { + id, + kind, + health_checks, + }| { + ( + id, + match kind { + DependencyKind::Exists => CurrentDependencyInfo::Exists, + DependencyKind::Running => { + CurrentDependencyInfo::Running { health_checks } + } + }, + ) + }, + ) + .collect(), + ); + for (dep, entry) in db.as_public_mut().as_package_data_mut().as_entries_mut()? { + if let Some(info) = dependencies.0.get(&dep) { + entry.as_current_dependents_mut().insert(id, info)?; + } else { + entry.as_current_dependents_mut().remove(id)?; + } + } + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(id) + .or_not_found(id)? + .as_current_dependencies_mut() + .ser(&dependencies) + }) + .await +} diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index f555be531..23e1bb540 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -11,9 +11,8 @@ use tokio::sync::{Mutex, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock}; use tracing::instrument; use crate::context::RpcContext; -use crate::db::model::{ - PackageDataEntry, PackageDataEntryInstalled, PackageDataEntryInstalling, - PackageDataEntryRestoring, PackageDataEntryUpdating, StaticFiles, +use crate::db::model::package::{ + InstallingInfo, InstallingState, PackageDataEntry, PackageState, UpdatingState, }; use crate::disk::mount::guard::GenericMountGuard; use crate::install::PKG_ARCHIVE_DIR; @@ -27,6 +26,7 @@ use crate::s9pk::manifest::PackageId; use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::S9pk; use crate::service::{LoadDisposition, Service}; +use crate::status::{MainStatus, Status}; pub type DownloadInstallFuture = BoxFuture<'static, Result>; pub type InstallFuture = BoxFuture<'static, Result<(), Error>>; @@ -95,9 +95,10 @@ impl ServiceMap { mut s9pk: S9pk, recovery_source: Option, ) -> Result { - let manifest = Arc::new(s9pk.as_manifest().clone()); + let manifest = s9pk.as_manifest().clone(); let id = manifest.id.clone(); let icon = s9pk.icon_data_url().await?; + let developer_key = s9pk.as_archive().signer(); let mut service = self.get_mut(&id).await; let op_name = if recovery_source.is_none() { @@ -135,49 +136,51 @@ impl ServiceMap { let id = id.clone(); let install_progress = progress.snapshot(); move |db| { - let pde = match db - .as_public() - .as_package_data() - .as_idx(&id) - .map(|x| x.de()) - .transpose()? - { - Some(PackageDataEntry::Installed(PackageDataEntryInstalled { - installed, - static_files, - .. - })) => PackageDataEntry::Updating(PackageDataEntryUpdating { - install_progress, - installed, - manifest: (*manifest).clone(), - static_files, - }), - None if restoring => { - PackageDataEntry::Restoring(PackageDataEntryRestoring { - install_progress, - static_files: StaticFiles::local( - &manifest.id, - &manifest.version, - icon, - ), - manifest: (*manifest).clone(), - }) - } - None => PackageDataEntry::Installing(PackageDataEntryInstalling { - install_progress, - static_files: StaticFiles::local(&manifest.id, &manifest.version, icon), - manifest: (*manifest).clone(), - }), - _ => { - return Err(Error::new( - eyre!("Cannot install over a package in a transient state"), - crate::ErrorKind::InvalidRequest, - )) - } + if let Some(pde) = db.as_public_mut().as_package_data_mut().as_idx_mut(&id) { + let prev = pde.as_state_info().expect_installed()?.de()?; + pde.as_state_info_mut() + .ser(&PackageState::Updating(UpdatingState { + manifest: prev.manifest, + installing_info: InstallingInfo { + new_manifest: manifest, + progress: install_progress, + }, + }))?; + } else { + let installing = InstallingState { + installing_info: InstallingInfo { + new_manifest: manifest, + progress: install_progress, + }, + }; + db.as_public_mut().as_package_data_mut().insert( + &id, + &PackageDataEntry { + state_info: if restoring { + PackageState::Restoring(installing) + } else { + PackageState::Installing(installing) + }, + status: Status { + configured: false, + main: MainStatus::Stopped, + dependency_config_errors: Default::default(), + }, + marketplace_url: None, + developer_key, + icon, + last_backup: None, + dependency_info: Default::default(), + current_dependents: Default::default(), // TODO: initialize + current_dependencies: Default::default(), + interface_addresses: Default::default(), + hosts: Default::default(), + store_exposed_ui: Default::default(), + store_exposed_dependents: Default::default(), + }, + )?; }; - db.as_public_mut() - .as_package_data_mut() - .insert(&manifest.id, &pde) + Ok(()) } })) .await?; @@ -200,7 +203,8 @@ impl ServiceMap { v.as_public_mut() .as_package_data_mut() .as_idx_mut(&deref_id) - .and_then(|e| e.as_install_progress_mut()) + .and_then(|e| e.as_state_info_mut().as_installing_info_mut()) + .map(|i| i.as_progress_mut()) }, Some(Duration::from_millis(100)), ))); diff --git a/core/startos/src/update/mod.rs b/core/startos/src/update/mod.rs index 9f2b58135..c327bbb61 100644 --- a/core/startos/src/update/mod.rs +++ b/core/startos/src/update/mod.rs @@ -14,7 +14,7 @@ use tokio_stream::StreamExt; use tracing::instrument; use crate::context::RpcContext; -use crate::db::model::UpdateProgress; +use crate::db::model::public::UpdateProgress; use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::guard::MountGuard; diff --git a/patch-db b/patch-db index 3dc11afd4..e4a3f7b57 160000 --- a/patch-db +++ b/patch-db @@ -1 +1 @@ -Subproject commit 3dc11afd46d93094ac52ae1fef311a91c4561e8c +Subproject commit e4a3f7b577df56c611b21d5ad03eb459e80fb919 diff --git a/sdk/lib/config/configDependencies.ts b/sdk/lib/config/configDependencies.ts index 6b31abc81..be0475b0f 100644 --- a/sdk/lib/config/configDependencies.ts +++ b/sdk/lib/config/configDependencies.ts @@ -3,7 +3,7 @@ import { Dependency } from "../types" export type ConfigDependencies = { exists(id: keyof T["dependencies"]): Dependency - running(id: keyof T["dependencies"]): Dependency + running(id: keyof T["dependencies"], healthChecks: string[]): Dependency } export const configDependenciesSet = < @@ -16,10 +16,11 @@ export const configDependenciesSet = < } as Dependency }, - running(id: keyof T["dependencies"]) { + running(id: keyof T["dependencies"], healthChecks: string[]) { return { id, kind: "running", + healthChecks, } as Dependency }, }) diff --git a/sdk/lib/test/startosTypeValidation.test.ts b/sdk/lib/test/startosTypeValidation.test.ts index e3824a970..ae6931bdc 100644 --- a/sdk/lib/test/startosTypeValidation.test.ts +++ b/sdk/lib/test/startosTypeValidation.test.ts @@ -13,6 +13,7 @@ import { ExposeUiParams } from "../../../core/startos/bindings/ExposeUiParams" import { GetSslCertificateParams } from "../../../core/startos/bindings/GetSslCertificateParams" import { GetSslKeyParams } from "../../../core/startos/bindings/GetSslKeyParams" import { GetServiceInterfaceParams } from "../../../core/startos/bindings/GetServiceInterfaceParams" +import { SetDependenciesParams } from "../../../core/startos/bindings/SetDependenciesParams" function typeEquality(_a: ExpectedType) {} describe("startosTypeValidation ", () => { @@ -46,6 +47,7 @@ describe("startosTypeValidation ", () => { | "clearBindings" | "bind" | "getHostInfo" + | "setDependencies" )]: Effects[K] extends Function ? Parameters[0] : never }>({ executeAction: {} as ExecuteAction, @@ -67,6 +69,7 @@ describe("startosTypeValidation ", () => { getSslCertificate: {} as GetSslCertificateParams, getSslKey: {} as GetSslKeyParams, getServiceInterface: {} as GetServiceInterfaceParams, + setDependencies: {} as SetDependenciesParams, }) typeEquality[0]>( testInput as ExecuteAction, diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index 0ab0b73db..3935555e9 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -286,10 +286,7 @@ export type Effects = { createOverlayedImage(options: { imageId: string }): Promise<[string, string]> /** A low level api used by destroyOverlay + makeOverlay:destroy */ - destroyOverlayedImage(options: { - imageId: string - guid: string - }): Promise + destroyOverlayedImage(options: { guid: string }): Promise /** Removes all network bindings */ clearBindings(): Promise @@ -467,7 +464,9 @@ export type Effects = { }): Promise /** Set the dependencies of what the service needs, usually ran during the set config as a best practice */ - setDependencies(dependencies: Dependencies): Promise + setDependencies(options: { + dependencies: Dependencies + }): Promise /** Exists could be useful during the runtime to know if some service exists, option dep */ exists(options: { packageId: PackageId }): Promise /** Exists could be useful during the runtime to know if some service is running, option dep */ @@ -585,7 +584,7 @@ export type KnownError = export type Dependency = { id: PackageId kind: DependencyKind -} +} & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] }) export type Dependencies = Array export type DeepPartial = T extends {}