diff --git a/container-runtime/src/Adapters/HostSystemStartOs.ts b/container-runtime/src/Adapters/HostSystemStartOs.ts index e7db00e07..b745799e1 100644 --- a/container-runtime/src/Adapters/HostSystemStartOs.ts +++ b/container-runtime/src/Adapters/HostSystemStartOs.ts @@ -256,6 +256,18 @@ export class HostSystemStartOs implements Effects { T.Effects["setDependencies"] > } + checkDependencies( + options: Parameters[0], + ): ReturnType { + return this.rpcRound("checkDependencies", options) as ReturnType< + T.Effects["checkDependencies"] + > + } + getDependencies(): ReturnType { + return this.rpcRound("getDependencies", null) as ReturnType< + T.Effects["getDependencies"] + > + } setHealth(...[options]: Parameters) { return this.rpcRound("setHealth", options) as ReturnType< T.Effects["setHealth"] diff --git a/core/Cargo.lock b/core/Cargo.lock index 455272344..a93a77e1e 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -3121,9 +3121,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" diff --git a/core/models/src/version.rs b/core/models/src/version.rs index 1e4798ba1..012f362aa 100644 --- a/core/models/src/version.rs +++ b/core/models/src/version.rs @@ -3,8 +3,10 @@ use std::ops::Deref; use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use ts_rs::TS; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, TS)] +#[ts(type = "string")] pub struct Version { version: emver::Version, string: String, diff --git a/core/startos/src/registry/admin.rs b/core/startos/src/registry/admin.rs index 5fc538602..d416ebdbd 100644 --- a/core/startos/src/registry/admin.rs +++ b/core/startos/src/registry/admin.rs @@ -74,12 +74,14 @@ async fn do_upload( mut url: Url, user: &str, pass: &str, + pkg_id: &str, body: Body, ) -> Result<(), Error> { url.set_path("/admin/v0/upload"); let req = httpc .post(url) .header(header::ACCEPT, "text/plain") + .query(&["id", pkg_id]) .basic_auth(user, Some(pass)) .body(body) .build()?; @@ -198,6 +200,7 @@ pub async fn publish( registry.clone(), &user, &pass, + &pkg.id, Body::wrap_stream(file_stream), ) .await?; diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index c60f8b230..0ad45aad1 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -11,6 +11,7 @@ use clap::Parser; use emver::VersionRange; use imbl::OrdMap; use imbl_value::{json, InternedString}; +use itertools::Itertools; use models::{ ActionId, DataUrl, HealthCheckId, HostId, Id, ImageId, PackageId, ServiceInterfaceId, VolumeId, }; @@ -23,6 +24,7 @@ use url::Url; use crate::db::model::package::{ ActionMetadata, CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, + ManifestPreference, }; use crate::disk::mount::filesystem::idmapped::IdMapped; use crate::disk::mount::filesystem::loop_dev::LoopDev; @@ -154,6 +156,18 @@ pub fn service_effect_handler() -> ParentHandler { .no_display() .with_remote_cli::(), ) + .subcommand( + "getDependencies", + from_fn_async(get_dependencies) + .no_display() + .with_remote_cli::(), + ) + .subcommand( + "checkDependencies", + from_fn_async(check_dependencies) + .no_display() + .with_remote_cli::(), + ) .subcommand("getSystemSmtp", from_fn_async(get_system_smtp).no_cli()) .subcommand("getContainerIp", from_fn_async(get_container_ip).no_cli()) .subcommand( @@ -178,6 +192,7 @@ pub fn service_effect_handler() -> ParentHandler { .subcommand("removeAction", from_fn_async(remove_action).no_cli()) .subcommand("reverseProxy", from_fn_async(reverse_proxy).no_cli()) .subcommand("mount", from_fn_async(mount).no_cli()) + // TODO Callbacks } @@ -1270,3 +1285,138 @@ async fn set_dependencies( }) .await } + +async fn get_dependencies(ctx: EffectContext) -> Result, Error> { + let ctx = ctx.deref()?; + let id = &ctx.id; + let db = ctx.ctx.db.peek().await; + let data = db + .as_public() + .as_package_data() + .as_idx(id) + .or_not_found(id)? + .as_current_dependencies() + .de()?; + + data.0 + .into_iter() + .map(|(id, current_dependency_info)| { + let CurrentDependencyInfo { + registry_url, + version_spec, + kind, + .. + } = current_dependency_info; + Ok::<_, Error>(match kind { + CurrentDependencyKind::Exists => DependencyRequirement::Exists { + id, + registry_url, + version_spec, + }, + CurrentDependencyKind::Running { health_checks } => { + DependencyRequirement::Running { + id, + health_checks, + version_spec, + registry_url, + } + } + }) + }) + .try_collect() +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "camelCase")] +#[ts(export)] +struct CheckDependenciesParam { + package_ids: Option>, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +struct CheckDependenciesResult { + package_id: PackageId, + is_installed: bool, + is_running: bool, + health_checks: Vec, + #[ts(type = "string | null")] + version: Option, +} + +async fn check_dependencies( + ctx: EffectContext, + CheckDependenciesParam { package_ids }: CheckDependenciesParam, +) -> Result, Error> { + let ctx = ctx.deref()?; + let db = ctx.ctx.db.peek().await; + let current_dependencies = db + .as_public() + .as_package_data() + .as_idx(&ctx.id) + .or_not_found(&ctx.id)? + .as_current_dependencies() + .de()?; + let package_ids: Vec<_> = package_ids + .unwrap_or_else(|| current_dependencies.0.keys().cloned().collect()) + .into_iter() + .filter_map(|x| { + let info = current_dependencies.0.get(&x)?; + Some((x, info)) + }) + .collect(); + let mut results = Vec::with_capacity(package_ids.len()); + + for (package_id, dependency_info) in package_ids { + let Some(package) = db.as_public().as_package_data().as_idx(&package_id) else { + results.push(CheckDependenciesResult { + package_id, + is_installed: false, + is_running: false, + health_checks: vec![], + version: None, + }); + continue; + }; + let installed_version = package + .as_state_info() + .as_manifest(ManifestPreference::New) + .as_version() + .de()? + .into_version(); + let version = Some(installed_version.clone()); + if !installed_version.satisfies(&dependency_info.version_spec) { + results.push(CheckDependenciesResult { + package_id, + is_installed: false, + is_running: false, + health_checks: vec![], + version, + }); + continue; + } + let is_installed = true; + let status = package.as_status().as_main().de()?; + let is_running = if is_installed { + status.running() + } else { + false + }; + let health_checks = status + .health() + .cloned() + .unwrap_or_default() + .into_iter() + .map(|(_, val)| val) + .collect(); + results.push(CheckDependenciesResult { + package_id, + is_installed, + is_running, + health_checks, + version, + }); + } + Ok(results) +} diff --git a/core/startos/src/status/health_check.rs b/core/startos/src/status/health_check.rs index cd5616527..90b20f8c5 100644 --- a/core/startos/src/status/health_check.rs +++ b/core/startos/src/status/health_check.rs @@ -1,7 +1,12 @@ +use std::str::FromStr; + +use clap::builder::ValueParserFactory; pub use models::HealthCheckId; use serde::{Deserialize, Serialize}; use ts_rs::TS; +use crate::util::clap::FromStrParser; + #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, TS)] #[serde(rename_all = "camelCase")] pub struct HealthCheckResult { @@ -9,6 +14,45 @@ pub struct HealthCheckResult { #[serde(flatten)] pub kind: HealthCheckResultKind, } +// healthCheckName:kind:message OR healthCheckName:kind +impl FromStr for HealthCheckResult { + type Err = color_eyre::eyre::Report; + fn from_str(s: &str) -> Result { + let from_parts = |name: &str, kind: &str, message: Option<&str>| { + let message = message.map(|x| x.to_string()); + let kind = match kind { + "success" => HealthCheckResultKind::Success { message }, + "disabled" => HealthCheckResultKind::Disabled { message }, + "starting" => HealthCheckResultKind::Starting { message }, + "loading" => HealthCheckResultKind::Loading { + message: message.unwrap_or_default(), + }, + "failure" => HealthCheckResultKind::Failure { + message: message.unwrap_or_default(), + }, + _ => return Err(color_eyre::eyre::eyre!("Invalid health check kind")), + }; + Ok(Self { + name: name.to_string(), + kind, + }) + }; + let parts = s.split(':').collect::>(); + match &*parts { + [name, kind, message] => from_parts(name, kind, Some(message)), + [name, kind] => from_parts(name, kind, None), + _ => Err(color_eyre::eyre::eyre!( + "Could not match the shape of the result ${parts:?}" + )), + } + } +} +impl ValueParserFactory for HealthCheckResult { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, TS)] #[serde(rename_all = "camelCase")] diff --git a/core/startos/src/status/mod.rs b/core/startos/src/status/mod.rs index 645c082f4..09d10fb2d 100644 --- a/core/startos/src/status/mod.rs +++ b/core/startos/src/status/mod.rs @@ -91,4 +91,13 @@ impl MainStatus { }; MainStatus::BackingUp { started, health } } + + pub fn health(&self) -> Option<&OrdMap> { + match self { + MainStatus::Running { health, .. } => Some(health), + MainStatus::BackingUp { health, .. } => Some(health), + MainStatus::Stopped | MainStatus::Stopping { .. } | MainStatus::Restarting => None, + MainStatus::Starting { .. } => None, + } + } } diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index 43dc3ba5f..4d7514d88 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -24,7 +24,7 @@ import { ValidIfNoStupidEscape, } from "./types" import * as patterns from "./util/patterns" -import { DependencyConfig, Update } from "./dependencyConfig/DependencyConfig" +import { DependencyConfig, Update } from "./dependencies/DependencyConfig" import { BackupSet, Backups } from "./backup/Backups" import { smtpConfig } from "./config/configConstants" import { Daemons } from "./mainFn/Daemons" @@ -35,7 +35,7 @@ import { List } from "./config/builder/list" import { Migration } from "./inits/migrations/Migration" import { Install, InstallFn } from "./inits/setupInstall" import { setupActions } from "./actions/setupActions" -import { setupDependencyConfig } from "./dependencyConfig/setupDependencyConfig" +import { setupDependencyConfig } from "./dependencies/setupDependencyConfig" import { SetupBackupsParams, setupBackups } from "./backup/setupBackups" import { setupInit } from "./inits/setupInit" import { @@ -77,6 +77,7 @@ import * as T from "./types" import { Checker, EmVer } from "./emverLite/mod" import { ExposedStorePaths } from "./store/setupExposeStore" import { PathBuilder, extractJsonPath, pathBuilder } from "./store/PathBuilder" +import { checkAllDependencies } from "./dependencies/dependencies" // prettier-ignore type AnyNeverCond = @@ -124,6 +125,7 @@ export class StartSdk { } return { + checkAllDependencies, serviceInterface: { getOwn: (effects: E, id: ServiceInterfaceId) => removeConstType()( @@ -284,7 +286,7 @@ export class StartSdk { Type extends Record = ExtractConfigType, >( spec: ConfigType, - write: Save, + write: Save, read: Read, ) => setupConfig(spec, write, read), setupConfigRead: < @@ -301,7 +303,7 @@ export class StartSdk { | Config, never>, >( _configSpec: ConfigSpec, - fn: Save, + fn: Save, ) => fn, setupDependencyConfig: >( config: Config | Config, diff --git a/sdk/lib/config/configDependencies.ts b/sdk/lib/config/configDependencies.ts index be0475b0f..2ab091e18 100644 --- a/sdk/lib/config/configDependencies.ts +++ b/sdk/lib/config/configDependencies.ts @@ -1,9 +1,12 @@ import { SDKManifest } from "../manifest/ManifestTypes" -import { Dependency } from "../types" +import { Dependencies } from "../types" export type ConfigDependencies = { - exists(id: keyof T["dependencies"]): Dependency - running(id: keyof T["dependencies"], healthChecks: string[]): Dependency + exists(id: keyof T["dependencies"]): Dependencies[number] + running( + id: keyof T["dependencies"], + healthChecks: string[], + ): Dependencies[number] } export const configDependenciesSet = < @@ -13,7 +16,7 @@ export const configDependenciesSet = < return { id, kind: "exists", - } as Dependency + } as Dependencies[number] }, running(id: keyof T["dependencies"], healthChecks: string[]) { @@ -21,6 +24,6 @@ export const configDependenciesSet = < id, kind: "running", healthChecks, - } as Dependency + } as Dependencies[number] }, }) diff --git a/sdk/lib/config/setupConfig.ts b/sdk/lib/config/setupConfig.ts index a49e545a8..ba82dbad6 100644 --- a/sdk/lib/config/setupConfig.ts +++ b/sdk/lib/config/setupConfig.ts @@ -11,16 +11,13 @@ export type DependenciesReceipt = void & { } export type Save< - Store, A extends | Record | Config, any> | Config, never>, - Manifest extends SDKManifest, > = (options: { effects: Effects input: ExtractConfigType & Record - dependencies: D.ConfigDependencies }) => Promise<{ dependenciesReceipt: DependenciesReceipt interfacesReceipt: InterfacesReceipt @@ -53,7 +50,7 @@ export function setupConfig< Type extends Record = ExtractConfigType, >( spec: Config | Config, - write: Save, + write: Save, read: Read, ) { const validator = spec.validator @@ -66,9 +63,8 @@ export function setupConfig< await effects.clearBindings() await effects.clearServiceInterfaces() const { restart } = await write({ - input: JSON.parse(JSON.stringify(input)), + input: JSON.parse(JSON.stringify(input)) as any, effects, - dependencies: D.configDependenciesSet(), }) if (restart) { await effects.restart() diff --git a/sdk/lib/dependencyConfig/DependencyConfig.ts b/sdk/lib/dependencies/DependencyConfig.ts similarity index 100% rename from sdk/lib/dependencyConfig/DependencyConfig.ts rename to sdk/lib/dependencies/DependencyConfig.ts diff --git a/sdk/lib/dependencies/dependencies.ts b/sdk/lib/dependencies/dependencies.ts new file mode 100644 index 000000000..c074d2ad7 --- /dev/null +++ b/sdk/lib/dependencies/dependencies.ts @@ -0,0 +1,115 @@ +import { + Effects, + PackageId, + DependencyRequirement, + SetHealth, + CheckDependencyResult, +} from "../types" + +export type CheckAllDependencies = { + notRunning: () => Promise + + notInstalled: () => Promise + + healthErrors: () => Promise<{ [id: string]: SetHealth[] }> + throwIfNotRunning: () => Promise + throwIfNotValid: () => Promise + throwIfNotInstalled: () => Promise + throwIfError: () => Promise + isValid: () => Promise +} +export function checkAllDependencies(effects: Effects): CheckAllDependencies { + const dependenciesPromise = effects.getDependencies() + const resultsPromise = dependenciesPromise.then((dependencies) => + effects.checkDependencies({ + packageIds: dependencies.map((dep) => dep.id), + }), + ) + + 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 = result.healthChecks + .filter((x) => dependency.healthChecks.includes(x.id)) + .filter((x) => !!x.message) + if (healthChecks.length === 0) continue + answer[result.packageId] = healthChecks + } + return answer + } + const notInstalled = () => + resultsPromise.then((x) => x.filter((x) => !x.isInstalled)) + const notRunning = async () => { + const results = await resultsPromise + const dependenciesById = await dependenciesByIdPromise + return results.filter((x) => { + const dependency = dependenciesById[x.packageId] + if (!dependency) return false + if (dependency.kind !== "running") return false + return !x.isRunning + }) + } + const entries = (x: { [k: string]: B }) => Object.entries(x) + const first = (x: A[]): A | undefined => x[0] + const sinkVoid = (x: A) => void 0 + const throwIfError = () => + 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 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(), + throwIfError(), + ]).then(sinkVoid) + + const isValid = () => + throwIfNotValid().then( + () => true, + () => false, + ) + + return { + notRunning, + notInstalled, + healthErrors, + throwIfNotRunning, + throwIfNotValid, + throwIfNotInstalled, + throwIfError, + isValid, + } +} diff --git a/sdk/lib/dependencyConfig/index.ts b/sdk/lib/dependencies/index.ts similarity index 100% rename from sdk/lib/dependencyConfig/index.ts rename to sdk/lib/dependencies/index.ts diff --git a/sdk/lib/dependencyConfig/setupDependencyConfig.ts b/sdk/lib/dependencies/setupDependencyConfig.ts similarity index 100% rename from sdk/lib/dependencyConfig/setupDependencyConfig.ts rename to sdk/lib/dependencies/setupDependencyConfig.ts diff --git a/sdk/lib/health/HealthCheck.ts b/sdk/lib/health/HealthCheck.ts index e1fbee97b..d7bbc2f44 100644 --- a/sdk/lib/health/HealthCheck.ts +++ b/sdk/lib/health/HealthCheck.ts @@ -7,6 +7,7 @@ import { TriggerInput } from "../trigger/TriggerInput" import { defaultTrigger } from "../trigger/defaultTrigger" import { once } from "../util/once" import { Overlay } from "../util/Overlay" +import { object, unknown } from "ts-matches" export function healthCheck(o: { effects: Effects @@ -66,8 +67,7 @@ export function healthCheck(o: { return {} as HealthReceipt } function asMessage(e: unknown) { - if (typeof e === "object" && e != null && "message" in e) - return String(e.message) + if (object({ message: unknown }).test(e)) return String(e.message) const value = String(e) if (value.length == null) return null return value diff --git a/sdk/lib/index.browser.ts b/sdk/lib/index.browser.ts index ff422a7f2..d7c10093f 100644 --- a/sdk/lib/index.browser.ts +++ b/sdk/lib/index.browser.ts @@ -4,7 +4,7 @@ export { setupExposeStore } from "./store/setupExposeStore" export * as config from "./config" export * as CB from "./config/builder" export * as CT from "./config/configTypes" -export * as dependencyConfig from "./dependencyConfig" +export * as dependencyConfig from "./dependencies" export * as manifest from "./manifest" export * as types from "./types" export * as T from "./types" diff --git a/sdk/lib/index.ts b/sdk/lib/index.ts index ef3d2ccb0..89d147ed1 100644 --- a/sdk/lib/index.ts +++ b/sdk/lib/index.ts @@ -12,7 +12,7 @@ export * as backup from "./backup" export * as config from "./config" export * as CB from "./config/builder" export * as CT from "./config/configTypes" -export * as dependencyConfig from "./dependencyConfig" +export * as dependencyConfig from "./dependencies" export * as daemons from "./mainFn/Daemons" export * as health from "./health" export * as healthFns from "./health/checkFns" diff --git a/sdk/lib/interfaces/Host.ts b/sdk/lib/interfaces/Host.ts index ab64cd7b0..2f1353cc1 100644 --- a/sdk/lib/interfaces/Host.ts +++ b/sdk/lib/interfaces/Host.ts @@ -161,7 +161,7 @@ export class Host { options: BindOptionsByKnownProtocol, protoInfo: KnownProtocols[keyof KnownProtocols], ): AddSslOptions | null { - if ("noAddSsl" in options && options.noAddSsl) return null + if (inObject("noAddSsl", options) && options.noAddSsl) return null if ("withSsl" in protoInfo && protoInfo.withSsl) return { // addXForwardedHeaders: null, @@ -174,6 +174,13 @@ export class Host { } } +function inObject( + key: Key, + obj: any, +): obj is { [K in Key]: unknown } { + return key in obj +} + export class StaticHost extends Host { constructor(options: { effects: Effects; id: string }) { super({ ...options, kind: "static" }) diff --git a/sdk/lib/osBindings/CheckDependenciesParam.ts b/sdk/lib/osBindings/CheckDependenciesParam.ts new file mode 100644 index 000000000..54580a7ff --- /dev/null +++ b/sdk/lib/osBindings/CheckDependenciesParam.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PackageId } from "./PackageId" + +export type CheckDependenciesParam = { packageIds: Array | null } diff --git a/sdk/lib/osBindings/CheckDependenciesResult.ts b/sdk/lib/osBindings/CheckDependenciesResult.ts new file mode 100644 index 000000000..c102c733a --- /dev/null +++ b/sdk/lib/osBindings/CheckDependenciesResult.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HealthCheckResult } from "./HealthCheckResult" +import type { PackageId } from "./PackageId" + +export type CheckDependenciesResult = { + packageId: PackageId + isInstalled: boolean + isRunning: boolean + healthChecks: Array + version: string | null +} diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts index 67129fba1..215f16b8b 100644 --- a/sdk/lib/osBindings/index.ts +++ b/sdk/lib/osBindings/index.ts @@ -12,6 +12,8 @@ export { BindInfo } from "./BindInfo" export { BindOptions } from "./BindOptions" export { BindParams } from "./BindParams" export { Callback } from "./Callback" +export { CheckDependenciesParam } from "./CheckDependenciesParam" +export { CheckDependenciesResult } from "./CheckDependenciesResult" export { ChrootParams } from "./ChrootParams" export { CreateOverlayedImageParams } from "./CreateOverlayedImageParams" export { CurrentDependencies } from "./CurrentDependencies" diff --git a/sdk/lib/test/startosTypeValidation.test.ts b/sdk/lib/test/startosTypeValidation.test.ts index 0743db4fa..fd11ab5b6 100644 --- a/sdk/lib/test/startosTypeValidation.test.ts +++ b/sdk/lib/test/startosTypeValidation.test.ts @@ -1,5 +1,5 @@ import { Effects } from "../types" -import { ExecuteAction } from ".././osBindings" +import { CheckDependenciesParam, ExecuteAction } from ".././osBindings" import { CreateOverlayedImageParams } from ".././osBindings" import { DestroyOverlayedImageParams } from ".././osBindings" import { BindParams } from ".././osBindings" @@ -64,6 +64,8 @@ describe("startosTypeValidation ", () => { removeAction: {} as RemoveActionParams, reverseProxy: {} as ReverseProxyParams, mount: {} as MountParams, + checkDependencies: {} as CheckDependenciesParam, + getDependencies: undefined, }) typeEquality[0]>( testInput as ExecuteAction, diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index d33bda79f..96e256766 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -1,6 +1,11 @@ export * as configTypes from "./config/configTypes" -import { HealthCheckId } from "./osBindings" -import { HealthCheckResult } from "./osBindings" + +import { + DependencyRequirement, + SetHealth, + HealthCheckResult, +} from "./osBindings" + import { MainEffects, ServiceInterfaceType, Signals } from "./StartSdk" import { InputSpec } from "./config/configTypes" import { DependenciesReceipt } from "./config/setupConfig" @@ -11,6 +16,7 @@ import { ExposedStorePaths } from "./store/setupExposeStore" import { UrlString } from "./util/getServiceInterface" export * from "./osBindings" export { SDKManifest } from "./manifest/ManifestTypes" +export { HealthReceipt } from "./health/HealthReceipt" export type ExportedAction = (options: { effects: Effects @@ -471,16 +477,22 @@ export type Effects = { algorithm: "ecdsa" | "ed25519" | null }) => Promise - setHealth( - o: HealthCheckResult & { - id: HealthCheckId - }, - ): Promise + setHealth(o: SetHealth): Promise /** Set the dependencies of what the service needs, usually ran during the set config as a best practice */ setDependencies(options: { dependencies: Dependencies }): Promise + + /** Get the list of the dependencies, both the dynamic set by the effect of setDependencies and the end result any required in the manifest */ + getDependencies(): Promise + + /** When one wants to checks the status of several services during the checking of dependencies. The result will include things like the status + * of the service and what the current health checks are. + */ + checkDependencies(options: { + packageIds: PackageId[] | null + }): Promise /** Exists could be useful during the runtime to know if some service exists, option dep */ exists(options: { packageId: PackageId }): Promise /** Exists could be useful during the runtime to know if some service is running, option dep */ @@ -578,13 +590,17 @@ export type KnownError = errorCode: [number, string] | readonly [number, string] } -export type Dependency = { - id: PackageId - versionSpec: string - registryUrl: string -} & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] }) -export type Dependencies = Array +export type Dependencies = Array export type DeepPartial = T extends {} ? { [P in keyof T]?: DeepPartial } : T + +export type CheckDependencyResult = { + packageId: PackageId + isInstalled: boolean + isRunning: boolean + healthChecks: SetHealth[] + version: string | null +} +export type CheckResults = CheckDependencyResult[] diff --git a/sdk/lib/util/Overlay.ts b/sdk/lib/util/Overlay.ts index 50c596801..fecb718d5 100644 --- a/sdk/lib/util/Overlay.ts +++ b/sdk/lib/util/Overlay.ts @@ -77,7 +77,7 @@ export class Overlay { stdout: string | Buffer stderr: string | Buffer }> { - const imageMeta = await fs + const imageMeta: any = await fs .readFile(`/media/startos/images/${this.imageId}.json`, { encoding: "utf8", }) @@ -147,7 +147,7 @@ export class Overlay { command: string[], options?: CommandOptions, ): Promise { - const imageMeta = await fs + const imageMeta: any = await fs .readFile(`/media/startos/images/${this.imageId}.json`, { encoding: "utf8", })