From a3e7e7c6c99dc7897132f6f1100885067a00b621 Mon Sep 17 00:00:00 2001 From: Mariusz Kogen Date: Wed, 7 Aug 2024 08:51:15 +0200 Subject: [PATCH 01/10] version bump --- core/Cargo.lock | 2 +- core/startos/Cargo.toml | 2 +- core/startos/src/version/mod.rs | 2 +- web/package.json | 2 +- web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/Cargo.lock b/core/Cargo.lock index 2108ac851..f31bc21b1 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -4961,7 +4961,7 @@ dependencies = [ [[package]] name = "start-os" -version = "0.3.6-alpha.3" +version = "0.3.6-alpha.4" dependencies = [ "aes", "async-compression", diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index 1e1cd4737..3bab5c6f3 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -14,7 +14,7 @@ keywords = [ name = "start-os" readme = "README.md" repository = "https://github.com/Start9Labs/start-os" -version = "0.3.6-alpha.3" +version = "0.3.6-alpha.4" license = "MIT" [lib] diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index be5c8d9a5..8e114334d 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -22,7 +22,7 @@ mod v0_3_6_alpha_5; mod v0_3_6_alpha_6; mod v0_3_6_alpha_7; -pub type Current = v0_3_6_alpha_3::Version; // VERSION_BUMP +pub type Current = v0_3_6_alpha_4::Version; // VERSION_BUMP #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(untagged)] diff --git a/web/package.json b/web/package.json index 1697f1343..c46090544 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "startos-ui", - "version": "0.3.6-alpha.3", + "version": "0.3.6-alpha.4", "author": "Start9 Labs, Inc", "homepage": "https://start9.com/", "license": "MIT", diff --git a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html b/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html index 79fe3f8da..6c259a6d0 100644 --- a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html +++ b/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html @@ -12,7 +12,7 @@

This Release

-

0.3.6-alpha.3

+

0.3.6-alpha.4

This is an ALPHA release! DO NOT use for production data!
Expect that any data you create or store on this version of the OS can be LOST FOREVER!
From 9f1a9a7d9cd3277072fa5f4da17ac9568de4a38e Mon Sep 17 00:00:00 2001 From: Mariusz Kogen Date: Wed, 7 Aug 2024 10:40:25 +0200 Subject: [PATCH 02/10] fix CI build * update nodejs * set up python --- .github/workflows/startos-iso.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/startos-iso.yaml b/.github/workflows/startos-iso.yaml index 010c79594..bc0fb626e 100644 --- a/.github/workflows/startos-iso.yaml +++ b/.github/workflows/startos-iso.yaml @@ -45,7 +45,7 @@ on: - next/* env: - NODEJS_VERSION: "18.15.0" + NODEJS_VERSION: "20.16.0" ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}' jobs: @@ -75,6 +75,11 @@ jobs: with: submodules: recursive + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - uses: actions/setup-node@v4 with: node-version: ${{ env.NODEJS_VERSION }} @@ -148,6 +153,11 @@ jobs: with: submodules: recursive + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Install dependencies run: | sudo apt-get update From 419d4986f65d0acb6fd8ba5b1016aab78c0d226c Mon Sep 17 00:00:00 2001 From: J H Date: Wed, 7 Aug 2024 06:19:04 -0600 Subject: [PATCH 03/10] fix: Inject for actions and health --- .../DockerProcedureContainer.ts | 12 ++++--- .../Systems/SystemForEmbassy/MainLoop.ts | 34 +++++++++++++------ .../Systems/SystemForEmbassy/index.ts | 18 ++++++---- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts index 012a70eee..40a152bab 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts @@ -88,15 +88,19 @@ export class DockerProcedureContainer { return new DockerProcedureContainer(overlay) } - async exec(commands: string[]) { + async exec(commands: string[], { destroy = true } = {}) { try { return await this.overlay.exec(commands) } finally { - await this.overlay.destroy() + if (destroy) await this.overlay.destroy() } } - async execFail(commands: string[], timeoutMs: number | null) { + async execFail( + commands: string[], + timeoutMs: number | null, + { destroy = true } = {}, + ) { try { const res = await this.overlay.exec(commands, {}, timeoutMs) if (res.exitCode !== 0) { @@ -110,7 +114,7 @@ export class DockerProcedureContainer { } return res } finally { - await this.overlay.destroy() + if (destroy) await this.overlay.destroy() } } diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts index f8f0a2d6e..e29170543 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts @@ -14,6 +14,10 @@ const EMBASSY_PROPERTIES_LOOP = 30 * 1000 * Also, this has an ability to clean itself up too if need be. */ export class MainLoop { + private _mainDockerContainer?: DockerProcedureContainer + get mainDockerContainer() { + return this._mainDockerContainer + } private healthLoops?: { name: string interval: NodeJS.Timeout @@ -54,6 +58,7 @@ export class MainLoop { this.system.manifest.main, this.system.manifest.volumes, ) + this._mainDockerContainer = dockerProcedureContainer if (jsMain) { throw new Error("Unreachable") } @@ -126,6 +131,7 @@ export class MainLoop { await main?.daemon.stop().catch((e) => console.error(e)) this.effects.setMainStatus({ status: "stopped" }) if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval)) + delete this._mainDockerContainer } private constructHealthLoops() { @@ -138,17 +144,25 @@ export class MainLoop { const actionProcedure = value const timeChanged = Date.now() - start if (actionProcedure.type === "docker") { - const container = await DockerProcedureContainer.of( - effects, - manifest.id, - actionProcedure, - manifest.volumes, + // prettier-ignore + const container = + actionProcedure.inject && this._mainDockerContainer ? + this._mainDockerContainer : + await DockerProcedureContainer.of( + effects, + manifest.id, + actionProcedure, + manifest.volumes, + ) + const shouldDestroy = container !== this._mainDockerContainer + const executed = await container.exec( + [ + actionProcedure.entrypoint, + ...actionProcedure.args, + JSON.stringify(timeChanged), + ], + { destroy: shouldDestroy }, ) - const executed = await container.exec([ - actionProcedure.entrypoint, - ...actionProcedure.args, - JSON.stringify(timeChanged), - ]) if (executed.exitCode === 0) { await effects.setHealth({ id: healthId, diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index ffdb02988..f639d00cd 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -799,12 +799,17 @@ export class SystemForEmbassy implements System { const actionProcedure = this.manifest.actions?.[actionId]?.implementation if (!actionProcedure) return { message: "Action not found", value: null } if (actionProcedure.type === "docker") { - const container = await DockerProcedureContainer.of( - effects, - this.manifest.id, - actionProcedure, - this.manifest.volumes, - ) + const container = + actionProcedure.inject && this.currentRunning?.mainDockerContainer + ? this.currentRunning?.mainDockerContainer + : await DockerProcedureContainer.of( + effects, + this.manifest.id, + actionProcedure, + this.manifest.volumes, + ) + const shouldDestroy = + container !== this.currentRunning?.mainDockerContainer return JSON.parse( ( await container.execFail( @@ -814,6 +819,7 @@ export class SystemForEmbassy implements System { JSON.stringify(formData), ], timeoutMs, + { destroy: shouldDestroy }, ) ).stdout.toString(), ) From bd7adafee06f53b9c9391059487c1149801d7396 Mon Sep 17 00:00:00 2001 From: J H Date: Wed, 7 Aug 2024 16:38:35 -0600 Subject: [PATCH 04/10] Fix: sdk setupManifest pass through docs --- sdk/lib/manifest/setupManifest.ts | 10 ++++++++-- web/patchdb-ui-seed.json | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/sdk/lib/manifest/setupManifest.ts b/sdk/lib/manifest/setupManifest.ts index e3b746874..a0d0e18b3 100644 --- a/sdk/lib/manifest/setupManifest.ts +++ b/sdk/lib/manifest/setupManifest.ts @@ -3,6 +3,11 @@ import { ImageConfig, ImageId, VolumeId } from "../osBindings" import { SDKManifest, SDKImageConfig } from "./ManifestTypes" import { SDKVersion } from "../StartSdk" +/** + * This is an example of a function that takes a manifest and returns a new manifest with additional properties + * @param manifest Manifests are the description of the package + * @returns The manifest with additional properties + */ export function setupManifest< Id extends string, Version extends string, @@ -10,15 +15,16 @@ export function setupManifest< VolumesTypes extends VolumeId, AssetTypes extends VolumeId, ImagesTypes extends ImageId, - Manifest extends SDKManifest & { + Manifest extends { dependencies: Dependencies id: Id assets: AssetTypes[] images: Record volumes: VolumesTypes[] + version: Version }, Satisfies extends string[] = [], ->(manifest: Manifest & { version: Version }): Manifest & T.Manifest { +>(manifest: SDKManifest & Manifest): Manifest & T.Manifest { const images = Object.entries(manifest.images).reduce( (images, [k, v]) => { v.arch = v.arch || ["aarch64", "x86_64"] diff --git a/web/patchdb-ui-seed.json b/web/patchdb-ui-seed.json index 0e15cd2cf..807483c3a 100644 --- a/web/patchdb-ui-seed.json +++ b/web/patchdb-ui-seed.json @@ -21,5 +21,5 @@ "ackInstructions": {}, "theme": "Dark", "widgets": [], - "ack-welcome": "0.3.6-alpha.3" + "ack-welcome": "0.3.6-alpha.4" } From 058bfe0737dd0055e18a27d63aba36fc16611219 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 8 Aug 2024 11:10:02 -0600 Subject: [PATCH 05/10] sdk updates --- .../Systems/SystemForEmbassy/index.ts | 1 - core/startos/src/net/service_interface.rs | 1 - .../startos/src/service/effects/dependency.rs | 4 +- core/startos/src/service/effects/health.rs | 4 +- .../src/service/effects/net/interface.rs | 3 - core/startos/src/service/mod.rs | 4 +- core/startos/src/status/health_check.rs | 36 ++++---- core/startos/src/status/mod.rs | 18 ++-- sdk/lib/StartSdk.ts | 4 +- sdk/lib/health/HealthCheck.ts | 92 ++++++++----------- sdk/lib/health/checkFns/CheckResult.ts | 6 -- sdk/lib/health/checkFns/HealthCheckResult.ts | 3 + sdk/lib/health/checkFns/checkPortListening.ts | 12 +-- sdk/lib/health/checkFns/checkWebUrl.ts | 8 +- sdk/lib/health/checkFns/index.ts | 2 +- sdk/lib/health/checkFns/runHealthScript.ts | 11 +-- sdk/lib/interfaces/Origin.ts | 2 - sdk/lib/interfaces/ServiceInterfaceBuilder.ts | 1 - sdk/lib/mainFn/Daemons.ts | 4 +- sdk/lib/mainFn/HealthDaemon.ts | 51 ++++------ sdk/lib/osBindings/CheckDependenciesResult.ts | 4 +- .../ExportServiceInterfaceParams.ts | 1 - sdk/lib/osBindings/MainStatus.ts | 6 +- ...eckResult.ts => NamedHealthCheckResult.ts} | 2 +- sdk/lib/osBindings/ServiceInterface.ts | 1 - sdk/lib/osBindings/index.ts | 2 +- sdk/lib/test/host.test.ts | 1 - sdk/lib/trigger/TriggerInput.ts | 1 - sdk/lib/trigger/changeOnFirstSuccess.ts | 8 +- sdk/lib/trigger/defaultTrigger.ts | 6 +- sdk/lib/trigger/lastStatus.ts | 33 +++++++ sdk/lib/trigger/successFailure.ts | 31 +------ sdk/lib/types.ts | 4 +- sdk/lib/util/Overlay.ts | 17 ++++ sdk/lib/util/getServiceInterface.ts | 2 - .../app-show-health-checks.component.ts | 6 +- .../app-show/pipes/health-color.pipe.ts | 2 +- .../app-show/pipes/to-health-checks.pipe.ts | 2 +- .../ui/src/app/services/api/api.fixures.ts | 7 -- .../ui/src/app/services/api/api.types.ts | 2 +- .../ui/src/app/services/api/mock-patch.ts | 6 -- 41 files changed, 190 insertions(+), 221 deletions(-) delete mode 100644 sdk/lib/health/checkFns/CheckResult.ts create mode 100644 sdk/lib/health/checkFns/HealthCheckResult.ts rename sdk/lib/osBindings/{HealthCheckResult.ts => NamedHealthCheckResult.ts} (85%) create mode 100644 sdk/lib/trigger/lastStatus.ts diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index f639d00cd..8b74a7217 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -445,7 +445,6 @@ export class SystemForEmbassy implements System { id: `${id}-${internal}`, description: interfaceValue.description, hasPrimary: false, - disabled: false, type: interfaceValue.ui && (origin.scheme === "http" || origin.sslScheme === "https") diff --git a/core/startos/src/net/service_interface.rs b/core/startos/src/net/service_interface.rs index dbe228ef2..b1824140b 100644 --- a/core/startos/src/net/service_interface.rs +++ b/core/startos/src/net/service_interface.rs @@ -67,7 +67,6 @@ pub struct ServiceInterface { pub name: String, pub description: String, pub has_primary: bool, - pub disabled: bool, pub masked: bool, pub address_info: AddressInfo, #[serde(rename = "type")] diff --git a/core/startos/src/service/effects/dependency.rs b/core/startos/src/service/effects/dependency.rs index ad5ec2e9b..fbd02800c 100644 --- a/core/startos/src/service/effects/dependency.rs +++ b/core/startos/src/service/effects/dependency.rs @@ -17,7 +17,7 @@ use crate::disk::mount::filesystem::idmapped::IdMapped; use crate::disk::mount::filesystem::{FileSystem, MountType}; use crate::rpc_continuations::Guid; use crate::service::effects::prelude::*; -use crate::status::health_check::HealthCheckResult; +use crate::status::health_check::NamedHealthCheckResult; use crate::util::clap::FromStrParser; use crate::util::Invoke; use crate::volume::data_dir; @@ -319,7 +319,7 @@ pub struct CheckDependenciesResult { is_installed: bool, is_running: bool, config_satisfied: bool, - health_checks: BTreeMap, + health_checks: BTreeMap, #[ts(type = "string | null")] version: Option, } diff --git a/core/startos/src/service/effects/health.rs b/core/startos/src/service/effects/health.rs index c8ef8fc4e..aad06a004 100644 --- a/core/startos/src/service/effects/health.rs +++ b/core/startos/src/service/effects/health.rs @@ -1,7 +1,7 @@ use models::HealthCheckId; use crate::service::effects::prelude::*; -use crate::status::health_check::HealthCheckResult; +use crate::status::health_check::NamedHealthCheckResult; use crate::status::MainStatus; #[derive(Debug, Clone, Serialize, Deserialize, TS)] @@ -10,7 +10,7 @@ use crate::status::MainStatus; pub struct SetHealth { id: HealthCheckId, #[serde(flatten)] - result: HealthCheckResult, + result: NamedHealthCheckResult, } pub async fn set_health( context: EffectContext, diff --git a/core/startos/src/service/effects/net/interface.rs b/core/startos/src/service/effects/net/interface.rs index e636e9b57..6cd4cd4c9 100644 --- a/core/startos/src/service/effects/net/interface.rs +++ b/core/startos/src/service/effects/net/interface.rs @@ -16,7 +16,6 @@ pub struct ExportServiceInterfaceParams { name: String, description: String, has_primary: bool, - disabled: bool, masked: bool, address_info: AddressInfo, r#type: ServiceInterfaceType, @@ -28,7 +27,6 @@ pub async fn export_service_interface( name, description, has_primary, - disabled, masked, address_info, r#type, @@ -42,7 +40,6 @@ pub async fn export_service_interface( name, description, has_primary, - disabled, masked, address_info, interface_type: r#type, diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index 2beb5c9fa..6a99841d6 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -27,7 +27,7 @@ use crate::progress::{NamedProgress, Progress}; use crate::rpc_continuations::Guid; use crate::s9pk::S9pk; use crate::service::service_map::InstallProgressHandles; -use crate::status::health_check::HealthCheckResult; +use crate::status::health_check::NamedHealthCheckResult; use crate::util::actor::concurrent::ConcurrentActor; use crate::util::io::create_file; use crate::util::serde::{NoOutput, Pem}; @@ -493,7 +493,7 @@ impl Service { #[derive(Debug, Clone)] pub struct RunningStatus { - health: OrdMap, + health: OrdMap, started: DateTime, } diff --git a/core/startos/src/status/health_check.rs b/core/startos/src/status/health_check.rs index 90b20f8c5..1b1e2a7b6 100644 --- a/core/startos/src/status/health_check.rs +++ b/core/startos/src/status/health_check.rs @@ -9,25 +9,25 @@ use crate::util::clap::FromStrParser; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, TS)] #[serde(rename_all = "camelCase")] -pub struct HealthCheckResult { +pub struct NamedHealthCheckResult { pub name: String, #[serde(flatten)] - pub kind: HealthCheckResultKind, + pub kind: NamedHealthCheckResultKind, } // healthCheckName:kind:message OR healthCheckName:kind -impl FromStr for HealthCheckResult { +impl FromStr for NamedHealthCheckResult { type Err = color_eyre::eyre::Report; fn from_str(s: &str) -> Result { let from_parts = |name: &str, kind: &str, message: Option<&str>| { let message = message.map(|x| x.to_string()); let kind = match kind { - "success" => HealthCheckResultKind::Success { message }, - "disabled" => HealthCheckResultKind::Disabled { message }, - "starting" => HealthCheckResultKind::Starting { message }, - "loading" => HealthCheckResultKind::Loading { + "success" => NamedHealthCheckResultKind::Success { message }, + "disabled" => NamedHealthCheckResultKind::Disabled { message }, + "starting" => NamedHealthCheckResultKind::Starting { message }, + "loading" => NamedHealthCheckResultKind::Loading { message: message.unwrap_or_default(), }, - "failure" => HealthCheckResultKind::Failure { + "failure" => NamedHealthCheckResultKind::Failure { message: message.unwrap_or_default(), }, _ => return Err(color_eyre::eyre::eyre!("Invalid health check kind")), @@ -47,7 +47,7 @@ impl FromStr for HealthCheckResult { } } } -impl ValueParserFactory for HealthCheckResult { +impl ValueParserFactory for NamedHealthCheckResult { type Parser = FromStrParser; fn value_parser() -> Self::Parser { FromStrParser::new() @@ -57,40 +57,44 @@ impl ValueParserFactory for HealthCheckResult { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, TS)] #[serde(rename_all = "camelCase")] #[serde(tag = "result")] -pub enum HealthCheckResultKind { +pub enum NamedHealthCheckResultKind { Success { message: Option }, Disabled { message: Option }, Starting { message: Option }, Loading { message: String }, Failure { message: String }, } -impl std::fmt::Display for HealthCheckResult { +impl std::fmt::Display for NamedHealthCheckResult { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let name = &self.name; match &self.kind { - HealthCheckResultKind::Success { message } => { + NamedHealthCheckResultKind::Success { message } => { if let Some(message) = message { write!(f, "{name}: Succeeded ({message})") } else { write!(f, "{name}: Succeeded") } } - HealthCheckResultKind::Disabled { message } => { + NamedHealthCheckResultKind::Disabled { message } => { if let Some(message) = message { write!(f, "{name}: Disabled ({message})") } else { write!(f, "{name}: Disabled") } } - HealthCheckResultKind::Starting { message } => { + NamedHealthCheckResultKind::Starting { message } => { if let Some(message) = message { write!(f, "{name}: Starting ({message})") } else { write!(f, "{name}: Starting") } } - HealthCheckResultKind::Loading { message } => write!(f, "{name}: Loading ({message})"), - HealthCheckResultKind::Failure { message } => write!(f, "{name}: Failed ({message})"), + NamedHealthCheckResultKind::Loading { message } => { + write!(f, "{name}: Loading ({message})") + } + NamedHealthCheckResultKind::Failure { message } => { + write!(f, "{name}: Failed ({message})") + } } } } diff --git a/core/startos/src/status/mod.rs b/core/startos/src/status/mod.rs index c1d3a36ad..1701a965e 100644 --- a/core/startos/src/status/mod.rs +++ b/core/startos/src/status/mod.rs @@ -1,4 +1,5 @@ -use std::{collections::BTreeMap, sync::Arc}; +use std::collections::BTreeMap; +use std::sync::Arc; use chrono::{DateTime, Utc}; use imbl::OrdMap; @@ -6,8 +7,9 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; use self::health_check::HealthCheckId; -use crate::status::health_check::HealthCheckResult; -use crate::{prelude::*, util::GeneralGuard}; +use crate::prelude::*; +use crate::status::health_check::NamedHealthCheckResult; +use crate::util::GeneralGuard; pub mod health_check; #[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)] @@ -32,15 +34,15 @@ pub enum MainStatus { Running { #[ts(type = "string")] started: DateTime, - #[ts(as = "BTreeMap")] - health: OrdMap, + #[ts(as = "BTreeMap")] + health: OrdMap, }, #[serde(rename_all = "camelCase")] BackingUp { #[ts(type = "string | null")] started: Option>, - #[ts(as = "BTreeMap")] - health: OrdMap, + #[ts(as = "BTreeMap")] + health: OrdMap, }, } impl MainStatus { @@ -93,7 +95,7 @@ impl MainStatus { MainStatus::BackingUp { started, health } } - pub fn health(&self) -> Option<&OrdMap> { + pub fn health(&self) -> Option<&OrdMap> { match self { MainStatus::Running { health, .. } => Some(health), MainStatus::BackingUp { health, .. } => Some(health), diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index 7cdedff90..484817b76 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -293,8 +293,8 @@ export class StartSdk { ) }, HealthCheck: { - of(o: HealthCheckParams) { - return healthCheck(o) + of(o: HealthCheckParams) { + return healthCheck(o) }, }, Dependency: { diff --git a/sdk/lib/health/HealthCheck.ts b/sdk/lib/health/HealthCheck.ts index 4b72dbf61..adb00e296 100644 --- a/sdk/lib/health/HealthCheck.ts +++ b/sdk/lib/health/HealthCheck.ts @@ -1,5 +1,5 @@ import { Effects } from "../types" -import { CheckResult } from "./checkFns/CheckResult" +import { HealthCheckResult } from "./checkFns/HealthCheckResult" import { HealthReceipt } from "./HealthReceipt" import { Trigger } from "../trigger" import { TriggerInput } from "../trigger/TriggerInput" @@ -9,66 +9,52 @@ import { Overlay } from "../util/Overlay" import { object, unknown } from "ts-matches" import * as T from "../types" -export type HealthCheckParams = { +export type HealthCheckParams = { effects: Effects name: string - image: { - id: keyof Manifest["images"] & T.ImageId - sharedRun?: boolean - } trigger?: Trigger - fn(overlay: Overlay): Promise | CheckResult + fn(): Promise | HealthCheckResult onFirstSuccess?: () => unknown | Promise } -export function healthCheck( - o: HealthCheckParams, -) { +export function healthCheck(o: HealthCheckParams) { new Promise(async () => { - const overlay = await Overlay.of(o.effects, o.image) - try { - let currentValue: TriggerInput = { - hadSuccess: false, + let currentValue: TriggerInput = {} + const getCurrentValue = () => currentValue + const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue) + const triggerFirstSuccess = once(() => + Promise.resolve( + "onFirstSuccess" in o && o.onFirstSuccess + ? o.onFirstSuccess() + : undefined, + ), + ) + for ( + let res = await trigger.next(); + !res.done; + res = await trigger.next() + ) { + try { + const { result, message } = await o.fn() + await o.effects.setHealth({ + name: o.name, + id: o.name, + result, + message: message || "", + }) + currentValue.lastResult = result + await triggerFirstSuccess().catch((err) => { + console.error(err) + }) + } catch (e) { + await o.effects.setHealth({ + name: o.name, + id: o.name, + result: "failure", + message: asMessage(e) || "", + }) + currentValue.lastResult = "failure" } - const getCurrentValue = () => currentValue - const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue) - const triggerFirstSuccess = once(() => - Promise.resolve( - "onFirstSuccess" in o && o.onFirstSuccess - ? o.onFirstSuccess() - : undefined, - ), - ) - for ( - let res = await trigger.next(); - !res.done; - res = await trigger.next() - ) { - try { - const { status, message } = await o.fn(overlay) - await o.effects.setHealth({ - name: o.name, - id: o.name, - result: status, - message: message || "", - }) - currentValue.hadSuccess = true - currentValue.lastResult = "success" - await triggerFirstSuccess().catch((err) => { - console.error(err) - }) - } catch (e) { - await o.effects.setHealth({ - name: o.name, - id: o.name, - result: "failure", - message: asMessage(e) || "", - }) - currentValue.lastResult = "failure" - } - } - } finally { - await overlay.destroy() } }) return {} as HealthReceipt diff --git a/sdk/lib/health/checkFns/CheckResult.ts b/sdk/lib/health/checkFns/CheckResult.ts deleted file mode 100644 index 8b46ee5c4..000000000 --- a/sdk/lib/health/checkFns/CheckResult.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { HealthStatus } from "../../types" - -export type CheckResult = { - status: HealthStatus - message: string | null -} diff --git a/sdk/lib/health/checkFns/HealthCheckResult.ts b/sdk/lib/health/checkFns/HealthCheckResult.ts new file mode 100644 index 000000000..ba2468488 --- /dev/null +++ b/sdk/lib/health/checkFns/HealthCheckResult.ts @@ -0,0 +1,3 @@ +import { T } from "../.." + +export type HealthCheckResult = Omit diff --git a/sdk/lib/health/checkFns/checkPortListening.ts b/sdk/lib/health/checkFns/checkPortListening.ts index 4cc0738da..94d0becc0 100644 --- a/sdk/lib/health/checkFns/checkPortListening.ts +++ b/sdk/lib/health/checkFns/checkPortListening.ts @@ -1,6 +1,6 @@ import { Effects } from "../../types" import { stringFromStdErrOut } from "../../util/stringFromStdErrOut" -import { CheckResult } from "./CheckResult" +import { HealthCheckResult } from "./HealthCheckResult" import { promisify } from "node:util" import * as CP from "node:child_process" @@ -32,8 +32,8 @@ export async function checkPortListening( timeoutMessage?: string timeout?: number }, -): Promise { - return Promise.race([ +): Promise { + return Promise.race([ Promise.resolve().then(async () => { const hasAddress = containsAddress( @@ -45,10 +45,10 @@ export async function checkPortListening( port, ) if (hasAddress) { - return { status: "success", message: options.successMessage } + return { result: "success", message: options.successMessage } } return { - status: "failure", + result: "failure", message: options.errorMessage, } }), @@ -56,7 +56,7 @@ export async function checkPortListening( setTimeout( () => resolve({ - status: "failure", + result: "failure", message: options.timeoutMessage || `Timeout trying to check port ${port}`, }), diff --git a/sdk/lib/health/checkFns/checkWebUrl.ts b/sdk/lib/health/checkFns/checkWebUrl.ts index 8f61ae2ef..b25c792e1 100644 --- a/sdk/lib/health/checkFns/checkWebUrl.ts +++ b/sdk/lib/health/checkFns/checkWebUrl.ts @@ -1,5 +1,5 @@ import { Effects } from "../../types" -import { CheckResult } from "./CheckResult" +import { HealthCheckResult } from "./HealthCheckResult" import { timeoutPromise } from "./index" import "isomorphic-fetch" @@ -17,12 +17,12 @@ export const checkWebUrl = async ( successMessage = `Reached ${url}`, errorMessage = `Error while fetching URL: ${url}`, } = {}, -): Promise => { +): Promise => { return Promise.race([fetch(url), timeoutPromise(timeout)]) .then( (x) => ({ - status: "success", + result: "success", message: successMessage, }) as const, ) @@ -30,6 +30,6 @@ export const checkWebUrl = async ( console.warn(`Error while fetching URL: ${url}`) console.error(JSON.stringify(e)) console.error(e.toString()) - return { status: "failure" as const, message: errorMessage } + return { result: "failure" as const, message: errorMessage } }) } diff --git a/sdk/lib/health/checkFns/index.ts b/sdk/lib/health/checkFns/index.ts index d33d5ad0d..2de37e38c 100644 --- a/sdk/lib/health/checkFns/index.ts +++ b/sdk/lib/health/checkFns/index.ts @@ -1,6 +1,6 @@ import { runHealthScript } from "./runHealthScript" export { checkPortListening } from "./checkPortListening" -export { CheckResult } from "./CheckResult" +export { HealthCheckResult } from "./HealthCheckResult" export { checkWebUrl } from "./checkWebUrl" export function timeoutPromise(ms: number, { message = "Timed out" } = {}) { diff --git a/sdk/lib/health/checkFns/runHealthScript.ts b/sdk/lib/health/checkFns/runHealthScript.ts index f0f41ee91..87fc6c69c 100644 --- a/sdk/lib/health/checkFns/runHealthScript.ts +++ b/sdk/lib/health/checkFns/runHealthScript.ts @@ -1,7 +1,7 @@ import { Effects } from "../../types" import { Overlay } from "../../util/Overlay" import { stringFromStdErrOut } from "../../util/stringFromStdErrOut" -import { CheckResult } from "./CheckResult" +import { HealthCheckResult } from "./HealthCheckResult" import { timeoutPromise } from "./index" /** @@ -12,7 +12,6 @@ import { timeoutPromise } from "./index" * @returns */ export const runHealthScript = async ( - effects: Effects, runCommand: string[], overlay: Overlay, { @@ -21,7 +20,7 @@ export const runHealthScript = async ( message = (res: string) => `Have ran script ${runCommand} and the result: ${res}`, } = {}, -): Promise => { +): Promise => { const res = await Promise.race([ overlay.exec(runCommand), timeoutPromise(timeout), @@ -29,10 +28,10 @@ export const runHealthScript = async ( console.warn(errorMessage) console.warn(JSON.stringify(e)) console.warn(e.toString()) - throw { status: "failure", message: errorMessage } as CheckResult + throw { result: "failure", message: errorMessage } as HealthCheckResult }) return { - status: "success", + result: "success", message: message(res.stdout.toString()), - } as CheckResult + } as HealthCheckResult } diff --git a/sdk/lib/interfaces/Origin.ts b/sdk/lib/interfaces/Origin.ts index 52afe1ed3..cc84728ec 100644 --- a/sdk/lib/interfaces/Origin.ts +++ b/sdk/lib/interfaces/Origin.ts @@ -47,7 +47,6 @@ export class Origin { name, description, hasPrimary, - disabled, id, type, username, @@ -69,7 +68,6 @@ export class Origin { name, description, hasPrimary, - disabled, addressInfo, type, masked, diff --git a/sdk/lib/interfaces/ServiceInterfaceBuilder.ts b/sdk/lib/interfaces/ServiceInterfaceBuilder.ts index 14eaee1d3..49d8020d6 100644 --- a/sdk/lib/interfaces/ServiceInterfaceBuilder.ts +++ b/sdk/lib/interfaces/ServiceInterfaceBuilder.ts @@ -21,7 +21,6 @@ export class ServiceInterfaceBuilder { id: string description: string hasPrimary: boolean - disabled: boolean type: ServiceInterfaceType username: string | null path: string diff --git a/sdk/lib/mainFn/Daemons.ts b/sdk/lib/mainFn/Daemons.ts index c766e2f2e..3134d0459 100644 --- a/sdk/lib/mainFn/Daemons.ts +++ b/sdk/lib/mainFn/Daemons.ts @@ -1,6 +1,6 @@ import { NO_TIMEOUT, SIGKILL, SIGTERM, Signals } from "../StartSdk" import { HealthReceipt } from "../health/HealthReceipt" -import { CheckResult } from "../health/checkFns" +import { HealthCheckResult } from "../health/checkFns" import { Trigger } from "../trigger" import { TriggerInput } from "../trigger/TriggerInput" @@ -23,7 +23,7 @@ export const cpExec = promisify(CP.exec) export const cpExecFile = promisify(CP.execFile) export type Ready = { display: string | null - fn: () => Promise | CheckResult + fn: () => Promise | HealthCheckResult trigger?: Trigger } diff --git a/sdk/lib/mainFn/HealthDaemon.ts b/sdk/lib/mainFn/HealthDaemon.ts index 84865e59b..7cb15cd42 100644 --- a/sdk/lib/mainFn/HealthDaemon.ts +++ b/sdk/lib/mainFn/HealthDaemon.ts @@ -1,8 +1,8 @@ -import { CheckResult } from "../health/checkFns" +import { HealthCheckResult } from "../health/checkFns" import { defaultTrigger } from "../trigger/defaultTrigger" import { Ready } from "./Daemons" import { Daemon } from "./Daemon" -import { Effects } from "../types" +import { Effects, SetHealth } from "../types" import { DEFAULT_SIGTERM_TIMEOUT } from "." const oncePromise = () => { @@ -21,10 +21,9 @@ const oncePromise = () => { * */ export class HealthDaemon { - #health: CheckResult = { status: "starting", message: null } + #health: HealthCheckResult = { result: "starting", message: null } #healthWatchers: Array<() => unknown> = [] #running = false - #hadSuccess = false constructor( readonly daemon: Promise, readonly daemonIndex: number, @@ -77,7 +76,7 @@ export class HealthDaemon { ;(await this.daemon).stop() this.turnOffHealthCheck() - this.setHealth({ status: "starting", message: null }) + this.setHealth({ result: "starting", message: null }) } } @@ -88,8 +87,7 @@ export class HealthDaemon { private async setupHealthCheck() { if (this.#healthCheckCleanup) return const trigger = (this.ready.trigger ?? defaultTrigger)(() => ({ - hadSuccess: this.#hadSuccess, - lastResult: this.#health.status, + lastResult: this.#health.result, })) const { promise: status, resolve: setStatus } = oncePromise<{ @@ -101,19 +99,16 @@ export class HealthDaemon { !res.done; res = await Promise.race([status, trigger.next()]) ) { - const response: CheckResult = await Promise.resolve( + const response: HealthCheckResult = await Promise.resolve( this.ready.fn(), ).catch((err) => { console.error(err) return { - status: "failure", + result: "failure", message: "message" in err ? err.message : String(err), } }) - this.setHealth(response) - if (response.status === "success") { - this.#hadSuccess = true - } + await this.setHealth(response) } }).catch((err) => console.error(`Daemon ${this.id} failed: ${err}`)) @@ -123,37 +118,23 @@ export class HealthDaemon { } } - private setHealth(health: CheckResult) { + private async setHealth(health: HealthCheckResult) { this.#health = health this.#healthWatchers.forEach((watcher) => watcher()) const display = this.ready.display - const status = health.status + const result = health.result if (!display) { return } - if ( - status === "success" || - status === "disabled" || - status === "starting" - ) { - this.effects.setHealth({ - result: status, - message: health.message, - id: this.id, - name: display, - }) - } else { - this.effects.setHealth({ - result: health.status, - message: health.message || "", - id: this.id, - name: display, - }) - } + await this.effects.setHealth({ + ...health, + id: this.id, + name: display, + } as SetHealth) } private async updateStatus() { const healths = this.dependencies.map((d) => d.#health) - this.changeRunning(healths.every((x) => x.status === "success")) + this.changeRunning(healths.every((x) => x.result === "success")) } } diff --git a/sdk/lib/osBindings/CheckDependenciesResult.ts b/sdk/lib/osBindings/CheckDependenciesResult.ts index d349bdf18..de58264bc 100644 --- a/sdk/lib/osBindings/CheckDependenciesResult.ts +++ b/sdk/lib/osBindings/CheckDependenciesResult.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { HealthCheckId } from "./HealthCheckId" -import type { HealthCheckResult } from "./HealthCheckResult" +import type { NamedHealthCheckResult } from "./NamedHealthCheckResult" import type { PackageId } from "./PackageId" export type CheckDependenciesResult = { @@ -8,6 +8,6 @@ export type CheckDependenciesResult = { isInstalled: boolean isRunning: boolean configSatisfied: boolean - healthChecks: { [key: HealthCheckId]: HealthCheckResult } + healthChecks: { [key: HealthCheckId]: NamedHealthCheckResult } version: string | null } diff --git a/sdk/lib/osBindings/ExportServiceInterfaceParams.ts b/sdk/lib/osBindings/ExportServiceInterfaceParams.ts index b93e83f7c..28ac89916 100644 --- a/sdk/lib/osBindings/ExportServiceInterfaceParams.ts +++ b/sdk/lib/osBindings/ExportServiceInterfaceParams.ts @@ -8,7 +8,6 @@ export type ExportServiceInterfaceParams = { name: string description: string hasPrimary: boolean - disabled: boolean masked: boolean addressInfo: AddressInfo type: ServiceInterfaceType diff --git a/sdk/lib/osBindings/MainStatus.ts b/sdk/lib/osBindings/MainStatus.ts index 6acdce14a..a528aa187 100644 --- a/sdk/lib/osBindings/MainStatus.ts +++ b/sdk/lib/osBindings/MainStatus.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { HealthCheckId } from "./HealthCheckId" -import type { HealthCheckResult } from "./HealthCheckResult" +import type { NamedHealthCheckResult } from "./NamedHealthCheckResult" export type MainStatus = | { status: "stopped" } @@ -11,10 +11,10 @@ export type MainStatus = | { status: "running" started: string - health: { [key: HealthCheckId]: HealthCheckResult } + health: { [key: HealthCheckId]: NamedHealthCheckResult } } | { status: "backingUp" started: string | null - health: { [key: HealthCheckId]: HealthCheckResult } + health: { [key: HealthCheckId]: NamedHealthCheckResult } } diff --git a/sdk/lib/osBindings/HealthCheckResult.ts b/sdk/lib/osBindings/NamedHealthCheckResult.ts similarity index 85% rename from sdk/lib/osBindings/HealthCheckResult.ts rename to sdk/lib/osBindings/NamedHealthCheckResult.ts index 6fa3d3f8c..c967e9b34 100644 --- a/sdk/lib/osBindings/HealthCheckResult.ts +++ b/sdk/lib/osBindings/NamedHealthCheckResult.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type HealthCheckResult = { name: string } & ( +export type NamedHealthCheckResult = { name: string } & ( | { result: "success"; message: string | null } | { result: "disabled"; message: string | null } | { result: "starting"; message: string | null } diff --git a/sdk/lib/osBindings/ServiceInterface.ts b/sdk/lib/osBindings/ServiceInterface.ts index 91ac77515..9bcec0056 100644 --- a/sdk/lib/osBindings/ServiceInterface.ts +++ b/sdk/lib/osBindings/ServiceInterface.ts @@ -8,7 +8,6 @@ export type ServiceInterface = { name: string description: string hasPrimary: boolean - disabled: boolean masked: boolean addressInfo: AddressInfo type: ServiceInterfaceType diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts index 2708aef8c..74baabfd9 100644 --- a/sdk/lib/osBindings/index.ts +++ b/sdk/lib/osBindings/index.ts @@ -69,7 +69,6 @@ export { Governor } from "./Governor" export { Guid } from "./Guid" export { HardwareRequirements } from "./HardwareRequirements" export { HealthCheckId } from "./HealthCheckId" -export { HealthCheckResult } from "./HealthCheckResult" export { HostAddress } from "./HostAddress" export { HostId } from "./HostId" export { HostKind } from "./HostKind" @@ -98,6 +97,7 @@ export { MaybeUtf8String } from "./MaybeUtf8String" export { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" export { MountParams } from "./MountParams" export { MountTarget } from "./MountTarget" +export { NamedHealthCheckResult } from "./NamedHealthCheckResult" export { NamedProgress } from "./NamedProgress" export { OnionHostname } from "./OnionHostname" export { OsIndex } from "./OsIndex" diff --git a/sdk/lib/test/host.test.ts b/sdk/lib/test/host.test.ts index a8ae317ed..64d486a94 100644 --- a/sdk/lib/test/host.test.ts +++ b/sdk/lib/test/host.test.ts @@ -16,7 +16,6 @@ describe("host", () => { id: "foo", description: "A Foo", hasPrimary: false, - disabled: false, type: "ui", username: "bar", path: "/baz", diff --git a/sdk/lib/trigger/TriggerInput.ts b/sdk/lib/trigger/TriggerInput.ts index 9a52d8ca5..82fe79e07 100644 --- a/sdk/lib/trigger/TriggerInput.ts +++ b/sdk/lib/trigger/TriggerInput.ts @@ -2,5 +2,4 @@ import { HealthStatus } from "../types" export type TriggerInput = { lastResult?: HealthStatus - hadSuccess?: boolean } diff --git a/sdk/lib/trigger/changeOnFirstSuccess.ts b/sdk/lib/trigger/changeOnFirstSuccess.ts index 4c45afe31..3da7284df 100644 --- a/sdk/lib/trigger/changeOnFirstSuccess.ts +++ b/sdk/lib/trigger/changeOnFirstSuccess.ts @@ -5,10 +5,12 @@ export function changeOnFirstSuccess(o: { afterFirstSuccess: Trigger }): Trigger { return async function* (getInput) { - const beforeFirstSuccess = o.beforeFirstSuccess(getInput) - yield let currentValue = getInput() - beforeFirstSuccess.next() + while (!currentValue.lastResult) { + yield + currentValue = getInput() + } + const beforeFirstSuccess = o.beforeFirstSuccess(getInput) for ( let res = await beforeFirstSuccess.next(); currentValue?.lastResult !== "success" && !res.done; diff --git a/sdk/lib/trigger/defaultTrigger.ts b/sdk/lib/trigger/defaultTrigger.ts index bd52dc7cc..69cac2773 100644 --- a/sdk/lib/trigger/defaultTrigger.ts +++ b/sdk/lib/trigger/defaultTrigger.ts @@ -2,7 +2,7 @@ import { cooldownTrigger } from "./cooldownTrigger" import { changeOnFirstSuccess } from "./changeOnFirstSuccess" import { successFailure } from "./successFailure" -export const defaultTrigger = successFailure({ - duringSuccess: cooldownTrigger(0), - duringError: cooldownTrigger(30000), +export const defaultTrigger = changeOnFirstSuccess({ + beforeFirstSuccess: cooldownTrigger(1000), + afterFirstSuccess: cooldownTrigger(30000), }) diff --git a/sdk/lib/trigger/lastStatus.ts b/sdk/lib/trigger/lastStatus.ts new file mode 100644 index 000000000..90b8c9851 --- /dev/null +++ b/sdk/lib/trigger/lastStatus.ts @@ -0,0 +1,33 @@ +import { Trigger } from "." +import { HealthStatus } from "../types" + +export type LastStatusTriggerParams = { [k in HealthStatus]?: Trigger } & { + default: Trigger +} + +export function lastStatus(o: LastStatusTriggerParams): Trigger { + return async function* (getInput) { + let trigger = o.default(getInput) + const triggers: { + [k in HealthStatus]?: AsyncIterator + } & { default: AsyncIterator } = { + default: trigger, + } + while (true) { + let currentValue = getInput() + let prev: HealthStatus | "default" | undefined = currentValue.lastResult + if (!prev) { + yield + continue + } + if (!(prev in o)) { + prev = "default" + } + if (!triggers[prev]) { + triggers[prev] = o[prev]!(getInput) + } + await triggers[prev]?.next() + yield + } + } +} diff --git a/sdk/lib/trigger/successFailure.ts b/sdk/lib/trigger/successFailure.ts index 1bab27289..7febcd356 100644 --- a/sdk/lib/trigger/successFailure.ts +++ b/sdk/lib/trigger/successFailure.ts @@ -1,32 +1,7 @@ import { Trigger } from "." +import { lastStatus } from "./lastStatus" -export function successFailure(o: { +export const successFailure = (o: { duringSuccess: Trigger duringError: Trigger -}): Trigger { - return async function* (getInput) { - while (true) { - const beforeSuccess = o.duringSuccess(getInput) - yield - let currentValue = getInput() - beforeSuccess.next() - for ( - let res = await beforeSuccess.next(); - currentValue?.lastResult !== "success" && !res.done; - res = await beforeSuccess.next() - ) { - yield - currentValue = getInput() - } - const duringError = o.duringError(getInput) - for ( - let res = await duringError.next(); - currentValue?.lastResult === "success" && !res.done; - res = await duringError.next() - ) { - yield - currentValue = getInput() - } - } - } -} +}) => lastStatus({ success: o.duringSuccess, default: o.duringError }) diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index 9a3157ed3..1fdbcc66d 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -3,7 +3,7 @@ export * as configTypes from "./config/configTypes" import { DependencyRequirement, SetHealth, - HealthCheckResult, + NamedHealthCheckResult, SetMainStatus, ServiceInterface, Host, @@ -174,7 +174,7 @@ export type Daemon = { [DaemonProof]: never } -export type HealthStatus = HealthCheckResult["result"] +export type HealthStatus = NamedHealthCheckResult["result"] export type SmtpValue = { server: string port: number diff --git a/sdk/lib/util/Overlay.ts b/sdk/lib/util/Overlay.ts index 93cb44238..29ddadfb1 100644 --- a/sdk/lib/util/Overlay.ts +++ b/sdk/lib/util/Overlay.ts @@ -39,6 +39,23 @@ export class Overlay { return new Overlay(effects, id, rootfs, guid) } + static async with( + effects: T.Effects, + image: { id: T.ImageId; sharedRun?: boolean }, + mounts: { options: MountOptions; path: string }[], + fn: (overlay: Overlay) => Promise, + ): Promise { + const overlay = await Overlay.of(effects, image) + try { + for (let mount of mounts) { + await overlay.mount(mount.options, mount.path) + } + return await fn(overlay) + } finally { + await overlay.destroy() + } + } + async mount(options: MountOptions, path: string): Promise { path = path.startsWith("/") ? `${this.rootfs}${path}` diff --git a/sdk/lib/util/getServiceInterface.ts b/sdk/lib/util/getServiceInterface.ts index 9148c8f9a..fd0fef779 100644 --- a/sdk/lib/util/getServiceInterface.ts +++ b/sdk/lib/util/getServiceInterface.ts @@ -50,8 +50,6 @@ export type ServiceInterfaceFilled = { description: string /** Whether or not the interface has a primary URL */ hasPrimary: boolean - /** Whether or not the interface disabled */ - disabled: boolean /** Whether or not to mask the URIs for this interface. Useful if the URIs contain sensitive information, such as a password, macaroon, or API key */ masked: boolean /** Information about the host for this binding */ diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts index fef84a5ba..f470bc43a 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts @@ -10,15 +10,15 @@ import { ConnectionService } from 'src/app/services/connection.service' }) export class AppShowHealthChecksComponent { @Input() - healthChecks!: Record + healthChecks!: Record constructor(readonly connection$: ConnectionService) {} - isLoading(result: T.HealthCheckResult['result']): boolean { + isLoading(result: T.NamedHealthCheckResult['result']): boolean { return result === 'starting' || result === 'loading' } - isReady(result: T.HealthCheckResult['result']): boolean { + isReady(result: T.NamedHealthCheckResult['result']): boolean { return result !== 'failure' && result !== 'loading' } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/health-color.pipe.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/health-color.pipe.ts index 30d71d427..1f27b5e46 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/health-color.pipe.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/health-color.pipe.ts @@ -5,7 +5,7 @@ import { T } from '@start9labs/start-sdk' name: 'healthColor', }) export class HealthColorPipe implements PipeTransform { - transform(val: T.HealthCheckResult['result']): string { + transform(val: T.NamedHealthCheckResult['result']): string { switch (val) { case 'success': return 'success' diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts index f66773f2c..24153caf9 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-health-checks.pipe.ts @@ -14,7 +14,7 @@ export class ToHealthChecksPipe implements PipeTransform { transform( manifest: T.Manifest, - ): Observable | null> { + ): Observable | null> { return this.patch.watch$('packageData', manifest.id, 'status', 'main').pipe( map(main => { return main.status === 'running' && !isEmptyObject(main.health) diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index d1ce9c47c..a7ae06193 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -1699,7 +1699,6 @@ export module Mock { ui: { id: 'ui', hasPrimary: false, - disabled: false, masked: false, name: 'Web UI', description: @@ -1717,7 +1716,6 @@ export module Mock { rpc: { id: 'rpc', hasPrimary: false, - disabled: false, masked: false, name: 'RPC', description: @@ -1735,7 +1733,6 @@ export module Mock { p2p: { id: 'p2p', hasPrimary: true, - disabled: false, masked: false, name: 'P2P', description: @@ -1876,7 +1873,6 @@ export module Mock { ui: { id: 'ui', hasPrimary: false, - disabled: false, masked: false, name: 'Web UI', description: 'A launchable web app for Bitcoin Proxy', @@ -1925,7 +1921,6 @@ export module Mock { grpc: { id: 'grpc', hasPrimary: false, - disabled: false, masked: false, name: 'GRPC', description: @@ -1943,7 +1938,6 @@ export module Mock { lndconnect: { id: 'lndconnect', hasPrimary: false, - disabled: false, masked: true, name: 'LND Connect', description: @@ -1961,7 +1955,6 @@ export module Mock { p2p: { id: 'p2p', hasPrimary: true, - disabled: false, masked: false, name: 'P2P', description: diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index 5742ba67f..ef0e80d20 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -535,7 +535,7 @@ export interface DependencyErrorConfigUnsatisfied { export interface DependencyErrorHealthChecksFailed { type: 'healthChecksFailed' - check: T.HealthCheckResult + check: T.NamedHealthCheckResult } export interface DependencyErrorTransitive { diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 8b4c28d1e..5a6c7b815 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -132,7 +132,6 @@ export const mockPatchData: DataModel = { ui: { id: 'ui', hasPrimary: false, - disabled: false, masked: false, name: 'Web UI', description: @@ -150,7 +149,6 @@ export const mockPatchData: DataModel = { rpc: { id: 'rpc', hasPrimary: false, - disabled: false, masked: false, name: 'RPC', description: @@ -168,7 +166,6 @@ export const mockPatchData: DataModel = { p2p: { id: 'p2p', hasPrimary: true, - disabled: false, masked: false, name: 'P2P', description: @@ -311,7 +308,6 @@ export const mockPatchData: DataModel = { grpc: { id: 'grpc', hasPrimary: false, - disabled: false, masked: false, name: 'GRPC', description: @@ -329,7 +325,6 @@ export const mockPatchData: DataModel = { lndconnect: { id: 'lndconnect', hasPrimary: false, - disabled: false, masked: true, name: 'LND Connect', description: @@ -347,7 +342,6 @@ export const mockPatchData: DataModel = { p2p: { id: 'p2p', hasPrimary: true, - disabled: false, masked: false, name: 'P2P', description: From 0e598660b431683fd35ab95fbf45da8a557d8807 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 8 Aug 2024 15:54:46 -0600 Subject: [PATCH 06/10] redesign checkDependencies api --- sdk/lib/StartSdk.ts | 13 +- sdk/lib/dependencies/dependencies.ts | 301 +++++++++++------- sdk/lib/exver/index.ts | 134 ++++---- sdk/lib/osBindings/CheckDependenciesResult.ts | 5 +- 4 files changed, 274 insertions(+), 179 deletions(-) diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index 484817b76..c31218208 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -75,7 +75,10 @@ import * as T from "./types" import { testTypeVersion, ValidateExVer } from "./exver" import { ExposedStorePaths } from "./store/setupExposeStore" import { PathBuilder, extractJsonPath, pathBuilder } from "./store/PathBuilder" -import { checkAllDependencies } from "./dependencies/dependencies" +import { + CheckDependencies, + checkDependencies, +} from "./dependencies/dependencies" import { health } from "." import { GetSslCertificate } from "./util/GetSslCertificate" @@ -142,7 +145,13 @@ export class StartSdk { } return { - checkAllDependencies, + checkDependencies: checkDependencies as < + DependencyId extends keyof Manifest["dependencies"] & + PackageId = keyof Manifest["dependencies"] & PackageId, + >( + effects: Effects, + packageIds?: DependencyId[], + ) => Promise>, serviceInterface: { getOwn: (effects: E, id: ServiceInterfaceId) => removeCallbackTypes(effects)( diff --git a/sdk/lib/dependencies/dependencies.ts b/sdk/lib/dependencies/dependencies.ts index 28b04a07b..287f63b06 100644 --- a/sdk/lib/dependencies/dependencies.ts +++ b/sdk/lib/dependencies/dependencies.ts @@ -1,131 +1,206 @@ +import { ExtendedVersion, VersionRange } from "../exver" import { Effects, PackageId, DependencyRequirement, SetHealth, CheckDependenciesResult, + HealthCheckId, } from "../types" -export type CheckAllDependencies = { - notInstalled: () => Promise - notRunning: () => Promise - configNotSatisfied: () => Promise - healthErrors: () => Promise<{ [id: string]: SetHealth[] }> +export type CheckDependencies = { + installedSatisfied: (packageId: DependencyId) => boolean + installedVersionSatisfied: (packageId: DependencyId) => boolean + runningSatisfied: (packageId: DependencyId) => boolean + configSatisfied: (packageId: DependencyId) => boolean + healthCheckSatisfied: ( + packageId: DependencyId, + healthCheckId: HealthCheckId, + ) => boolean + satisfied: () => boolean - isValid: () => Promise - - throwIfNotRunning: () => Promise - throwIfNotInstalled: () => Promise - throwIfConfigNotSatisfied: () => Promise - throwIfHealthError: () => Promise - - throwIfNotValid: () => Promise + throwIfInstalledNotSatisfied: (packageId: DependencyId) => void + throwIfInstalledVersionNotSatisfied: (packageId: DependencyId) => void + throwIfRunningNotSatisfied: (packageId: DependencyId) => void + throwIfConfigNotSatisfied: (packageId: DependencyId) => void + throwIfHealthNotSatisfied: ( + packageId: DependencyId, + healthCheckId?: HealthCheckId, + ) => void + throwIfNotSatisfied: (packageId?: DependencyId) => void } -export function checkAllDependencies(effects: Effects): CheckAllDependencies { - const dependenciesPromise = effects.getDependencies() - const resultsPromise = dependenciesPromise.then((dependencies) => +export async function checkDependencies< + DependencyId extends PackageId = PackageId, +>( + effects: Effects, + packageIds?: DependencyId[], +): Promise> { + let [dependencies, results] = await Promise.all([ + effects.getDependencies(), effects.checkDependencies({ - packageIds: dependencies.map((dep) => dep.id), + packageIds, }), - ) - - const dependenciesByIdPromise = dependenciesPromise.then((d) => - d.reduce( - (acc, dep) => { - acc[dep.id] = dep - return acc - }, - {} as { [id: PackageId]: DependencyRequirement }, - ), - ) - - const healthErrors = async () => { - const results = await resultsPromise - const dependenciesById = await dependenciesByIdPromise - const answer: { [id: PackageId]: SetHealth[] } = {} - for (const result of results) { - const dependency = dependenciesById[result.packageId] - if (!dependency) continue - if (dependency.kind !== "running") continue - - const healthChecks = Object.entries(result.healthChecks) - .map(([id, hc]) => ({ ...hc, id })) - .filter((x) => !!x.message) - if (healthChecks.length === 0) continue - answer[result.packageId] = healthChecks - } - return answer - } - const configNotSatisfied = () => - resultsPromise.then((x) => x.filter((x) => !x.configSatisfied)) - const notInstalled = () => - resultsPromise.then((x) => x.filter((x) => !x.isInstalled)) - const notRunning = async () => { - const results = await resultsPromise - const dependenciesById = await dependenciesByIdPromise - return results.filter((x) => { - const dependency = dependenciesById[x.packageId] - if (!dependency) return false - if (dependency.kind !== "running") return false - return !x.isRunning - }) - } - const entries = (x: { [k: string]: B }) => Object.entries(x) - const first = (x: A[]): A | undefined => x[0] - const sinkVoid = (x: A) => void 0 - const throwIfHealthError = () => - healthErrors() - .then(entries) - .then(first) - .then((x) => { - if (!x) return - const [id, healthChecks] = x - if (healthChecks.length > 0) - throw `Package ${id} has the following errors: ${healthChecks.map((x) => x.message).join(", ")}` - }) - - const throwIfConfigNotSatisfied = () => - configNotSatisfied().then((results) => { - throw new Error( - `Package ${results[0].packageId} does not have a valid configuration`, - ) - }) - - const throwIfNotRunning = () => - notRunning().then((results) => { - if (results[0]) - throw new Error(`Package ${results[0].packageId} is not running`) - }) - - const throwIfNotInstalled = () => - notInstalled().then((results) => { - if (results[0]) - throw new Error(`Package ${results[0].packageId} is not installed`) - }) - const throwIfNotValid = async () => - Promise.all([ - throwIfNotRunning(), - throwIfNotInstalled(), - throwIfConfigNotSatisfied(), - throwIfHealthError(), - ]).then(sinkVoid) - - const isValid = () => - throwIfNotValid().then( - () => true, - () => false, + ]) + if (packageIds) { + dependencies = dependencies.filter((d) => + (packageIds as PackageId[]).includes(d.id), ) + } + + const find = (packageId: DependencyId) => { + const dependencyRequirement = dependencies.find((d) => d.id === packageId) + const dependencyResult = results.find((d) => d.packageId === packageId) + if (!dependencyRequirement || !dependencyResult) { + throw new Error(`Unknown DependencyId ${packageId}`) + } + return { requirement: dependencyRequirement, result: dependencyResult } + } + + const installedSatisfied = (packageId: DependencyId) => + !!find(packageId).result.installedVersion + const installedVersionSatisfied = (packageId: DependencyId) => { + const dep = find(packageId) + return ( + !!dep.result.installedVersion && + ExtendedVersion.parse(dep.result.installedVersion).satisfies( + VersionRange.parse(dep.requirement.versionRange), + ) + ) + } + const runningSatisfied = (packageId: DependencyId) => { + const dep = find(packageId) + return dep.requirement.kind !== "running" || dep.result.isRunning + } + const configSatisfied = (packageId: DependencyId) => + find(packageId).result.configSatisfied + const healthCheckSatisfied = ( + packageId: DependencyId, + healthCheckId?: HealthCheckId, + ) => { + const dep = find(packageId) + if ( + healthCheckId && + (dep.requirement.kind !== "running" || + !dep.requirement.healthChecks.includes(healthCheckId)) + ) { + throw new Error(`Unknown HealthCheckId ${healthCheckId}`) + } + const errors = Object.entries(dep.result.healthChecks) + .filter(([id, _]) => (healthCheckId ? id === healthCheckId : true)) + .filter(([_, res]) => res.result !== "success") + return errors.length === 0 + } + const pkgSatisfied = (packageId: DependencyId) => + installedSatisfied(packageId) && + installedVersionSatisfied(packageId) && + runningSatisfied(packageId) && + configSatisfied(packageId) && + healthCheckSatisfied(packageId) + const satisfied = (packageId?: DependencyId) => + packageId + ? pkgSatisfied(packageId) + : dependencies.every((d) => pkgSatisfied(d.id as DependencyId)) + + const throwIfInstalledNotSatisfied = (packageId: DependencyId) => { + const dep = find(packageId) + if (!dep.result.installedVersion) { + throw new Error(`${dep.result.title || packageId} is not installed`) + } + } + const throwIfInstalledVersionNotSatisfied = (packageId: DependencyId) => { + const dep = find(packageId) + if (!dep.result.installedVersion) { + throw new Error(`${dep.result.title || packageId} is not installed`) + } + if ( + ![dep.result.installedVersion, ...dep.result.satisfies].find((v) => + ExtendedVersion.parse(v).satisfies( + VersionRange.parse(dep.requirement.versionRange), + ), + ) + ) { + throw new Error( + `Installed version ${dep.result.installedVersion} of ${dep.result.title || packageId} does not match expected version range ${dep.requirement.versionRange}`, + ) + } + } + const throwIfRunningNotSatisfied = (packageId: DependencyId) => { + const dep = find(packageId) + if (dep.requirement.kind === "running" && !dep.result.isRunning) { + throw new Error(`${dep.result.title || packageId} is not running`) + } + } + const throwIfConfigNotSatisfied = (packageId: DependencyId) => { + const dep = find(packageId) + if (!dep.result.configSatisfied) { + throw new Error( + `${dep.result.title || packageId}'s configuration does not satisfy requirements`, + ) + } + } + const throwIfHealthNotSatisfied = ( + packageId: DependencyId, + healthCheckId?: HealthCheckId, + ) => { + const dep = find(packageId) + if ( + healthCheckId && + (dep.requirement.kind !== "running" || + !dep.requirement.healthChecks.includes(healthCheckId)) + ) { + throw new Error(`Unknown HealthCheckId ${healthCheckId}`) + } + const errors = Object.entries(dep.result.healthChecks) + .filter(([id, _]) => (healthCheckId ? id === healthCheckId : true)) + .filter(([_, res]) => res.result !== "success") + if (errors.length) { + throw new Error( + errors + .map( + ([_, e]) => + `Health Check ${e.name} of ${dep.result.title || packageId} failed with status ${e.result}${e.message ? `: ${e.message}` : ""}`, + ) + .join("; "), + ) + } + } + const throwIfPkgNotSatisfied = (packageId: DependencyId) => { + throwIfInstalledNotSatisfied(packageId) + throwIfInstalledVersionNotSatisfied(packageId) + throwIfRunningNotSatisfied(packageId) + throwIfConfigNotSatisfied(packageId) + throwIfHealthNotSatisfied(packageId) + } + const throwIfNotSatisfied = (packageId?: DependencyId) => + packageId + ? throwIfPkgNotSatisfied(packageId) + : (() => { + const err = dependencies.flatMap((d) => { + try { + throwIfPkgNotSatisfied(d.id as DependencyId) + } catch (e) { + if (e instanceof Error) return [e.message] + throw e + } + return [] + }) + if (err.length) { + throw new Error(err.join("; ")) + } + })() return { - notRunning, - notInstalled, - configNotSatisfied, - healthErrors, - throwIfNotRunning, + installedSatisfied, + installedVersionSatisfied, + runningSatisfied, + configSatisfied, + healthCheckSatisfied, + satisfied, + throwIfInstalledNotSatisfied, + throwIfInstalledVersionNotSatisfied, + throwIfRunningNotSatisfied, throwIfConfigNotSatisfied, - throwIfNotValid, - throwIfNotInstalled, - throwIfHealthError, - isValid, + throwIfHealthNotSatisfied, + throwIfNotSatisfied, } } diff --git a/sdk/lib/exver/index.ts b/sdk/lib/exver/index.ts index 913194875..012cb532e 100644 --- a/sdk/lib/exver/index.ts +++ b/sdk/lib/exver/index.ts @@ -44,7 +44,7 @@ type Not = { } export class VersionRange { - private constructor(private atom: Anchor | And | Or | Not | P.Any | P.None) {} + private constructor(public atom: Anchor | And | Or | Not | P.Any | P.None) {} toString(): string { switch (this.atom.type) { @@ -63,67 +63,6 @@ export class VersionRange { } } - /** - * Returns a boolean indicating whether a given version satisfies the VersionRange - * !( >= 1:1 <= 2:2) || <=#bitcoin:1.2.0-alpha:0 - */ - satisfiedBy(version: ExtendedVersion): boolean { - switch (this.atom.type) { - case "Anchor": - const otherVersion = this.atom.version - switch (this.atom.operator) { - case "=": - return version.equals(otherVersion) - case ">": - return version.greaterThan(otherVersion) - case "<": - return version.lessThan(otherVersion) - case ">=": - return version.greaterThanOrEqual(otherVersion) - case "<=": - return version.lessThanOrEqual(otherVersion) - case "!=": - return !version.equals(otherVersion) - case "^": - const nextMajor = this.atom.version.incrementMajor() - if ( - version.greaterThanOrEqual(otherVersion) && - version.lessThan(nextMajor) - ) { - return true - } else { - return false - } - case "~": - const nextMinor = this.atom.version.incrementMinor() - if ( - version.greaterThanOrEqual(otherVersion) && - version.lessThan(nextMinor) - ) { - return true - } else { - return false - } - } - case "And": - return ( - this.atom.left.satisfiedBy(version) && - this.atom.right.satisfiedBy(version) - ) - case "Or": - return ( - this.atom.left.satisfiedBy(version) || - this.atom.right.satisfiedBy(version) - ) - case "Not": - return !this.atom.value.satisfiedBy(version) - case "Any": - return true - case "None": - return false - } - } - private static parseAtom(atom: P.VersionRangeAtom): VersionRange { switch (atom.type) { case "Not": @@ -207,6 +146,10 @@ export class VersionRange { static none() { return new VersionRange({ type: "None" }) } + + satisfiedBy(version: Version | ExtendedVersion) { + return version.satisfies(this) + } } export class Version { @@ -266,6 +209,12 @@ export class Version { const parsed = P.parse(version, { startRule: "Version" }) return new Version(parsed.number, parsed.prerelease) } + + satisfies(versionRange: VersionRange): boolean { + return new ExtendedVersion(null, this, new Version([0], [])).satisfies( + versionRange, + ) + } } // #flavor:0.1.2-beta.1:0 @@ -404,6 +353,67 @@ export class ExtendedVersion { updatedDownstream, ) } + + /** + * Returns a boolean indicating whether a given version satisfies the VersionRange + * !( >= 1:1 <= 2:2) || <=#bitcoin:1.2.0-alpha:0 + */ + satisfies(versionRange: VersionRange): boolean { + switch (versionRange.atom.type) { + case "Anchor": + const otherVersion = versionRange.atom.version + switch (versionRange.atom.operator) { + case "=": + return this.equals(otherVersion) + case ">": + return this.greaterThan(otherVersion) + case "<": + return this.lessThan(otherVersion) + case ">=": + return this.greaterThanOrEqual(otherVersion) + case "<=": + return this.lessThanOrEqual(otherVersion) + case "!=": + return !this.equals(otherVersion) + case "^": + const nextMajor = versionRange.atom.version.incrementMajor() + if ( + this.greaterThanOrEqual(otherVersion) && + this.lessThan(nextMajor) + ) { + return true + } else { + return false + } + case "~": + const nextMinor = versionRange.atom.version.incrementMinor() + if ( + this.greaterThanOrEqual(otherVersion) && + this.lessThan(nextMinor) + ) { + return true + } else { + return false + } + } + case "And": + return ( + this.satisfies(versionRange.atom.left) && + this.satisfies(versionRange.atom.right) + ) + case "Or": + return ( + this.satisfies(versionRange.atom.left) || + this.satisfies(versionRange.atom.right) + ) + case "Not": + return !this.satisfies(versionRange.atom.value) + case "Any": + return true + case "None": + return false + } + } } export const testTypeExVer = (t: T & ValidateExVer) => t diff --git a/sdk/lib/osBindings/CheckDependenciesResult.ts b/sdk/lib/osBindings/CheckDependenciesResult.ts index de58264bc..a435ff87f 100644 --- a/sdk/lib/osBindings/CheckDependenciesResult.ts +++ b/sdk/lib/osBindings/CheckDependenciesResult.ts @@ -5,9 +5,10 @@ import type { PackageId } from "./PackageId" export type CheckDependenciesResult = { packageId: PackageId - isInstalled: boolean + title: string | null + installedVersion: string | null + satisfies: string[] isRunning: boolean configSatisfied: boolean healthChecks: { [key: HealthCheckId]: NamedHealthCheckResult } - version: string | null } From c289629a285d9a5e1e5e6a91ba11812935f4952c Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 8 Aug 2024 20:05:30 -0600 Subject: [PATCH 07/10] bump sdk version --- .../startos/src/service/effects/dependency.rs | 57 +++++++------------ sdk/package-lock.json | 4 +- sdk/package.json | 2 +- 3 files changed, 23 insertions(+), 40 deletions(-) diff --git a/core/startos/src/service/effects/dependency.rs b/core/startos/src/service/effects/dependency.rs index fbd02800c..26582d061 100644 --- a/core/startos/src/service/effects/dependency.rs +++ b/core/startos/src/service/effects/dependency.rs @@ -4,8 +4,10 @@ use std::str::FromStr; use clap::builder::ValueParserFactory; use exver::VersionRange; +use imbl::OrdMap; +use imbl_value::InternedString; use itertools::Itertools; -use models::{HealthCheckId, PackageId, VolumeId}; +use models::{HealthCheckId, PackageId, VersionString, VolumeId}; use patch_db::json_ptr::JsonPointer; use tokio::process::Command; @@ -316,12 +318,16 @@ pub struct CheckDependenciesParam { #[ts(export)] pub struct CheckDependenciesResult { package_id: PackageId, - is_installed: bool, + #[ts(type = "string | null")] + title: Option, + #[ts(type = "string | null")] + installed_version: Option, + #[ts(type = "string[]")] + satisfies: BTreeSet, is_running: bool, config_satisfied: bool, - health_checks: BTreeMap, - #[ts(type = "string | null")] - version: Option, + #[ts(as = "BTreeMap::")] + health_checks: OrdMap, } pub async fn check_dependencies( context: EffectContext, @@ -347,36 +353,23 @@ pub async fn check_dependencies( let mut results = Vec::with_capacity(package_ids.len()); for (package_id, dependency_info) in package_ids { + let title = dependency_info.title.clone(); let Some(package) = db.as_public().as_package_data().as_idx(&package_id) else { results.push(CheckDependenciesResult { package_id, - is_installed: false, + title, + installed_version: None, + satisfies: BTreeSet::new(), is_running: false, config_satisfied: false, health_checks: Default::default(), - version: None, }); continue; }; let manifest = package.as_state_info().as_manifest(ManifestPreference::New); let installed_version = manifest.as_version().de()?.into_version(); let satisfies = manifest.as_satisfies().de()?; - let version = Some(installed_version.clone()); - if ![installed_version] - .into_iter() - .chain(satisfies.into_iter().map(|v| v.into_version())) - .any(|v| v.satisfies(&dependency_info.version_range)) - { - results.push(CheckDependenciesResult { - package_id, - is_installed: false, - is_running: false, - config_satisfied: false, - health_checks: Default::default(), - version, - }); - continue; - } + let installed_version = Some(installed_version.clone()); let is_installed = true; let status = package.as_status().as_main().de()?; let is_running = if is_installed { @@ -384,25 +377,15 @@ pub async fn check_dependencies( } else { false }; - let health_checks = - if let CurrentDependencyKind::Running { health_checks } = &dependency_info.kind { - status - .health() - .cloned() - .unwrap_or_default() - .into_iter() - .filter(|(id, _)| health_checks.contains(id)) - .collect() - } else { - Default::default() - }; + let health_checks = status.health().cloned().unwrap_or_default(); results.push(CheckDependenciesResult { package_id, - is_installed, + title, + installed_version, + satisfies, is_running, config_satisfied: dependency_info.config_satisfied, health_checks, - version, }); } Ok(results) diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 06d456019..0e2bbf7cb 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha5", + "version": "0.3.6-alpha7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha5", + "version": "0.3.6-alpha7", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/sdk/package.json b/sdk/package.json index 1f9090b14..b2b2ceb44 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha6", + "version": "0.3.6-alpha7", "description": "Software development kit to facilitate packaging services for StartOS", "main": "./cjs/lib/index.js", "types": "./cjs/lib/index.d.ts", From 9237984782cd629ef79de09298ab0d4184408229 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 8 Aug 2024 22:26:42 -0600 Subject: [PATCH 08/10] remove disabled from createInterface --- sdk/lib/StartSdk.ts | 1 - sdk/lib/types.ts | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index c31218208..9989604cf 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -256,7 +256,6 @@ export class StartSdk { id: string description: string hasPrimary: boolean - disabled: boolean type: ServiceInterfaceType username: null | string path: string diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index 1fdbcc66d..6c5ed0ab8 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -249,15 +249,15 @@ export type SdkPropertiesValue = } | { type: "string" - /** Value */ + /** The value to display to the user */ value: string /** A human readable description or explanation of the value */ description?: string - /** (string/number only) Whether or not to mask the value, for example, when displaying a password */ + /** Whether or not to mask the value, for example, when displaying a password */ masked: boolean - /** (string/number only) Whether or not to include a button for copying the value to clipboard */ + /** Whether or not to include a button for copying the value to clipboard */ copyable?: boolean - /** (string/number only) Whether or not to include a button for displaying the value as a QR code */ + /** Whether or not to include a button for displaying the value as a QR code */ qr?: boolean } @@ -273,15 +273,15 @@ export type PropertiesValue = } | { type: "string" - /** Value */ + /** The value to display to the user */ value: string /** A human readable description or explanation of the value */ description: string | null - /** (string/number only) Whether or not to mask the value, for example, when displaying a password */ + /** Whether or not to mask the value, for example, when displaying a password */ masked: boolean - /** (string/number only) Whether or not to include a button for copying the value to clipboard */ + /** Whether or not to include a button for copying the value to clipboard */ copyable: boolean | null - /** (string/number only) Whether or not to include a button for displaying the value as a QR code */ + /** Whether or not to include a button for displaying the value as a QR code */ qr: boolean | null } From 30885cee01592aa674b0cd8bc0a05d391b48a6a7 Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:00:13 -0600 Subject: [PATCH 09/10] fix: Gitea/ Synapse/ Nostr types for manifest + config (#2704) --- .../Systems/SystemForEmbassy/MainLoop.ts | 2 +- .../__fixtures__/giteaManifest.ts | 123 ++++++++ .../__fixtures__/nostrConfig2.ts | 187 ++++++++++++ .../__fixtures__/synapseManifest.ts | 191 ++++++++++++ .../transformConfigSpec.test.ts.snap | 277 ++++++++++++++++++ .../SystemForEmbassy/matchManifest.test.ts | 12 + .../Systems/SystemForEmbassy/matchManifest.ts | 11 +- .../transformConfigSpec.test.ts | 5 + .../SystemForEmbassy/transformConfigSpec.ts | 49 +++- 9 files changed, 849 insertions(+), 8 deletions(-) create mode 100644 container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/giteaManifest.ts create mode 100644 container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/nostrConfig2.ts create mode 100644 container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/synapseManifest.ts create mode 100644 container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.test.ts diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts index e29170543..7f778c151 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts @@ -168,7 +168,7 @@ export class MainLoop { id: healthId, name: value.name, result: "success", - message: actionProcedure["success-message"], + message: actionProcedure["success-message"] ?? null, }) return } diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/giteaManifest.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/giteaManifest.ts new file mode 100644 index 000000000..1b3a8ba94 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/giteaManifest.ts @@ -0,0 +1,123 @@ +export default { + "eos-version": "0.3.5.1", + id: "gitea", + "git-hash": "91fada3edf30357a2e75c281d32f8888c87fcc2d\n", + title: "Gitea", + version: "1.22.0", + description: { + short: "A painless self-hosted Git service.", + long: "Gitea is a community managed lightweight code hosting solution written in Go. It is published under the MIT license.\n", + }, + assets: { + license: "LICENSE", + instructions: "instructions.md", + icon: "icon.png", + "docker-images": null, + assets: null, + scripts: null, + }, + build: ["make"], + "release-notes": + "* Upstream code update\n* Fix deprecated config options\n* Full list of upstream changes available [here](https://github.com/go-gitea/gitea/compare/v1.21.8...v1.22.0)\n", + license: "MIT", + "wrapper-repo": "https://github.com/Start9Labs/gitea-startos", + "upstream-repo": "https://github.com/go-gitea/gitea", + "support-site": "https://docs.gitea.io/en-us/", + "marketing-site": "https://gitea.io/en-us/", + "donation-url": null, + alerts: { + install: null, + uninstall: null, + restore: null, + start: null, + stop: null, + }, + main: { + type: "docker", + image: "main", + system: false, + entrypoint: "/usr/local/bin/docker_entrypoint.sh", + args: [], + inject: false, + mounts: { main: "/data" }, + "io-format": null, + "sigterm-timeout": null, + "shm-size-mb": null, + "gpu-acceleration": false, + }, + "health-checks": { + "user-signups-off": { + name: "User Signups Off", + "success-message": null, + type: "script", + args: [], + timeout: null, + }, + web: { + name: "Web & Git HTTP Tor Interfaces", + "success-message": + "Gitea is ready to be visited in a web browser and git can be used with SSH over TOR.", + type: "script", + args: [], + timeout: null, + }, + }, + config: { + get: { type: "script", args: [] }, + set: { type: "script", args: [] }, + }, + properties: { type: "script", args: [] }, + volumes: { main: { type: "data" } }, + interfaces: { + main: { + name: "Web UI / Git HTTPS/SSH", + description: + "Port 80: Browser Interface and HTTP Git Interface / Port 22: Git SSH Interface", + "tor-config": { "port-mapping": { "22": "22", "80": "3000" } }, + "lan-config": { "443": { ssl: true, internal: 3000 } }, + ui: true, + protocols: ["tcp", "http", "ssh", "git"], + }, + }, + backup: { + create: { + type: "docker", + image: "compat", + system: true, + entrypoint: "compat", + args: ["duplicity", "create", "/mnt/backup", "/root/data"], + inject: false, + mounts: { BACKUP: "/mnt/backup", main: "/root/data" }, + "io-format": "yaml", + "sigterm-timeout": null, + "shm-size-mb": null, + "gpu-acceleration": false, + }, + restore: { + type: "docker", + image: "compat", + system: true, + entrypoint: "compat", + args: ["duplicity", "restore", "/mnt/backup", "/root/data"], + inject: false, + mounts: { BACKUP: "/mnt/backup", main: "/root/data" }, + "io-format": "yaml", + "sigterm-timeout": null, + "shm-size-mb": null, + "gpu-acceleration": false, + }, + }, + migrations: { + from: { "*": { type: "script", args: ["from"] } }, + to: { "*": { type: "script", args: ["to"] } }, + }, + actions: {}, + dependencies: {}, + containers: null, + replaces: [], + "hardware-requirements": { + device: {}, + ram: null, + arch: ["x86_64", "aarch64"], + }, +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/nostrConfig2.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/nostrConfig2.ts new file mode 100644 index 000000000..0cea482c7 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/nostrConfig2.ts @@ -0,0 +1,187 @@ +export default { + "tor-address": { + name: "Tor Address", + description: "The Tor address for the websocket server.", + type: "pointer", + subtype: "package", + "package-id": "nostr", + target: "tor-address", + interface: "websocket", + }, + "lan-address": { + name: "Tor Address", + description: "The LAN address for the websocket server.", + type: "pointer", + subtype: "package", + "package-id": "nostr", + target: "lan-address", + interface: "websocket", + }, + "relay-type": { + type: "union", + name: "Relay Type", + warning: + "Running a public relay carries risk. Your relay can be spammed, resulting in large amounts of disk usage.", + tag: { + id: "type", + name: "Relay Type", + description: + "Private or public. A private relay (highly recommended) restricts write access to specific pubkeys. Anyone can write to a public relay.", + "variant-names": { private: "Private", public: "Public" }, + }, + default: "private", + variants: { + private: { + pubkey_whitelist: { + name: "Pubkey Whitelist (hex)", + description: + "A list of pubkeys that are permitted to publish through your relay. A minimum, you need to enter your own Nostr hex (not npub) pubkey. Go to https://damus.io/key/ to convert from npub to hex.", + type: "list", + range: "[1,*)", + subtype: "string", + spec: { + placeholder: "hex (not npub) pubkey", + pattern: "[0-9a-fA-F]{64}", + "pattern-description": + "Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.", + }, + default: [], + }, + }, + public: { + info: { + name: "Relay Info", + description: "General public info about your relay", + type: "object", + spec: { + name: { + name: "Relay Name", + description: "Your relay's human-readable identifier", + type: "string", + nullable: true, + placeholder: "Bob's Public Relay", + pattern: ".{3,32}", + "pattern-description": + "Must be at least 3 character and no more than 32 characters", + masked: false, + }, + description: { + name: "Relay Description", + description: "A more detailed description for your relay", + type: "string", + nullable: true, + placeholder: "The best relay in town", + pattern: ".{6,256}", + "pattern-description": + "Must be at least 6 character and no more than 256 characters", + masked: false, + }, + pubkey: { + name: "Admin contact pubkey (hex)", + description: + "The Nostr hex (not npub) pubkey of the relay administrator", + type: "string", + nullable: true, + placeholder: "hex (not npub) pubkey", + pattern: "[0-9a-fA-F]{64}", + "pattern-description": + "Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.", + masked: false, + }, + contact: { + name: "Admin contact email", + description: "The email address of the relay administrator", + type: "string", + nullable: true, + pattern: "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+", + "pattern-description": "Must be a valid email address.", + masked: false, + }, + }, + }, + limits: { + name: "Limits", + description: + "Data limits to protect your relay from using too many resources", + type: "object", + spec: { + messages_per_sec: { + name: "Messages Per Second Limit", + description: + "Limit events created per second, averaged over one minute. Note: this is for the server as a whole, not per connection.", + type: "number", + nullable: false, + range: "[1,*)", + integral: true, + default: 2, + units: "messages/sec", + }, + subscriptions_per_min: { + name: "Subscriptions Per Minute Limit", + description: + "Limit client subscriptions created per second, averaged over one minute. Strongly recommended to set this to a low value such as 10 to ensure fair service.", + type: "number", + nullable: false, + range: "[1,*)", + integral: true, + default: 10, + units: "subscriptions", + }, + max_blocking_threads: { + name: "Max Blocking Threads", + description: + "Maximum number of blocking threads used for database connections.", + type: "number", + nullable: false, + range: "[0,*)", + integral: true, + units: "threads", + default: 16, + }, + max_event_bytes: { + name: "Max Event Size", + description: + "Limit the maximum size of an EVENT message. Set to 0 for unlimited", + type: "number", + nullable: false, + range: "[0,*)", + integral: true, + units: "bytes", + default: 131072, + }, + max_ws_message_bytes: { + name: "Max Websocket Message Size", + description: "Maximum WebSocket message in bytes.", + type: "number", + nullable: false, + range: "[0,*)", + integral: true, + units: "bytes", + default: 131072, + }, + max_ws_frame_bytes: { + name: "Max Websocket Frame Size", + description: "Maximum WebSocket frame size in bytes.", + type: "number", + nullable: false, + range: "[0,*)", + integral: true, + units: "bytes", + default: 131072, + }, + event_kind_blacklist: { + name: "Event Kind Blacklist", + description: + "Events with these kinds will be discarded. For a list of event kinds, see here: https://github.com/nostr-protocol/nips#event-kinds", + type: "list", + range: "[0,*)", + subtype: "number", + spec: { integral: true, placeholder: 30023, range: "(0,100000]" }, + default: [], + }, + }, + }, + }, + }, + }, +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/synapseManifest.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/synapseManifest.ts new file mode 100644 index 000000000..18b520097 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/synapseManifest.ts @@ -0,0 +1,191 @@ +export default { + id: "synapse", + title: "Synapse", + version: "1.98.0", + "release-notes": + "* Upstream code update\n* Synapse Admin updated to the latest version - ([full changelog](https://github.com/Awesome-Technologies/synapse-admin/compare/0.8.7...0.9.1))\n* Instructions update\n* Updated package and upstream repositories links\n* Full list of upstream changes available [here](https://github.com/element-hq/synapse/compare/v1.95.1...v1.98.0)\n", + license: "Apache-2.0", + "wrapper-repo": "https://github.com/Start9Labs/synapse-startos", + "upstream-repo": "https://github.com/element-hq/synapse", + "support-site": "https://github.com/element-hq/synapse/issues", + "marketing-site": "https://matrix.org/", + build: ["make"], + description: { + short: + "Synapse is a battle-tested implementation of the Matrix protocol, the killer of all messaging apps.", + long: "Synapse is the battle-tested, reference implementation of the Matrix protocol. Matrix is a next-generation, federated, full-featured, encrypted, independent messaging system. There are no trusted third parties involved. (see matrix.org for details).", + }, + assets: { + license: "LICENSE", + icon: "icon.png", + instructions: "instructions.md", + }, + main: { + type: "docker", + image: "main", + entrypoint: "docker_entrypoint.sh", + args: [], + mounts: { + main: "/data", + cert: "/mnt/cert", + "admin-cert": "/mnt/admin-cert", + }, + }, + "health-checks": { + federation: { + name: "Federation", + type: "docker", + image: "main", + system: false, + entrypoint: "check-federation.sh", + args: [], + mounts: {}, + "io-format": "json", + inject: true, + }, + "synapse-admin": { + name: "Admin interface", + "success-message": + "Synapse Admin is ready to be visited in a web browser.", + type: "docker", + image: "main", + system: false, + entrypoint: "check-ui.sh", + args: [], + mounts: {}, + "io-format": "yaml", + inject: true, + }, + "user-signups-off": { + name: "User Signups Off", + type: "docker", + image: "main", + system: false, + entrypoint: "user-signups-off.sh", + args: [], + mounts: {}, + "io-format": "yaml", + inject: true, + }, + }, + config: { + get: { + type: "script", + }, + set: { + type: "script", + }, + }, + properties: { + type: "script", + }, + volumes: { + main: { + type: "data", + }, + cert: { + type: "certificate", + "interface-id": "main", + }, + "admin-cert": { + type: "certificate", + "interface-id": "admin", + }, + }, + alerts: { + start: + "After your first run, Synapse needs a little time to establish a stable TOR connection over federation. We kindly ask for your patience during this process. Remember, great things take time! 🕒", + }, + interfaces: { + main: { + name: "Homeserver Address", + description: + "Used by clients and other servers to connect with your homeserver", + "tor-config": { + "port-mapping": { + "80": "80", + "443": "443", + "8448": "8448", + }, + }, + ui: false, + protocols: ["tcp", "http", "matrix"], + }, + admin: { + name: "Admin Portal", + description: "A web application for administering your Synapse server", + "tor-config": { + "port-mapping": { + "80": "8080", + "443": "4433", + }, + }, + "lan-config": { + "443": { + ssl: true, + internal: 8080, + }, + }, + ui: true, + protocols: ["tcp", "http"], + }, + }, + dependencies: {}, + backup: { + create: { + type: "docker", + image: "compat", + system: true, + entrypoint: "compat", + args: ["duplicity", "create", "/mnt/backup", "/data"], + mounts: { + BACKUP: "/mnt/backup", + main: "/data", + }, + }, + restore: { + type: "docker", + image: "compat", + system: true, + entrypoint: "compat", + args: ["duplicity", "restore", "/mnt/backup", "/data"], + mounts: { + BACKUP: "/mnt/backup", + main: "/data", + }, + }, + }, + actions: { + "reset-first-user": { + name: "Reset First User", + description: + "This action will reset the password of the first user in your database to a random value.", + "allowed-statuses": ["stopped"], + implementation: { + type: "docker", + image: "main", + system: false, + entrypoint: "docker_entrypoint.sh", + args: ["reset-first-user"], + mounts: { + main: "/data", + }, + "io-format": "json", + }, + }, + }, + migrations: { + from: { + "*": { + type: "script", + args: ["from"], + }, + }, + to: { + "*": { + type: "script", + args: ["to"], + }, + }, + }, +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap index 9eb6e97cf..01e2d0763 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap @@ -750,6 +750,283 @@ exports[`transformConfigSpec transformConfigSpec(nostr) 1`] = ` } `; +exports[`transformConfigSpec transformConfigSpec(nostr2) 1`] = ` +{ + "relay-type": { + "default": "private", + "description": "Private or public. A private relay (highly recommended) restricts write access to specific pubkeys. Anyone can write to a public relay.", + "disabled": false, + "immutable": false, + "name": "Relay Type", + "required": true, + "type": "union", + "variants": { + "private": { + "name": "Private", + "spec": { + "pubkey_whitelist": { + "default": [], + "description": "A list of pubkeys that are permitted to publish through your relay. A minimum, you need to enter your own Nostr hex (not npub) pubkey. Go to https://damus.io/key/ to convert from npub to hex.", + "disabled": false, + "maxLength": null, + "minLength": 1, + "name": "Pubkey Whitelist (hex)", + "spec": { + "generate": null, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "patterns": [ + { + "description": "Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.", + "regex": "[0-9a-fA-F]{64}", + }, + ], + "placeholder": "hex (not npub) pubkey", + "type": "text", + }, + "type": "list", + "warning": null, + }, + }, + }, + "public": { + "name": "Public", + "spec": { + "info": { + "description": "General public info about your relay", + "name": "Relay Info", + "spec": { + "contact": { + "default": null, + "description": "The email address of the relay administrator", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Admin contact email", + "patterns": [ + { + "description": "Must be a valid email address.", + "regex": "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+", + }, + ], + "placeholder": null, + "required": false, + "type": "text", + "warning": null, + }, + "description": { + "default": null, + "description": "A more detailed description for your relay", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Relay Description", + "patterns": [ + { + "description": "Must be at least 6 character and no more than 256 characters", + "regex": ".{6,256}", + }, + ], + "placeholder": "The best relay in town", + "required": false, + "type": "text", + "warning": null, + }, + "name": { + "default": null, + "description": "Your relay's human-readable identifier", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Relay Name", + "patterns": [ + { + "description": "Must be at least 3 character and no more than 32 characters", + "regex": ".{3,32}", + }, + ], + "placeholder": "Bob's Public Relay", + "required": false, + "type": "text", + "warning": null, + }, + "pubkey": { + "default": null, + "description": "The Nostr hex (not npub) pubkey of the relay administrator", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Admin contact pubkey (hex)", + "patterns": [ + { + "description": "Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.", + "regex": "[0-9a-fA-F]{64}", + }, + ], + "placeholder": "hex (not npub) pubkey", + "required": false, + "type": "text", + "warning": null, + }, + }, + "type": "object", + "warning": null, + }, + "limits": { + "description": "Data limits to protect your relay from using too many resources", + "name": "Limits", + "spec": { + "event_kind_blacklist": { + "default": [], + "description": "Events with these kinds will be discarded. For a list of event kinds, see here: https://github.com/nostr-protocol/nips#event-kinds", + "disabled": false, + "maxLength": null, + "minLength": null, + "name": "Event Kind Blacklist", + "spec": { + "generate": null, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "patterns": [ + { + "description": "Integral number type", + "regex": "[0-9]+", + }, + ], + "placeholder": "30023", + "type": "text", + }, + "type": "list", + "warning": null, + }, + "max_blocking_threads": { + "default": 16, + "description": "Maximum number of blocking threads used for database connections.", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": null, + "name": "Max Blocking Threads", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "threads", + "warning": null, + }, + "max_event_bytes": { + "default": 131072, + "description": "Limit the maximum size of an EVENT message. Set to 0 for unlimited", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": null, + "name": "Max Event Size", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "bytes", + "warning": null, + }, + "max_ws_frame_bytes": { + "default": 131072, + "description": "Maximum WebSocket frame size in bytes.", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": null, + "name": "Max Websocket Frame Size", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "bytes", + "warning": null, + }, + "max_ws_message_bytes": { + "default": 131072, + "description": "Maximum WebSocket message in bytes.", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": null, + "name": "Max Websocket Message Size", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "bytes", + "warning": null, + }, + "messages_per_sec": { + "default": 2, + "description": "Limit events created per second, averaged over one minute. Note: this is for the server as a whole, not per connection.", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": 1, + "name": "Messages Per Second Limit", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "messages/sec", + "warning": null, + }, + "subscriptions_per_min": { + "default": 10, + "description": "Limit client subscriptions created per second, averaged over one minute. Strongly recommended to set this to a low value such as 10 to ensure fair service.", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": 1, + "name": "Subscriptions Per Minute Limit", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "subscriptions", + "warning": null, + }, + }, + "type": "object", + "warning": null, + }, + }, + }, + }, + "warning": null, + }, +} +`; + exports[`transformConfigSpec transformConfigSpec(searNXG) 1`] = ` { "enable-metrics": { diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.test.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.test.ts new file mode 100644 index 000000000..3730dd3b6 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.test.ts @@ -0,0 +1,12 @@ +import { matchManifest } from "./matchManifest" +import giteaManifest from "./__fixtures__/giteaManifest" +import synapseManifest from "./__fixtures__/synapseManifest" + +describe("matchManifest", () => { + test("gittea", () => { + matchManifest.unsafeCast(giteaManifest) + }) + test("synapse", () => { + matchManifest.unsafeCast(synapseManifest) + }) +}) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts index 8ce6cabbc..bd8856b42 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts @@ -55,10 +55,13 @@ export const matchManifest = object( string, every( matchProcedure, - object({ - name: string, - ["success-message"]: string, - }), + object( + { + name: string, + ["success-message"]: string, + }, + ["success-message"], + ), ), ]), config: object({ diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.test.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.test.ts index 79caef377..93b43910b 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.test.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.test.ts @@ -3,6 +3,7 @@ import fixtureEmbasyPagesConfig from "./__fixtures__/embasyPagesConfig" import searNXG from "./__fixtures__/searNXG" import bitcoind from "./__fixtures__/bitcoind" import nostr from "./__fixtures__/nostr" +import nostrConfig2 from "./__fixtures__/nostrConfig2" describe("transformConfigSpec", () => { test("matchOldConfigSpec(embassyPages.homepage.variants[web-page])", () => { @@ -30,4 +31,8 @@ describe("transformConfigSpec", () => { const spec = matchOldConfigSpec.unsafeCast(nostr) expect(transformConfigSpec(spec)).toMatchSnapshot() }) + test("transformConfigSpec(nostr2)", () => { + const spec = matchOldConfigSpec.unsafeCast(nostrConfig2) + expect(transformConfigSpec(spec)).toMatchSnapshot() + }) }) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts index 706e0b941..5ce601c57 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts @@ -74,7 +74,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec { integer: oldVal.integral, step: null, units: oldVal.units || null, - placeholder: oldVal.placeholder || null, + placeholder: oldVal.placeholder ? String(oldVal.placeholder) : null, } } else if (oldVal.type === "object") { newVal = { @@ -267,6 +267,31 @@ function getListSpec( {}, ), } + } else if (isNumberList(oldVal)) { + return { + ...partial, + type: "list", + default: oldVal.default.map(String) as string[], + spec: { + type: "text", + patterns: oldVal.spec.integral + ? [{ regex: "[0-9]+", description: "Integral number type" }] + : [ + { + regex: "[-+]?[0-9]*\\.?[0-9]+", + description: "Number type", + }, + ], + minLength: null, + maxLength: null, + masked: false, + generate: null, + inputmode: "text", + placeholder: oldVal.spec.placeholder + ? String(oldVal.spec.placeholder) + : null, + }, + } } else if (isStringList(oldVal)) { return { ...partial, @@ -337,11 +362,16 @@ function isStringList( ): val is OldValueSpecList & { subtype: "string" } { return val.subtype === "string" } +function isNumberList( + val: OldValueSpecList, +): val is OldValueSpecList & { subtype: "number" } { + return val.subtype === "number" +} function isObjectList( val: OldValueSpecList, ): val is OldValueSpecList & { subtype: "object" } { - if (["number", "union"].includes(val.subtype)) { + if (["union"].includes(val.subtype)) { throw new Error("Invalid list subtype. enum, string, and object permitted.") } return val.subtype === "object" @@ -398,7 +428,7 @@ export const matchOldValueSpecNumber = object( description: string, warning: string, units: string, - placeholder: string, + placeholder: anyOf(number, string), }, ["default", "description", "warning", "units", "placeholder"], ) @@ -499,6 +529,15 @@ const matchOldListValueSpecEnum = object({ values: array(string), "value-names": dictionary([string, string]), }) +const matchOldListValueSpecNumber = object( + { + range: string, + integral: boolean, + units: string, + placeholder: anyOf(number, string), + }, + ["units", "placeholder"], +) // represents a spec for a list const matchOldValueSpecList = every( @@ -531,6 +570,10 @@ const matchOldValueSpecList = every( subtype: literals("object"), spec: matchOldListValueSpecObject, }), + object({ + subtype: literals("number"), + spec: matchOldListValueSpecNumber, + }), ), ) type OldValueSpecList = typeof matchOldValueSpecList._TYPE From 46a893a8b663f49d683cd2c1d5e6d729b9866120 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Sat, 10 Aug 2024 05:46:54 -0600 Subject: [PATCH 10/10] fix bug with setup wiz recovery --- .../setup-wizard/src/app/pages/recover/recover.page.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts b/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts index dd1dae9ce..a3b03049e 100644 --- a/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts +++ b/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts @@ -4,7 +4,7 @@ import { ErrorService } from '@start9labs/shared' import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page' import { ApiService, - StartOSDiskInfoWithId, + StartOSDiskInfoFull, } from 'src/app/services/api/api.service' import { StateService } from 'src/app/services/state.service' import { PasswordPage } from '../../modals/password/password.page' @@ -16,7 +16,7 @@ import { PasswordPage } from '../../modals/password/password.page' }) export class RecoverPage { loading = true - servers: StartOSDiskInfoWithId[] = [] + servers: StartOSDiskInfoFull[] = [] constructor( private readonly apiService: ApiService, @@ -78,7 +78,7 @@ export class RecoverPage { await modal.present() } - async select(server: StartOSDiskInfoWithId) { + async select(server: StartOSDiskInfoFull) { const modal = await this.modalController.create({ component: PasswordPage, componentProps: { passwordHash: server.passwordHash }, @@ -90,7 +90,7 @@ export class RecoverPage { type: 'backup', target: { type: 'disk', - logicalname: res.data.logicalname, + logicalname: server.partition.logicalname, }, serverId: server.id, password: res.data.password,