mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
sdk updates
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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<HealthCheckId, HealthCheckResult>,
|
||||
health_checks: BTreeMap<HealthCheckId, NamedHealthCheckResult>,
|
||||
#[ts(type = "string | null")]
|
||||
version: Option<exver::ExtendedVersion>,
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -293,8 +293,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,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"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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