Merge branch 'integration/new-container-runtime' of github.com:Start9Labs/start-os into integration/new-container-runtime

This commit is contained in:
Aiden McClelland
2024-03-18 15:15:55 -06:00
111 changed files with 2332 additions and 2042 deletions

View File

@@ -26,7 +26,7 @@ PATCH_DB_CLIENT_SRC := $(shell git ls-files --recurse-submodules patch-db/client
GZIP_BIN := $(shell which pigz || which gzip) GZIP_BIN := $(shell which pigz || which gzip)
TAR_BIN := $(shell which gtar || which tar) TAR_BIN := $(shell which gtar || which tar)
COMPILED_TARGETS := $(BINS) system-images/compat/docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar system-images/binfmt/docker-images/$(ARCH).tar COMPILED_TARGETS := $(BINS) system-images/compat/docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar system-images/binfmt/docker-images/$(ARCH).tar
ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; fi) $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; fi') $(PLATFORM_FILE) ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; fi) $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; fi') $(PLATFORM_FILE) sdk/lib/test
ifeq ($(REMOTE),) ifeq ($(REMOTE),)
mkdir = mkdir -p $1 mkdir = mkdir -p $1
@@ -87,7 +87,7 @@ clean:
format: format:
cd core && cargo +nightly fmt cd core && cargo +nightly fmt
test: $(CORE_SRC) $(ENVIRONMENT_FILE) test: $(CORE_SRC) $(ENVIRONMENT_FILE)
cd core && cargo build && cargo test cd core && cargo build && cargo test
cli: cli:
@@ -109,7 +109,7 @@ results/$(BASENAME).$(IMAGE_TYPE) results/$(BASENAME).squashfs: $(IMAGE_RECIPE_S
./image-recipe/run-local-build.sh "results/$(BASENAME).deb" ./image-recipe/run-local-build.sh "results/$(BASENAME).deb"
# For creating os images. DO NOT USE # For creating os images. DO NOT USE
install: $(ALL_TARGETS) install: $(ALL_TARGETS)
$(call mkdir,$(DESTDIR)/usr/bin) $(call mkdir,$(DESTDIR)/usr/bin)
$(call cp,core/target/$(ARCH)-unknown-linux-musl/release/startbox,$(DESTDIR)/usr/bin/startbox) $(call cp,core/target/$(ARCH)-unknown-linux-musl/release/startbox,$(DESTDIR)/usr/bin/startbox)
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/startd) $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/startd)
@@ -173,11 +173,14 @@ container-runtime/node_modules: container-runtime/package.json container-runtime
npm --prefix container-runtime ci npm --prefix container-runtime ci
touch container-runtime/node_modules touch container-runtime/node_modules
core/startos/bindings: $(CORE_SRC) $(ENVIRONMENT_FILE) $(PLATFORM_FILE) core/startos/bindings: $(shell git ls-files core) $(ENVIRONMENT_FILE) $(PLATFORM_FILE)
(cd core/ && cargo test) (cd core/ && cargo test)
touch core/startos/bindings touch core/startos/bindings
sdk/dist: $(shell git ls-files sdk) core/startos/bindings sdk/lib/test: $(shell git ls-files sdk) core/startos/bindings
(cd sdk && make test)
sdk/dist: $(shell git ls-files sdk)
(cd sdk && make bundle) (cd sdk && make bundle)
container-runtime/dist: container-runtime/node_modules $(shell git ls-files container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json container-runtime/dist: container-runtime/node_modules $(shell git ls-files container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json
@@ -209,7 +212,8 @@ $(BINS): $(CORE_SRC) $(ENVIRONMENT_FILE)
cd core && ARCH=$(ARCH) ./build-prod.sh cd core && ARCH=$(ARCH) ./build-prod.sh
touch $(BINS) touch $(BINS)
web/node_modules: web/package.json web/node_modules: web/package.json sdk/dist
(cd sdk && make bundle)
npm --prefix web ci npm --prefix web ci
web/dist/raw/ui: $(WEB_UI_SRC) $(WEB_SHARED_SRC) web/dist/raw/ui: $(WEB_UI_SRC) $(WEB_SHARED_SRC)

View File

@@ -117,10 +117,7 @@ export class HostSystemStartOs implements Effects {
T.Effects["createOverlayedImage"] T.Effects["createOverlayedImage"]
> >
} }
destroyOverlayedImage(options: { destroyOverlayedImage(options: { guid: string }): Promise<void> {
imageId: string
guid: string
}): Promise<void> {
return this.rpcRound("destroyOverlayedImage", options) as ReturnType< return this.rpcRound("destroyOverlayedImage", options) as ReturnType<
T.Effects["destroyOverlayedImage"] T.Effects["destroyOverlayedImage"]
> >
@@ -196,16 +193,13 @@ export class HostSystemStartOs implements Effects {
T.Effects["getServicePortForward"] T.Effects["getServicePortForward"]
> >
} }
getSslCertificate( getSslCertificate(options: Parameters<T.Effects["getSslCertificate"]>[0]) {
...[packageId, algorithm]: Parameters<T.Effects["getSslCertificate"]> return this.rpcRound("getSslCertificate", options) as ReturnType<
) { T.Effects["getSslCertificate"]
return this.rpcRound("getSslCertificate", { >
packageId,
algorithm,
}) as ReturnType<T.Effects["getSslCertificate"]>
} }
getSslKey(...[packageId, algorithm]: Parameters<T.Effects["getSslKey"]>) { getSslKey(options: Parameters<T.Effects["getSslKey"]>[0]) {
return this.rpcRound("getSslKey", { packageId, algorithm }) as ReturnType< return this.rpcRound("getSslKey", options) as ReturnType<
T.Effects["getSslKey"] T.Effects["getSslKey"]
> >
} }

View File

@@ -33,11 +33,16 @@ export class DockerProcedureContainer {
await overlay.mount({ type: "assets", id: mount }, mounts[mount]) await overlay.mount({ type: "assets", id: mount }, mounts[mount])
} else if (volumeMount.type === "certificate") { } else if (volumeMount.type === "certificate") {
volumeMount volumeMount
const certChain = await effects.getSslCertificate( const certChain = await effects.getSslCertificate({
null, packageId: null,
volumeMount["interface-id"], hostId: volumeMount["interface-id"],
) algorithm: null,
const key = await effects.getSslKey(null, volumeMount["interface-id"]) })
const key = await effects.getSslKey({
packageId: null,
hostId: volumeMount["interface-id"],
algorithm: null,
})
await fs.writeFile( await fs.writeFile(
`${path}/${volumeMount["interface-id"]}.cert.pem`, `${path}/${volumeMount["interface-id"]}.cert.pem`,
certChain.join("\n"), certChain.join("\n"),

View File

@@ -186,6 +186,7 @@ export class MainLoop {
if ("result" in result) { if ("result" in result) {
await effects.setHealth({ await effects.setHealth({
message: null,
name: healthId, name: healthId,
status: "passing", status: "passing",
}) })

View File

@@ -1,8 +1,9 @@
use std::path::Path; use std::path::Path;
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use crate::Id; use crate::{Id, InvalidId};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, ts_rs::TS)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, ts_rs::TS)]
pub struct HealthCheckId(Id); pub struct HealthCheckId(Id);
@@ -11,6 +12,12 @@ impl std::fmt::Display for HealthCheckId {
write!(f, "{}", &self.0) write!(f, "{}", &self.0)
} }
} }
impl FromStr for HealthCheckId {
type Err = InvalidId;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Id::from_str(s).map(HealthCheckId)
}
}
impl AsRef<str> for HealthCheckId { impl AsRef<str> for HealthCheckId {
fn as_ref(&self) -> &str { fn as_ref(&self) -> &str {
self.0.as_ref() self.0.as_ref()

View File

@@ -1,4 +1,5 @@
use std::borrow::Borrow; use std::borrow::Borrow;
use std::str::FromStr;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
@@ -59,6 +60,12 @@ impl TryFrom<&str> for Id {
} }
} }
} }
impl FromStr for Id {
type Err = InvalidId;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s)
}
}
impl From<Id> for InternedString { impl From<Id> for InternedString {
fn from(value: Id) -> Self { fn from(value: Id) -> Self {
value.0 value.0

View File

@@ -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 { BindOptions } from "./BindOptions";
export interface AddressInfo { username: string | null, hostId: string, bindOptions: BindOptions, suffix: string, }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AllowedStatuses = "only-running" | "only-stopped" | "any" | "disabled";

View File

@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AddSslOptions } from "./AddSslOptions";
import type { BindOptionsSecure } from "./BindOptionsSecure";
export interface BindOptions { scheme: string | null, preferredExternalPort: number, addSsl: AddSslOptions | null, secure: BindOptionsSecure | null, }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface BindOptionsSecure { ssl: boolean, }

View File

@@ -1,5 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AddSslOptions } from "./AddSslOptions"; import type { AddSslOptions } from "./AddSslOptions";
import type { BindKind } from "./BindKind"; import type { BindKind } from "./BindKind";
import type { BindOptionsSecure } from "./BindOptionsSecure";
export interface BindParams { kind: BindKind, id: string, internalPort: number, scheme: string, preferredExternalPort: number, addSsl: AddSslOptions | null, secure: boolean, ssl: boolean, } export interface BindParams { kind: BindKind, id: string, internalPort: number, scheme: string, preferredExternalPort: number, addSsl: AddSslOptions | null, secure: BindOptionsSecure | null, }

View File

@@ -1,4 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ImageId } from "./ImageId";
export interface CreateOverlayedImageParams { imageId: ImageId, } export interface CreateOverlayedImageParams { imageId: string, }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type DependencyKind = "exists" | "running";

View File

@@ -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 { DependencyKind } from "./DependencyKind";
export interface DependencyRequirement { id: string, kind: DependencyKind, healthChecks: string[], }

View File

@@ -1,4 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ImageId } from "./ImageId";
export interface DestroyOverlayedImageParams { imageId: ImageId, guid: string, } export interface DestroyOverlayedImageParams { guid: string, }

View File

@@ -1,5 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ActionId } from "./ActionId";
import type { PackageId } from "./PackageId";
export interface ExecuteAction { serviceId: PackageId | null, actionId: ActionId, input: any, } export interface ExecuteAction { serviceId: string | null, actionId: string, input: any, }

View File

@@ -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 { AllowedStatuses } from "./AllowedStatuses";
export interface ExportActionParams { name: string, description: string, id: string, input: {[key: string]: any}, allowedStatuses: AllowedStatuses, group: string | null, }

View File

@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AddressInfo } from "./AddressInfo";
import type { ServiceInterfaceType } from "./ServiceInterfaceType";
export interface ExportServiceInterfaceParams { id: string, name: string, description: string, hasPrimary: boolean, disabled: boolean, masked: boolean, addressInfo: AddressInfo, type: ServiceInterfaceType, }

View File

@@ -1,4 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ExposedUI } from "./ExposedUI";
export interface ExposeUiParams { paths: Array<ExposedUI>, } export type ExposeUiParams = { "type": "object", value: {[key: string]: ExposeUiParams}, } | { "type": "string", path: string, description: string | null, masked: boolean, copyable: boolean | null, qr: boolean | null, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ExposedUI { path: string, title: string, description: string | null, masked: boolean | null, copyable: boolean | null, qr: boolean | null, }

View File

@@ -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 { Callback } from "./Callback";
export interface GetPrimaryUrlParams { packageId: string | null, serviceInterfaceId: string, callback: Callback, }

View File

@@ -1,5 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Callback } from "./Callback"; import type { Callback } from "./Callback";
import type { PackageId } from "./PackageId";
export interface GetServiceInterfaceParams { packageId: PackageId | null, serviceInterfaceId: string, callback: Callback, } export interface GetServiceInterfaceParams { packageId: string | null, serviceInterfaceId: string, callback: Callback, }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface GetServicePortForwardParams { packageId: string | null, internalPort: number, }

View File

@@ -1,4 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // 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 interface GetStoreParams { packageId: PackageId | null, path: string, } export interface GetStoreParams { packageId: string | null, path: string, }

View File

@@ -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 { Callback } from "./Callback";
export interface GetSystemSmtpParams { callback: Callback, }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type HealthCheckString = "passing" | "disabled" | "starting" | "warning" | "failure";

View File

@@ -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 { Callback } from "./Callback";
export interface ListServiceInterfacesParams { packageId: string | null, callback: Callback, }

View File

@@ -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 { MountTarget } from "./MountTarget";
export interface MountParams { location: string, target: MountTarget, }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface MountTarget { packageId: string, volumeId: string, path: string, readonly: boolean, }

View File

@@ -1,4 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // 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 interface ParamsMaybePackageId { packageId: PackageId | null, } export interface ParamsMaybePackageId { packageId: string | null, }

View File

@@ -1,4 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // 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 interface ParamsPackageId { packageId: PackageId, } export interface ParamsPackageId { packageId: string, }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface RemoveActionParams { id: string, }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface RemoveAddressParams { id: string, }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ReverseProxyBind { ip: string | null, port: number, ssl: boolean, }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ReverseProxyDestination { ip: string | null, port: number, ssl: boolean, }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ReverseProxyHttp { headers: null | {[key: string]: string}, }

View File

@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ReverseProxyBind } from "./ReverseProxyBind";
import type { ReverseProxyDestination } from "./ReverseProxyDestination";
import type { ReverseProxyHttp } from "./ReverseProxyHttp";
export interface ReverseProxyParams { bind: ReverseProxyBind, dst: ReverseProxyDestination, http: ReverseProxyHttp, }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ServiceInterfaceType = "ui" | "p2p" | "api";

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DependencyRequirement } from "./DependencyRequirement";
export interface SetDependenciesParams { dependencies: Array<DependencyRequirement>, }

View File

@@ -1,5 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // 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 { HealthCheckString } from "./HealthCheckString"; import type { HealthCheckString } from "./HealthCheckString";
export interface SetHealth { name: HealthCheckId, status: HealthCheckString, message: string | null, } export interface SetHealth { name: string, status: HealthCheckString, message: string | null, }

View File

@@ -18,7 +18,8 @@ use crate::auth::check_password_against_db;
use crate::backup::os::OsBackup; use crate::backup::os::OsBackup;
use crate::backup::{BackupReport, ServerBackupReport}; use crate::backup::{BackupReport, ServerBackupReport};
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::db::model::{BackupProgress, DatabaseModel}; use crate::db::model::public::BackupProgress;
use crate::db::model::DatabaseModel;
use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::backup::BackupMountGuard;
use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
@@ -174,7 +175,7 @@ pub async fn backup_all(
.as_package_data() .as_package_data()
.as_entries()? .as_entries()?
.into_iter() .into_iter()
.filter(|(_, m)| m.expect_as_installed().is_ok()) .filter(|(_, m)| m.as_state_info().expect_installed().is_ok())
.map(|(id, _)| id) .map(|(id, _)| id)
.collect() .collect()
}; };

View File

@@ -19,7 +19,7 @@ use super::setup::CURRENT_SECRET;
use crate::account::AccountInfo; use crate::account::AccountInfo;
use crate::context::config::ServerConfig; use crate::context::config::ServerConfig;
use crate::core::rpc_continuations::{RequestGuid, RestHandler, RpcContinuation, WebSocketHandler}; use crate::core::rpc_continuations::{RequestGuid, RestHandler, RpcContinuation, WebSocketHandler};
use crate::db::model::CurrentDependents; use crate::db::model::package::CurrentDependents;
use crate::db::prelude::PatchDbExt; use crate::db::prelude::PatchDbExt;
use crate::dependencies::compute_dependency_config_errs; use crate::dependencies::compute_dependency_config_errs;
use crate::disk::OsPartitionInfo; use crate::disk::OsPartitionInfo;
@@ -219,12 +219,7 @@ impl RpcContext {
for (package_id, package) in for (package_id, package) in
f.as_public_mut().as_package_data_mut().as_entries_mut()? f.as_public_mut().as_package_data_mut().as_entries_mut()?
{ {
for (k, v) in package for (k, v) in package.clone().into_current_dependencies().into_entries()? {
.as_installed_mut()
.into_iter()
.flat_map(|i| i.clone().into_current_dependencies().into_entries())
.flatten()
{
let mut entry: BTreeMap<_, _> = let mut entry: BTreeMap<_, _> =
current_dependents.remove(&k).unwrap_or_default(); current_dependents.remove(&k).unwrap_or_default();
entry.insert(package_id.clone(), v.de()?); entry.insert(package_id.clone(), v.de()?);
@@ -236,16 +231,7 @@ impl RpcContext {
.as_public_mut() .as_public_mut()
.as_package_data_mut() .as_package_data_mut()
.as_idx_mut(&package_id) .as_idx_mut(&package_id)
.and_then(|pde| pde.expect_as_installed_mut().ok()) .map(|i| i.as_current_dependents_mut())
.map(|i| i.as_installed_mut().as_current_dependents_mut())
{
deps.ser(&CurrentDependents(current_dependents))?;
} else if let Some(deps) = f
.as_public_mut()
.as_package_data_mut()
.as_idx_mut(&package_id)
.and_then(|pde| pde.expect_as_removing_mut().ok())
.map(|i| i.as_removing_mut().as_current_dependents_mut())
{ {
deps.ser(&CurrentDependents(current_dependents))?; deps.ser(&CurrentDependents(current_dependents))?;
} }
@@ -261,23 +247,18 @@ impl RpcContext {
let peek = self.db.peek().await; let peek = self.db.peek().await;
for (package_id, package) in peek.as_public().as_package_data().as_entries()?.into_iter() { for (package_id, package) in peek.as_public().as_package_data().as_entries()?.into_iter() {
let package = package.clone(); let package = package.clone();
if let Some(current_dependencies) = package let current_dependencies = package.as_current_dependencies().de()?;
.as_installed() all_dependency_config_errs.insert(
.and_then(|x| x.as_current_dependencies().de().ok()) package_id.clone(),
{ compute_dependency_config_errs(
let manifest = package.as_manifest().de()?; self,
all_dependency_config_errs.insert( &peek,
package_id.clone(), &package_id,
compute_dependency_config_errs( &current_dependencies,
self, &Default::default(),
&peek, )
&manifest, .await?,
&current_dependencies, );
&Default::default(),
)
.await?,
);
}
} }
self.db self.db
.mutate(|v| { .mutate(|v| {
@@ -286,7 +267,6 @@ impl RpcContext {
.as_public_mut() .as_public_mut()
.as_package_data_mut() .as_package_data_mut()
.as_idx_mut(&package_id) .as_idx_mut(&package_id)
.and_then(|pde| pde.as_installed_mut())
.map(|i| i.as_status_mut().as_dependency_config_errors_mut()) .map(|i| i.as_status_mut().as_dependency_config_errors_mut())
{ {
config_errors.ser(&errs)?; config_errors.ser(&errs)?;

View File

@@ -1,626 +0,0 @@
use std::collections::{BTreeMap, BTreeSet};
use std::net::{Ipv4Addr, Ipv6Addr};
use chrono::{DateTime, Utc};
use emver::VersionRange;
use imbl_value::InternedString;
use ipnet::{Ipv4Net, Ipv6Net};
use isocountry::CountryCode;
use itertools::Itertools;
use models::{DataUrl, HealthCheckId, HostId, PackageId};
use openssl::hash::MessageDigest;
use patch_db::json_ptr::JsonPointer;
use patch_db::{HasModel, Value};
use reqwest::Url;
use serde::{Deserialize, Serialize};
use torut::onion::OnionAddressV3;
use crate::account::AccountInfo;
use crate::auth::Sessions;
use crate::backup::target::cifs::CifsTargets;
use crate::net::forward::AvailablePorts;
use crate::net::host::HostInfo;
use crate::net::keys::KeyStore;
use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr};
use crate::notifications::Notifications;
use crate::prelude::*;
use crate::progress::FullProgress;
use crate::s9pk::manifest::Manifest;
use crate::ssh::SshKeys;
use crate::status::Status;
use crate::util::cpupower::Governor;
use crate::util::serde::Pem;
use crate::util::Version;
use crate::version::{Current, VersionT};
use crate::{ARCH, PLATFORM};
fn get_arch() -> InternedString {
(*ARCH).into()
}
fn get_platform() -> InternedString {
(&*PLATFORM).into()
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct Database {
pub public: Public,
pub private: Private,
}
impl Database {
pub fn init(account: &AccountInfo) -> Result<Self, Error> {
let lan_address = account.hostname.lan_address().parse().unwrap();
Ok(Database {
public: Public {
server_info: ServerInfo {
arch: get_arch(),
platform: get_platform(),
id: account.server_id.clone(),
version: Current::new().semver().into(),
hostname: account.hostname.no_dot_host_name(),
last_backup: None,
last_wifi_region: None,
eos_version_compat: Current::new().compat().clone(),
lan_address,
onion_address: account.tor_key.public().get_onion_address(),
tor_address: format!(
"https://{}",
account.tor_key.public().get_onion_address()
)
.parse()
.unwrap(),
ip_info: BTreeMap::new(),
status_info: ServerStatus {
backup_progress: None,
updated: false,
update_progress: None,
shutting_down: false,
restarting: false,
},
wifi: WifiInfo {
ssids: Vec::new(),
connected: None,
selected: None,
},
unread_notification_count: 0,
connection_addresses: ConnectionAddresses {
tor: Vec::new(),
clearnet: Vec::new(),
},
password_hash: account.password.clone(),
pubkey: ssh_key::PublicKey::from(&account.ssh_key)
.to_openssh()
.unwrap(),
ca_fingerprint: account
.root_ca_cert
.digest(MessageDigest::sha256())
.unwrap()
.iter()
.map(|x| format!("{x:X}"))
.join(":"),
ntp_synced: false,
zram: true,
governor: None,
},
package_data: AllPackageData::default(),
ui: serde_json::from_str(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../web/patchdb-ui-seed.json"
)))
.unwrap(),
},
private: Private {
key_store: KeyStore::new(account)?,
password: account.password.clone(),
ssh_privkey: Pem(account.ssh_key.clone()),
ssh_pubkeys: SshKeys::new(),
available_ports: AvailablePorts::new(),
sessions: Sessions::new(),
notifications: Notifications::new(),
cifs: CifsTargets::new(),
package_stores: BTreeMap::new(),
}, // TODO
})
}
}
pub type DatabaseModel = Model<Database>;
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
// #[macro_debug]
pub struct Public {
pub server_info: ServerInfo,
pub package_data: AllPackageData,
pub ui: Value,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct Private {
pub key_store: KeyStore,
pub password: String, // argon2 hash
pub ssh_privkey: Pem<ssh_key::PrivateKey>,
pub ssh_pubkeys: SshKeys,
pub available_ports: AvailablePorts,
pub sessions: Sessions,
pub notifications: Notifications,
pub cifs: CifsTargets,
#[serde(default)]
pub package_stores: BTreeMap<PackageId, Value>,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct ServerInfo {
#[serde(default = "get_arch")]
pub arch: InternedString,
#[serde(default = "get_platform")]
pub platform: InternedString,
pub id: String,
pub hostname: String,
pub version: Version,
pub last_backup: Option<DateTime<Utc>>,
/// Used in the wifi to determine the region to set the system to
pub last_wifi_region: Option<CountryCode>,
pub eos_version_compat: VersionRange,
pub lan_address: Url,
pub onion_address: OnionAddressV3,
/// for backwards compatibility
pub tor_address: Url,
pub ip_info: BTreeMap<String, IpInfo>,
#[serde(default)]
pub status_info: ServerStatus,
pub wifi: WifiInfo,
pub unread_notification_count: u64,
pub connection_addresses: ConnectionAddresses,
pub password_hash: String,
pub pubkey: String,
pub ca_fingerprint: String,
#[serde(default)]
pub ntp_synced: bool,
#[serde(default)]
pub zram: bool,
pub governor: Option<Governor>,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct IpInfo {
pub ipv4_range: Option<Ipv4Net>,
pub ipv4: Option<Ipv4Addr>,
pub ipv6_range: Option<Ipv6Net>,
pub ipv6: Option<Ipv6Addr>,
}
impl IpInfo {
pub async fn for_interface(iface: &str) -> Result<Self, Error> {
let (ipv4, ipv4_range) = get_iface_ipv4_addr(iface).await?.unzip();
let (ipv6, ipv6_range) = get_iface_ipv6_addr(iface).await?.unzip();
Ok(Self {
ipv4_range,
ipv4,
ipv6_range,
ipv6,
})
}
}
#[derive(Debug, Default, Deserialize, Serialize, HasModel)]
#[model = "Model<Self>"]
pub struct BackupProgress {
pub complete: bool,
}
#[derive(Debug, Default, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct ServerStatus {
pub backup_progress: Option<BTreeMap<PackageId, BackupProgress>>,
pub updated: bool,
pub update_progress: Option<UpdateProgress>,
#[serde(default)]
pub shutting_down: bool,
#[serde(default)]
pub restarting: bool,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct UpdateProgress {
pub size: Option<u64>,
pub downloaded: u64,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct WifiInfo {
pub ssids: Vec<String>,
pub selected: Option<String>,
pub connected: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct ServerSpecs {
pub cpu: String,
pub disk: String,
pub memory: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct ConnectionAddresses {
pub tor: Vec<String>,
pub clearnet: Vec<String>,
}
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct AllPackageData(pub BTreeMap<PackageId, PackageDataEntry>);
impl Map for AllPackageData {
type Key = PackageId;
type Value = PackageDataEntry;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
Ok(key)
}
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
Ok(key.clone().into())
}
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct StaticFiles {
license: String,
instructions: String,
icon: DataUrl<'static>,
}
impl StaticFiles {
pub fn local(id: &PackageId, version: &Version, icon: DataUrl<'static>) -> Self {
StaticFiles {
license: format!("/public/package-data/{}/{}/LICENSE.md", id, version),
instructions: format!("/public/package-data/{}/{}/INSTRUCTIONS.md", id, version),
icon,
}
}
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct PackageDataEntryInstalling {
pub static_files: StaticFiles,
pub manifest: Manifest,
pub install_progress: FullProgress,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct PackageDataEntryUpdating {
pub static_files: StaticFiles,
pub manifest: Manifest,
pub installed: InstalledPackageInfo,
pub install_progress: FullProgress,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct PackageDataEntryRestoring {
pub static_files: StaticFiles,
pub manifest: Manifest,
pub install_progress: FullProgress,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct PackageDataEntryRemoving {
pub static_files: StaticFiles,
pub manifest: Manifest,
pub removing: InstalledPackageInfo,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct PackageDataEntryInstalled {
pub static_files: StaticFiles,
pub manifest: Manifest,
pub installed: InstalledPackageInfo,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(tag = "state")]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
// #[macro_debug]
pub enum PackageDataEntry {
Installing(PackageDataEntryInstalling),
Updating(PackageDataEntryUpdating),
Restoring(PackageDataEntryRestoring),
Removing(PackageDataEntryRemoving),
Installed(PackageDataEntryInstalled),
}
impl Model<PackageDataEntry> {
pub fn expect_into_installed(self) -> Result<Model<PackageDataEntryInstalled>, Error> {
if let PackageDataEntryMatchModel::Installed(a) = self.into_match() {
Ok(a)
} else {
Err(Error::new(
eyre!("package is not in installed state"),
ErrorKind::InvalidRequest,
))
}
}
pub fn expect_as_installed(&self) -> Result<&Model<PackageDataEntryInstalled>, Error> {
if let PackageDataEntryMatchModelRef::Installed(a) = self.as_match() {
Ok(a)
} else {
Err(Error::new(
eyre!("package is not in installed state"),
ErrorKind::InvalidRequest,
))
}
}
pub fn expect_as_installed_mut(
&mut self,
) -> Result<&mut Model<PackageDataEntryInstalled>, Error> {
if let PackageDataEntryMatchModelMut::Installed(a) = self.as_match_mut() {
Ok(a)
} else {
Err(Error::new(
eyre!("package is not in installed state"),
ErrorKind::InvalidRequest,
))
}
}
pub fn expect_into_removing(self) -> Result<Model<PackageDataEntryRemoving>, Error> {
if let PackageDataEntryMatchModel::Removing(a) = self.into_match() {
Ok(a)
} else {
Err(Error::new(
eyre!("package is not in removing state"),
ErrorKind::InvalidRequest,
))
}
}
pub fn expect_as_removing(&self) -> Result<&Model<PackageDataEntryRemoving>, Error> {
if let PackageDataEntryMatchModelRef::Removing(a) = self.as_match() {
Ok(a)
} else {
Err(Error::new(
eyre!("package is not in removing state"),
ErrorKind::InvalidRequest,
))
}
}
pub fn expect_as_removing_mut(
&mut self,
) -> Result<&mut Model<PackageDataEntryRemoving>, Error> {
if let PackageDataEntryMatchModelMut::Removing(a) = self.as_match_mut() {
Ok(a)
} else {
Err(Error::new(
eyre!("package is not in removing state"),
ErrorKind::InvalidRequest,
))
}
}
pub fn expect_as_installing_mut(
&mut self,
) -> Result<&mut Model<PackageDataEntryInstalling>, Error> {
if let PackageDataEntryMatchModelMut::Installing(a) = self.as_match_mut() {
Ok(a)
} else {
Err(Error::new(
eyre!("package is not in installing state"),
ErrorKind::InvalidRequest,
))
}
}
pub fn into_manifest(self) -> Model<Manifest> {
match self.into_match() {
PackageDataEntryMatchModel::Installing(a) => a.into_manifest(),
PackageDataEntryMatchModel::Updating(a) => a.into_installed().into_manifest(),
PackageDataEntryMatchModel::Restoring(a) => a.into_manifest(),
PackageDataEntryMatchModel::Removing(a) => a.into_manifest(),
PackageDataEntryMatchModel::Installed(a) => a.into_manifest(),
PackageDataEntryMatchModel::Error(_) => Model::from(Value::Null),
}
}
pub fn as_manifest(&self) -> &Model<Manifest> {
match self.as_match() {
PackageDataEntryMatchModelRef::Installing(a) => a.as_manifest(),
PackageDataEntryMatchModelRef::Updating(a) => a.as_installed().as_manifest(),
PackageDataEntryMatchModelRef::Restoring(a) => a.as_manifest(),
PackageDataEntryMatchModelRef::Removing(a) => a.as_manifest(),
PackageDataEntryMatchModelRef::Installed(a) => a.as_manifest(),
PackageDataEntryMatchModelRef::Error(_) => (&Value::Null).into(),
}
}
pub fn into_installed(self) -> Option<Model<InstalledPackageInfo>> {
match self.into_match() {
PackageDataEntryMatchModel::Installing(_) => None,
PackageDataEntryMatchModel::Updating(a) => Some(a.into_installed()),
PackageDataEntryMatchModel::Restoring(_) => None,
PackageDataEntryMatchModel::Removing(_) => None,
PackageDataEntryMatchModel::Installed(a) => Some(a.into_installed()),
PackageDataEntryMatchModel::Error(_) => None,
}
}
pub fn as_installed(&self) -> Option<&Model<InstalledPackageInfo>> {
match self.as_match() {
PackageDataEntryMatchModelRef::Installing(_) => None,
PackageDataEntryMatchModelRef::Updating(a) => Some(a.as_installed()),
PackageDataEntryMatchModelRef::Restoring(_) => None,
PackageDataEntryMatchModelRef::Removing(_) => None,
PackageDataEntryMatchModelRef::Installed(a) => Some(a.as_installed()),
PackageDataEntryMatchModelRef::Error(_) => None,
}
}
pub fn as_installed_mut(&mut self) -> Option<&mut Model<InstalledPackageInfo>> {
match self.as_match_mut() {
PackageDataEntryMatchModelMut::Installing(_) => None,
PackageDataEntryMatchModelMut::Updating(a) => Some(a.as_installed_mut()),
PackageDataEntryMatchModelMut::Restoring(_) => None,
PackageDataEntryMatchModelMut::Removing(_) => None,
PackageDataEntryMatchModelMut::Installed(a) => Some(a.as_installed_mut()),
PackageDataEntryMatchModelMut::Error(_) => None,
}
}
pub fn as_install_progress(&self) -> Option<&Model<FullProgress>> {
match self.as_match() {
PackageDataEntryMatchModelRef::Installing(a) => Some(a.as_install_progress()),
PackageDataEntryMatchModelRef::Updating(a) => Some(a.as_install_progress()),
PackageDataEntryMatchModelRef::Restoring(a) => Some(a.as_install_progress()),
PackageDataEntryMatchModelRef::Removing(_) => None,
PackageDataEntryMatchModelRef::Installed(_) => None,
PackageDataEntryMatchModelRef::Error(_) => None,
}
}
pub fn as_install_progress_mut(&mut self) -> Option<&mut Model<FullProgress>> {
match self.as_match_mut() {
PackageDataEntryMatchModelMut::Installing(a) => Some(a.as_install_progress_mut()),
PackageDataEntryMatchModelMut::Updating(a) => Some(a.as_install_progress_mut()),
PackageDataEntryMatchModelMut::Restoring(a) => Some(a.as_install_progress_mut()),
PackageDataEntryMatchModelMut::Removing(_) => None,
PackageDataEntryMatchModelMut::Installed(_) => None,
PackageDataEntryMatchModelMut::Error(_) => None,
}
}
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct InstalledPackageInfo {
pub status: Status,
pub marketplace_url: Option<Url>,
#[serde(default)]
#[serde(with = "crate::util::serde::ed25519_pubkey")]
pub developer_key: ed25519_dalek::VerifyingKey,
pub manifest: Manifest,
pub last_backup: Option<DateTime<Utc>>,
pub dependency_info: BTreeMap<PackageId, StaticDependencyInfo>,
pub current_dependents: CurrentDependents,
pub current_dependencies: CurrentDependencies,
pub interface_addresses: InterfaceAddressMap,
pub hosts: HostInfo,
pub store_exposed_ui: Vec<ExposedUI>,
pub store_exposed_dependents: Vec<JsonPointer>,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[model = "Model<Self>"]
pub struct ExposedDependent {
path: String,
title: String,
description: Option<String>,
masked: Option<bool>,
copyable: Option<bool>,
qr: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, Serialize, HasModel, ts_rs::TS)]
#[model = "Model<Self>"]
pub struct ExposedUI {
#[ts(type = "string")]
pub path: JsonPointer,
pub title: String,
pub description: Option<String>,
pub masked: Option<bool>,
pub copyable: Option<bool>,
pub qr: Option<bool>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct CurrentDependents(pub BTreeMap<PackageId, CurrentDependencyInfo>);
impl CurrentDependents {
pub fn map(
mut self,
transform: impl Fn(
BTreeMap<PackageId, CurrentDependencyInfo>,
) -> BTreeMap<PackageId, CurrentDependencyInfo>,
) -> Self {
self.0 = transform(self.0);
self
}
}
impl Map for CurrentDependents {
type Key = PackageId;
type Value = CurrentDependencyInfo;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
Ok(key)
}
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
Ok(key.clone().into())
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct CurrentDependencies(pub BTreeMap<PackageId, CurrentDependencyInfo>);
impl CurrentDependencies {
pub fn map(
mut self,
transform: impl Fn(
BTreeMap<PackageId, CurrentDependencyInfo>,
) -> BTreeMap<PackageId, CurrentDependencyInfo>,
) -> Self {
self.0 = transform(self.0);
self
}
}
impl Map for CurrentDependencies {
type Key = PackageId;
type Value = CurrentDependencyInfo;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
Ok(key)
}
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
Ok(key.clone().into())
}
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct StaticDependencyInfo {
pub title: String,
pub icon: DataUrl<'static>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct CurrentDependencyInfo {
#[serde(default)]
pub health_checks: BTreeSet<HealthCheckId>,
}
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct InterfaceAddressMap(pub BTreeMap<HostId, InterfaceAddresses>);
impl Map for InterfaceAddressMap {
type Key = HostId;
type Value = InterfaceAddresses;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
Ok(key)
}
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
Ok(key.clone().into())
}
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct InterfaceAddresses {
pub tor_address: Option<String>,
pub lan_address: Option<String>,
}

View File

@@ -0,0 +1,48 @@
use std::collections::BTreeMap;
use patch_db::HasModel;
use serde::{Deserialize, Serialize};
use crate::account::AccountInfo;
use crate::auth::Sessions;
use crate::backup::target::cifs::CifsTargets;
use crate::db::model::private::Private;
use crate::db::model::public::Public;
use crate::net::forward::AvailablePorts;
use crate::net::keys::KeyStore;
use crate::notifications::Notifications;
use crate::prelude::*;
use crate::ssh::SshKeys;
use crate::util::serde::Pem;
pub mod package;
pub mod private;
pub mod public;
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct Database {
pub public: Public,
pub private: Private,
}
impl Database {
pub fn init(account: &AccountInfo) -> Result<Self, Error> {
Ok(Self {
public: Public::init(account)?,
private: Private {
key_store: KeyStore::new(account)?,
password: account.password.clone(),
ssh_privkey: Pem(account.ssh_key.clone()),
ssh_pubkeys: SshKeys::new(),
available_ports: AvailablePorts::new(),
sessions: Sessions::new(),
notifications: Notifications::new(),
cifs: CifsTargets::new(),
package_stores: BTreeMap::new(),
}, // TODO
})
}
}
pub type DatabaseModel = Model<Database>;

View File

@@ -0,0 +1,424 @@
use std::collections::{BTreeMap, BTreeSet};
use chrono::{DateTime, Utc};
use imbl_value::InternedString;
use models::{DataUrl, HealthCheckId, HostId, PackageId};
use patch_db::json_ptr::JsonPointer;
use patch_db::HasModel;
use reqwest::Url;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::net::host::HostInfo;
use crate::prelude::*;
use crate::progress::FullProgress;
use crate::s9pk::manifest::Manifest;
use crate::status::Status;
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct AllPackageData(pub BTreeMap<PackageId, PackageDataEntry>);
impl Map for AllPackageData {
type Key = PackageId;
type Value = PackageDataEntry;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
Ok(key)
}
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
Ok(key.clone().into())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ManifestPreference {
Old,
New,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[serde(tag = "state")]
#[model = "Model<Self>"]
pub enum PackageState {
Installing(InstallingState),
Restoring(InstallingState),
Updating(UpdatingState),
Installed(InstalledState),
Removing(InstalledState),
}
impl PackageState {
pub fn expect_installed(&self) -> Result<&InstalledState, Error> {
match self {
Self::Installed(a) => Ok(a),
a => Err(Error::new(
eyre!(
"Package {} is not in installed state",
self.as_manifest(ManifestPreference::Old).id
),
ErrorKind::InvalidRequest,
)),
}
}
pub fn into_installing_info(self) -> Option<InstallingInfo> {
match self {
Self::Installing(InstallingState { installing_info })
| Self::Restoring(InstallingState { installing_info }) => Some(installing_info),
Self::Updating(UpdatingState {
installing_info, ..
}) => Some(installing_info),
Self::Installed(_) | Self::Removing(_) => None,
}
}
pub fn as_installing_info(&self) -> Option<&InstallingInfo> {
match self {
Self::Installing(InstallingState { installing_info })
| Self::Restoring(InstallingState { installing_info }) => Some(installing_info),
Self::Updating(UpdatingState {
installing_info, ..
}) => Some(installing_info),
Self::Installed(_) | Self::Removing(_) => None,
}
}
pub fn as_installing_info_mut(&mut self) -> Option<&mut InstallingInfo> {
match self {
Self::Installing(InstallingState { installing_info })
| Self::Restoring(InstallingState { installing_info }) => Some(installing_info),
Self::Updating(UpdatingState {
installing_info, ..
}) => Some(installing_info),
Self::Installed(_) | Self::Removing(_) => None,
}
}
pub fn into_manifest(self, preference: ManifestPreference) -> Manifest {
match self {
Self::Installing(InstallingState {
installing_info: InstallingInfo { new_manifest, .. },
})
| Self::Restoring(InstallingState {
installing_info: InstallingInfo { new_manifest, .. },
}) => new_manifest,
Self::Updating(UpdatingState { manifest, .. })
if preference == ManifestPreference::Old =>
{
manifest
}
Self::Updating(UpdatingState {
installing_info: InstallingInfo { new_manifest, .. },
..
}) => new_manifest,
Self::Installed(InstalledState { manifest })
| Self::Removing(InstalledState { manifest }) => manifest,
}
}
pub fn as_manifest(&self, preference: ManifestPreference) -> &Manifest {
match self {
Self::Installing(InstallingState {
installing_info: InstallingInfo { new_manifest, .. },
})
| Self::Restoring(InstallingState {
installing_info: InstallingInfo { new_manifest, .. },
}) => new_manifest,
Self::Updating(UpdatingState { manifest, .. })
if preference == ManifestPreference::Old =>
{
manifest
}
Self::Updating(UpdatingState {
installing_info: InstallingInfo { new_manifest, .. },
..
}) => new_manifest,
Self::Installed(InstalledState { manifest })
| Self::Removing(InstalledState { manifest }) => manifest,
}
}
pub fn as_manifest_mut(&mut self, preference: ManifestPreference) -> &mut Manifest {
match self {
Self::Installing(InstallingState {
installing_info: InstallingInfo { new_manifest, .. },
})
| Self::Restoring(InstallingState {
installing_info: InstallingInfo { new_manifest, .. },
}) => new_manifest,
Self::Updating(UpdatingState { manifest, .. })
if preference == ManifestPreference::Old =>
{
manifest
}
Self::Updating(UpdatingState {
installing_info: InstallingInfo { new_manifest, .. },
..
}) => new_manifest,
Self::Installed(InstalledState { manifest })
| Self::Removing(InstalledState { manifest }) => manifest,
}
}
}
impl Model<PackageState> {
pub fn expect_installed(&self) -> Result<&Model<InstalledState>, Error> {
match self.as_match() {
PackageStateMatchModelRef::Installed(a) => Ok(a),
a => Err(Error::new(
eyre!(
"Package {} is not in installed state",
self.as_manifest(ManifestPreference::Old).as_id().de()?
),
ErrorKind::InvalidRequest,
)),
}
}
pub fn into_installing_info(self) -> Option<Model<InstallingInfo>> {
match self.into_match() {
PackageStateMatchModel::Installing(s) | PackageStateMatchModel::Restoring(s) => {
Some(s.into_installing_info())
}
PackageStateMatchModel::Updating(s) => Some(s.into_installing_info()),
PackageStateMatchModel::Installed(_) | PackageStateMatchModel::Removing(_) => None,
PackageStateMatchModel::Error(_) => None,
}
}
pub fn as_installing_info(&self) -> Option<&Model<InstallingInfo>> {
match self.as_match() {
PackageStateMatchModelRef::Installing(s) | PackageStateMatchModelRef::Restoring(s) => {
Some(s.as_installing_info())
}
PackageStateMatchModelRef::Updating(s) => Some(s.as_installing_info()),
PackageStateMatchModelRef::Installed(_) | PackageStateMatchModelRef::Removing(_) => {
None
}
PackageStateMatchModelRef::Error(_) => None,
}
}
pub fn as_installing_info_mut(&mut self) -> Option<&mut Model<InstallingInfo>> {
match self.as_match_mut() {
PackageStateMatchModelMut::Installing(s) | PackageStateMatchModelMut::Restoring(s) => {
Some(s.as_installing_info_mut())
}
PackageStateMatchModelMut::Updating(s) => Some(s.as_installing_info_mut()),
PackageStateMatchModelMut::Installed(_) | PackageStateMatchModelMut::Removing(_) => {
None
}
PackageStateMatchModelMut::Error(_) => None,
}
}
pub fn into_manifest(self, preference: ManifestPreference) -> Model<Manifest> {
match self.into_match() {
PackageStateMatchModel::Installing(s) | PackageStateMatchModel::Restoring(s) => {
s.into_installing_info().into_new_manifest()
}
PackageStateMatchModel::Updating(s) if preference == ManifestPreference::Old => {
s.into_manifest()
}
PackageStateMatchModel::Updating(s) => s.into_installing_info().into_new_manifest(),
PackageStateMatchModel::Installed(s) | PackageStateMatchModel::Removing(s) => {
s.into_manifest()
}
PackageStateMatchModel::Error(_) => Value::Null.into(),
}
}
pub fn as_manifest(&self, preference: ManifestPreference) -> &Model<Manifest> {
match self.as_match() {
PackageStateMatchModelRef::Installing(s) | PackageStateMatchModelRef::Restoring(s) => {
s.as_installing_info().as_new_manifest()
}
PackageStateMatchModelRef::Updating(s) if preference == ManifestPreference::Old => {
s.as_manifest()
}
PackageStateMatchModelRef::Updating(s) => s.as_installing_info().as_new_manifest(),
PackageStateMatchModelRef::Installed(s) | PackageStateMatchModelRef::Removing(s) => {
s.as_manifest()
}
PackageStateMatchModelRef::Error(_) => (&Value::Null).into(),
}
}
pub fn as_manifest_mut(
&mut self,
preference: ManifestPreference,
) -> Result<&mut Model<Manifest>, Error> {
Ok(match self.as_match_mut() {
PackageStateMatchModelMut::Installing(s) | PackageStateMatchModelMut::Restoring(s) => {
s.as_installing_info_mut().as_new_manifest_mut()
}
PackageStateMatchModelMut::Updating(s) if preference == ManifestPreference::Old => {
s.as_manifest_mut()
}
PackageStateMatchModelMut::Updating(s) => {
s.as_installing_info_mut().as_new_manifest_mut()
}
PackageStateMatchModelMut::Installed(s) | PackageStateMatchModelMut::Removing(s) => {
s.as_manifest_mut()
}
PackageStateMatchModelMut::Error(s) => {
return Err(Error::new(
eyre!("could not determine package state to get manifest"),
ErrorKind::Database,
))
}
})
}
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct InstallingState {
pub installing_info: InstallingInfo,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct UpdatingState {
pub manifest: Manifest,
pub installing_info: InstallingInfo,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct InstalledState {
pub manifest: Manifest,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct InstallingInfo {
pub new_manifest: Manifest,
pub progress: FullProgress,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct PackageDataEntry {
pub state_info: PackageState,
pub status: Status,
pub marketplace_url: Option<Url>,
#[serde(default)]
#[serde(with = "crate::util::serde::ed25519_pubkey")]
pub developer_key: ed25519_dalek::VerifyingKey,
pub icon: DataUrl<'static>,
pub last_backup: Option<DateTime<Utc>>,
pub dependency_info: BTreeMap<PackageId, StaticDependencyInfo>,
pub current_dependents: CurrentDependents,
pub current_dependencies: CurrentDependencies,
pub interface_addresses: InterfaceAddressMap,
pub hosts: HostInfo,
pub store_exposed_ui: Vec<ExposedUI>,
pub store_exposed_dependents: Vec<JsonPointer>,
}
impl AsRef<PackageDataEntry> for PackageDataEntry {
fn as_ref(&self) -> &PackageDataEntry {
self
}
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[model = "Model<Self>"]
pub struct ExposedDependent {
path: String,
title: String,
description: Option<String>,
masked: Option<bool>,
copyable: Option<bool>,
qr: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)]
#[model = "Model<Self>"]
#[ts(export)]
pub struct ExposedUI {
#[ts(type = "string")]
pub path: JsonPointer,
pub title: String,
pub description: Option<String>,
pub masked: Option<bool>,
pub copyable: Option<bool>,
pub qr: Option<bool>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct CurrentDependents(pub BTreeMap<PackageId, CurrentDependencyInfo>);
impl CurrentDependents {
pub fn map(
mut self,
transform: impl Fn(
BTreeMap<PackageId, CurrentDependencyInfo>,
) -> BTreeMap<PackageId, CurrentDependencyInfo>,
) -> Self {
self.0 = transform(self.0);
self
}
}
impl Map for CurrentDependents {
type Key = PackageId;
type Value = CurrentDependencyInfo;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
Ok(key)
}
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
Ok(key.clone().into())
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct CurrentDependencies(pub BTreeMap<PackageId, CurrentDependencyInfo>);
impl CurrentDependencies {
pub fn map(
mut self,
transform: impl Fn(
BTreeMap<PackageId, CurrentDependencyInfo>,
) -> BTreeMap<PackageId, CurrentDependencyInfo>,
) -> Self {
self.0 = transform(self.0);
self
}
}
impl Map for CurrentDependencies {
type Key = PackageId;
type Value = CurrentDependencyInfo;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
Ok(key)
}
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
Ok(key.clone().into())
}
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct StaticDependencyInfo {
pub title: String,
pub icon: DataUrl<'static>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
#[serde(tag = "kind")]
pub enum CurrentDependencyInfo {
Exists,
#[serde(rename_all = "kebab-case")]
Running {
#[serde(default)]
health_checks: BTreeSet<HealthCheckId>,
},
}
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct InterfaceAddressMap(pub BTreeMap<HostId, InterfaceAddresses>);
impl Map for InterfaceAddressMap {
type Key = HostId;
type Value = InterfaceAddresses;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
Ok(key)
}
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
Ok(key.clone().into())
}
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct InterfaceAddresses {
pub tor_address: Option<String>,
pub lan_address: Option<String>,
}

View File

@@ -0,0 +1,30 @@
use std::collections::BTreeMap;
use models::PackageId;
use patch_db::{HasModel, Value};
use serde::{Deserialize, Serialize};
use crate::auth::Sessions;
use crate::backup::target::cifs::CifsTargets;
use crate::net::forward::AvailablePorts;
use crate::net::keys::KeyStore;
use crate::notifications::Notifications;
use crate::prelude::*;
use crate::ssh::SshKeys;
use crate::util::serde::Pem;
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct Private {
pub key_store: KeyStore,
pub password: String, // argon2 hash
pub ssh_privkey: Pem<ssh_key::PrivateKey>,
pub ssh_pubkeys: SshKeys,
pub available_ports: AvailablePorts,
pub sessions: Sessions,
pub notifications: Notifications,
pub cifs: CifsTargets,
#[serde(default)]
pub package_stores: BTreeMap<PackageId, Value>,
}

View File

@@ -0,0 +1,210 @@
use std::collections::BTreeMap;
use std::net::{Ipv4Addr, Ipv6Addr};
use chrono::{DateTime, Utc};
use emver::VersionRange;
use imbl_value::InternedString;
use ipnet::{Ipv4Net, Ipv6Net};
use isocountry::CountryCode;
use itertools::Itertools;
use models::PackageId;
use openssl::hash::MessageDigest;
use patch_db::{HasModel, Value};
use reqwest::Url;
use serde::{Deserialize, Serialize};
use torut::onion::OnionAddressV3;
use crate::account::AccountInfo;
use crate::db::model::package::AllPackageData;
use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr};
use crate::prelude::*;
use crate::util::cpupower::Governor;
use crate::util::Version;
use crate::version::{Current, VersionT};
use crate::{ARCH, PLATFORM};
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
// #[macro_debug]
pub struct Public {
pub server_info: ServerInfo,
pub package_data: AllPackageData,
pub ui: Value,
}
impl Public {
pub fn init(account: &AccountInfo) -> Result<Self, Error> {
let lan_address = account.hostname.lan_address().parse().unwrap();
Ok(Self {
server_info: ServerInfo {
arch: get_arch(),
platform: get_platform(),
id: account.server_id.clone(),
version: Current::new().semver().into(),
hostname: account.hostname.no_dot_host_name(),
last_backup: None,
last_wifi_region: None,
eos_version_compat: Current::new().compat().clone(),
lan_address,
onion_address: account.tor_key.public().get_onion_address(),
tor_address: format!("https://{}", account.tor_key.public().get_onion_address())
.parse()
.unwrap(),
ip_info: BTreeMap::new(),
status_info: ServerStatus {
backup_progress: None,
updated: false,
update_progress: None,
shutting_down: false,
restarting: false,
},
wifi: WifiInfo {
ssids: Vec::new(),
connected: None,
selected: None,
},
unread_notification_count: 0,
connection_addresses: ConnectionAddresses {
tor: Vec::new(),
clearnet: Vec::new(),
},
password_hash: account.password.clone(),
pubkey: ssh_key::PublicKey::from(&account.ssh_key)
.to_openssh()
.unwrap(),
ca_fingerprint: account
.root_ca_cert
.digest(MessageDigest::sha256())
.unwrap()
.iter()
.map(|x| format!("{x:X}"))
.join(":"),
ntp_synced: false,
zram: true,
governor: None,
},
package_data: AllPackageData::default(),
ui: serde_json::from_str(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../web/patchdb-ui-seed.json"
)))
.with_kind(ErrorKind::Deserialization)?,
})
}
}
fn get_arch() -> InternedString {
(*ARCH).into()
}
fn get_platform() -> InternedString {
(&*PLATFORM).into()
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct ServerInfo {
#[serde(default = "get_arch")]
pub arch: InternedString,
#[serde(default = "get_platform")]
pub platform: InternedString,
pub id: String,
pub hostname: String,
pub version: Version,
pub last_backup: Option<DateTime<Utc>>,
/// Used in the wifi to determine the region to set the system to
pub last_wifi_region: Option<CountryCode>,
pub eos_version_compat: VersionRange,
pub lan_address: Url,
pub onion_address: OnionAddressV3,
/// for backwards compatibility
pub tor_address: Url,
pub ip_info: BTreeMap<String, IpInfo>,
#[serde(default)]
pub status_info: ServerStatus,
pub wifi: WifiInfo,
pub unread_notification_count: u64,
pub connection_addresses: ConnectionAddresses,
pub password_hash: String,
pub pubkey: String,
pub ca_fingerprint: String,
#[serde(default)]
pub ntp_synced: bool,
#[serde(default)]
pub zram: bool,
pub governor: Option<Governor>,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct IpInfo {
pub ipv4_range: Option<Ipv4Net>,
pub ipv4: Option<Ipv4Addr>,
pub ipv6_range: Option<Ipv6Net>,
pub ipv6: Option<Ipv6Addr>,
}
impl IpInfo {
pub async fn for_interface(iface: &str) -> Result<Self, Error> {
let (ipv4, ipv4_range) = get_iface_ipv4_addr(iface).await?.unzip();
let (ipv6, ipv6_range) = get_iface_ipv6_addr(iface).await?.unzip();
Ok(Self {
ipv4_range,
ipv4,
ipv6_range,
ipv6,
})
}
}
#[derive(Debug, Default, Deserialize, Serialize, HasModel)]
#[model = "Model<Self>"]
pub struct BackupProgress {
pub complete: bool,
}
#[derive(Debug, Default, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct ServerStatus {
pub backup_progress: Option<BTreeMap<PackageId, BackupProgress>>,
pub updated: bool,
pub update_progress: Option<UpdateProgress>,
#[serde(default)]
pub shutting_down: bool,
#[serde(default)]
pub restarting: bool,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct UpdateProgress {
pub size: Option<u64>,
pub downloaded: u64,
}
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[model = "Model<Self>"]
pub struct WifiInfo {
pub ssids: Vec<String>,
pub selected: Option<String>,
pub connected: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct ServerSpecs {
pub cpu: String,
pub disk: String,
pub memory: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct ConnectionAddresses {
pub tor: Vec<String>,
pub clearnet: Vec<String>,
}

View File

@@ -124,6 +124,12 @@ impl<T: Serialize + DeserializeOwned> Model<T> {
self.ser(&orig)?; self.ser(&orig)?;
Ok(res) Ok(res)
} }
pub fn map_mutate(&mut self, f: impl FnOnce(T) -> Result<T, Error>) -> Result<T, Error> {
let mut orig = self.de()?;
let res = f(orig)?;
self.ser(&res)?;
Ok(res)
}
} }
impl<T> Clone for Model<T> { impl<T> Clone for Model<T> {
fn clone(&self) -> Self { fn clone(&self) -> Self {

View File

@@ -10,7 +10,8 @@ use tracing::instrument;
use crate::config::{Config, ConfigSpec, ConfigureContext}; use crate::config::{Config, ConfigSpec, ConfigureContext};
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::db::model::{CurrentDependencies, Database}; use crate::db::model::package::CurrentDependencies;
use crate::db::model::Database;
use crate::prelude::*; use crate::prelude::*;
use crate::s9pk::manifest::Manifest; use crate::s9pk::manifest::Manifest;
use crate::status::DependencyConfigErrors; use crate::status::DependencyConfigErrors;
@@ -195,52 +196,19 @@ pub async fn configure_logic(
todo!() todo!()
} }
#[instrument(skip_all)]
pub fn add_dependent_to_current_dependents_lists(
db: &mut Model<Database>,
dependent_id: &PackageId,
current_dependencies: &CurrentDependencies,
) -> Result<(), Error> {
for (dependency, dep_info) in &current_dependencies.0 {
if let Some(dependency_dependents) = db
.as_public_mut()
.as_package_data_mut()
.as_idx_mut(dependency)
.and_then(|pde| pde.as_installed_mut())
.map(|i| i.as_current_dependents_mut())
{
dependency_dependents.insert(dependent_id, dep_info)?;
}
}
Ok(())
}
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn compute_dependency_config_errs( pub async fn compute_dependency_config_errs(
ctx: &RpcContext, ctx: &RpcContext,
db: &Peeked, db: &Peeked,
manifest: &Manifest, id: &PackageId,
current_dependencies: &CurrentDependencies, current_dependencies: &CurrentDependencies,
dependency_config: &BTreeMap<PackageId, Config>, dependency_config: &BTreeMap<PackageId, Config>,
) -> Result<DependencyConfigErrors, Error> { ) -> Result<DependencyConfigErrors, Error> {
let mut dependency_config_errs = BTreeMap::new(); let mut dependency_config_errs = BTreeMap::new();
for (dependency, _dep_info) in current_dependencies for (dependency, _dep_info) in current_dependencies.0.iter() {
.0
.iter()
.filter(|(dep_id, _)| dep_id != &&manifest.id)
{
// check if config passes dependency check // check if config passes dependency check
if let Some(cfg) = &manifest if let Some(error) = todo!() {
.dependencies dependency_config_errs.insert(dependency.clone(), error);
.0
.get(dependency)
.or_not_found(dependency)?
.config
{
let error = todo!();
{
dependency_config_errs.insert(dependency.clone(), error);
}
} }
} }
Ok(DependencyConfigErrors(dependency_config_errs)) Ok(DependencyConfigErrors(dependency_config_errs))

View File

@@ -11,7 +11,7 @@ use tracing::instrument;
use crate::account::AccountInfo; use crate::account::AccountInfo;
use crate::context::config::ServerConfig; use crate::context::config::ServerConfig;
use crate::db::model::ServerStatus; use crate::db::model::public::ServerStatus;
use crate::disk::mount::util::unmount; use crate::disk::mount::util::unmount;
use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH; use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH;
use crate::prelude::*; use crate::prelude::*;

View File

@@ -18,10 +18,7 @@ use tracing::instrument;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::core::rpc_continuations::{RequestGuid, RpcContinuation}; use crate::core::rpc_continuations::{RequestGuid, RpcContinuation};
use crate::db::model::{ use crate::db::model::package::{ManifestPreference, PackageState, PackageStateMatchModelRef};
PackageDataEntry, PackageDataEntryInstalled, PackageDataEntryMatchModelRef,
PackageDataEntryRemoving,
};
use crate::prelude::*; use crate::prelude::*;
use crate::progress::{FullProgress, PhasedProgressBar}; use crate::progress::{FullProgress, PhasedProgressBar};
use crate::s9pk::manifest::PackageId; use crate::s9pk::manifest::PackageId;
@@ -40,27 +37,27 @@ pub async fn list(ctx: RpcContext) -> Result<Value, Error> {
Ok(ctx.db.peek().await.as_public().as_package_data().as_entries()? Ok(ctx.db.peek().await.as_public().as_package_data().as_entries()?
.iter() .iter()
.filter_map(|(id, pde)| { .filter_map(|(id, pde)| {
let status = match pde.as_match() { let status = match pde.as_state_info().as_match() {
PackageDataEntryMatchModelRef::Installed(_) => { PackageStateMatchModelRef::Installed(_) => {
"installed" "installed"
} }
PackageDataEntryMatchModelRef::Installing(_) => { PackageStateMatchModelRef::Installing(_) => {
"installing" "installing"
} }
PackageDataEntryMatchModelRef::Updating(_) => { PackageStateMatchModelRef::Updating(_) => {
"updating" "updating"
} }
PackageDataEntryMatchModelRef::Restoring(_) => { PackageStateMatchModelRef::Restoring(_) => {
"restoring" "restoring"
} }
PackageDataEntryMatchModelRef::Removing(_) => { PackageStateMatchModelRef::Removing(_) => {
"removing" "removing"
} }
PackageDataEntryMatchModelRef::Error(_) => { PackageStateMatchModelRef::Error(_) => {
"error" "error"
} }
}; };
serde_json::to_value(json!({ "status":status, "id": id.clone(), "version": pde.as_manifest().as_version().de().ok()?})) serde_json::to_value(json!({ "status": status, "id": id.clone(), "version": pde.as_state_info().as_manifest(ManifestPreference::Old).as_version().de().ok()?}))
.ok() .ok()
}) })
.collect()) .collect())
@@ -212,7 +209,7 @@ pub async fn sideload(ctx: RpcContext) -> Result<SideloadResponse, Error> {
.as_public() .as_public()
.as_package_data() .as_package_data()
.as_idx(&id) .as_idx(&id)
.and_then(|e| e.as_install_progress()) .and_then(|e| e.as_state_info().as_installing_info()).map(|i| i.as_progress())
{ {
Ok::<_, ()>(p.de()?) Ok::<_, ()>(p.de()?)
} else { } else {
@@ -407,31 +404,18 @@ pub async fn uninstall(
) -> Result<PackageId, Error> { ) -> Result<PackageId, Error> {
ctx.db ctx.db
.mutate(|db| { .mutate(|db| {
let (manifest, static_files, installed) = match db let entry = db
.as_public() .as_public_mut()
.as_package_data() .as_package_data_mut()
.as_idx(&id) .as_idx_mut(&id)
.or_not_found(&id)? .or_not_found(&id)?;
.de()? entry.as_state_info_mut().map_mutate(|s| match s {
{ PackageState::Installed(s) => Ok(PackageState::Removing(s)),
PackageDataEntry::Installed(PackageDataEntryInstalled { _ => Err(Error::new(
manifest, eyre!("Package {id} is not installed."),
static_files, crate::ErrorKind::NotFound,
installed, )),
}) => (manifest, static_files, installed), })
_ => {
return Err(Error::new(
eyre!("Package is not installed."),
crate::ErrorKind::NotFound,
));
}
};
let pde = PackageDataEntry::Removing(PackageDataEntryRemoving {
manifest,
static_files,
removing: installed,
});
db.as_public_mut().as_package_data_mut().insert(&id, &pde)
}) })
.await?; .await?;

View File

@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::db::model::IpInfo; use crate::db::model::public::IpInfo;
use crate::net::utils::{iface_is_physical, list_interfaces}; use crate::net::utils::{iface_is_physical, list_interfaces};
use crate::prelude::*; use crate::prelude::*;
use crate::Error; use crate::Error;

View File

@@ -205,8 +205,6 @@ impl NetService {
.as_package_data_mut() .as_package_data_mut()
.as_idx_mut(pkg_id) .as_idx_mut(pkg_id)
.or_not_found(pkg_id)? .or_not_found(pkg_id)?
.as_installed_mut()
.or_not_found(pkg_id)?
.as_hosts_mut(); .as_hosts_mut();
hosts.add_binding(&mut ports, kind, &id, internal_port, options)?; hosts.add_binding(&mut ports, kind, &id, internal_port, options)?;
let host = hosts let host = hosts

View File

@@ -4,9 +4,10 @@ use models::PackageId;
use rpc_toolkit::command; use rpc_toolkit::command;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::context::RpcContext;
use crate::db::model::package::ExposedUI;
use crate::prelude::*; use crate::prelude::*;
use crate::Error; use crate::Error;
use crate::{context::RpcContext, db::model::ExposedUI};
pub fn display_properties(response: Value) { pub fn display_properties(response: Value) {
println!("{}", response); println!("{}", response);
@@ -59,8 +60,6 @@ pub async fn properties(
.as_package_data() .as_package_data()
.as_idx(&id) .as_idx(&id)
.or_not_found(&id)? .or_not_found(&id)?
.as_installed()
.or_not_found(&id)?
.as_store_exposed_ui() .as_store_exposed_ui()
.de()? .de()?
.into_properties(&data)) .into_properties(&data))

View File

@@ -16,9 +16,8 @@ use crate::action::ActionResult;
use crate::config::action::ConfigRes; use crate::config::action::ConfigRes;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::core::rpc_continuations::RequestGuid; use crate::core::rpc_continuations::RequestGuid;
use crate::db::model::{ use crate::db::model::package::{
InstalledPackageInfo, PackageDataEntry, PackageDataEntryInstalled, PackageDataEntryMatchModel, InstalledState, PackageDataEntry, PackageState, PackageStateMatchModelRef, UpdatingState,
StaticFiles,
}; };
use crate::disk::mount::guard::GenericMountGuard; use crate::disk::mount::guard::GenericMountGuard;
use crate::install::PKG_ARCHIVE_DIR; use crate::install::PKG_ARCHIVE_DIR;
@@ -28,7 +27,7 @@ use crate::s9pk::S9pk;
use crate::service::service_map::InstallProgressHandles; use crate::service::service_map::InstallProgressHandles;
use crate::service::transition::TransitionKind; use crate::service::transition::TransitionKind;
use crate::status::health_check::HealthCheckResult; use crate::status::health_check::HealthCheckResult;
use crate::status::{MainStatus, Status}; use crate::status::MainStatus;
use crate::util::actor::{Actor, BackgroundJobs, SimpleActor}; use crate::util::actor::{Actor, BackgroundJobs, SimpleActor};
use crate::volume::data_dir; use crate::volume::data_dir;
@@ -100,7 +99,7 @@ impl Service {
) -> Result<Option<Self>, Error> { ) -> Result<Option<Self>, Error> {
let handle_installed = { let handle_installed = {
let ctx = ctx.clone(); let ctx = ctx.clone();
move |s9pk: S9pk, i: Model<InstalledPackageInfo>| async move { move |s9pk: S9pk, i: Model<PackageDataEntry>| async move {
for volume_id in &s9pk.as_manifest().volumes { for volume_id in &s9pk.as_manifest().volumes {
let tmp_path = let tmp_path =
data_dir(&ctx.datadir, &s9pk.as_manifest().id.clone(), volume_id); data_dir(&ctx.datadir, &s9pk.as_manifest().id.clone(), volume_id);
@@ -118,16 +117,18 @@ impl Service {
}; };
let s9pk_dir = ctx.datadir.join(PKG_ARCHIVE_DIR).join("installed"); // TODO: make this based on hash let s9pk_dir = ctx.datadir.join(PKG_ARCHIVE_DIR).join("installed"); // TODO: make this based on hash
let s9pk_path = s9pk_dir.join(id).with_extension("s9pk"); let s9pk_path = s9pk_dir.join(id).with_extension("s9pk");
match ctx let Some(entry) = ctx
.db .db
.peek() .peek()
.await .await
.into_public() .into_public()
.into_package_data() .into_package_data()
.into_idx(id) .into_idx(id)
.map(|pde| pde.into_match()) else {
{ return Ok(None);
Some(PackageDataEntryMatchModel::Installing(_)) => { };
match entry.as_state_info().as_match() {
PackageStateMatchModelRef::Installing(_) => {
if disposition == LoadDisposition::Retry { if disposition == LoadDisposition::Retry {
if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| { if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| {
tracing::error!("Error opening s9pk for install: {e}"); tracing::error!("Error opening s9pk for install: {e}");
@@ -150,14 +151,17 @@ impl Service {
.await?; .await?;
Ok(None) Ok(None)
} }
Some(PackageDataEntryMatchModel::Updating(e)) => { PackageStateMatchModelRef::Updating(s) => {
if disposition == LoadDisposition::Retry if disposition == LoadDisposition::Retry
&& e.as_install_progress().de()?.phases.iter().any( && s.as_installing_info()
|NamedProgress { name, progress }| { .as_progress()
.de()?
.phases
.iter()
.any(|NamedProgress { name, progress }| {
name.eq_ignore_ascii_case("download") name.eq_ignore_ascii_case("download")
&& progress == &Progress::Complete(true) && progress == &Progress::Complete(true)
}, })
)
{ {
if let Ok(s9pk) = S9pk::open(&s9pk_path, Some(id)).await.map_err(|e| { if let Ok(s9pk) = S9pk::open(&s9pk_path, Some(id)).await.map_err(|e| {
tracing::error!("Error opening s9pk for update: {e}"); tracing::error!("Error opening s9pk for update: {e}");
@@ -166,7 +170,7 @@ impl Service {
if let Ok(service) = Self::install( if let Ok(service) = Self::install(
ctx.clone(), ctx.clone(),
s9pk, s9pk,
Some(e.as_installed().as_manifest().as_version().de()?), Some(s.as_manifest().as_version().de()?),
None, None,
) )
.await .await
@@ -181,24 +185,28 @@ impl Service {
let s9pk = S9pk::open(s9pk_path, Some(id)).await?; let s9pk = S9pk::open(s9pk_path, Some(id)).await?;
ctx.db ctx.db
.mutate({ .mutate({
let manifest = s9pk.as_manifest().clone();
|db| { |db| {
db.as_public_mut() db.as_public_mut()
.as_package_data_mut() .as_package_data_mut()
.as_idx_mut(&manifest.id) .as_idx_mut(&id)
.or_not_found(&manifest.id)? .or_not_found(&id)?
.ser(&PackageDataEntry::Installed(PackageDataEntryInstalled { .as_state_info_mut()
static_files: e.as_static_files().de()?, .map_mutate(|s| {
manifest, if let PackageState::Updating(UpdatingState {
installed: e.as_installed().de()?, manifest, ..
})) }) = s
{
Ok(PackageState::Installed(InstalledState { manifest }))
} else {
Err(Error::new(eyre!("Race condition detected - package state changed during load"), ErrorKind::Database))
}
})
} }
}) })
.await?; .await?;
handle_installed(s9pk, e.as_installed().clone()).await handle_installed(s9pk, entry).await
} }
Some(PackageDataEntryMatchModel::Removing(_)) PackageStateMatchModelRef::Removing(_) | PackageStateMatchModelRef::Restoring(_) => {
| Some(PackageDataEntryMatchModel::Restoring(_)) => {
if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| { if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| {
tracing::error!("Error opening s9pk for removal: {e}"); tracing::error!("Error opening s9pk for removal: {e}");
tracing::debug!("{e:?}") tracing::debug!("{e:?}")
@@ -230,18 +238,13 @@ impl Service {
Ok(None) Ok(None)
} }
Some(PackageDataEntryMatchModel::Installed(i)) => { PackageStateMatchModelRef::Installed(_) => {
handle_installed( handle_installed(S9pk::open(s9pk_path, Some(id)).await?, entry).await
S9pk::open(s9pk_path, Some(id)).await?,
i.as_installed().clone(),
)
.await
} }
Some(PackageDataEntryMatchModel::Error(e)) => Err(Error::new( PackageStateMatchModelRef::Error(e) => Err(Error::new(
eyre!("Failed to parse PackageDataEntry, found {e:?}"), eyre!("Failed to parse PackageDataEntry, found {e:?}"),
ErrorKind::Deserialization, ErrorKind::Deserialization,
)), )),
None => Ok(None),
} }
} }
@@ -255,7 +258,6 @@ impl Service {
let manifest = s9pk.as_manifest().clone(); let manifest = s9pk.as_manifest().clone();
let developer_key = s9pk.as_archive().signer(); let developer_key = s9pk.as_archive().signer();
let icon = s9pk.icon_data_url().await?; let icon = s9pk.icon_data_url().await?;
let static_files = StaticFiles::local(&manifest.id, &manifest.version, icon);
let service = Self::new(ctx.clone(), s9pk, StartStop::Stop).await?; let service = Self::new(ctx.clone(), s9pk, StartStop::Stop).await?;
service service
.seed .seed
@@ -270,32 +272,19 @@ impl Service {
} }
ctx.db ctx.db
.mutate(|d| { .mutate(|d| {
d.as_public_mut() let entry = d
.as_public_mut()
.as_package_data_mut() .as_package_data_mut()
.as_idx_mut(&manifest.id) .as_idx_mut(&manifest.id)
.or_not_found(&manifest.id)? .or_not_found(&manifest.id)?;
.ser(&PackageDataEntry::Installed(PackageDataEntryInstalled { entry
installed: InstalledPackageInfo { .as_state_info_mut()
current_dependencies: Default::default(), // TODO .ser(&PackageState::Installed(InstalledState { manifest }))?;
current_dependents: Default::default(), // TODO entry.as_developer_key_mut().ser(&developer_key)?;
dependency_info: Default::default(), // TODO entry.as_icon_mut().ser(&icon)?;
developer_key, // TODO: marketplace url
status: Status { // TODO: dependency info
configured: false, // TODO Ok(())
main: MainStatus::Stopped, // TODO
dependency_config_errors: Default::default(), // TODO
},
interface_addresses: Default::default(), // TODO
marketplace_url: None, // TODO
manifest: manifest.clone(),
last_backup: None, // TODO
hosts: Default::default(), // TODO
store_exposed_dependents: Default::default(), // TODO
store_exposed_ui: Default::default(), // TODO
},
manifest,
static_files,
}))
}) })
.await?; .await?;
Ok(service) Ok(service)
@@ -466,11 +455,7 @@ impl Actor for ServiceActor {
seed.ctx seed.ctx
.db .db
.mutate(|d| { .mutate(|d| {
if let Some(i) = d if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id)
.as_public_mut()
.as_package_data_mut()
.as_idx_mut(&id)
.and_then(|p| p.as_installed_mut())
{ {
i.as_status_mut().as_main_mut().ser(&main_status)?; i.as_status_mut().as_main_mut().ser(&main_status)?;
} }

View File

@@ -1,3 +1,4 @@
use std::collections::BTreeSet;
use std::ffi::OsString; use std::ffi::OsString;
use std::os::unix::process::CommandExt; use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -6,14 +7,16 @@ use std::sync::{Arc, Weak};
use clap::builder::ValueParserFactory; use clap::builder::ValueParserFactory;
use clap::Parser; use clap::Parser;
use imbl::OrdMap;
use imbl_value::{json, InternedString}; use imbl_value::{json, InternedString};
use models::{ActionId, HealthCheckId, ImageId, PackageId}; use models::{ActionId, HealthCheckId, ImageId, InvalidId, PackageId};
use patch_db::json_ptr::JsonPointer; use patch_db::json_ptr::JsonPointer;
use rpc_toolkit::{from_fn, from_fn_async, AnyContext, Context, Empty, HandlerExt, ParentHandler}; use rpc_toolkit::{from_fn, from_fn_async, AnyContext, Context, Empty, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use tokio::process::Command; use tokio::process::Command;
use ts_rs::TS; use ts_rs::TS;
use crate::db::model::ExposedUI; use crate::db::model::package::{CurrentDependencies, CurrentDependencyInfo, ExposedUI};
use crate::disk::mount::filesystem::idmapped::IdMapped; use crate::disk::mount::filesystem::idmapped::IdMapped;
use crate::disk::mount::filesystem::loop_dev::LoopDev; use crate::disk::mount::filesystem::loop_dev::LoopDev;
use crate::disk::mount::filesystem::overlayfs::OverlayGuard; use crate::disk::mount::filesystem::overlayfs::OverlayGuard;
@@ -131,29 +134,250 @@ pub fn service_effect_handler() -> ParentHandler {
.subcommand("clearBindings", from_fn_async(clear_bindings).no_cli()) .subcommand("clearBindings", from_fn_async(clear_bindings).no_cli())
.subcommand("bind", from_fn_async(bind).no_cli()) .subcommand("bind", from_fn_async(bind).no_cli())
.subcommand("getHostInfo", from_fn_async(get_host_info).no_cli()) .subcommand("getHostInfo", from_fn_async(get_host_info).no_cli())
// TODO @DrBonez when we get the new api for 4.0 .subcommand(
// .subcommand("setDependencies",from_fn_async(set_dependencies).no_cli()) "setDependencies",
// .subcommand("embassyGetInterface",from_fn_async(embassy_get_interface).no_cli()) from_fn_async(set_dependencies)
// .subcommand("mount",from_fn_async(mount).no_cli()) .no_display()
// .subcommand("removeAction",from_fn_async(remove_action).no_cli()) .with_remote_cli::<ContainerCliContext>(),
// .subcommand("removeAddress",from_fn_async(remove_address).no_cli()) )
// .subcommand("exportAction",from_fn_async(export_action).no_cli()) .subcommand("getSystemSmtp", from_fn_async(get_system_smtp).no_cli())
// .subcommand("clearServiceInterfaces",from_fn_async(clear_network_interfaces).no_cli()) .subcommand("getContainerIp", from_fn_async(get_container_ip).no_cli())
// .subcommand("exportServiceInterface",from_fn_async(export_network_interface).no_cli()) .subcommand(
// .subcommand("getHostnames",from_fn_async(get_hostnames).no_cli()) "getServicePortForward",
// .subcommand("getInterface",from_fn_async(get_interface).no_cli()) from_fn_async(get_service_port_forward).no_cli(),
// .subcommand("listInterface",from_fn_async(list_interface).no_cli()) )
// .subcommand("getIPHostname",from_fn_async(get_ip_hostname).no_cli()) .subcommand(
// .subcommand("getContainerIp",from_fn_async(get_container_ip).no_cli()) "clearServiceInterfaces",
// .subcommand("getLocalHostname",from_fn_async(get_local_hostname).no_cli()) from_fn_async(clear_network_interfaces).no_cli(),
// .subcommand("getPrimaryUrl",from_fn_async(get_primary_url).no_cli()) )
// .subcommand("getServicePortForward",from_fn_async(get_service_port_forward).no_cli()) .subcommand(
// .subcommand("getServiceTorHostname",from_fn_async(get_service_tor_hostname).no_cli()) "exportServiceInterface",
// .subcommand("getSystemSmtp",from_fn_async(get_system_smtp).no_cli()) from_fn_async(export_service_interface).no_cli(),
// .subcommand("reverseProxy",from_fn_async(reverse_proxy).no_cli()) )
.subcommand("getPrimaryUrl", from_fn_async(get_primary_url).no_cli())
.subcommand(
"listServiceInterfaces",
from_fn_async(list_service_interfaces).no_cli(),
)
.subcommand("removeAddress", from_fn_async(remove_address).no_cli())
.subcommand("exportAction", from_fn_async(export_action).no_cli())
.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 // TODO Callbacks
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct GetSystemSmtpParams {
callback: Callback,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct GetServicePortForwardParams {
#[ts(type = "string | null")]
package_id: Option<PackageId>,
internal_port: u32,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct BindOptionsSecure {
ssl: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct BindOptions {
scheme: Option<String>,
preferred_external_port: u32,
add_ssl: Option<AddSslOptions>,
secure: Option<BindOptionsSecure>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct AddressInfo {
username: Option<String>,
host_id: String,
bind_options: BindOptions,
suffix: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
enum ServiceInterfaceType {
Ui,
P2p,
Api,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct ExportServiceInterfaceParams {
id: String,
name: String,
description: String,
has_primary: bool,
disabled: bool,
masked: bool,
address_info: AddressInfo,
r#type: ServiceInterfaceType,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct GetPrimaryUrlParams {
#[ts(type = "string | null")]
package_id: Option<PackageId>,
service_interface_id: String,
callback: Callback,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct ListServiceInterfacesParams {
#[ts(type = "string | null")]
package_id: Option<PackageId>,
callback: Callback,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct RemoveAddressParams {
id: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "kebab-case")]
enum AllowedStatuses {
OnlyRunning,
OnlyStopped,
Any,
Disabled,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct ExportActionParams {
name: String,
description: String,
id: String,
#[ts(type = "{[key: string]: any}")]
input: Value,
allowed_statuses: AllowedStatuses,
group: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct RemoveActionParams {
id: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct ReverseProxyBind {
ip: Option<String>,
port: u32,
ssl: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct ReverseProxyDestination {
ip: Option<String>,
port: u32,
ssl: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct ReverseProxyHttp {
#[ts(type = "null | {[key: string]: string}")]
headers: Option<OrdMap<String, String>>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct ReverseProxyParams {
bind: ReverseProxyBind,
dst: ReverseProxyDestination,
http: ReverseProxyHttp,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct MountTarget {
#[ts(type = "string")]
package_id: PackageId,
volume_id: String,
path: String,
readonly: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
struct MountParams {
location: String,
target: MountTarget,
}
async fn get_system_smtp(
context: EffectContext,
data: GetSystemSmtpParams,
) -> Result<Value, Error> {
todo!()
}
async fn get_container_ip(context: EffectContext, _: Empty) -> Result<Value, Error> {
todo!()
}
async fn get_service_port_forward(
context: EffectContext,
data: GetServicePortForwardParams,
) -> Result<Value, Error> {
todo!()
}
async fn clear_network_interfaces(context: EffectContext, _: Empty) -> Result<Value, Error> {
todo!()
}
async fn export_service_interface(
context: EffectContext,
data: ExportServiceInterfaceParams,
) -> Result<Value, Error> {
todo!()
}
async fn get_primary_url(
context: EffectContext,
data: GetPrimaryUrlParams,
) -> Result<Value, Error> {
todo!()
}
async fn list_service_interfaces(
context: EffectContext,
data: ListServiceInterfacesParams,
) -> Result<Value, Error> {
todo!()
}
async fn remove_address(context: EffectContext, data: RemoveAddressParams) -> Result<Value, Error> {
todo!()
}
async fn export_action(context: EffectContext, data: ExportActionParams) -> Result<Value, Error> {
todo!()
}
async fn remove_action(context: EffectContext, data: RemoveActionParams) -> Result<Value, Error> {
todo!()
}
async fn reverse_proxy(context: EffectContext, data: ReverseProxyParams) -> Result<Value, Error> {
todo!()
}
async fn mount(context: EffectContext, data: MountParams) -> Result<Value, Error> {
todo!()
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)] #[ts(export)]
struct Callback(#[ts(type = "() => void")] i64); struct Callback(#[ts(type = "() => void")] i64);
@@ -170,7 +394,8 @@ enum GetHostInfoParamsKind {
struct GetHostInfoParams { struct GetHostInfoParams {
kind: Option<GetHostInfoParamsKind>, kind: Option<GetHostInfoParamsKind>,
service_interface_id: String, service_interface_id: String,
package_id: Option<String>, #[ts(type = "string | null")]
package_id: Option<PackageId>,
callback: Callback, callback: Callback,
} }
async fn get_host_info( async fn get_host_info(
@@ -211,8 +436,7 @@ struct BindParams {
scheme: String, scheme: String,
preferred_external_port: u32, preferred_external_port: u32,
add_ssl: Option<AddSslOptions>, add_ssl: Option<AddSslOptions>,
secure: bool, secure: Option<BindOptionsSecure>,
ssl: bool,
} }
async fn bind(_: AnyContext, BindParams { .. }: BindParams) -> Result<Value, Error> { async fn bind(_: AnyContext, BindParams { .. }: BindParams) -> Result<Value, Error> {
todo!() todo!()
@@ -222,6 +446,7 @@ async fn bind(_: AnyContext, BindParams { .. }: BindParams) -> Result<Value, Err
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
struct GetServiceInterfaceParams { struct GetServiceInterfaceParams {
#[ts(type = "string | null")]
package_id: Option<PackageId>, package_id: Option<PackageId>,
service_interface_id: String, service_interface_id: String,
callback: Callback, callback: Callback,
@@ -375,6 +600,7 @@ async fn get_ssl_key(
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
struct GetStoreParams { struct GetStoreParams {
#[ts(type = "string | null")]
package_id: Option<PackageId>, package_id: Option<PackageId>,
#[ts(type = "string")] #[ts(type = "string")]
path: JsonPointer, path: JsonPointer,
@@ -457,8 +683,6 @@ async fn expose_for_dependents(
.as_package_data_mut() .as_package_data_mut()
.as_idx_mut(&package_id) .as_idx_mut(&package_id)
.or_not_found(&package_id)? .or_not_found(&package_id)?
.as_installed_mut()
.or_not_found(&package_id)?
.as_store_exposed_dependents_mut() .as_store_exposed_dependents_mut()
.ser(&paths) .ser(&paths)
}) })
@@ -467,37 +691,45 @@ async fn expose_for_dependents(
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[serde(tag = "type")]
#[ts(export)] #[ts(export)]
struct ExposeUiParams { enum ExposeUiParams {
paths: Vec<ExposedUI>, Object {
#[ts(type = "{[key: string]: ExposeUiParams}")]
value: OrdMap<String, ExposeUiParams>,
},
String {
path: String,
description: Option<String>,
masked: bool,
copyable: Option<bool>,
qr: Option<bool>,
},
} }
async fn expose_ui( async fn expose_ui(context: EffectContext, params: ExposeUiParams) -> Result<(), Error> {
context: EffectContext, todo!()
ExposeUiParams { paths }: ExposeUiParams, // let context = context.deref()?;
) -> Result<(), Error> { // let package_id = context.id.clone();
let context = context.deref()?; // context
let package_id = context.id.clone(); // .ctx
context // .db
.ctx // .mutate(|db| {
.db // db.as_public_mut()
.mutate(|db| { // .as_package_data_mut()
db.as_public_mut() // .as_idx_mut(&package_id)
.as_package_data_mut() // .or_not_found(&package_id)?
.as_idx_mut(&package_id) // .as_store_exposed_ui_mut()
.or_not_found(&package_id)? // .ser(&paths)
.as_installed_mut() // })
.or_not_found(&package_id)? // .await?;
.as_store_exposed_ui_mut() // Ok(())
.ser(&paths)
})
.await?;
Ok(())
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct ParamsPackageId { struct ParamsPackageId {
#[ts(type = "string")]
package_id: PackageId, package_id: PackageId,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)]
@@ -505,6 +737,7 @@ struct ParamsPackageId {
#[command(rename_all = "camelCase")] #[command(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
struct ParamsMaybePackageId { struct ParamsMaybePackageId {
#[ts(type = "string | null")]
package_id: Option<PackageId>, package_id: Option<PackageId>,
} }
@@ -523,7 +756,9 @@ async fn exists(context: EffectContext, params: ParamsPackageId) -> Result<Value
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
struct ExecuteAction { struct ExecuteAction {
#[ts(type = "string | null")]
service_id: Option<PackageId>, service_id: Option<PackageId>,
#[ts(type = "string")]
action_id: ActionId, action_id: ActionId,
#[ts(type = "any")] #[ts(type = "any")]
input: Value, input: Value,
@@ -560,8 +795,6 @@ async fn get_configured(context: EffectContext, _: Empty) -> Result<Value, Error
.as_package_data() .as_package_data()
.as_idx(&package_id) .as_idx(&package_id)
.or_not_found(&package_id)? .or_not_found(&package_id)?
.as_installed()
.or_not_found(&package_id)?
.as_status() .as_status()
.as_configured() .as_configured()
.de()?; .de()?;
@@ -577,25 +810,21 @@ async fn stopped(context: EffectContext, params: ParamsMaybePackageId) -> Result
.as_package_data() .as_package_data()
.as_idx(&package_id) .as_idx(&package_id)
.or_not_found(&package_id)? .or_not_found(&package_id)?
.as_installed()
.or_not_found(&package_id)?
.as_status() .as_status()
.as_main() .as_main()
.de()?; .de()?;
Ok(json!(matches!(package, MainStatus::Stopped))) Ok(json!(matches!(package, MainStatus::Stopped)))
} }
async fn running(context: EffectContext, params: ParamsMaybePackageId) -> Result<Value, Error> { async fn running(context: EffectContext, params: ParamsPackageId) -> Result<Value, Error> {
dbg!("Starting the running {params:?}"); dbg!("Starting the running {params:?}");
let context = context.deref()?; let context = context.deref()?;
let peeked = context.ctx.db.peek().await; let peeked = context.ctx.db.peek().await;
let package_id = params.package_id.unwrap_or_else(|| context.id.clone()); let package_id = params.package_id;
let package = peeked let package = peeked
.as_public() .as_public()
.as_package_data() .as_package_data()
.as_idx(&package_id) .as_idx(&package_id)
.or_not_found(&package_id)? .or_not_found(&package_id)?
.as_installed()
.or_not_found(&package_id)?
.as_status() .as_status()
.as_main() .as_main()
.de()?; .de()?;
@@ -646,8 +875,6 @@ async fn set_configured(context: EffectContext, params: SetConfigured) -> Result
.as_package_data_mut() .as_package_data_mut()
.as_idx_mut(package_id) .as_idx_mut(package_id)
.or_not_found(package_id)? .or_not_found(package_id)?
.as_installed_mut()
.or_not_found(package_id)?
.as_status_mut() .as_status_mut()
.as_configured_mut() .as_configured_mut()
.ser(&params.configured) .ser(&params.configured)
@@ -701,6 +928,7 @@ async fn set_main_status(context: EffectContext, params: SetMainStatus) -> Resul
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
struct SetHealth { struct SetHealth {
#[ts(type = "string")]
name: HealthCheckId, name: HealthCheckId,
status: HealthCheckString, status: HealthCheckString,
message: Option<String>, message: Option<String>,
@@ -726,8 +954,6 @@ async fn set_health(
.as_package_data() .as_package_data()
.as_idx(package_id) .as_idx(package_id)
.or_not_found(package_id)? .or_not_found(package_id)?
.as_installed()
.or_not_found(package_id)?
.as_status() .as_status()
.as_main() .as_main()
.de()?; .de()?;
@@ -757,8 +983,6 @@ async fn set_health(
.as_package_data_mut() .as_package_data_mut()
.as_idx_mut(package_id) .as_idx_mut(package_id)
.or_not_found(package_id)? .or_not_found(package_id)?
.as_installed_mut()
.or_not_found(package_id)?
.as_status_mut() .as_status_mut()
.as_main_mut() .as_main_mut()
.ser(&main) .ser(&main)
@@ -771,7 +995,6 @@ async fn set_health(
#[command(rename_all = "camelCase")] #[command(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct DestroyOverlayedImageParams { pub struct DestroyOverlayedImageParams {
image_id: ImageId,
#[ts(type = "string")] #[ts(type = "string")]
guid: InternedString, guid: InternedString,
} }
@@ -779,7 +1002,7 @@ pub struct DestroyOverlayedImageParams {
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn destroy_overlayed_image( pub async fn destroy_overlayed_image(
ctx: EffectContext, ctx: EffectContext,
DestroyOverlayedImageParams { image_id, guid }: DestroyOverlayedImageParams, DestroyOverlayedImageParams { guid }: DestroyOverlayedImageParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
let ctx = ctx.deref()?; let ctx = ctx.deref()?;
if ctx if ctx
@@ -799,6 +1022,7 @@ pub async fn destroy_overlayed_image(
#[command(rename_all = "camelCase")] #[command(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct CreateOverlayedImageParams { pub struct CreateOverlayedImageParams {
#[ts(type = "string")]
image_id: ImageId, image_id: ImageId,
} }
@@ -864,3 +1088,127 @@ pub async fn create_overlayed_image(
)) ))
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
enum DependencyKind {
Exists,
Running,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
struct DependencyRequirement {
#[ts(type = "string")]
id: PackageId,
kind: DependencyKind,
#[serde(default)]
#[ts(type = "string[]")]
health_checks: BTreeSet<HealthCheckId>,
}
// filebrowser:exists,bitcoind:running:foo+bar+baz
impl FromStr for DependencyRequirement {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once(':') {
Some((id, "e")) | Some((id, "exists")) => Ok(Self {
id: id.parse()?,
kind: DependencyKind::Exists,
health_checks: BTreeSet::new(),
}),
Some((id, rest)) => {
let health_checks = match rest.split_once(":") {
Some(("r", rest)) | Some(("running", rest)) => rest
.split('+')
.map(|id| id.parse().map_err(Error::from))
.collect(),
Some((kind, _)) => Err(Error::new(
eyre!("unknown dependency kind {kind}"),
ErrorKind::InvalidRequest,
)),
None => match rest {
"r" | "running" => Ok(BTreeSet::new()),
kind => Err(Error::new(
eyre!("unknown dependency kind {kind}"),
ErrorKind::InvalidRequest,
)),
},
}?;
Ok(Self {
id: id.parse()?,
kind: DependencyKind::Running,
health_checks,
})
}
None => Ok(Self {
id: s.parse()?,
kind: DependencyKind::Running,
health_checks: BTreeSet::new(),
}),
}
}
}
impl ValueParserFactory for DependencyRequirement {
type Parser = FromStrParser<Self>;
fn value_parser() -> Self::Parser {
FromStrParser::new()
}
}
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[command(rename_all = "camelCase")]
#[ts(export)]
struct SetDependenciesParams {
dependencies: Vec<DependencyRequirement>,
}
async fn set_dependencies(
ctx: EffectContext,
SetDependenciesParams { dependencies }: SetDependenciesParams,
) -> Result<(), Error> {
let ctx = ctx.deref()?;
let id = &ctx.id;
ctx.ctx
.db
.mutate(|db| {
let dependencies = CurrentDependencies(
dependencies
.into_iter()
.map(
|DependencyRequirement {
id,
kind,
health_checks,
}| {
(
id,
match kind {
DependencyKind::Exists => CurrentDependencyInfo::Exists,
DependencyKind::Running => {
CurrentDependencyInfo::Running { health_checks }
}
},
)
},
)
.collect(),
);
for (dep, entry) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
if let Some(info) = dependencies.0.get(&dep) {
entry.as_current_dependents_mut().insert(id, info)?;
} else {
entry.as_current_dependents_mut().remove(id)?;
}
}
db.as_public_mut()
.as_package_data_mut()
.as_idx_mut(id)
.or_not_found(id)?
.as_current_dependencies_mut()
.ser(&dependencies)
})
.await
}

View File

@@ -11,9 +11,8 @@ use tokio::sync::{Mutex, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock};
use tracing::instrument; use tracing::instrument;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::db::model::{ use crate::db::model::package::{
PackageDataEntry, PackageDataEntryInstalled, PackageDataEntryInstalling, InstallingInfo, InstallingState, PackageDataEntry, PackageState, UpdatingState,
PackageDataEntryRestoring, PackageDataEntryUpdating, StaticFiles,
}; };
use crate::disk::mount::guard::GenericMountGuard; use crate::disk::mount::guard::GenericMountGuard;
use crate::install::PKG_ARCHIVE_DIR; use crate::install::PKG_ARCHIVE_DIR;
@@ -27,6 +26,7 @@ use crate::s9pk::manifest::PackageId;
use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::merkle_archive::source::FileSource;
use crate::s9pk::S9pk; use crate::s9pk::S9pk;
use crate::service::{LoadDisposition, Service}; use crate::service::{LoadDisposition, Service};
use crate::status::{MainStatus, Status};
pub type DownloadInstallFuture = BoxFuture<'static, Result<InstallFuture, Error>>; pub type DownloadInstallFuture = BoxFuture<'static, Result<InstallFuture, Error>>;
pub type InstallFuture = BoxFuture<'static, Result<(), Error>>; pub type InstallFuture = BoxFuture<'static, Result<(), Error>>;
@@ -95,9 +95,10 @@ impl ServiceMap {
mut s9pk: S9pk<S>, mut s9pk: S9pk<S>,
recovery_source: Option<impl GenericMountGuard>, recovery_source: Option<impl GenericMountGuard>,
) -> Result<DownloadInstallFuture, Error> { ) -> Result<DownloadInstallFuture, Error> {
let manifest = Arc::new(s9pk.as_manifest().clone()); let manifest = s9pk.as_manifest().clone();
let id = manifest.id.clone(); let id = manifest.id.clone();
let icon = s9pk.icon_data_url().await?; let icon = s9pk.icon_data_url().await?;
let developer_key = s9pk.as_archive().signer();
let mut service = self.get_mut(&id).await; let mut service = self.get_mut(&id).await;
let op_name = if recovery_source.is_none() { let op_name = if recovery_source.is_none() {
@@ -135,49 +136,51 @@ impl ServiceMap {
let id = id.clone(); let id = id.clone();
let install_progress = progress.snapshot(); let install_progress = progress.snapshot();
move |db| { move |db| {
let pde = match db if let Some(pde) = db.as_public_mut().as_package_data_mut().as_idx_mut(&id) {
.as_public() let prev = pde.as_state_info().expect_installed()?.de()?;
.as_package_data() pde.as_state_info_mut()
.as_idx(&id) .ser(&PackageState::Updating(UpdatingState {
.map(|x| x.de()) manifest: prev.manifest,
.transpose()? installing_info: InstallingInfo {
{ new_manifest: manifest,
Some(PackageDataEntry::Installed(PackageDataEntryInstalled { progress: install_progress,
installed, },
static_files, }))?;
.. } else {
})) => PackageDataEntry::Updating(PackageDataEntryUpdating { let installing = InstallingState {
install_progress, installing_info: InstallingInfo {
installed, new_manifest: manifest,
manifest: (*manifest).clone(), progress: install_progress,
static_files, },
}), };
None if restoring => { db.as_public_mut().as_package_data_mut().insert(
PackageDataEntry::Restoring(PackageDataEntryRestoring { &id,
install_progress, &PackageDataEntry {
static_files: StaticFiles::local( state_info: if restoring {
&manifest.id, PackageState::Restoring(installing)
&manifest.version, } else {
icon, PackageState::Installing(installing)
), },
manifest: (*manifest).clone(), status: Status {
}) configured: false,
} main: MainStatus::Stopped,
None => PackageDataEntry::Installing(PackageDataEntryInstalling { dependency_config_errors: Default::default(),
install_progress, },
static_files: StaticFiles::local(&manifest.id, &manifest.version, icon), marketplace_url: None,
manifest: (*manifest).clone(), developer_key,
}), icon,
_ => { last_backup: None,
return Err(Error::new( dependency_info: Default::default(),
eyre!("Cannot install over a package in a transient state"), current_dependents: Default::default(), // TODO: initialize
crate::ErrorKind::InvalidRequest, current_dependencies: Default::default(),
)) interface_addresses: Default::default(),
} hosts: Default::default(),
store_exposed_ui: Default::default(),
store_exposed_dependents: Default::default(),
},
)?;
}; };
db.as_public_mut() Ok(())
.as_package_data_mut()
.insert(&manifest.id, &pde)
} }
})) }))
.await?; .await?;
@@ -200,7 +203,8 @@ impl ServiceMap {
v.as_public_mut() v.as_public_mut()
.as_package_data_mut() .as_package_data_mut()
.as_idx_mut(&deref_id) .as_idx_mut(&deref_id)
.and_then(|e| e.as_install_progress_mut()) .and_then(|e| e.as_state_info_mut().as_installing_info_mut())
.map(|i| i.as_progress_mut())
}, },
Some(Duration::from_millis(100)), Some(Duration::from_millis(100)),
))); )));

View File

@@ -25,6 +25,7 @@ impl std::fmt::Display for HealthCheckResult {
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub enum HealthCheckString { pub enum HealthCheckString {
Passing, Passing,
Disabled, Disabled,

View File

@@ -14,7 +14,7 @@ use tokio_stream::StreamExt;
use tracing::instrument; use tracing::instrument;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::db::model::UpdateProgress; use crate::db::model::public::UpdateProgress;
use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::bind::Bind;
use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::guard::MountGuard; use crate::disk::mount::guard::MountGuard;

View File

@@ -19,9 +19,14 @@ import {
BackupOptions, BackupOptions,
DeepPartial, DeepPartial,
MaybePromise, MaybePromise,
ServiceInterfaceId,
PackageId,
EnsureStorePath,
ExtractStore,
DaemonReturned,
ValidIfNoStupidEscape,
} from "./types" } from "./types"
import * as patterns from "./util/patterns" import * as patterns from "./util/patterns"
import { Utils } from "./util/utils"
import { DependencyConfig, Update } from "./dependencyConfig/DependencyConfig" import { DependencyConfig, Update } from "./dependencyConfig/DependencyConfig"
import { BackupSet, Backups } from "./backup/Backups" import { BackupSet, Backups } from "./backup/Backups"
import { smtpConfig } from "./config/configConstants" import { smtpConfig } from "./config/configConstants"
@@ -53,6 +58,19 @@ import {
} from "./interfaces/setupInterfaces" } from "./interfaces/setupInterfaces"
import { successFailure } from "./trigger/successFailure" import { successFailure } from "./trigger/successFailure"
import { SetupExports } from "./inits/setupExports" import { SetupExports } from "./inits/setupExports"
import { HealthReceipt } from "./health/HealthReceipt"
import { MultiHost, Scheme, SingleHost, StaticHost } from "./interfaces/Host"
import { ServiceInterfaceBuilder } from "./interfaces/ServiceInterfaceBuilder"
import { GetSystemSmtp } from "./util/GetSystemSmtp"
import nullIfEmpty from "./util/nullIfEmpty"
import {
GetServiceInterface,
getServiceInterface,
} from "./util/getServiceInterface"
import { getServiceInterfaces } from "./util/getServiceInterfaces"
import { getStore } from "./store/getStore"
import { CommandOptions, MountOptions, Overlay } from "./util/Overlay"
import { splitCommand } from "./util/splitCommand"
// prettier-ignore // prettier-ignore
type AnyNeverCond<T extends any[], Then, Else> = type AnyNeverCond<T extends any[], Then, Else> =
@@ -61,6 +79,17 @@ type AnyNeverCond<T extends any[], Then, Else> =
T extends [any, ...infer U] ? AnyNeverCond<U,Then, Else> : T extends [any, ...infer U] ? AnyNeverCond<U,Then, Else> :
never never
export type ServiceInterfaceType = "ui" | "p2p" | "api"
export type MainEffects = Effects & { _type: "main" }
export type Signals = NodeJS.Signals
export const SIGTERM: Signals = "SIGTERM"
export const SIGKILL: Signals = "SIGTERM"
export const NO_TIMEOUT = -1
function removeConstType<E>() {
return <T>(t: T) => t as T & (E extends MainEffects ? {} : { const: never })
}
export class StartSdk<Manifest extends SDKManifest, Store> { export class StartSdk<Manifest extends SDKManifest, Store> {
private constructor(readonly manifest: Manifest) {} private constructor(readonly manifest: Manifest) {}
static of() { static of() {
@@ -75,7 +104,78 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
build(isReady: AnyNeverCond<[Manifest, Store], "Build not ready", true>) { build(isReady: AnyNeverCond<[Manifest, Store], "Build not ready", true>) {
return { return {
serviceInterface: {
getOwn: <E extends Effects>(effects: E, id: ServiceInterfaceId) =>
removeConstType<E>()(
getServiceInterface(effects, {
id,
packageId: null,
}),
),
get: <E extends Effects>(
effects: E,
opts: { id: ServiceInterfaceId; packageId: PackageId },
) => removeConstType<E>()(getServiceInterface(effects, opts)),
getAllOwn: <E extends Effects>(effects: E) =>
removeConstType<E>()(
getServiceInterfaces(effects, {
packageId: null,
}),
),
getAll: <E extends Effects>(
effects: E,
opts: { packageId: PackageId },
) => removeConstType<E>()(getServiceInterfaces(effects, opts)),
},
store: {
get: <E extends Effects, Path extends string = never>(
effects: E,
packageId: string,
path: EnsureStorePath<Store, Path>,
) =>
removeConstType<E>()(
getStore<Store, Path>(effects, path as any, {
packageId,
}),
),
getOwn: <E extends Effects, Path extends string>(
effects: E,
path: EnsureStorePath<Store, Path>,
) => removeConstType<E>()(getStore<Store, Path>(effects, path as any)),
setOwn: <E extends Effects, Path extends string | never>(
effects: E,
path: EnsureStorePath<Store, Path>,
value: ExtractStore<Store, Path>,
) => effects.store.set<Store, Path>({ value, path: path as any }),
},
host: {
static: (effects: Effects, id: string) =>
new StaticHost({ id, effects }),
single: (effects: Effects, id: string) =>
new SingleHost({ id, effects }),
multi: (effects: Effects, id: string) => new MultiHost({ id, effects }),
},
nullIfEmpty,
configConstants: { smtpConfig }, configConstants: { smtpConfig },
createInterface: (
effects: Effects,
options: {
name: string
id: string
description: string
hasPrimary: boolean
disabled: boolean
type: ServiceInterfaceType
username: null | string
path: string
search: Record<string, string>
schemeOverride: { ssl: Scheme; noSsl: Scheme } | null
masked: boolean
},
) => new ServiceInterfaceBuilder({ ...options, effects }),
createAction: < createAction: <
ConfigType extends ConfigType extends
| Record<string, any> | Record<string, any>
@@ -88,13 +188,34 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
}, },
fn: (options: { fn: (options: {
effects: Effects effects: Effects
utils: Utils<Manifest, Store>
input: Type input: Type
}) => Promise<ActionResult>, }) => Promise<ActionResult>,
) => { ) => {
const { input, ...rest } = metaData const { input, ...rest } = metaData
return createAction<Manifest, Store, ConfigType, Type>(rest, fn, input) return createAction<Manifest, Store, ConfigType, Type>(rest, fn, input)
}, },
getSystemSmtp: <E extends Effects>(effects: E) =>
removeConstType<E>()(new GetSystemSmtp(effects)),
runCommand: async <A extends string>(
effects: Effects,
imageId: Manifest["images"][number],
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
},
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> => {
const commands = splitCommand(command)
const overlay = await Overlay.of(effects, imageId)
try {
for (let mount of options.mounts || []) {
await overlay.mount(mount.options, mount.path)
}
return await overlay.exec(commands)
} finally {
await overlay.destroy()
}
},
createDynamicAction: < createDynamicAction: <
ConfigType extends ConfigType extends
| Record<string, any> | Record<string, any>
@@ -104,11 +225,9 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
>( >(
metaData: (options: { metaData: (options: {
effects: Effects effects: Effects
utils: Utils<Manifest, Store>
}) => MaybePromise<Omit<ActionMetadata, "input">>, }) => MaybePromise<Omit<ActionMetadata, "input">>,
fn: (options: { fn: (options: {
effects: Effects effects: Effects
utils: Utils<Manifest, Store>
input: Type input: Type
}) => Promise<ActionResult>, }) => Promise<ActionResult>,
input: Config<Type, Store> | Config<Type, never>, input: Config<Type, Store> | Config<Type, never>,
@@ -193,9 +312,8 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
) => setupInterfaces(config, fn), ) => setupInterfaces(config, fn),
setupMain: ( setupMain: (
fn: (o: { fn: (o: {
effects: Effects effects: MainEffects
started(onTerm: () => PromiseLike<void>): PromiseLike<void> started(onTerm: () => PromiseLike<void>): PromiseLike<void>
utils: Utils<Manifest, Store, {}>
}) => Promise<Daemons<Manifest, any>>, }) => Promise<Daemons<Manifest, any>>,
) => setupMain<Manifest, Store>(fn), ) => setupMain<Manifest, Store>(fn),
setupMigrations: < setupMigrations: <
@@ -232,7 +350,15 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
spec: Spec, spec: Spec,
) => Config.of<Spec, Store>(spec), ) => Config.of<Spec, Store>(spec),
}, },
Daemons: { of: Daemons.of }, Daemons: {
of(config: {
effects: Effects
started: (onTerm: () => PromiseLike<void>) => PromiseLike<void>
healthReceipts: HealthReceipt[]
}) {
return Daemons.of<Manifest>(config)
},
},
DependencyConfig: { DependencyConfig: {
of< of<
LocalConfig extends Record<string, any>, LocalConfig extends Record<string, any>,
@@ -248,7 +374,6 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
dependencyConfig: (options: { dependencyConfig: (options: {
effects: Effects effects: Effects
localConfig: LocalConfig localConfig: LocalConfig
utils: Utils<Manifest, Store>
}) => Promise<void | DeepPartial<RemoteConfig>> }) => Promise<void | DeepPartial<RemoteConfig>>
update?: Update<void | DeepPartial<RemoteConfig>, RemoteConfig> update?: Update<void | DeepPartial<RemoteConfig>, RemoteConfig>
}) { }) {
@@ -332,14 +457,8 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
Migration: { Migration: {
of: <Version extends ManifestVersion>(options: { of: <Version extends ManifestVersion>(options: {
version: Version version: Version
up: (opts: { up: (opts: { effects: Effects }) => Promise<void>
effects: Effects down: (opts: { effects: Effects }) => Promise<void>
utils: Utils<Manifest, Store>
}) => Promise<void>
down: (opts: {
effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<void>
}) => Migration.of<Manifest, Store, Version>(options), }) => Migration.of<Manifest, Store, Version>(options),
}, },
Value: { Value: {

View File

@@ -1,15 +1,10 @@
import { Config, ExtractConfigType } from "../config/builder/config" import { Config, ExtractConfigType } from "../config/builder/config"
import { SDKManifest } from "../manifest/ManifestTypes" import { SDKManifest } from "../manifest/ManifestTypes"
import { ActionMetadata, ActionResult, Effects, ExportedAction } from "../types" import { ActionMetadata, ActionResult, Effects, ExportedAction } from "../types"
import { createUtils } from "../util"
import { Utils } from "../util/utils"
export type MaybeFn<Manifest extends SDKManifest, Store, Value> = export type MaybeFn<Manifest extends SDKManifest, Store, Value> =
| Value | Value
| ((options: { | ((options: { effects: Effects }) => Promise<Value> | Value)
effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<Value> | Value)
export class CreatedAction< export class CreatedAction<
Manifest extends SDKManifest, Manifest extends SDKManifest,
Store, Store,
@@ -27,7 +22,6 @@ export class CreatedAction<
>, >,
readonly fn: (options: { readonly fn: (options: {
effects: Effects effects: Effects
utils: Utils<Manifest, Store>
input: Type input: Type
}) => Promise<ActionResult>, }) => Promise<ActionResult>,
readonly input: Config<Type, Store>, readonly input: Config<Type, Store>,
@@ -44,11 +38,7 @@ export class CreatedAction<
Type extends Record<string, any> = ExtractConfigType<ConfigType>, Type extends Record<string, any> = ExtractConfigType<ConfigType>,
>( >(
metaData: MaybeFn<Manifest, Store, Omit<ActionMetadata, "input">>, metaData: MaybeFn<Manifest, Store, Omit<ActionMetadata, "input">>,
fn: (options: { fn: (options: { effects: Effects; input: Type }) => Promise<ActionResult>,
effects: Effects
utils: Utils<Manifest, Store>
input: Type
}) => Promise<ActionResult>,
inputConfig: Config<Type, Store> | Config<Type, never>, inputConfig: Config<Type, Store> | Config<Type, never>,
) { ) {
return new CreatedAction<Manifest, Store, ConfigType, Type>( return new CreatedAction<Manifest, Store, ConfigType, Type>(
@@ -61,7 +51,6 @@ export class CreatedAction<
exportedAction: ExportedAction = ({ effects, input }) => { exportedAction: ExportedAction = ({ effects, input }) => {
return this.fn({ return this.fn({
effects, effects,
utils: createUtils(effects),
input: this.validator.unsafeCast(input), input: this.validator.unsafeCast(input),
}) })
} }
@@ -69,21 +58,17 @@ export class CreatedAction<
run = async ({ effects, input }: { effects: Effects; input?: Type }) => { run = async ({ effects, input }: { effects: Effects; input?: Type }) => {
return this.fn({ return this.fn({
effects, effects,
utils: createUtils(effects),
input: this.validator.unsafeCast(input), input: this.validator.unsafeCast(input),
}) })
} }
async metaData(options: { effects: Effects; utils: Utils<Manifest, Store> }) { async metaData(options: { effects: Effects }) {
if (this.myMetaData instanceof Function) if (this.myMetaData instanceof Function)
return await this.myMetaData(options) return await this.myMetaData(options)
return this.myMetaData return this.myMetaData
} }
async ActionMetadata(options: { async ActionMetadata(options: { effects: Effects }): Promise<ActionMetadata> {
effects: Effects
utils: Utils<Manifest, Store>
}): Promise<ActionMetadata> {
return { return {
...(await this.metaData(options)), ...(await this.metaData(options)),
input: await this.input.build(options), input: await this.input.build(options),
@@ -93,7 +78,6 @@ export class CreatedAction<
async getConfig({ effects }: { effects: Effects }) { async getConfig({ effects }: { effects: Effects }) {
return this.input.build({ return this.input.build({
effects, effects,
utils: createUtils(effects) as any,
}) })
} }
} }

View File

@@ -1,17 +1,12 @@
import { SDKManifest } from "../manifest/ManifestTypes" import { SDKManifest } from "../manifest/ManifestTypes"
import { Effects, ExpectedExports } from "../types" import { Effects, ExpectedExports } from "../types"
import { createUtils } from "../util"
import { once } from "../util/once" import { once } from "../util/once"
import { Utils } from "../util/utils"
import { CreatedAction } from "./createAction" import { CreatedAction } from "./createAction"
export function setupActions<Manifest extends SDKManifest, Store>( export function setupActions<Manifest extends SDKManifest, Store>(
...createdActions: CreatedAction<Manifest, Store, any>[] ...createdActions: CreatedAction<Manifest, Store, any>[]
) { ) {
const myActions = async (options: { const myActions = async (options: { effects: Effects }) => {
effects: Effects
utils: Utils<Manifest, Store>
}) => {
const actions: Record<string, CreatedAction<Manifest, Store, any>> = {} const actions: Record<string, CreatedAction<Manifest, Store, any>> = {}
for (const action of createdActions) { for (const action of createdActions) {
const actionMetadata = await action.metaData(options) const actionMetadata = await action.metaData(options)
@@ -24,17 +19,11 @@ export function setupActions<Manifest extends SDKManifest, Store>(
actionsMetadata: ExpectedExports.actionsMetadata actionsMetadata: ExpectedExports.actionsMetadata
} = { } = {
actions(options: { effects: Effects }) { actions(options: { effects: Effects }) {
const utils = createUtils<Manifest, Store>(options.effects) return myActions(options)
return myActions({
...options,
utils,
})
}, },
async actionsMetadata({ effects }: { effects: Effects }) { async actionsMetadata({ effects }: { effects: Effects }) {
const utils = createUtils<Manifest, Store>(effects)
return Promise.all( return Promise.all(
createdActions.map((x) => x.ActionMetadata({ effects, utils })), createdActions.map((x) => x.ActionMetadata({ effects })),
) )
}, },
} }

View File

@@ -42,7 +42,7 @@ export class Backups<M extends SDKManifest> {
private constructor( private constructor(
private options = DEFAULT_OPTIONS, private options = DEFAULT_OPTIONS,
private backupSet = [] as BackupSet<M["volumes"][0]>[], private backupSet = [] as BackupSet<M["volumes"][number]>[],
) {} ) {}
static volumes<M extends SDKManifest = never>( static volumes<M extends SDKManifest = never>(
...volumeNames: Array<M["volumes"][0]> ...volumeNames: Array<M["volumes"][0]>

View File

@@ -4,7 +4,7 @@ import { ExpectedExports } from "../types"
import { _ } from "../util" import { _ } from "../util"
export type SetupBackupsParams<M extends SDKManifest> = Array< export type SetupBackupsParams<M extends SDKManifest> = Array<
M["volumes"][0] | Backups<M> M["volumes"][number] | Backups<M>
> >
export function setupBackups<M extends SDKManifest>( export function setupBackups<M extends SDKManifest>(

View File

@@ -1,5 +1,4 @@
import { ValueSpec } from "../configTypes" import { ValueSpec } from "../configTypes"
import { Utils } from "../../util/utils"
import { Value } from "./value" import { Value } from "./value"
import { _ } from "../../util" import { _ } from "../../util"
import { Effects } from "../../types" import { Effects } from "../../types"
@@ -7,7 +6,6 @@ import { Parser, object } from "ts-matches"
export type LazyBuildOptions<Store> = { export type LazyBuildOptions<Store> = {
effects: Effects effects: Effects
utils: Utils<any, Store>
} }
export type LazyBuild<Store, ExpectedOut> = ( export type LazyBuild<Store, ExpectedOut> = (
options: LazyBuildOptions<Store>, options: LazyBuildOptions<Store>,

View File

@@ -1,4 +1,5 @@
import { SmtpValue } from "../types" import { SmtpValue } from "../types"
import { GetSystemSmtp } from "../util/GetSystemSmtp"
import { email } from "../util/patterns" import { email } from "../util/patterns"
import { Config, ConfigSpecOf } from "./builder/config" import { Config, ConfigSpecOf } from "./builder/config"
import { Value } from "./builder/value" import { Value } from "./builder/value"
@@ -47,8 +48,8 @@ export const customSmtp = Config.of<ConfigSpecOf<SmtpValue>, never>({
* For service config. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings * For service config. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings
*/ */
export const smtpConfig = Value.filteredUnion( export const smtpConfig = Value.filteredUnion(
async ({ effects, utils }) => { async ({ effects }) => {
const smtp = await utils.getSystemSmtp().once() const smtp = await new GetSystemSmtp(effects).once()
return smtp ? [] : ["system"] return smtp ? [] : ["system"]
}, },
{ {

View File

@@ -3,7 +3,7 @@ import { Dependency } from "../types"
export type ConfigDependencies<T extends SDKManifest> = { export type ConfigDependencies<T extends SDKManifest> = {
exists(id: keyof T["dependencies"]): Dependency exists(id: keyof T["dependencies"]): Dependency
running(id: keyof T["dependencies"]): Dependency running(id: keyof T["dependencies"], healthChecks: string[]): Dependency
} }
export const configDependenciesSet = < export const configDependenciesSet = <
@@ -16,10 +16,11 @@ export const configDependenciesSet = <
} as Dependency } as Dependency
}, },
running(id: keyof T["dependencies"]) { running(id: keyof T["dependencies"], healthChecks: string[]) {
return { return {
id, id,
kind: "running", kind: "running",
healthChecks,
} as Dependency } as Dependency
}, },
}) })

View File

@@ -2,7 +2,6 @@ import { Effects, ExpectedExports } from "../types"
import { SDKManifest } from "../manifest/ManifestTypes" import { SDKManifest } from "../manifest/ManifestTypes"
import * as D from "./configDependencies" import * as D from "./configDependencies"
import { Config, ExtractConfigType } from "./builder/config" import { Config, ExtractConfigType } from "./builder/config"
import { Utils, createUtils } from "../util/utils"
import nullIfEmpty from "../util/nullIfEmpty" import nullIfEmpty from "../util/nullIfEmpty"
import { InterfaceReceipt } from "../interfaces/interfaceReceipt" import { InterfaceReceipt } from "../interfaces/interfaceReceipt"
import { InterfacesReceipt as InterfacesReceipt } from "../interfaces/setupInterfaces" import { InterfacesReceipt as InterfacesReceipt } from "../interfaces/setupInterfaces"
@@ -22,7 +21,6 @@ export type Save<
> = (options: { > = (options: {
effects: Effects effects: Effects
input: ExtractConfigType<A> & Record<string, any> input: ExtractConfigType<A> & Record<string, any>
utils: Utils<Manifest, Store>
dependencies: D.ConfigDependencies<Manifest> dependencies: D.ConfigDependencies<Manifest>
}) => Promise<{ }) => Promise<{
dependenciesReceipt: DependenciesReceipt dependenciesReceipt: DependenciesReceipt
@@ -38,7 +36,6 @@ export type Read<
| Config<Record<string, any>, never>, | Config<Record<string, any>, never>,
> = (options: { > = (options: {
effects: Effects effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<void | (ExtractConfigType<A> & Record<string, any>)> }) => Promise<void | (ExtractConfigType<A> & Record<string, any>)>
/** /**
* We want to setup a config export with a get and set, this * We want to setup a config export with a get and set, this
@@ -72,7 +69,6 @@ export function setupConfig<
const { restart } = await write({ const { restart } = await write({
input: JSON.parse(JSON.stringify(input)), input: JSON.parse(JSON.stringify(input)),
effects, effects,
utils: createUtils(effects),
dependencies: D.configDependenciesSet<Manifest>(), dependencies: D.configDependenciesSet<Manifest>(),
}) })
if (restart) { if (restart) {
@@ -80,14 +76,10 @@ export function setupConfig<
} }
}) as ExpectedExports.setConfig, }) as ExpectedExports.setConfig,
getConfig: (async ({ effects }) => { getConfig: (async ({ effects }) => {
const myUtils = createUtils<Manifest, Store>(effects) const configValue = nullIfEmpty((await read({ effects })) || null)
const configValue = nullIfEmpty(
(await read({ effects, utils: myUtils })) || null,
)
return { return {
spec: await spec.build({ spec: await spec.build({
effects, effects,
utils: myUtils as any,
}), }),
config: configValue, config: configValue,
} }

View File

@@ -3,7 +3,6 @@ import {
DeepPartial, DeepPartial,
Effects, Effects,
} from "../types" } from "../types"
import { Utils, createUtils } from "../util/utils"
import { deepEqual } from "../util/deepEqual" import { deepEqual } from "../util/deepEqual"
import { deepMerge } from "../util/deepMerge" import { deepMerge } from "../util/deepMerge"
import { SDKManifest } from "../manifest/ManifestTypes" import { SDKManifest } from "../manifest/ManifestTypes"
@@ -29,7 +28,6 @@ export class DependencyConfig<
readonly dependencyConfig: (options: { readonly dependencyConfig: (options: {
effects: Effects effects: Effects
localConfig: Input localConfig: Input
utils: Utils<Manifest, Store>
}) => Promise<void | DeepPartial<RemoteConfig>>, }) => Promise<void | DeepPartial<RemoteConfig>>,
readonly update: Update< readonly update: Update<
void | DeepPartial<RemoteConfig>, void | DeepPartial<RemoteConfig>,
@@ -41,7 +39,6 @@ export class DependencyConfig<
return this.dependencyConfig({ return this.dependencyConfig({
localConfig: options.localConfig as Input, localConfig: options.localConfig as Input,
effects: options.effects, effects: options.effects,
utils: createUtils<Manifest, Store>(options.effects),
}) })
} }
} }

View File

@@ -6,52 +6,59 @@ import { Trigger } from "../trigger"
import { TriggerInput } from "../trigger/TriggerInput" import { TriggerInput } from "../trigger/TriggerInput"
import { defaultTrigger } from "../trigger/defaultTrigger" import { defaultTrigger } from "../trigger/defaultTrigger"
import { once } from "../util/once" import { once } from "../util/once"
import { Overlay } from "../util/Overlay"
export function healthCheck(o: { export function healthCheck(o: {
effects: Effects effects: Effects
name: string name: string
imageId: string
trigger?: Trigger trigger?: Trigger
fn(): Promise<CheckResult> | CheckResult fn(overlay: Overlay): Promise<CheckResult> | CheckResult
onFirstSuccess?: () => unknown | Promise<unknown> onFirstSuccess?: () => unknown | Promise<unknown>
}) { }) {
new Promise(async () => { new Promise(async () => {
let currentValue: TriggerInput = { const overlay = await Overlay.of(o.effects, o.imageId)
hadSuccess: false, try {
} let currentValue: TriggerInput = {
const getCurrentValue = () => currentValue hadSuccess: false,
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()
await o.effects.setHealth({
name: o.name,
status,
message,
})
currentValue.hadSuccess = true
currentValue.lastResult = "passing"
await triggerFirstSuccess().catch((err) => {
console.error(err)
})
} catch (e) {
await o.effects.setHealth({
name: o.name,
status: "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,
status,
message,
})
currentValue.hadSuccess = true
currentValue.lastResult = "passing"
await triggerFirstSuccess().catch((err) => {
console.error(err)
})
} catch (e) {
await o.effects.setHealth({
name: o.name,
status: "failure",
message: asMessage(e),
})
currentValue.lastResult = "failure"
}
}
} finally {
await overlay.destroy()
} }
}) })
return {} as HealthReceipt return {} as HealthReceipt

View File

@@ -1,7 +1,12 @@
import { Effects } from "../../types" import { Effects } from "../../types"
import { createUtils } from "../../util"
import { stringFromStdErrOut } from "../../util/stringFromStdErrOut" import { stringFromStdErrOut } from "../../util/stringFromStdErrOut"
import { CheckResult } from "./CheckResult" import { CheckResult } from "./CheckResult"
import { promisify } from "node:util"
import * as CP from "node:child_process"
const cpExec = promisify(CP.exec)
const cpExecFile = promisify(CP.execFile)
export function containsAddress(x: string, port: number) { export function containsAddress(x: string, port: number) {
const readPorts = x const readPorts = x
.split("\n") .split("\n")
@@ -28,20 +33,15 @@ export async function checkPortListening(
timeout?: number timeout?: number
}, },
): Promise<CheckResult> { ): Promise<CheckResult> {
const utils = createUtils(effects)
return Promise.race<CheckResult>([ return Promise.race<CheckResult>([
Promise.resolve().then(async () => { Promise.resolve().then(async () => {
const hasAddress = const hasAddress =
containsAddress( containsAddress(
await utils.childProcess await cpExec(`cat /proc/net/tcp`, {}).then(stringFromStdErrOut),
.exec(`cat /proc/net/tcp`, {})
.then(stringFromStdErrOut),
port, port,
) || ) ||
containsAddress( containsAddress(
await utils.childProcess await cpExec("cat /proc/net/udp", {}).then(stringFromStdErrOut),
.exec("cat /proc/net/udp", {})
.then(stringFromStdErrOut),
port, port,
) )
if (hasAddress) { if (hasAddress) {

View File

@@ -1,5 +1,5 @@
import { CommandType, Effects } from "../../types" import { Effects } from "../../types"
import { createUtils } from "../../util" import { Overlay } from "../../util/Overlay"
import { stringFromStdErrOut } from "../../util/stringFromStdErrOut" import { stringFromStdErrOut } from "../../util/stringFromStdErrOut"
import { CheckResult } from "./CheckResult" import { CheckResult } from "./CheckResult"
import { timeoutPromise } from "./index" import { timeoutPromise } from "./index"
@@ -13,7 +13,8 @@ import { timeoutPromise } from "./index"
*/ */
export const runHealthScript = async ( export const runHealthScript = async (
effects: Effects, effects: Effects,
runCommand: string, runCommand: string[],
overlay: Overlay,
{ {
timeout = 30000, timeout = 30000,
errorMessage = `Error while running command: ${runCommand}`, errorMessage = `Error while running command: ${runCommand}`,
@@ -21,9 +22,8 @@ export const runHealthScript = async (
`Have ran script ${runCommand} and the result: ${res}`, `Have ran script ${runCommand} and the result: ${res}`,
} = {}, } = {},
): Promise<CheckResult> => { ): Promise<CheckResult> => {
const utils = createUtils(effects)
const res = await Promise.race([ const res = await Promise.race([
utils.childProcess.exec(runCommand, { timeout }).then(stringFromStdErrOut), overlay.exec(runCommand),
timeoutPromise(timeout), timeoutPromise(timeout),
]).catch((e) => { ]).catch((e) => {
console.warn(errorMessage) console.warn(errorMessage)
@@ -33,6 +33,6 @@ export const runHealthScript = async (
}) })
return { return {
status: "passing", status: "passing",
message: message(res), message: message(res.stdout.toString()),
} as CheckResult } as CheckResult
} }

View File

@@ -1,9 +1,9 @@
export { Daemons } from "./mainFn/Daemons" export { Daemons } from "./mainFn/Daemons"
export { EmVer } from "./emverLite/mod" export { EmVer } from "./emverLite/mod"
export { Overlay } from "./util/Overlay" export { Overlay } from "./util/Overlay"
export { Utils } from "./util/utils"
export { StartSdk } from "./StartSdk" export { StartSdk } from "./StartSdk"
export { setupManifest } from "./manifest/setupManifest" export { setupManifest } from "./manifest/setupManifest"
export { FileHelper } from "./util/fileHelper"
export * as actions from "./actions" export * as actions from "./actions"
export * as backup from "./backup" export * as backup from "./backup"
export * as config from "./config" export * as config from "./config"

View File

@@ -1,6 +1,5 @@
import { ManifestVersion, SDKManifest } from "../../manifest/ManifestTypes" import { ManifestVersion, SDKManifest } from "../../manifest/ManifestTypes"
import { Effects } from "../../types" import { Effects } from "../../types"
import { Utils } from "../../util/utils"
export class Migration< export class Migration<
Manifest extends SDKManifest, Manifest extends SDKManifest,
@@ -10,14 +9,8 @@ export class Migration<
constructor( constructor(
readonly options: { readonly options: {
version: Version version: Version
up: (opts: { up: (opts: { effects: Effects }) => Promise<void>
effects: Effects down: (opts: { effects: Effects }) => Promise<void>
utils: Utils<Manifest, Store>
}) => Promise<void>
down: (opts: {
effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<void>
}, },
) {} ) {}
static of< static of<
@@ -26,23 +19,17 @@ export class Migration<
Version extends ManifestVersion, Version extends ManifestVersion,
>(options: { >(options: {
version: Version version: Version
up: (opts: { up: (opts: { effects: Effects }) => Promise<void>
effects: Effects down: (opts: { effects: Effects }) => Promise<void>
utils: Utils<Manifest, Store>
}) => Promise<void>
down: (opts: {
effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<void>
}) { }) {
return new Migration<Manifest, Store, Version>(options) return new Migration<Manifest, Store, Version>(options)
} }
async up(opts: { effects: Effects; utils: Utils<Manifest, Store> }) { async up(opts: { effects: Effects }) {
this.up(opts) this.up(opts)
} }
async down(opts: { effects: Effects; utils: Utils<Manifest, Store> }) { async down(opts: { effects: Effects }) {
this.down(opts) this.down(opts)
} }
} }

View File

@@ -1,7 +1,6 @@
import { EmVer } from "../../emverLite/mod" import { EmVer } from "../../emverLite/mod"
import { SDKManifest } from "../../manifest/ManifestTypes" import { SDKManifest } from "../../manifest/ManifestTypes"
import { ExpectedExports } from "../../types" import { ExpectedExports } from "../../types"
import { createUtils } from "../../util"
import { once } from "../../util/once" import { once } from "../../util/once"
import { Migration } from "./Migration" import { Migration } from "./Migration"
@@ -32,13 +31,12 @@ export class Migrations<Manifest extends SDKManifest, Store> {
effects, effects,
previousVersion, previousVersion,
}: Parameters<ExpectedExports.init>[0]) { }: Parameters<ExpectedExports.init>[0]) {
const utils = createUtils<Manifest, Store>(effects)
if (!!previousVersion) { if (!!previousVersion) {
const previousVersionEmVer = EmVer.parse(previousVersion) const previousVersionEmVer = EmVer.parse(previousVersion)
for (const [_, migration] of this.sortedMigrations() for (const [_, migration] of this.sortedMigrations()
.filter((x) => x[0].greaterThan(previousVersionEmVer)) .filter((x) => x[0].greaterThan(previousVersionEmVer))
.filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) {
await migration.up({ effects, utils }) await migration.up({ effects })
} }
} }
} }
@@ -46,14 +44,13 @@ export class Migrations<Manifest extends SDKManifest, Store> {
effects, effects,
nextVersion, nextVersion,
}: Parameters<ExpectedExports.uninit>[0]) { }: Parameters<ExpectedExports.uninit>[0]) {
const utils = createUtils<Manifest, Store>(effects)
if (!!nextVersion) { if (!!nextVersion) {
const nextVersionEmVer = EmVer.parse(nextVersion) const nextVersionEmVer = EmVer.parse(nextVersion)
const reversed = [...this.sortedMigrations()].reverse() const reversed = [...this.sortedMigrations()].reverse()
for (const [_, migration] of reversed for (const [_, migration] of reversed
.filter((x) => x[0].greaterThan(nextVersionEmVer)) .filter((x) => x[0].greaterThan(nextVersionEmVer))
.filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) {
await migration.down({ effects, utils }) await migration.down({ effects })
} }
} }
} }

View File

@@ -1,16 +1,12 @@
import { Effects, ExposeServicePaths, ExposeUiPaths } from "../types" import { Effects, ExposeServicePaths, ExposeUiPaths } from "../types"
import { Utils } from "../util/utils"
export type SetupExports<Store> = (opts: { export type SetupExports<Store> = (opts: { effects: Effects }) =>
effects: Effects
utils: Utils<any, Store>
}) =>
| { | {
ui: ExposeUiPaths<Store> ui: { [k: string]: ExposeUiPaths<Store> }
services: ExposeServicePaths<Store> services: ExposeServicePaths<Store>
} }
| Promise<{ | Promise<{
ui: ExposeUiPaths<Store> ui: { [k: string]: ExposeUiPaths<Store> }
services: ExposeServicePaths<Store> services: ExposeServicePaths<Store>
}> }>

View File

@@ -1,7 +1,6 @@
import { SetInterfaces } from "../interfaces/setupInterfaces" import { SetInterfaces } from "../interfaces/setupInterfaces"
import { SDKManifest } from "../manifest/ManifestTypes" import { SDKManifest } from "../manifest/ManifestTypes"
import { ExpectedExports } from "../types" import { ExpectedExports, ExposeUiPaths, ExposeUiPathsAll } from "../types"
import { createUtils } from "../util"
import { Migrations } from "./migrations/setupMigrations" import { Migrations } from "./migrations/setupMigrations"
import { SetupExports } from "./setupExports" import { SetupExports } from "./setupExports"
import { Install } from "./setupInstall" import { Install } from "./setupInstall"
@@ -19,20 +18,20 @@ export function setupInit<Manifest extends SDKManifest, Store>(
} { } {
return { return {
init: async (opts) => { init: async (opts) => {
const utils = createUtils<Manifest, Store>(opts.effects)
await migrations.init(opts) await migrations.init(opts)
await install.init(opts) await install.init(opts)
await setInterfaces({ await setInterfaces({
...opts, ...opts,
input: null, input: null,
utils,
})
const { services, ui } = await setupExports({
...opts,
utils,
}) })
const { services, ui } = await setupExports(opts)
await opts.effects.exposeForDependents(services) await opts.effects.exposeForDependents(services)
await opts.effects.exposeUi({ paths: ui }) await opts.effects.exposeUi(
forExpose({
type: "object",
value: ui,
}),
)
}, },
uninit: async (opts) => { uninit: async (opts) => {
await migrations.uninit(opts) await migrations.uninit(opts)
@@ -40,3 +39,21 @@ export function setupInit<Manifest extends SDKManifest, Store>(
}, },
} }
} }
function forExpose<Store>(ui: ExposeUiPaths<Store>): ExposeUiPathsAll {
if (ui.type === ("object" as const)) {
return {
type: "object" as const,
value: Object.fromEntries(
Object.entries(ui.value).map(([key, value]) => [key, forExpose(value)]),
),
}
}
return {
description: null,
copyable: null,
qr: null,
...ui,
}
}

View File

@@ -1,10 +1,8 @@
import { SDKManifest } from "../manifest/ManifestTypes" import { SDKManifest } from "../manifest/ManifestTypes"
import { Effects, ExpectedExports } from "../types" import { Effects, ExpectedExports } from "../types"
import { Utils, createUtils } from "../util/utils"
export type InstallFn<Manifest extends SDKManifest, Store> = (opts: { export type InstallFn<Manifest extends SDKManifest, Store> = (opts: {
effects: Effects effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<void> }) => Promise<void>
export class Install<Manifest extends SDKManifest, Store> { export class Install<Manifest extends SDKManifest, Store> {
private constructor(readonly fn: InstallFn<Manifest, Store>) {} private constructor(readonly fn: InstallFn<Manifest, Store>) {}
@@ -21,7 +19,6 @@ export class Install<Manifest extends SDKManifest, Store> {
if (!previousVersion) if (!previousVersion)
await this.fn({ await this.fn({
effects, effects,
utils: createUtils(effects),
}) })
} }
} }

View File

@@ -1,10 +1,8 @@
import { SDKManifest } from "../manifest/ManifestTypes" import { SDKManifest } from "../manifest/ManifestTypes"
import { Effects, ExpectedExports } from "../types" import { Effects, ExpectedExports } from "../types"
import { Utils, createUtils } from "../util/utils"
export type UninstallFn<Manifest extends SDKManifest, Store> = (opts: { export type UninstallFn<Manifest extends SDKManifest, Store> = (opts: {
effects: Effects effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<void> }) => Promise<void>
export class Uninstall<Manifest extends SDKManifest, Store> { export class Uninstall<Manifest extends SDKManifest, Store> {
private constructor(readonly fn: UninstallFn<Manifest, Store>) {} private constructor(readonly fn: UninstallFn<Manifest, Store>) {}
@@ -21,7 +19,6 @@ export class Uninstall<Manifest extends SDKManifest, Store> {
if (!nextVersion) if (!nextVersion)
await this.fn({ await this.fn({
effects, effects,
utils: createUtils(effects),
}) })
} }
} }

View File

@@ -59,12 +59,13 @@ type AddSslOptions = {
preferredExternalPort: number preferredExternalPort: number
addXForwardedHeaders: boolean | null /** default: false */ addXForwardedHeaders: boolean | null /** default: false */
} }
type Security = { secure: false; ssl: false } | { secure: true; ssl: boolean } type Security = { ssl: boolean }
export type BindOptions = { export type BindOptions = {
scheme: Scheme scheme: Scheme
preferredExternalPort: number preferredExternalPort: number
addSsl: AddSslOptions | null addSsl: AddSslOptions | null
} & Security secure: Security | null
}
type KnownProtocols = typeof knownProtocols type KnownProtocols = typeof knownProtocols
type ProtocolsWithSslVariants = { type ProtocolsWithSslVariants = {
[K in keyof KnownProtocols]: KnownProtocols[K] extends { [K in keyof KnownProtocols]: KnownProtocols[K] extends {
@@ -79,16 +80,17 @@ type NotProtocolsWithSslVariants = Exclude<
> >
type BindOptionsByKnownProtocol = type BindOptionsByKnownProtocol =
| ({ | {
protocol: ProtocolsWithSslVariants protocol: ProtocolsWithSslVariants
preferredExternalPort?: number preferredExternalPort?: number
scheme: Scheme | null scheme?: Scheme
} & ({ noAddSsl: true } | { addSsl?: Partial<AddSslOptions> })) addSsl?: Partial<AddSslOptions>
}
| { | {
protocol: NotProtocolsWithSslVariants protocol: NotProtocolsWithSslVariants
preferredExternalPort?: number preferredExternalPort?: number
scheme: Scheme | null scheme?: Scheme
addSsl: AddSslOptions | null addSsl?: AddSslOptions
} }
type BindOptionsByProtocol = BindOptionsByKnownProtocol | BindOptions type BindOptionsByProtocol = BindOptionsByKnownProtocol | BindOptions
@@ -120,17 +122,12 @@ export class Host {
private async bindPortForUnknown( private async bindPortForUnknown(
internalPort: number, internalPort: number,
options: options: {
| ({ scheme: Scheme
scheme: Scheme preferredExternalPort: number
preferredExternalPort: number addSsl: AddSslOptions | null
addSsl: AddSslOptions | null secure: { ssl: boolean } | null
} & { secure: false; ssl: false }) },
| ({
scheme: Scheme
preferredExternalPort: number
addSsl: AddSslOptions | null
} & { secure: true; ssl: boolean }),
) { ) {
await this.options.effects.bind({ await this.options.effects.bind({
kind: this.options.kind, kind: this.options.kind,
@@ -154,18 +151,13 @@ export class Host {
knownProtocols[options.protocol].defaultPort knownProtocols[options.protocol].defaultPort
const addSsl = this.getAddSsl(options, protoInfo) const addSsl = this.getAddSsl(options, protoInfo)
const security: Security = !protoInfo.secure const secure: Security | null = !protoInfo.secure ? null : { ssl: false }
? {
secure: protoInfo.secure,
ssl: protoInfo.ssl,
}
: { secure: false, ssl: false }
const newOptions = { const newOptions = {
scheme, scheme,
preferredExternalPort, preferredExternalPort,
addSsl, addSsl,
...security, secure,
} }
await this.options.effects.bind({ await this.options.effects.bind({

View File

@@ -1,5 +1,5 @@
import { ServiceInterfaceType } from "../StartSdk"
import { Effects } from "../types" import { Effects } from "../types"
import { ServiceInterfaceType } from "../util/utils"
import { Scheme } from "./Host" import { Scheme } from "./Host"
/** /**

View File

@@ -1,7 +1,6 @@
import { Config } from "../config/builder/config" import { Config } from "../config/builder/config"
import { SDKManifest } from "../manifest/ManifestTypes" import { SDKManifest } from "../manifest/ManifestTypes"
import { AddressInfo, Effects } from "../types" import { AddressInfo, Effects } from "../types"
import { Utils } from "../util/utils"
import { AddressReceipt } from "./AddressReceipt" import { AddressReceipt } from "./AddressReceipt"
export type InterfacesReceipt = Array<AddressInfo[] & AddressReceipt> export type InterfacesReceipt = Array<AddressInfo[] & AddressReceipt>
@@ -10,11 +9,7 @@ export type SetInterfaces<
Store, Store,
ConfigInput extends Record<string, any>, ConfigInput extends Record<string, any>,
Output extends InterfacesReceipt, Output extends InterfacesReceipt,
> = (opts: { > = (opts: { effects: Effects; input: null | ConfigInput }) => Promise<Output>
effects: Effects
input: null | ConfigInput
utils: Utils<Manifest, Store>
}) => Promise<Output>
export type SetupInterfaces = < export type SetupInterfaces = <
Manifest extends SDKManifest, Manifest extends SDKManifest,
Store, Store,

View File

@@ -1,3 +1,4 @@
import { NO_TIMEOUT, SIGKILL, SIGTERM, Signals } from "../StartSdk"
import { HealthReceipt } from "../health/HealthReceipt" import { HealthReceipt } from "../health/HealthReceipt"
import { CheckResult } from "../health/checkFns" import { CheckResult } from "../health/checkFns"
import { SDKManifest } from "../manifest/ManifestTypes" import { SDKManifest } from "../manifest/ManifestTypes"
@@ -5,9 +6,22 @@ import { Trigger } from "../trigger"
import { TriggerInput } from "../trigger/TriggerInput" import { TriggerInput } from "../trigger/TriggerInput"
import { defaultTrigger } from "../trigger/defaultTrigger" import { defaultTrigger } from "../trigger/defaultTrigger"
import { DaemonReturned, Effects, ValidIfNoStupidEscape } from "../types" import { DaemonReturned, Effects, ValidIfNoStupidEscape } from "../types"
import { createUtils } from "../util"
import { Signals } from "../util/utils"
import { Mounts } from "./Mounts" import { Mounts } from "./Mounts"
import { CommandOptions, MountOptions, Overlay } from "../util/Overlay"
import { splitCommand } from "../util/splitCommand"
import { promisify } from "node:util"
import * as CP from "node:child_process"
const cpExec = promisify(CP.exec)
const cpExecFile = promisify(CP.execFile)
async function psTree(pid: number, overlay: Overlay): Promise<number[]> {
const { stdout } = await cpExec(`pstree -p ${pid}`)
const regex: RegExp = /\((\d+)\)/g
return [...stdout.toString().matchAll(regex)].map(([_all, pid]) =>
parseInt(pid),
)
}
type Daemon< type Daemon<
Manifest extends SDKManifest, Manifest extends SDKManifest,
Ids extends string, Ids extends string,
@@ -28,6 +42,89 @@ type Daemon<
} }
type ErrorDuplicateId<Id extends string> = `The id '${Id}' is already used` type ErrorDuplicateId<Id extends string> = `The id '${Id}' is already used`
const runDaemon =
<Manifest extends SDKManifest>() =>
async <A extends string>(
effects: Effects,
imageId: Manifest["images"][number],
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
overlay?: Overlay
},
): Promise<DaemonReturned> => {
const commands = splitCommand(command)
const overlay = options.overlay || (await Overlay.of(effects, imageId))
for (let mount of options.mounts || []) {
await overlay.mount(mount.options, mount.path)
}
const childProcess = await overlay.spawn(commands, {
env: options.env,
})
const answer = new Promise<null>((resolve, reject) => {
childProcess.stdout.on("data", (data: any) => {
console.log(data.toString())
})
childProcess.stderr.on("data", (data: any) => {
console.error(data.toString())
})
childProcess.on("exit", (code: any) => {
if (code === 0) {
return resolve(null)
}
return reject(new Error(`${commands[0]} exited with code ${code}`))
})
})
const pid = childProcess.pid
return {
async wait() {
const pids = pid ? await psTree(pid, overlay) : []
try {
return await answer
} finally {
for (const process of pids) {
cpExecFile("kill", [`-9`, String(process)]).catch((_) => {})
}
}
},
async term({ signal = SIGTERM, timeout = NO_TIMEOUT } = {}) {
const pids = pid ? await psTree(pid, overlay) : []
try {
childProcess.kill(signal)
if (timeout > NO_TIMEOUT) {
const didTimeout = await Promise.race([
new Promise((resolve) => setTimeout(resolve, timeout)).then(
() => true,
),
answer.then(() => false),
])
if (didTimeout) {
childProcess.kill(SIGKILL)
}
} else {
await answer
}
} finally {
await overlay.destroy()
}
try {
for (const process of pids) {
await cpExecFile("kill", [`-${signal}`, String(process)])
}
} finally {
for (const process of pids) {
cpExecFile("kill", [`-9`, String(process)]).catch((_) => {})
}
}
},
}
}
/** /**
* A class for defining and controlling the service daemons * A class for defining and controlling the service daemons
```ts ```ts
@@ -106,9 +203,8 @@ export class Daemons<Manifest extends SDKManifest, Ids extends string> {
) )
daemonsStarted[daemon.id] = requiredPromise.then(async () => { daemonsStarted[daemon.id] = requiredPromise.then(async () => {
const { command, imageId } = daemon const { command, imageId } = daemon
const utils = createUtils<Manifest>(effects)
const child = utils.runDaemon(imageId, command, { const child = runDaemon<Manifest>()(effects, imageId, command, {
env: daemon.env, env: daemon.env,
mounts: daemon.mounts.build(), mounts: daemon.mounts.build(),
}) })

View File

@@ -1,12 +1,11 @@
import { Effects, ExpectedExports } from "../types" import { ExpectedExports } from "../types"
import { createMainUtils } from "../util"
import { Utils, createUtils } from "../util/utils"
import { Daemons } from "./Daemons" import { Daemons } from "./Daemons"
import "../interfaces/ServiceInterfaceBuilder" import "../interfaces/ServiceInterfaceBuilder"
import "../interfaces/Origin" import "../interfaces/Origin"
import "./Daemons" import "./Daemons"
import { SDKManifest } from "../manifest/ManifestTypes" import { SDKManifest } from "../manifest/ManifestTypes"
import { MainEffects } from "../StartSdk"
/** /**
* Used to ensure that the main function is running with the valid proofs. * Used to ensure that the main function is running with the valid proofs.
@@ -20,16 +19,12 @@ import { SDKManifest } from "../manifest/ManifestTypes"
*/ */
export const setupMain = <Manifest extends SDKManifest, Store>( export const setupMain = <Manifest extends SDKManifest, Store>(
fn: (o: { fn: (o: {
effects: Effects effects: MainEffects
started(onTerm: () => PromiseLike<void>): PromiseLike<void> started(onTerm: () => PromiseLike<void>): PromiseLike<void>
utils: Utils<Manifest, Store, {}>
}) => Promise<Daemons<Manifest, any>>, }) => Promise<Daemons<Manifest, any>>,
): ExpectedExports.main => { ): ExpectedExports.main => {
return async (options) => { return async (options) => {
const result = await fn({ const result = await fn(options)
...options,
utils: createMainUtils<Manifest, Store>(options.effects),
})
return result return result
} }
} }

View File

@@ -4,6 +4,8 @@ import { List } from "../config/builder/list"
import { Value } from "../config/builder/value" import { Value } from "../config/builder/value"
import { Variants } from "../config/builder/variants" import { Variants } from "../config/builder/variants"
import { ValueSpec } from "../config/configTypes" import { ValueSpec } from "../config/configTypes"
import { setupManifest } from "../manifest/setupManifest"
import { StartSdk } from "../StartSdk"
describe("builder tests", () => { describe("builder tests", () => {
test("text", async () => { test("text", async () => {
@@ -379,17 +381,61 @@ describe("values", () => {
}) })
}) })
test("datetime", async () => { test("datetime", async () => {
const value = Value.dynamicDatetime<{ test: "a" }>(async ({ utils }) => { const sdk = StartSdk.of()
;async () => { .withManifest(
;(await utils.store.getOwn("/test").once()) satisfies "a" setupManifest({
} id: "testOutput",
title: "",
version: "1.0",
releaseNotes: "",
license: "",
replaces: [],
wrapperRepo: "",
upstreamRepo: "",
supportSite: "",
marketingSite: "",
donationUrl: null,
description: {
short: "",
long: "",
},
containers: {},
images: [],
volumes: [],
assets: [],
alerts: {
install: null,
update: null,
uninstall: null,
restore: null,
start: null,
stop: null,
},
dependencies: {
remoteTest: {
description: "",
requirement: { how: "", type: "opt-in" },
version: "1.0",
},
},
}),
)
.withStore<{ test: "a" }>()
.build(true)
return { const value = Value.dynamicDatetime<{ test: "a" }>(
name: "Testing", async ({ effects }) => {
required: { default: null }, ;async () => {
inputmode: "date", ;(await sdk.store.getOwn(effects, "/test").once()) satisfies "a"
} }
})
return {
name: "Testing",
required: { default: null },
inputmode: "date",
}
},
)
const validator = value.validator const validator = value.validator
validator.unsafeCast("2021-01-01") validator.unsafeCast("2021-01-01")
validator.unsafeCast(null) validator.unsafeCast(null)

View File

@@ -1,15 +1,13 @@
import { ServiceInterfaceBuilder } from "../interfaces/ServiceInterfaceBuilder" import { ServiceInterfaceBuilder } from "../interfaces/ServiceInterfaceBuilder"
import { Effects } from "../types" import { Effects } from "../types"
import { createUtils } from "../util" import { sdk } from "./output.sdk"
describe("host", () => { describe("host", () => {
test("Testing that the types work", () => { test("Testing that the types work", () => {
async function test(effects: Effects) { async function test(effects: Effects) {
const utils = createUtils<never, never>(effects) const foo = sdk.host.multi(effects, "foo")
const foo = utils.host.multi("foo")
const fooOrigin = await foo.bindPort(80, { const fooOrigin = await foo.bindPort(80, {
protocol: "http" as const, protocol: "http" as const,
scheme: null,
}) })
const fooInterface = new ServiceInterfaceBuilder({ const fooInterface = new ServiceInterfaceBuilder({
effects, effects,

View File

@@ -13,40 +13,25 @@ import { ExposeUiParams } from "../../../core/startos/bindings/ExposeUiParams"
import { GetSslCertificateParams } from "../../../core/startos/bindings/GetSslCertificateParams" import { GetSslCertificateParams } from "../../../core/startos/bindings/GetSslCertificateParams"
import { GetSslKeyParams } from "../../../core/startos/bindings/GetSslKeyParams" import { GetSslKeyParams } from "../../../core/startos/bindings/GetSslKeyParams"
import { GetServiceInterfaceParams } from "../../../core/startos/bindings/GetServiceInterfaceParams" import { GetServiceInterfaceParams } from "../../../core/startos/bindings/GetServiceInterfaceParams"
import { SetDependenciesParams } from "../../../core/startos/bindings/SetDependenciesParams"
import { GetSystemSmtpParams } from "../../../core/startos/bindings/GetSystemSmtpParams"
import { GetServicePortForwardParams } from "../../../core/startos/bindings/GetServicePortForwardParams"
import { ExportServiceInterfaceParams } from "../../../core/startos/bindings/ExportServiceInterfaceParams"
import { GetPrimaryUrlParams } from "../../../core/startos/bindings/GetPrimaryUrlParams"
import { ListServiceInterfacesParams } from "../../../core/startos/bindings/ListServiceInterfacesParams"
import { RemoveAddressParams } from "../../../core/startos/bindings/RemoveAddressParams"
import { ExportActionParams } from "../../../core/startos/bindings/ExportActionParams"
import { RemoveActionParams } from "../../../core/startos/bindings/RemoveActionParams"
import { ReverseProxyParams } from "../../../core/startos/bindings/ReverseProxyParams"
import { MountParams } from "../../../core/startos/bindings/MountParams"
function typeEquality<ExpectedType>(_a: ExpectedType) {} function typeEquality<ExpectedType>(_a: ExpectedType) {}
describe("startosTypeValidation ", () => { describe("startosTypeValidation ", () => {
test(`checking the params match`, () => { test(`checking the params match`, () => {
const testInput: any = {} const testInput: any = {}
typeEquality<{ typeEquality<{
[K in keyof Effects & [K in keyof Effects]: Effects[K] extends (args: infer A) => any
( ? A
| "gitInfo" : never
| "echo"
| "chroot"
| "exists"
| "executeAction"
| "getConfigured"
| "stopped"
| "running"
| "restart"
| "shutdown"
| "setConfigured"
| "setMainStatus"
| "setHealth"
| "getStore"
| "setStore"
| "exposeForDependents"
| "exposeUi"
| "createOverlayedImage"
| "destroyOverlayedImage"
| "getSslCertificate"
| "getSslKey"
| "getServiceInterface"
| "clearBindings"
| "bind"
| "getHostInfo"
)]: Effects[K] extends Function ? Parameters<Effects[K]>[0] : never
}>({ }>({
executeAction: {} as ExecuteAction, executeAction: {} as ExecuteAction,
createOverlayedImage: {} as CreateOverlayedImageParams, createOverlayedImage: {} as CreateOverlayedImageParams,
@@ -57,7 +42,7 @@ describe("startosTypeValidation ", () => {
exists: {} as ParamsPackageId, exists: {} as ParamsPackageId,
getConfigured: undefined, getConfigured: undefined,
stopped: {} as ParamsMaybePackageId, stopped: {} as ParamsMaybePackageId,
running: {} as ParamsMaybePackageId, running: {} as ParamsPackageId,
restart: undefined, restart: undefined,
shutdown: undefined, shutdown: undefined,
setConfigured: {} as SetConfigured, setConfigured: {} as SetConfigured,
@@ -67,6 +52,20 @@ describe("startosTypeValidation ", () => {
getSslCertificate: {} as GetSslCertificateParams, getSslCertificate: {} as GetSslCertificateParams,
getSslKey: {} as GetSslKeyParams, getSslKey: {} as GetSslKeyParams,
getServiceInterface: {} as GetServiceInterfaceParams, getServiceInterface: {} as GetServiceInterfaceParams,
setDependencies: {} as SetDependenciesParams,
store: {} as never,
getSystemSmtp: {} as GetSystemSmtpParams,
getContainerIp: undefined,
getServicePortForward: {} as GetServicePortForwardParams,
clearServiceInterfaces: undefined,
exportServiceInterface: {} as ExportServiceInterfaceParams,
getPrimaryUrl: {} as GetPrimaryUrlParams,
listServiceInterfaces: {} as ListServiceInterfacesParams,
removeAddress: {} as RemoveAddressParams,
exportAction: {} as ExportActionParams,
removeAction: {} as RemoveActionParams,
reverseProxy: {} as ReverseProxyParams,
mount: {} as MountParams,
}) })
typeEquality<Parameters<Effects["executeAction"]>[0]>( typeEquality<Parameters<Effects["executeAction"]>[0]>(
testInput as ExecuteAction, testInput as ExecuteAction,

View File

@@ -1,6 +1,5 @@
import { MainEffects, StartSdk } from "../StartSdk"
import { Effects } from "../types" import { Effects } from "../types"
import { createMainUtils } from "../util"
import { createUtils } from "../util/utils"
type Store = { type Store = {
config: { config: {
@@ -12,26 +11,31 @@ const todo = <A>(): A => {
throw new Error("not implemented") throw new Error("not implemented")
} }
const noop = () => {} const noop = () => {}
const sdk = StartSdk.of()
.withManifest({} as Manifest)
.withStore<Store>()
.build(true)
describe("Store", () => { describe("Store", () => {
test("types", async () => { test("types", async () => {
;async () => { ;async () => {
createUtils<Manifest, Store>(todo<Effects>()).store.setOwn("/config", { sdk.store.setOwn(todo<Effects>(), "/config", {
someValue: "a", someValue: "a",
}) })
createUtils<Manifest, Store>(todo<Effects>()).store.setOwn( sdk.store.setOwn(todo<Effects>(), "/config/someValue", "b")
"/config/someValue", sdk.store.setOwn(todo<Effects>(), "", {
"b",
)
createUtils<Manifest, Store>(todo<Effects>()).store.setOwn("", {
config: { someValue: "b" }, config: { someValue: "b" },
}) })
createUtils<Manifest, Store>(todo<Effects>()).store.setOwn( sdk.store.setOwn(
todo<Effects>(),
"/config/someValue", "/config/someValue",
// @ts-expect-error Type is wrong for the setting value // @ts-expect-error Type is wrong for the setting value
5, 5,
) )
createUtils(todo<Effects>()).store.setOwn( sdk.store.setOwn(
todo<Effects>(),
// @ts-expect-error Path is wrong // @ts-expect-error Path is wrong
"/config/someVae3lue", "/config/someVae3lue",
"someValue", "someValue",
@@ -52,49 +56,47 @@ describe("Store", () => {
path: "/config/some2Value", path: "/config/some2Value",
value: "a", value: "a",
}) })
;(await createMainUtils<Manifest, Store>(todo<Effects>()) ;(await sdk.store
.store.getOwn("/config/someValue") .getOwn(todo<MainEffects>(), "/config/someValue")
.const()) satisfies string .const()) satisfies string
;(await createMainUtils<Manifest, Store>(todo<Effects>()) ;(await sdk.store
.store.getOwn("/config") .getOwn(todo<MainEffects>(), "/config")
.const()) satisfies Store["config"] .const()) satisfies Store["config"]
await createMainUtils(todo<Effects>()) await sdk.store // @ts-expect-error Path is wrong
// @ts-expect-error Path is wrong .getOwn(todo<MainEffects>(), "/config/somdsfeValue")
.store.getOwn("/config/somdsfeValue")
.const() .const()
/// ----------------- ERRORS ----------------- /// ----------------- ERRORS -----------------
createUtils<Manifest, Store>(todo<Effects>()).store.setOwn("", { sdk.store.setOwn(todo<MainEffects>(), "", {
// @ts-expect-error Type is wrong for the setting value // @ts-expect-error Type is wrong for the setting value
config: { someValue: "notInAOrB" }, config: { someValue: "notInAOrB" },
}) })
createUtils<Manifest, Store>(todo<Effects>()).store.setOwn( sdk.store.setOwn(
todo<MainEffects>(),
"/config/someValue", "/config/someValue",
// @ts-expect-error Type is wrong for the setting value // @ts-expect-error Type is wrong for the setting value
"notInAOrB", "notInAOrB",
) )
;(await createUtils<Manifest, Store>(todo<Effects>()) ;(await sdk.store
.store.getOwn("/config/someValue") .getOwn(todo<Effects>(), "/config/someValue")
// @ts-expect-error Const should normally not be callable // @ts-expect-error Const should normally not be callable
.const()) satisfies string .const()) satisfies string
;(await createUtils<Manifest, Store>(todo<Effects>()) ;(await sdk.store
.store.getOwn("/config") .getOwn(todo<Effects>(), "/config")
// @ts-expect-error Const should normally not be callable // @ts-expect-error Const should normally not be callable
.const()) satisfies Store["config"] .const()) satisfies Store["config"]
await createUtils<Manifest, Store>(todo<Effects>()) await sdk.store // @ts-expect-error Path is wrong
// @ts-expect-error Path is wrong .getOwn("/config/somdsfeValue")
.store.getOwn("/config/somdsfeValue")
// @ts-expect-error Const should normally not be callable // @ts-expect-error Const should normally not be callable
.const() .const()
/// ///
;(await createUtils<Manifest, Store>(todo<Effects>()) ;(await sdk.store
.store.getOwn("/config/someValue") .getOwn(todo<MainEffects>(), "/config/someValue")
// @ts-expect-error satisfies type is wrong // @ts-expect-error satisfies type is wrong
.const()) satisfies number .const()) satisfies number
;(await createMainUtils(todo<Effects>()) ;(await sdk.store // @ts-expect-error Path is wrong
// @ts-expect-error Path is wrong .getOwn(todo<MainEffects>(), "/config/")
.store.getOwn("/config/")
.const()) satisfies Store["config"] .const()) satisfies Store["config"]
;(await todo<Effects>().store.get<Store, "/config/someValue">({ ;(await todo<Effects>().store.get<Store, "/config/someValue">({
path: "/config/someValue", path: "/config/someValue",

View File

@@ -1,11 +1,11 @@
export * as configTypes from "./config/configTypes" export * as configTypes from "./config/configTypes"
import { AddSslOptions } from "../../core/startos/bindings/AddSslOptions" import { AddSslOptions } from "../../core/startos/bindings/AddSslOptions"
import { MainEffects, ServiceInterfaceType, Signals } from "./StartSdk"
import { InputSpec } from "./config/configTypes" import { InputSpec } from "./config/configTypes"
import { DependenciesReceipt } from "./config/setupConfig" import { DependenciesReceipt } from "./config/setupConfig"
import { BindOptions, Scheme } from "./interfaces/Host" import { BindOptions, Scheme } from "./interfaces/Host"
import { Daemons } from "./mainFn/Daemons" import { Daemons } from "./mainFn/Daemons"
import { UrlString } from "./util/getServiceInterface" import { UrlString } from "./util/getServiceInterface"
import { ServiceInterfaceType, Signals } from "./util/utils"
export type ExportedAction = (options: { export type ExportedAction = (options: {
effects: Effects effects: Effects
@@ -59,7 +59,7 @@ export namespace ExpectedExports {
* package represents, like running a bitcoind in a bitcoind-wrapper. * package represents, like running a bitcoind in a bitcoind-wrapper.
*/ */
export type main = (options: { export type main = (options: {
effects: Effects effects: MainEffects
started(onTerm: () => PromiseLike<void>): PromiseLike<void> started(onTerm: () => PromiseLike<void>): PromiseLike<void>
}) => Promise<Daemons<any, any>> }) => Promise<Daemons<any, any>>
@@ -167,7 +167,7 @@ export type ActionMetadata = {
/** /**
* So the ordering of the actions is by alphabetical order of the group, then followed by the alphabetical of the actions * So the ordering of the actions is by alphabetical order of the group, then followed by the alphabetical of the actions
*/ */
group?: string group: string | null
} }
export declare const hostName: unique symbol export declare const hostName: unique symbol
// asdflkjadsf.onion | 1.2.3.4 // asdflkjadsf.onion | 1.2.3.4
@@ -261,24 +261,47 @@ export type ExposeServicePaths<Store = never> = {
paths: Store extends never ? string[] : ExposeAllServicePaths<Store>[] paths: Store extends never ? string[] : ExposeAllServicePaths<Store>[]
} }
export type ExposeUiPaths<Store> = Array<{ export type ExposeUiPaths<Store> =
/** The path to the value in the Store. [JsonPath](https://jsonpath.com/) */ | {
path: ExposeAllUiPaths<Store> type: "object"
/** A human readable title for the value */ value: { [k: string]: ExposeUiPaths<Store> }
title: string }
/** A human readable description or explanation of the value */ | {
description?: string type: "string"
/** (string/number only) Whether or not to mask the value, for example, when displaying a password */ /** The path to the value in the Store. [JsonPath](https://jsonpath.com/) */
masked?: boolean path: ExposeAllUiPaths<Store>
/** (string/number only) Whether or not to include a button for copying the value to clipboard */ /** A human readable description or explanation of the value */
copyable?: boolean description?: string
/** (string/number only) Whether or not to include a button for displaying the value as a QR code */ /** (string/number only) Whether or not to mask the value, for example, when displaying a password */
qr?: boolean masked: boolean
}> /** (string/number only) 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 */
qr?: boolean
}
export type ExposeUiPathsAll =
| {
type: "object"
value: { [k: string]: ExposeUiPathsAll }
}
| {
type: "string"
/** The path to the value in the Store. [JsonPath](https://jsonpath.com/) */
path: 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 */
masked: boolean
/** (string/number only) 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 */
qr: boolean | null
}
/** Used to reach out from the pure js runtime */ /** Used to reach out from the pure js runtime */
export type Effects = { export type Effects = {
executeAction<Input>(opts: { executeAction<Input>(opts: {
serviceId?: string serviceId: string | null
input: Input input: Input
}): Promise<unknown> }): Promise<unknown>
@@ -286,10 +309,7 @@ export type Effects = {
createOverlayedImage(options: { imageId: string }): Promise<[string, string]> createOverlayedImage(options: { imageId: string }): Promise<[string, string]>
/** A low level api used by destroyOverlay + makeOverlay:destroy */ /** A low level api used by destroyOverlay + makeOverlay:destroy */
destroyOverlayedImage(options: { destroyOverlayedImage(options: { guid: string }): Promise<void>
imageId: string
guid: string
}): Promise<void>
/** Removes all network bindings */ /** Removes all network bindings */
clearBindings(): Promise<void> clearBindings(): Promise<void>
@@ -302,8 +322,7 @@ export type Effects = {
scheme: Scheme scheme: Scheme
preferredExternalPort: number preferredExternalPort: number
addSsl: AddSslOptions | null addSsl: AddSslOptions | null
secure: boolean secure: { ssl: boolean } | null
ssl: boolean
}): Promise<void> }): Promise<void>
/** Retrieves the current hostname(s) associated with a host id */ /** Retrieves the current hostname(s) associated with a host id */
// getHostInfo(options: { // getHostInfo(options: {
@@ -362,10 +381,10 @@ export type Effects = {
/** /**
* Get the port address for another service * Get the port address for another service
*/ */
getServicePortForward( getServicePortForward(options: {
internalPort: number, internalPort: number
packageId?: string, packageId: string | null
): Promise<number> }): Promise<number>
/** Removes all network interfaces */ /** Removes all network interfaces */
clearServiceInterfaces(): Promise<void> clearServiceInterfaces(): Promise<void>
@@ -376,16 +395,7 @@ export type Effects = {
exposeForDependents(options: { paths: string[] }): Promise<void> exposeForDependents(options: { paths: string[] }): Promise<void>
exposeUi<Store = never>(options: { exposeUi(options: ExposeUiPathsAll): Promise<void>
paths: {
path: string
title: string
description?: string | undefined
masked?: boolean | undefined
copyable?: boolean | undefined
qr?: boolean | undefined
}[]
}): Promise<void>
/** /**
* There are times that we want to see the addresses that where exported * There are times that we want to see the addresses that where exported
* @param options.addressId If we want to filter the address id * @param options.addressId If we want to filter the address id
@@ -467,7 +477,9 @@ export type Effects = {
}): Promise<void> }): Promise<void>
/** Set the dependencies of what the service needs, usually ran during the set config as a best practice */ /** Set the dependencies of what the service needs, usually ran during the set config as a best practice */
setDependencies(dependencies: Dependencies): Promise<DependenciesReceipt> setDependencies(options: {
dependencies: Dependencies
}): Promise<DependenciesReceipt>
/** Exists could be useful during the runtime to know if some service exists, option dep */ /** Exists could be useful during the runtime to know if some service exists, option dep */
exists(options: { packageId: PackageId }): Promise<boolean> exists(options: { packageId: PackageId }): Promise<boolean>
/** Exists could be useful during the runtime to know if some service is running, option dep */ /** Exists could be useful during the runtime to know if some service is running, option dep */
@@ -477,20 +489,20 @@ export type Effects = {
reverseProxy(options: { reverseProxy(options: {
bind: { bind: {
/** Optional, default is 0.0.0.0 */ /** Optional, default is 0.0.0.0 */
ip?: string ip: string | null
port: number port: number
ssl: boolean ssl: boolean
} }
dst: { dst: {
/** Optional: default is 127.0.0.1 */ /** Optional: default is 127.0.0.1 */
ip?: string // optional, default 127.0.0.1 ip: string | null // optional, default 127.0.0.1
port: number port: number
ssl: boolean ssl: boolean
} }
http?: { http: {
// optional, will do TCP layer proxy only if not present // optional, will do TCP layer proxy only if not present
headers?: (headers: Record<string, string>) => Record<string, string> headers: Record<string, string> | null
} } | null
}): Promise<{ stop(): Promise<void> }> }): Promise<{ stop(): Promise<void> }>
restart(): void restart(): void
shutdown(): void shutdown(): void
@@ -585,7 +597,7 @@ export type KnownError =
export type Dependency = { export type Dependency = {
id: PackageId id: PackageId
kind: DependencyKind kind: DependencyKind
} } & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] })
export type Dependencies = Array<Dependency> export type Dependencies = Array<Dependency>
export type DeepPartial<T> = T extends {} export type DeepPartial<T> = T extends {}

View File

@@ -64,7 +64,7 @@ export class Overlay {
async destroy() { async destroy() {
const imageId = this.imageId const imageId = this.imageId
const guid = this.guid const guid = this.guid
await this.effects.destroyOverlayedImage({ imageId, guid }) await this.effects.destroyOverlayedImage({ guid })
} }
async exec( async exec(

View File

@@ -1,3 +1,4 @@
import { ServiceInterfaceType } from "../StartSdk"
import { import {
AddressInfo, AddressInfo,
Effects, Effects,
@@ -5,7 +6,6 @@ import {
Hostname, Hostname,
HostnameInfo, HostnameInfo,
} from "../types" } from "../types"
import { ServiceInterfaceType } from "./utils"
export type UrlString = string export type UrlString = string
export type HostId = string export type HostId = string

View File

@@ -7,7 +7,6 @@ import "./deepEqual"
import "./deepMerge" import "./deepMerge"
import "./Overlay" import "./Overlay"
import "./once" import "./once"
import * as utils from "./utils"
import { SDKManifest } from "../manifest/ManifestTypes" import { SDKManifest } from "../manifest/ManifestTypes"
// prettier-ignore // prettier-ignore
@@ -23,11 +22,6 @@ export const isKnownError = (e: unknown): e is T.KnownError =>
declare const affine: unique symbol declare const affine: unique symbol
export const createUtils = utils.createUtils
export const createMainUtils = <Manifest extends SDKManifest, Store>(
effects: T.Effects,
) => createUtils<Manifest, Store, {}>(effects)
type NeverPossible = { [affine]: string } type NeverPossible = { [affine]: string }
export type NoAny<A> = NeverPossible extends A export type NoAny<A> = NeverPossible extends A
? keyof NeverPossible extends keyof A ? keyof NeverPossible extends keyof A

View File

@@ -1,300 +0,0 @@
import nullIfEmpty from "./nullIfEmpty"
import {
CheckResult,
checkPortListening,
checkWebUrl,
} from "../health/checkFns"
import {
DaemonReturned,
Effects,
EnsureStorePath,
ExtractStore,
ServiceInterfaceId,
PackageId,
ValidIfNoStupidEscape,
} from "../types"
import { GetSystemSmtp } from "./GetSystemSmtp"
import { GetStore, getStore } from "../store/getStore"
import { MultiHost, Scheme, SingleHost, StaticHost } from "../interfaces/Host"
import { ServiceInterfaceBuilder } from "../interfaces/ServiceInterfaceBuilder"
import { GetServiceInterface, getServiceInterface } from "./getServiceInterface"
import {
GetServiceInterfaces,
getServiceInterfaces,
} from "./getServiceInterfaces"
import * as CP from "node:child_process"
import { promisify } from "node:util"
import { splitCommand } from "./splitCommand"
import { SDKManifest } from "../manifest/ManifestTypes"
import { MountOptions, Overlay, CommandOptions } from "./Overlay"
export type Signals = NodeJS.Signals
export const SIGTERM: Signals = "SIGTERM"
export const SIGKILL: Signals = "SIGTERM"
export const NO_TIMEOUT = -1
const childProcess = {
exec: promisify(CP.exec),
execFile: promisify(CP.execFile),
}
const cp = childProcess
export type ServiceInterfaceType = "ui" | "p2p" | "api"
export type Utils<
Manifest extends SDKManifest,
Store,
WrapperOverWrite = { const: never },
> = {
checkPortListening(
port: number,
options: {
errorMessage: string
successMessage: string
timeoutMessage?: string
timeout?: number
},
): Promise<CheckResult>
checkWebUrl(
url: string,
options?: {
timeout?: number
successMessage?: string
errorMessage?: string
},
): Promise<CheckResult>
childProcess: typeof childProcess
createInterface: (options: {
name: string
id: string
description: string
hasPrimary: boolean
disabled: boolean
type: ServiceInterfaceType
username: null | string
path: string
search: Record<string, string>
schemeOverride: { ssl: Scheme; noSsl: Scheme } | null
masked: boolean
}) => ServiceInterfaceBuilder
getSystemSmtp: () => GetSystemSmtp & WrapperOverWrite
host: {
static: (id: string) => StaticHost
single: (id: string) => SingleHost
multi: (id: string) => MultiHost
}
serviceInterface: {
getOwn: (id: ServiceInterfaceId) => GetServiceInterface & WrapperOverWrite
get: (opts: {
id: ServiceInterfaceId
packageId: PackageId
}) => GetServiceInterface & WrapperOverWrite
getAllOwn: () => GetServiceInterfaces & WrapperOverWrite
getAll: (opts: {
packageId: PackageId
}) => GetServiceInterfaces & WrapperOverWrite
}
nullIfEmpty: typeof nullIfEmpty
runCommand: <A extends string>(
imageId: Manifest["images"][number],
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
},
) => Promise<{ stdout: string | Buffer; stderr: string | Buffer }>
runDaemon: <A extends string>(
imageId: Manifest["images"][number],
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
overlay?: Overlay
},
) => Promise<DaemonReturned>
store: {
get: <Path extends string>(
packageId: string,
path: EnsureStorePath<Store, Path>,
) => GetStore<Store, Path> & WrapperOverWrite
getOwn: <Path extends string>(
path: EnsureStorePath<Store, Path>,
) => GetStore<Store, Path> & WrapperOverWrite
setOwn: <Path extends string | never>(
path: EnsureStorePath<Store, Path>,
value: ExtractStore<Store, Path>,
) => Promise<void>
}
}
export const createUtils = <
Manifest extends SDKManifest,
Store = never,
WrapperOverWrite = { const: never },
>(
effects: Effects,
): Utils<Manifest, Store, WrapperOverWrite> => {
return {
createInterface: (options: {
name: string
id: string
description: string
hasPrimary: boolean
disabled: boolean
type: ServiceInterfaceType
username: null | string
path: string
search: Record<string, string>
schemeOverride: { ssl: Scheme; noSsl: Scheme } | null
masked: boolean
}) => new ServiceInterfaceBuilder({ ...options, effects }),
childProcess,
getSystemSmtp: () =>
new GetSystemSmtp(effects) as GetSystemSmtp & WrapperOverWrite,
host: {
static: (id: string) => new StaticHost({ id, effects }),
single: (id: string) => new SingleHost({ id, effects }),
multi: (id: string) => new MultiHost({ id, effects }),
},
nullIfEmpty,
serviceInterface: {
getOwn: (id: ServiceInterfaceId) =>
getServiceInterface(effects, {
id,
packageId: null,
}) as GetServiceInterface & WrapperOverWrite,
get: (opts: { id: ServiceInterfaceId; packageId: PackageId }) =>
getServiceInterface(effects, opts) as GetServiceInterface &
WrapperOverWrite,
getAllOwn: () =>
getServiceInterfaces(effects, {
packageId: null,
}) as GetServiceInterfaces & WrapperOverWrite,
getAll: (opts: { packageId: PackageId }) =>
getServiceInterfaces(effects, opts) as GetServiceInterfaces &
WrapperOverWrite,
},
store: {
get: <Path extends string = never>(
packageId: string,
path: EnsureStorePath<Store, Path>,
) =>
getStore<Store, Path>(effects, path as any, {
packageId,
}) as any,
getOwn: <Path extends string>(path: EnsureStorePath<Store, Path>) =>
getStore<Store, Path>(effects, path as any) as any,
setOwn: <Path extends string | never>(
path: EnsureStorePath<Store, Path>,
value: ExtractStore<Store, Path>,
) => effects.store.set<Store, Path>({ value, path: path as any }),
},
runCommand: async <A extends string>(
imageId: Manifest["images"][number],
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
},
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> => {
const commands = splitCommand(command)
const overlay = await Overlay.of(effects, imageId)
try {
for (let mount of options.mounts || []) {
await overlay.mount(mount.options, mount.path)
}
return await overlay.exec(commands)
} finally {
await overlay.destroy()
}
},
runDaemon: async <A extends string>(
imageId: Manifest["images"][number],
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
overlay?: Overlay
},
): Promise<DaemonReturned> => {
const commands = splitCommand(command)
const overlay = options.overlay || (await Overlay.of(effects, imageId))
for (let mount of options.mounts || []) {
await overlay.mount(mount.options, mount.path)
}
const childProcess = await overlay.spawn(commands, {
env: options.env,
})
const answer = new Promise<null>((resolve, reject) => {
childProcess.stdout.on("data", (data: any) => {
console.log(data.toString())
})
childProcess.stderr.on("data", (data: any) => {
console.error(data.toString())
})
childProcess.on("exit", (code: any) => {
if (code === 0) {
return resolve(null)
}
return reject(new Error(`${commands[0]} exited with code ${code}`))
})
})
const pid = childProcess.pid
return {
async wait() {
const pids = pid ? await psTree(pid, overlay) : []
try {
return await answer
} finally {
for (const process of pids) {
cp.execFile("kill", [`-9`, String(process)]).catch((_) => {})
}
}
},
async term({ signal = SIGTERM, timeout = NO_TIMEOUT } = {}) {
const pids = pid ? await psTree(pid, overlay) : []
try {
childProcess.kill(signal)
if (timeout > NO_TIMEOUT) {
const didTimeout = await Promise.race([
new Promise((resolve) => setTimeout(resolve, timeout)).then(
() => true,
),
answer.then(() => false),
])
if (didTimeout) {
childProcess.kill(SIGKILL)
}
} else {
await answer
}
} finally {
await overlay.destroy()
}
try {
for (const process of pids) {
await cp.execFile("kill", [`-${signal}`, String(process)])
}
} finally {
for (const process of pids) {
cp.execFile("kill", [`-9`, String(process)]).catch((_) => {})
}
}
},
}
},
checkPortListening: checkPortListening.bind(null, effects),
checkWebUrl: checkWebUrl.bind(null, effects),
}
}
function noop(): void {}
async function psTree(pid: number, overlay: Overlay): Promise<number[]> {
const { stdout } = await childProcess.exec(`pstree -p ${pid}`)
const regex: RegExp = /\((\d+)\)/g
return [...stdout.toString().matchAll(regex)].map(([_all, pid]) =>
parseInt(pid),
)
}

View File

@@ -2,15 +2,15 @@
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-rev0.lib0.rc8.beta10", "version": "0.4.0-rev0.lib0.rc8.beta10",
"description": "Software development kit to facilitate packaging services for StartOS", "description": "Software development kit to facilitate packaging services for StartOS",
"main": "./cjs/lib/index.js", "main": "./cjs/sdk/lib/index.js",
"types": "./cjs/lib/index.d.ts", "types": "./cjs/sdk/lib/index.d.ts",
"module": "./mjs/lib/index.js", "module": "./mjs/sdk/lib/index.js",
"sideEffects": true, "sideEffects": true,
"exports": { "exports": {
".": { ".": {
"import": "./mjs/lib/index.js", "import": "./mjs/sdk/lib/index.js",
"require": "./cjs/lib/index.js", "require": "./cjs/sdk/lib/index.js",
"types": "./cjs/lib/index.d.ts" "types": "./cjs/sdk/lib/index.d.ts"
} }
}, },
"typesVersion": { "typesVersion": {
@@ -56,4 +56,4 @@
"tsx": "^4.7.1", "tsx": "^4.7.1",
"typescript": "^5.0.4" "typescript": "^5.0.4"
} }
} }

View File

@@ -40,8 +40,6 @@ const ICONS = [
'file-tray-stacked-outline', 'file-tray-stacked-outline',
'finger-print-outline', 'finger-print-outline',
'flash-outline', 'flash-outline',
'flask-outline',
'flash-off-outline',
'folder-open-outline', 'folder-open-outline',
'globe-outline', 'globe-outline',
'grid-outline', 'grid-outline',
@@ -70,6 +68,7 @@ const ICONS = [
'receipt-outline', 'receipt-outline',
'refresh', 'refresh',
'reload', 'reload',
'reload-circle-outline',
'remove', 'remove',
'remove-circle-outline', 'remove-circle-outline',
'remove-outline', 'remove-outline',

View File

@@ -10,7 +10,7 @@
<ion-content class="ion-padding-top with-widgets"> <ion-content class="ion-padding-top with-widgets">
<ion-item-group *ngIf="serviceInterfaces$ | async as serviceInterfaces"> <ion-item-group *ngIf="serviceInterfaces$ | async as serviceInterfaces">
<ng-container *ngIf="serviceInterfaces.ui.length"> <ng-container *ngIf="serviceInterfaces.ui.length">
<ion-item-divider>User Interfaces (UI)</ion-item-divider> <ion-item-divider>User Interfaces</ion-item-divider>
<app-interfaces-item <app-interfaces-item
*ngFor="let ui of serviceInterfaces.ui" *ngFor="let ui of serviceInterfaces.ui"
[iFace]="ui" [iFace]="ui"
@@ -18,7 +18,7 @@
</ng-container> </ng-container>
<ng-container *ngIf="serviceInterfaces.api.length"> <ng-container *ngIf="serviceInterfaces.api.length">
<ion-item-divider>Application Program Interfaces (API)</ion-item-divider> <ion-item-divider>Application Program Interfaces</ion-item-divider>
<app-interfaces-item <app-interfaces-item
*ngFor="let api of serviceInterfaces.api" *ngFor="let api of serviceInterfaces.api"
[iFace]="api" [iFace]="api"
@@ -26,7 +26,7 @@
</ng-container> </ng-container>
<ng-container *ngIf="serviceInterfaces.p2p.length"> <ng-container *ngIf="serviceInterfaces.p2p.length">
<ion-item-divider>Peer-To-Peer Interfaces (P2P)</ion-item-divider> <ion-item-divider>Peer-To-Peer Interfaces</ion-item-divider>
<app-interfaces-item <app-interfaces-item
*ngFor="let p2p of serviceInterfaces.p2p" *ngFor="let p2p of serviceInterfaces.p2p"
[iFace]="p2p" [iFace]="p2p"

View File

@@ -116,49 +116,54 @@ function getAddresses(
? [host.hostname] ? [host.hostname]
: [] : []
return hostnames const addresses: MappedAddress[] = []
.map(h => {
const addresses: MappedAddress[] = []
let name = '' hostnames.forEach(h => {
let hostname = '' let name = ''
let hostname = ''
if (h.kind === 'onion') { if (h.kind === 'onion') {
name = 'Tor' name = 'Tor'
hostname = h.hostname.value hostname = h.hostname.value
} else {
const hostnameKind = h.hostname.kind
if (hostnameKind === 'domain') {
name = 'Domain'
hostname = `${h.hostname.subdomain}.${h.hostname.domain}`
} else { } else {
name = h.hostname.kind name =
hostname = hostnameKind === 'local'
h.hostname.kind === 'domain' ? 'Local'
? `${h.hostname.subdomain}.${h.hostname.domain}` : `${h.networkInterfaceId} (${hostnameKind})`
: h.hostname.value hostname = h.hostname.value
} }
}
if (h.hostname.sslPort) { if (h.hostname.sslPort) {
const port = h.hostname.sslPort === 443 ? '' : `:${h.hostname.sslPort}` const port = h.hostname.sslPort === 443 ? '' : `:${h.hostname.sslPort}`
const scheme = addressInfo.bindOptions.addSsl?.scheme const scheme = addressInfo.bindOptions.addSsl?.scheme
? `${addressInfo.bindOptions.addSsl.scheme}://` ? `${addressInfo.bindOptions.addSsl.scheme}://`
: '' : ''
addresses.push({ addresses.push({
name, name: name === 'Tor' ? 'Tor (HTTPS)' : name,
url: `${scheme}${username}${hostname}${port}${suffix}`, url: `${scheme}${username}${hostname}${port}${suffix}`,
}) })
} }
if (h.hostname.port) { if (h.hostname.port) {
const port = h.hostname.port === 80 ? '' : `:${h.hostname.port}` const port = h.hostname.port === 80 ? '' : `:${h.hostname.port}`
const scheme = addressInfo.bindOptions.scheme const scheme = addressInfo.bindOptions.scheme
? `${addressInfo.bindOptions.scheme}://` ? `${addressInfo.bindOptions.scheme}://`
: '' : ''
addresses.push({ addresses.push({
name, name: name === 'Tor' ? 'Tor (HTTP)' : name,
url: `${scheme}${username}${hostname}${port}${suffix}`, url: `${scheme}${username}${hostname}${port}${suffix}`,
}) })
} }
})
return addresses return addresses
})
.flat()
} }

View File

@@ -1,24 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { ExperimentalFeaturesPage } from './experimental-features.page'
import { EmverPipesModule } from '@start9labs/shared'
const routes: Routes = [
{
path: '',
component: ExperimentalFeaturesPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
EmverPipesModule,
],
declarations: [ExperimentalFeaturesPage],
})
export class ExperimentalFeaturesPageModule {}

View File

@@ -1,36 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="system"></ion-back-button>
</ion-buttons>
<ion-title>Experimental Features</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="with-widgets">
<ion-item-group *ngIf="server$ | async as server">
<ion-item button (click)="presentAlertResetTor()">
<ion-icon slot="start" name="reload"></ion-icon>
<ion-label>
<h2>Reset Tor</h2>
<p>
Resetting the Tor daemon on your server may resolve Tor connectivity
issues.
</p>
</ion-label>
</ion-item>
<ion-item button (click)="presentAlertZram(server.zram)">
<ion-icon
slot="start"
[name]="server.zram ? 'flash-off-outline' : 'flash-outline'"
></ion-icon>
<ion-label>
<h2>{{ server.zram ? 'Disable' : 'Enable' }} zram</h2>
<p>
Zram creates compressed swap in memory, resulting in faster I/O for
low RAM devices
</p>
</ion-label>
</ion-item>
</ion-item-group>
</ion-content>

Some files were not shown because too many files have changed in this diff Show More