mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 12:33:40 +00:00
12
.github/workflows/startos-iso.yaml
vendored
12
.github/workflows/startos-iso.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
@@ -799,12 +798,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 +818,7 @@ export class SystemForEmbassy implements System {
|
||||
JSON.stringify(formData),
|
||||
],
|
||||
timeoutMs,
|
||||
{ destroy: shouldDestroy },
|
||||
)
|
||||
).stdout.toString(),
|
||||
)
|
||||
|
||||
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -17,7 +19,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;
|
||||
@@ -316,12 +318,16 @@ pub struct CheckDependenciesParam {
|
||||
#[ts(export)]
|
||||
pub struct CheckDependenciesResult {
|
||||
package_id: PackageId,
|
||||
is_installed: bool,
|
||||
#[ts(type = "string | null")]
|
||||
title: Option<InternedString>,
|
||||
#[ts(type = "string | null")]
|
||||
installed_version: Option<exver::ExtendedVersion>,
|
||||
#[ts(type = "string[]")]
|
||||
satisfies: BTreeSet<VersionString>,
|
||||
is_running: bool,
|
||||
config_satisfied: bool,
|
||||
health_checks: BTreeMap<HealthCheckId, HealthCheckResult>,
|
||||
#[ts(type = "string | null")]
|
||||
version: Option<exver::ExtendedVersion>,
|
||||
#[ts(as = "BTreeMap::<HealthCheckId, NamedHealthCheckResult>")]
|
||||
health_checks: OrdMap<HealthCheckId, NamedHealthCheckResult>,
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<HealthCheckId, HealthCheckResult>,
|
||||
health: OrdMap<HealthCheckId, NamedHealthCheckResult>,
|
||||
started: DateTime<Utc>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Self, Self::Err> {
|
||||
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<Self>;
|
||||
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<String> },
|
||||
Disabled { message: Option<String> },
|
||||
Starting { message: Option<String> },
|
||||
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})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Utc>,
|
||||
#[ts(as = "BTreeMap<HealthCheckId, HealthCheckResult>")]
|
||||
health: OrdMap<HealthCheckId, HealthCheckResult>,
|
||||
#[ts(as = "BTreeMap<HealthCheckId, NamedHealthCheckResult>")]
|
||||
health: OrdMap<HealthCheckId, NamedHealthCheckResult>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
BackingUp {
|
||||
#[ts(type = "string | null")]
|
||||
started: Option<DateTime<Utc>>,
|
||||
#[ts(as = "BTreeMap<HealthCheckId, HealthCheckResult>")]
|
||||
health: OrdMap<HealthCheckId, HealthCheckResult>,
|
||||
#[ts(as = "BTreeMap<HealthCheckId, NamedHealthCheckResult>")]
|
||||
health: OrdMap<HealthCheckId, NamedHealthCheckResult>,
|
||||
},
|
||||
}
|
||||
impl MainStatus {
|
||||
@@ -93,7 +95,7 @@ impl MainStatus {
|
||||
MainStatus::BackingUp { started, health }
|
||||
}
|
||||
|
||||
pub fn health(&self) -> Option<&OrdMap<HealthCheckId, HealthCheckResult>> {
|
||||
pub fn health(&self) -> Option<&OrdMap<HealthCheckId, NamedHealthCheckResult>> {
|
||||
match self {
|
||||
MainStatus::Running { health, .. } => Some(health),
|
||||
MainStatus::BackingUp { health, .. } => Some(health),
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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<Manifest extends T.Manifest, Store> {
|
||||
}
|
||||
|
||||
return {
|
||||
checkAllDependencies,
|
||||
checkDependencies: checkDependencies as <
|
||||
DependencyId extends keyof Manifest["dependencies"] &
|
||||
PackageId = keyof Manifest["dependencies"] & PackageId,
|
||||
>(
|
||||
effects: Effects,
|
||||
packageIds?: DependencyId[],
|
||||
) => Promise<CheckDependencies<DependencyId>>,
|
||||
serviceInterface: {
|
||||
getOwn: <E extends Effects>(effects: E, id: ServiceInterfaceId) =>
|
||||
removeCallbackTypes<E>(effects)(
|
||||
@@ -247,7 +256,6 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
||||
id: string
|
||||
description: string
|
||||
hasPrimary: boolean
|
||||
disabled: boolean
|
||||
type: ServiceInterfaceType
|
||||
username: null | string
|
||||
path: string
|
||||
@@ -293,8 +301,8 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
||||
)
|
||||
},
|
||||
HealthCheck: {
|
||||
of(o: HealthCheckParams<Manifest>) {
|
||||
return healthCheck<Manifest>(o)
|
||||
of(o: HealthCheckParams) {
|
||||
return healthCheck(o)
|
||||
},
|
||||
},
|
||||
Dependency: {
|
||||
|
||||
@@ -1,131 +1,206 @@
|
||||
import { ExtendedVersion, VersionRange } from "../exver"
|
||||
import {
|
||||
Effects,
|
||||
PackageId,
|
||||
DependencyRequirement,
|
||||
SetHealth,
|
||||
CheckDependenciesResult,
|
||||
HealthCheckId,
|
||||
} from "../types"
|
||||
|
||||
export type CheckAllDependencies = {
|
||||
notInstalled: () => Promise<CheckDependenciesResult[]>
|
||||
notRunning: () => Promise<CheckDependenciesResult[]>
|
||||
configNotSatisfied: () => Promise<CheckDependenciesResult[]>
|
||||
healthErrors: () => Promise<{ [id: string]: SetHealth[] }>
|
||||
export type CheckDependencies<DependencyId extends PackageId = PackageId> = {
|
||||
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<boolean>
|
||||
|
||||
throwIfNotRunning: () => Promise<void>
|
||||
throwIfNotInstalled: () => Promise<void>
|
||||
throwIfConfigNotSatisfied: () => Promise<void>
|
||||
throwIfHealthError: () => Promise<void>
|
||||
|
||||
throwIfNotValid: () => Promise<void>
|
||||
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<CheckDependencies<DependencyId>> {
|
||||
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 = <B>(x: { [k: string]: B }) => Object.entries(x)
|
||||
const first = <A>(x: A[]): A | undefined => x[0]
|
||||
const sinkVoid = <A>(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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 extends string>(t: T & ValidateExVer<T>) => t
|
||||
|
||||
@@ -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<Manifest extends T.Manifest> = {
|
||||
export type HealthCheckParams = {
|
||||
effects: Effects
|
||||
name: string
|
||||
image: {
|
||||
id: keyof Manifest["images"] & T.ImageId
|
||||
sharedRun?: boolean
|
||||
}
|
||||
trigger?: Trigger
|
||||
fn(overlay: Overlay): Promise<CheckResult> | CheckResult
|
||||
fn(): Promise<HealthCheckResult> | HealthCheckResult
|
||||
onFirstSuccess?: () => unknown | Promise<unknown>
|
||||
}
|
||||
|
||||
export function healthCheck<Manifest extends T.Manifest>(
|
||||
o: HealthCheckParams<Manifest>,
|
||||
) {
|
||||
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
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { HealthStatus } from "../../types"
|
||||
|
||||
export type CheckResult = {
|
||||
status: HealthStatus
|
||||
message: string | null
|
||||
}
|
||||
3
sdk/lib/health/checkFns/HealthCheckResult.ts
Normal file
3
sdk/lib/health/checkFns/HealthCheckResult.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { T } from "../.."
|
||||
|
||||
export type HealthCheckResult = Omit<T.NamedHealthCheckResult, "name">
|
||||
@@ -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<CheckResult> {
|
||||
return Promise.race<CheckResult>([
|
||||
): Promise<HealthCheckResult> {
|
||||
return Promise.race<HealthCheckResult>([
|
||||
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}`,
|
||||
}),
|
||||
|
||||
@@ -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<CheckResult> => {
|
||||
): Promise<HealthCheckResult> => {
|
||||
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 }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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" } = {}) {
|
||||
|
||||
@@ -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<CheckResult> => {
|
||||
): Promise<HealthCheckResult> => {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ export class Origin<T extends Host> {
|
||||
name,
|
||||
description,
|
||||
hasPrimary,
|
||||
disabled,
|
||||
id,
|
||||
type,
|
||||
username,
|
||||
@@ -69,7 +68,6 @@ export class Origin<T extends Host> {
|
||||
name,
|
||||
description,
|
||||
hasPrimary,
|
||||
disabled,
|
||||
addressInfo,
|
||||
type,
|
||||
masked,
|
||||
|
||||
@@ -21,7 +21,6 @@ export class ServiceInterfaceBuilder {
|
||||
id: string
|
||||
description: string
|
||||
hasPrimary: boolean
|
||||
disabled: boolean
|
||||
type: ServiceInterfaceType
|
||||
username: string | null
|
||||
path: string
|
||||
|
||||
@@ -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> | CheckResult
|
||||
fn: () => Promise<HealthCheckResult> | HealthCheckResult
|
||||
trigger?: Trigger
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = <T>() => {
|
||||
@@ -21,10 +21,9 @@ const oncePromise = <T>() => {
|
||||
*
|
||||
*/
|
||||
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<Daemon>,
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Version, Satisfies> & {
|
||||
Manifest extends {
|
||||
dependencies: Dependencies
|
||||
id: Id
|
||||
assets: AssetTypes[]
|
||||
images: Record<ImagesTypes, SDKImageConfig>
|
||||
volumes: VolumesTypes[]
|
||||
version: Version
|
||||
},
|
||||
Satisfies extends string[] = [],
|
||||
>(manifest: Manifest & { version: Version }): Manifest & T.Manifest {
|
||||
>(manifest: SDKManifest<Version, Satisfies> & Manifest): Manifest & T.Manifest {
|
||||
const images = Object.entries(manifest.images).reduce(
|
||||
(images, [k, v]) => {
|
||||
v.arch = v.arch || ["aarch64", "x86_64"]
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// 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 = {
|
||||
packageId: PackageId
|
||||
isInstalled: boolean
|
||||
title: string | null
|
||||
installedVersion: string | null
|
||||
satisfies: string[]
|
||||
isRunning: boolean
|
||||
configSatisfied: boolean
|
||||
healthChecks: { [key: HealthCheckId]: HealthCheckResult }
|
||||
version: string | null
|
||||
healthChecks: { [key: HealthCheckId]: NamedHealthCheckResult }
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ export type ExportServiceInterfaceParams = {
|
||||
name: string
|
||||
description: string
|
||||
hasPrimary: boolean
|
||||
disabled: boolean
|
||||
masked: boolean
|
||||
addressInfo: AddressInfo
|
||||
type: ServiceInterfaceType
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
@@ -8,7 +8,6 @@ export type ServiceInterface = {
|
||||
name: string
|
||||
description: string
|
||||
hasPrimary: boolean
|
||||
disabled: boolean
|
||||
masked: boolean
|
||||
addressInfo: AddressInfo
|
||||
type: ServiceInterfaceType
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -16,7 +16,6 @@ describe("host", () => {
|
||||
id: "foo",
|
||||
description: "A Foo",
|
||||
hasPrimary: false,
|
||||
disabled: false,
|
||||
type: "ui",
|
||||
username: "bar",
|
||||
path: "/baz",
|
||||
|
||||
@@ -2,5 +2,4 @@ import { HealthStatus } from "../types"
|
||||
|
||||
export type TriggerInput = {
|
||||
lastResult?: HealthStatus
|
||||
hadSuccess?: boolean
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
33
sdk/lib/trigger/lastStatus.ts
Normal file
33
sdk/lib/trigger/lastStatus.ts
Normal file
@@ -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<unknown, unknown, never>
|
||||
} & { default: AsyncIterator<unknown, unknown, never> } = {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,23 @@ export class Overlay {
|
||||
return new Overlay(effects, id, rootfs, guid)
|
||||
}
|
||||
|
||||
static async with<T>(
|
||||
effects: T.Effects,
|
||||
image: { id: T.ImageId; sharedRun?: boolean },
|
||||
mounts: { options: MountOptions; path: string }[],
|
||||
fn: (overlay: Overlay) => Promise<T>,
|
||||
): Promise<T> {
|
||||
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<Overlay> {
|
||||
path = path.startsWith("/")
|
||||
? `${this.rootfs}${path}`
|
||||
|
||||
@@ -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 */
|
||||
|
||||
4
sdk/package-lock.json
generated
4
sdk/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"ackInstructions": {},
|
||||
"theme": "Dark",
|
||||
"widgets": [],
|
||||
"ack-welcome": "0.3.6-alpha.3"
|
||||
"ack-welcome": "0.3.6-alpha.4"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<ion-content class="ion-padding">
|
||||
<h2>This Release</h2>
|
||||
|
||||
<h4>0.3.6-alpha.3</h4>
|
||||
<h4>0.3.6-alpha.4</h4>
|
||||
<h6>This is an ALPHA release! DO NOT use for production data!</h6>
|
||||
<h6>Expect that any data you create or store on this version of the OS can be LOST FOREVER!</h6>
|
||||
|
||||
|
||||
@@ -10,15 +10,15 @@ import { ConnectionService } from 'src/app/services/connection.service'
|
||||
})
|
||||
export class AppShowHealthChecksComponent {
|
||||
@Input()
|
||||
healthChecks!: Record<string, T.HealthCheckResult>
|
||||
healthChecks!: Record<string, T.NamedHealthCheckResult>
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -14,7 +14,7 @@ export class ToHealthChecksPipe implements PipeTransform {
|
||||
|
||||
transform(
|
||||
manifest: T.Manifest,
|
||||
): Observable<Record<string, T.HealthCheckResult | null> | null> {
|
||||
): Observable<Record<string, T.NamedHealthCheckResult | null> | null> {
|
||||
return this.patch.watch$('packageData', manifest.id, 'status', 'main').pipe(
|
||||
map(main => {
|
||||
return main.status === 'running' && !isEmptyObject(main.health)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -535,7 +535,7 @@ export interface DependencyErrorConfigUnsatisfied {
|
||||
|
||||
export interface DependencyErrorHealthChecksFailed {
|
||||
type: 'healthChecksFailed'
|
||||
check: T.HealthCheckResult
|
||||
check: T.NamedHealthCheckResult
|
||||
}
|
||||
|
||||
export interface DependencyErrorTransitive {
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user