mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 14:29:45 +00:00
Feature/sdk improvements (#2879)
* sdk improvements * subcontainer fixes, disable wifi on migration if not in use, filterable interfaces
This commit is contained in:
@@ -9,6 +9,9 @@ import {
|
|||||||
ExecOptions,
|
ExecOptions,
|
||||||
ExecSpawnable,
|
ExecSpawnable,
|
||||||
} from "@start9labs/start-sdk/package/lib/util/SubContainer"
|
} from "@start9labs/start-sdk/package/lib/util/SubContainer"
|
||||||
|
import { Mounts } from "@start9labs/start-sdk/package/lib/mainFn/Mounts"
|
||||||
|
import { Manifest } from "@start9labs/start-sdk/base/lib/osBindings"
|
||||||
|
import { BackupEffects } from "@start9labs/start-sdk/package/lib/backup/Backups"
|
||||||
export const exec = promisify(cp.exec)
|
export const exec = promisify(cp.exec)
|
||||||
export const execFile = promisify(cp.execFile)
|
export const execFile = promisify(cp.execFile)
|
||||||
|
|
||||||
@@ -42,8 +45,9 @@ export class DockerProcedureContainer {
|
|||||||
name: string,
|
name: string,
|
||||||
) {
|
) {
|
||||||
const subcontainer = await SubContainer.of(
|
const subcontainer = await SubContainer.of(
|
||||||
effects,
|
effects as BackupEffects,
|
||||||
{ imageId: data.image },
|
{ imageId: data.image },
|
||||||
|
null,
|
||||||
name,
|
name,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,14 +61,10 @@ export class DockerProcedureContainer {
|
|||||||
const volumeMount = volumes[mount]
|
const volumeMount = volumes[mount]
|
||||||
if (volumeMount.type === "data") {
|
if (volumeMount.type === "data") {
|
||||||
await subcontainer.mount(
|
await subcontainer.mount(
|
||||||
{ type: "volume", id: mount, subpath: null, readonly: false },
|
Mounts.of().addVolume(mount, null, mounts[mount], false),
|
||||||
mounts[mount],
|
|
||||||
)
|
)
|
||||||
} else if (volumeMount.type === "assets") {
|
} else if (volumeMount.type === "assets") {
|
||||||
await subcontainer.mount(
|
await subcontainer.mount(Mounts.of().addAssets(mount, mounts[mount]))
|
||||||
{ type: "assets", subpath: mount },
|
|
||||||
mounts[mount],
|
|
||||||
)
|
|
||||||
} else if (volumeMount.type === "certificate") {
|
} else if (volumeMount.type === "certificate") {
|
||||||
const hostnames = [
|
const hostnames = [
|
||||||
`${packageId}.embassy`,
|
`${packageId}.embassy`,
|
||||||
@@ -107,10 +107,7 @@ export class DockerProcedureContainer {
|
|||||||
})
|
})
|
||||||
.catch(console.warn)
|
.catch(console.warn)
|
||||||
} else if (volumeMount.type === "backup") {
|
} else if (volumeMount.type === "backup") {
|
||||||
await subcontainer.mount(
|
await subcontainer.mount(Mounts.of().addBackups(null, mounts[mount]))
|
||||||
{ type: "backup", subpath: null },
|
|
||||||
mounts[mount],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Daemon } from "@start9labs/start-sdk/package/lib/mainFn/Daemon"
|
|||||||
import { Effects } from "../../../Models/Effects"
|
import { Effects } from "../../../Models/Effects"
|
||||||
import { off } from "node:process"
|
import { off } from "node:process"
|
||||||
import { CommandController } from "@start9labs/start-sdk/package/lib/mainFn/CommandController"
|
import { CommandController } from "@start9labs/start-sdk/package/lib/mainFn/CommandController"
|
||||||
|
import { SDKManifest } from "@start9labs/start-sdk/base/lib/types"
|
||||||
|
|
||||||
const EMBASSY_HEALTH_INTERVAL = 15 * 1000
|
const EMBASSY_HEALTH_INTERVAL = 15 * 1000
|
||||||
const EMBASSY_PROPERTIES_LOOP = 30 * 1000
|
const EMBASSY_PROPERTIES_LOOP = 30 * 1000
|
||||||
@@ -24,7 +25,7 @@ export class MainLoop {
|
|||||||
}[]
|
}[]
|
||||||
|
|
||||||
private mainEvent?: {
|
private mainEvent?: {
|
||||||
daemon: Daemon
|
daemon: Daemon<SDKManifest>
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
@@ -72,6 +73,7 @@ export class MainLoop {
|
|||||||
env: {
|
env: {
|
||||||
TINI_SUBREAPER: "true",
|
TINI_SUBREAPER: "true",
|
||||||
},
|
},
|
||||||
|
mounts: null,
|
||||||
sigtermTimeout: utils.inMs(
|
sigtermTimeout: utils.inMs(
|
||||||
this.system.manifest.main["sigterm-timeout"],
|
this.system.manifest.main["sigterm-timeout"],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export const polyfillEffects = (
|
|||||||
effects,
|
effects,
|
||||||
subcontainer,
|
subcontainer,
|
||||||
[input.command, ...(input.args || [])],
|
[input.command, ...(input.args || [])],
|
||||||
{},
|
{ mounts: null },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
|
|||||||
13
core/Cargo.lock
generated
13
core/Cargo.lock
generated
@@ -5510,6 +5510,18 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha-crypt"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88e79009728d8311d42d754f2f319a975f9e38f156fd5e422d2451486c78b286"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"sha2 0.10.8",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -6091,6 +6103,7 @@ dependencies = [
|
|||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
"serde_yml",
|
"serde_yml",
|
||||||
|
"sha-crypt",
|
||||||
"sha2 0.10.8",
|
"sha2 0.10.8",
|
||||||
"shell-words",
|
"shell-words",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ serde_toml = { package = "toml", version = "0.8.2" }
|
|||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
serde_with = { version = "3.4.0", features = ["macros", "json"] }
|
serde_with = { version = "3.4.0", features = ["macros", "json"] }
|
||||||
serde_yaml = { package = "serde_yml", version = "0.0.10" }
|
serde_yaml = { package = "serde_yml", version = "0.0.10" }
|
||||||
|
sha-crypt = "0.5.0"
|
||||||
sha2 = "0.10.2"
|
sha2 = "0.10.2"
|
||||||
shell-words = "1"
|
shell-words = "1"
|
||||||
signal-hook = "0.3.17"
|
signal-hook = "0.3.17"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use josekit::jwk::Jwk;
|
|||||||
use rpc_toolkit::yajrc::RpcError;
|
use rpc_toolkit::yajrc::RpcError;
|
||||||
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler};
|
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ use crate::middleware::auth::{
|
|||||||
};
|
};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::crypto::EncryptedWire;
|
use crate::util::crypto::EncryptedWire;
|
||||||
|
use crate::util::io::create_file_mod;
|
||||||
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
|
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
|
||||||
use crate::{ensure_code, Error, ResultExt};
|
use crate::{ensure_code, Error, ResultExt};
|
||||||
|
|
||||||
@@ -41,6 +43,30 @@ impl Map for Sessions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn write_shadow(password: &str) -> Result<(), Error> {
|
||||||
|
let shadow_contents = tokio::fs::read_to_string("/etc/shadow").await?;
|
||||||
|
let mut shadow_file =
|
||||||
|
create_file_mod("/media/startos/config/overlay/etc/shadow", 0o640).await?;
|
||||||
|
for line in shadow_contents.lines() {
|
||||||
|
if line.starts_with("start9:") {
|
||||||
|
let rest = line.splitn(3, ":").nth(2).ok_or_else(|| {
|
||||||
|
Error::new(eyre!("malformed /etc/shadow"), ErrorKind::ParseSysInfo)
|
||||||
|
})?;
|
||||||
|
let pw = sha_crypt::sha512_simple(password, &sha_crypt::Sha512Params::default())
|
||||||
|
.map_err(|e| Error::new(eyre!("{e:?}"), ErrorKind::Serialization))?;
|
||||||
|
shadow_file
|
||||||
|
.write_all(format!("start9:{pw}:{rest}\n").as_bytes())
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
shadow_file.write_all(line.as_bytes()).await?;
|
||||||
|
shadow_file.write_all(b"\n").await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shadow_file.sync_all().await?;
|
||||||
|
tokio::fs::copy("/media/startos/config/overlay/etc/shadow", "/etc/shadow").await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, TS)]
|
#[derive(Clone, Serialize, Deserialize, TS)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
@@ -210,7 +236,7 @@ pub async fn login_impl(
|
|||||||
) -> Result<LoginRes, Error> {
|
) -> Result<LoginRes, Error> {
|
||||||
let password = password.unwrap_or_default().decrypt(&ctx)?;
|
let password = password.unwrap_or_default().decrypt(&ctx)?;
|
||||||
|
|
||||||
if ephemeral {
|
let tok = if ephemeral {
|
||||||
check_password_against_db(&ctx.db.peek().await, &password)?;
|
check_password_against_db(&ctx.db.peek().await, &password)?;
|
||||||
let hash_token = HashSessionToken::new();
|
let hash_token = HashSessionToken::new();
|
||||||
ctx.ephemeral_sessions.mutate(|s| {
|
ctx.ephemeral_sessions.mutate(|s| {
|
||||||
@@ -242,7 +268,16 @@ pub async fn login_impl(
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result
|
.result
|
||||||
|
}?;
|
||||||
|
|
||||||
|
if tokio::fs::metadata("/media/startos/config/overlay/etc/shadow")
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
write_shadow(&password).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(tok)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use tracing::instrument;
|
|||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::account::AccountInfo;
|
use crate::account::AccountInfo;
|
||||||
|
use crate::auth::write_shadow;
|
||||||
use crate::backup::restore::recover_full_embassy;
|
use crate::backup::restore::recover_full_embassy;
|
||||||
use crate::backup::target::BackupTargetFS;
|
use crate::backup::target::BackupTargetFS;
|
||||||
use crate::context::rpc::InitRpcContextPhases;
|
use crate::context::rpc::InitRpcContextPhases;
|
||||||
@@ -88,8 +89,8 @@ async fn setup_init(
|
|||||||
.db
|
.db
|
||||||
.mutate(|m| {
|
.mutate(|m| {
|
||||||
let mut account = AccountInfo::load(m)?;
|
let mut account = AccountInfo::load(m)?;
|
||||||
if let Some(password) = password {
|
if let Some(password) = &password {
|
||||||
account.set_password(&password)?;
|
account.set_password(password)?;
|
||||||
}
|
}
|
||||||
account.save(m)?;
|
account.save(m)?;
|
||||||
m.as_public_mut()
|
m.as_public_mut()
|
||||||
@@ -101,6 +102,10 @@ async fn setup_init(
|
|||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|
||||||
|
if let Some(password) = &password {
|
||||||
|
write_shadow(&password).await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok((account, init_result))
|
Ok((account, init_result))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,6 +351,8 @@ pub async fn complete(ctx: SetupContext) -> Result<SetupResult, Error> {
|
|||||||
.arg(format!("--hostname={}", res.hostname.0))
|
.arg(format!("--hostname={}", res.hostname.0))
|
||||||
.invoke(ErrorKind::ParseSysInfo)
|
.invoke(ErrorKind::ParseSysInfo)
|
||||||
.await?;
|
.await?;
|
||||||
|
Command::new("sync").invoke(ErrorKind::Filesystem).await?;
|
||||||
|
|
||||||
Ok(res.clone())
|
Ok(res.clone())
|
||||||
}
|
}
|
||||||
Some(Err(e)) => Err(e.clone_output()),
|
Some(Err(e)) => Err(e.clone_output()),
|
||||||
@@ -465,6 +472,8 @@ async fn fresh_setup(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
write_shadow(start_os_password).await?;
|
||||||
|
|
||||||
Ok(((&account).try_into()?, rpc_ctx))
|
Ok(((&account).try_into()?, rpc_ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -944,6 +944,23 @@ pub async fn create_file(path: impl AsRef<Path>) -> Result<File, Error> {
|
|||||||
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("create {path:?}")))
|
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("create {path:?}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_file_mod(path: impl AsRef<Path>, mode: u32) -> Result<File, Error> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
tokio::fs::create_dir_all(parent)
|
||||||
|
.await
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("mkdir -p {parent:?}")))?;
|
||||||
|
}
|
||||||
|
OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.mode(mode)
|
||||||
|
.open(path)
|
||||||
|
.await
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("create {path:?}")))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn append_file(path: impl AsRef<Path>) -> Result<File, Error> {
|
pub async fn append_file(path: impl AsRef<Path>) -> Result<File, Error> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ impl VersionT for Version {
|
|||||||
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> {
|
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> {
|
||||||
let host = db["public"]["serverInfo"]["host"].clone();
|
let host = db["public"]["serverInfo"]["host"].clone();
|
||||||
let mut wifi = db["public"]["serverInfo"]["wifi"].clone();
|
let mut wifi = db["public"]["serverInfo"]["wifi"].clone();
|
||||||
wifi["enabled"] = Value::Bool(true);
|
wifi["enabled"] = Value::Bool(!wifi["selected"].is_null());
|
||||||
let mut network_interfaces = db["public"]["serverInfo"]["networkInterfaces"].clone();
|
let mut network_interfaces = db["public"]["serverInfo"]["networkInterfaces"].clone();
|
||||||
for (k, v) in network_interfaces
|
for (k, v) in network_interfaces
|
||||||
.as_object_mut()
|
.as_object_mut()
|
||||||
|
|||||||
@@ -15,8 +15,28 @@ export const getHostname = (url: string): Hostname | null => {
|
|||||||
return last
|
return last
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FilterKinds = "onion" | "local" | "domain" | "ip" | "ipv4" | "ipv6"
|
||||||
|
export type Filter = {
|
||||||
|
visibility?: "public" | "private"
|
||||||
|
kind?: FilterKinds | FilterKinds[]
|
||||||
|
exclude?: Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
type Formats = "hostname-info" | "urlstring" | "url"
|
||||||
|
type FormatReturnTy<Format extends Formats> = Format extends "hostname-info"
|
||||||
|
? HostnameInfo
|
||||||
|
: Format extends "url"
|
||||||
|
? URL
|
||||||
|
: UrlString
|
||||||
|
|
||||||
export type Filled = {
|
export type Filled = {
|
||||||
hostnames: HostnameInfo[]
|
hostnames: HostnameInfo[]
|
||||||
|
|
||||||
|
filter: <Format extends Formats = "urlstring">(
|
||||||
|
filter: Filter,
|
||||||
|
format?: Format,
|
||||||
|
) => FormatReturnTy<Format>[]
|
||||||
|
|
||||||
publicHostnames: HostnameInfo[]
|
publicHostnames: HostnameInfo[]
|
||||||
onionHostnames: HostnameInfo[]
|
onionHostnames: HostnameInfo[]
|
||||||
localHostnames: HostnameInfo[]
|
localHostnames: HostnameInfo[]
|
||||||
@@ -97,6 +117,47 @@ export const addressHostToUrl = (
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterRec(
|
||||||
|
hostnames: HostnameInfo[],
|
||||||
|
filter: Filter,
|
||||||
|
invert: boolean,
|
||||||
|
): HostnameInfo[] {
|
||||||
|
if (filter.visibility === "public")
|
||||||
|
hostnames = hostnames.filter(
|
||||||
|
(h) => invert !== (h.kind === "onion" || h.public),
|
||||||
|
)
|
||||||
|
if (filter.visibility === "private")
|
||||||
|
hostnames = hostnames.filter(
|
||||||
|
(h) => invert !== (h.kind !== "onion" && !h.public),
|
||||||
|
)
|
||||||
|
if (filter.kind) {
|
||||||
|
const kind = new Set(
|
||||||
|
Array.isArray(filter.kind) ? filter.kind : [filter.kind],
|
||||||
|
)
|
||||||
|
if (kind.has("ip")) {
|
||||||
|
kind.add("ipv4")
|
||||||
|
kind.add("ipv6")
|
||||||
|
}
|
||||||
|
hostnames = hostnames.filter(
|
||||||
|
(h) =>
|
||||||
|
invert !==
|
||||||
|
((kind.has("onion") && h.kind === "onion") ||
|
||||||
|
(kind.has("local") &&
|
||||||
|
h.kind === "ip" &&
|
||||||
|
h.hostname.kind === "local") ||
|
||||||
|
(kind.has("domain") &&
|
||||||
|
h.kind === "ip" &&
|
||||||
|
h.hostname.kind === "domain") ||
|
||||||
|
(kind.has("ipv4") && h.kind === "ip" && h.hostname.kind === "ipv4") ||
|
||||||
|
(kind.has("ipv6") && h.kind === "ip" && h.hostname.kind === "ipv6")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.exclude) return filterRec(hostnames, filter.exclude, !invert)
|
||||||
|
|
||||||
|
return hostnames
|
||||||
|
}
|
||||||
|
|
||||||
export const filledAddress = (
|
export const filledAddress = (
|
||||||
host: Host,
|
host: Host,
|
||||||
addressInfo: AddressInfo,
|
addressInfo: AddressInfo,
|
||||||
@@ -107,6 +168,14 @@ export const filledAddress = (
|
|||||||
return {
|
return {
|
||||||
...addressInfo,
|
...addressInfo,
|
||||||
hostnames,
|
hostnames,
|
||||||
|
filter: <T extends Formats = "urlstring">(filter: Filter, format?: T) => {
|
||||||
|
const res = filterRec(hostnames, filter, false)
|
||||||
|
if (format === "hostname-info") return res as FormatReturnTy<T>[]
|
||||||
|
const urls = res.flatMap(toUrl)
|
||||||
|
if (format === "url")
|
||||||
|
return urls.map((u) => new URL(u)) as FormatReturnTy<T>[]
|
||||||
|
return urls as FormatReturnTy<T>[]
|
||||||
|
},
|
||||||
get publicHostnames() {
|
get publicHostnames() {
|
||||||
return hostnames.filter((h) => h.kind === "onion" || h.public)
|
return hostnames.filter((h) => h.kind === "onion" || h.public)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
|||||||
},
|
},
|
||||||
command: T.CommandType,
|
command: T.CommandType,
|
||||||
options: CommandOptions & {
|
options: CommandOptions & {
|
||||||
mounts: Mounts<Manifest>
|
mounts: Mounts<Manifest> | null
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* A name to use to refer to the ephemeral subcontainer for debugging purposes
|
* A name to use to refer to the ephemeral subcontainer for debugging purposes
|
||||||
@@ -766,25 +766,40 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
SubContainer: {
|
SubContainer: {
|
||||||
|
/**
|
||||||
|
* @description Create a new SubContainer
|
||||||
|
* @param effects
|
||||||
|
* @param image - what container image to use
|
||||||
|
* @param mounts - what to mount to the subcontainer
|
||||||
|
* @param name - a name to use to refer to the subcontainer for debugging purposes
|
||||||
|
*/
|
||||||
of(
|
of(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
image: {
|
image: {
|
||||||
imageId: T.ImageId & keyof Manifest["images"]
|
imageId: T.ImageId & keyof Manifest["images"]
|
||||||
sharedRun?: boolean
|
sharedRun?: boolean
|
||||||
},
|
},
|
||||||
|
mounts: Mounts<Manifest> | null,
|
||||||
name: string,
|
name: string,
|
||||||
) {
|
) {
|
||||||
return SubContainer.of(effects, image, name)
|
return SubContainer.of(effects, image, mounts, name)
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @description Create a new SubContainer
|
||||||
|
* @param effects
|
||||||
|
* @param image - what container image to use
|
||||||
|
* @param mounts - what to mount to the subcontainer
|
||||||
|
* @param name - a name to use to refer to the ephemeral subcontainer for debugging purposes
|
||||||
|
*/
|
||||||
with<T>(
|
with<T>(
|
||||||
effects: T.Effects,
|
effects: T.Effects,
|
||||||
image: {
|
image: {
|
||||||
imageId: T.ImageId & keyof Manifest["images"]
|
imageId: T.ImageId & keyof Manifest["images"]
|
||||||
sharedRun?: boolean
|
sharedRun?: boolean
|
||||||
},
|
},
|
||||||
mounts: { options: MountOptions; mountpoint: string }[],
|
mounts: Mounts<Manifest> | null,
|
||||||
name: string,
|
name: string,
|
||||||
fn: (subContainer: SubContainer) => Promise<T>,
|
fn: (subContainer: SubContainer<Manifest>) => Promise<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
return SubContainer.with(effects, image, mounts, name, fn)
|
return SubContainer.with(effects, image, mounts, name, fn)
|
||||||
},
|
},
|
||||||
@@ -1164,7 +1179,7 @@ export async function runCommand<Manifest extends T.SDKManifest>(
|
|||||||
image: { imageId: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean },
|
image: { imageId: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean },
|
||||||
command: T.CommandType,
|
command: T.CommandType,
|
||||||
options: CommandOptions & {
|
options: CommandOptions & {
|
||||||
mounts: Mounts<Manifest>
|
mounts: Mounts<Manifest> | null
|
||||||
},
|
},
|
||||||
name?: string,
|
name?: string,
|
||||||
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> {
|
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> {
|
||||||
@@ -1182,7 +1197,7 @@ export async function runCommand<Manifest extends T.SDKManifest>(
|
|||||||
return SubContainer.with(
|
return SubContainer.with(
|
||||||
effects,
|
effects,
|
||||||
image,
|
image,
|
||||||
options.mounts.build(),
|
options.mounts,
|
||||||
name ||
|
name ||
|
||||||
commands
|
commands
|
||||||
.map((c) => {
|
.map((c) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as T from "../../../base/lib/types"
|
import * as T from "../../../base/lib/types"
|
||||||
import * as child_process from "child_process"
|
import * as child_process from "child_process"
|
||||||
import * as fs from "fs/promises"
|
import * as fs from "fs/promises"
|
||||||
import { asError, StorePath } from "../util"
|
import { Affine, asError, StorePath } from "../util"
|
||||||
|
|
||||||
export const DEFAULT_OPTIONS: T.SyncOptions = {
|
export const DEFAULT_OPTIONS: T.SyncOptions = {
|
||||||
delete: true,
|
delete: true,
|
||||||
@@ -15,12 +15,18 @@ export type BackupSync<Volumes extends string> = {
|
|||||||
restoreOptions?: Partial<T.SyncOptions>
|
restoreOptions?: Partial<T.SyncOptions>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BackupEffects = T.Effects & Affine<"Backups">
|
||||||
|
|
||||||
export class Backups<M extends T.SDKManifest> {
|
export class Backups<M extends T.SDKManifest> {
|
||||||
private constructor(
|
private constructor(
|
||||||
private options = DEFAULT_OPTIONS,
|
private options = DEFAULT_OPTIONS,
|
||||||
private restoreOptions: Partial<T.SyncOptions> = {},
|
private restoreOptions: Partial<T.SyncOptions> = {},
|
||||||
private backupOptions: Partial<T.SyncOptions> = {},
|
private backupOptions: Partial<T.SyncOptions> = {},
|
||||||
private backupSet = [] as BackupSync<M["volumes"][number]>[],
|
private backupSet = [] as BackupSync<M["volumes"][number]>[],
|
||||||
|
private preBackup = async (effects: BackupEffects) => {},
|
||||||
|
private postBackup = async (effects: BackupEffects) => {},
|
||||||
|
private preRestore = async (effects: BackupEffects) => {},
|
||||||
|
private postRestore = async (effects: BackupEffects) => {},
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
static withVolumes<M extends T.SDKManifest = never>(
|
static withVolumes<M extends T.SDKManifest = never>(
|
||||||
@@ -93,6 +99,7 @@ export class Backups<M extends T.SDKManifest> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createBackup(effects: T.Effects) {
|
async createBackup(effects: T.Effects) {
|
||||||
|
await this.preBackup(effects as BackupEffects)
|
||||||
for (const item of this.backupSet) {
|
for (const item of this.backupSet) {
|
||||||
const rsyncResults = await runRsync({
|
const rsyncResults = await runRsync({
|
||||||
srcPath: item.dataPath,
|
srcPath: item.dataPath,
|
||||||
@@ -116,6 +123,7 @@ export class Backups<M extends T.SDKManifest> {
|
|||||||
await fs.writeFile("/media/startos/backup/dataVersion.txt", dataVersion, {
|
await fs.writeFile("/media/startos/backup/dataVersion.txt", dataVersion, {
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
})
|
})
|
||||||
|
await this.postBackup(effects as BackupEffects)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { HealthCheckResult } from "./HealthCheckResult"
|
import { HealthCheckResult } from "./HealthCheckResult"
|
||||||
import { timeoutPromise } from "./index"
|
import { timeoutPromise } from "./index"
|
||||||
import { SubContainer } from "../../util/SubContainer"
|
import { SubContainer } from "../../util/SubContainer"
|
||||||
|
import { SDKManifest } from "../../types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Running a health script, is used when we want to have a simple
|
* Running a health script, is used when we want to have a simple
|
||||||
@@ -9,9 +10,9 @@ import { SubContainer } from "../../util/SubContainer"
|
|||||||
* @param param0
|
* @param param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const runHealthScript = async (
|
export const runHealthScript = async <Manifest extends SDKManifest>(
|
||||||
runCommand: string[],
|
runCommand: string[],
|
||||||
subcontainer: SubContainer,
|
subcontainer: SubContainer<Manifest>,
|
||||||
{
|
{
|
||||||
timeout = 30000,
|
timeout = 30000,
|
||||||
errorMessage = `Error while running command: ${runCommand}`,
|
errorMessage = `Error while running command: ${runCommand}`,
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import {
|
|||||||
import { Drop, splitCommand } from "../util"
|
import { Drop, splitCommand } from "../util"
|
||||||
import * as cp from "child_process"
|
import * as cp from "child_process"
|
||||||
import * as fs from "node:fs/promises"
|
import * as fs from "node:fs/promises"
|
||||||
|
import { Mounts } from "./Mounts"
|
||||||
|
|
||||||
export class CommandController extends Drop {
|
export class CommandController<Manifest extends T.SDKManifest> extends Drop {
|
||||||
private constructor(
|
private constructor(
|
||||||
readonly runningAnswer: Promise<unknown>,
|
readonly runningAnswer: Promise<unknown>,
|
||||||
private state: { exited: boolean },
|
private state: { exited: boolean },
|
||||||
private readonly subcontainer: SubContainer,
|
private readonly subcontainer: SubContainer<Manifest>,
|
||||||
private process: cp.ChildProcess,
|
private process: cp.ChildProcess,
|
||||||
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
|
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
|
||||||
) {
|
) {
|
||||||
@@ -29,13 +30,13 @@ export class CommandController extends Drop {
|
|||||||
imageId: keyof Manifest["images"] & T.ImageId
|
imageId: keyof Manifest["images"] & T.ImageId
|
||||||
sharedRun?: boolean
|
sharedRun?: boolean
|
||||||
}
|
}
|
||||||
| SubContainer,
|
| SubContainer<Manifest>,
|
||||||
command: T.CommandType,
|
command: T.CommandType,
|
||||||
options: {
|
options: {
|
||||||
subcontainerName?: string
|
subcontainerName?: string
|
||||||
// Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms
|
// Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms
|
||||||
sigtermTimeout?: number
|
sigtermTimeout?: number
|
||||||
mounts?: { mountpoint: string; options: MountOptions }[]
|
mounts: Mounts<Manifest> | null
|
||||||
runAsInit?: boolean
|
runAsInit?: boolean
|
||||||
env?:
|
env?:
|
||||||
| {
|
| {
|
||||||
@@ -65,13 +66,12 @@ export class CommandController extends Drop {
|
|||||||
: await SubContainer.of(
|
: await SubContainer.of(
|
||||||
effects,
|
effects,
|
||||||
subcontainer,
|
subcontainer,
|
||||||
|
null,
|
||||||
options?.subcontainerName || commands.join(" "),
|
options?.subcontainerName || commands.join(" "),
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (let mount of options.mounts || []) {
|
if (options.mounts) await subc.mount(options.mounts)
|
||||||
await subc.mount(mount.options, mount.mountpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
let childProcess: cp.ChildProcess
|
let childProcess: cp.ChildProcess
|
||||||
if (options.runAsInit) {
|
if (options.runAsInit) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as T from "../../../base/lib/types"
|
|||||||
import { asError } from "../../../base/lib/util/asError"
|
import { asError } from "../../../base/lib/util/asError"
|
||||||
import { ExecSpawnable, MountOptions, SubContainer } from "../util/SubContainer"
|
import { ExecSpawnable, MountOptions, SubContainer } from "../util/SubContainer"
|
||||||
import { CommandController } from "./CommandController"
|
import { CommandController } from "./CommandController"
|
||||||
|
import { Mounts } from "./Mounts"
|
||||||
|
|
||||||
const TIMEOUT_INCREMENT_MS = 1000
|
const TIMEOUT_INCREMENT_MS = 1000
|
||||||
const MAX_TIMEOUT_MS = 30000
|
const MAX_TIMEOUT_MS = 30000
|
||||||
@@ -10,10 +11,12 @@ const MAX_TIMEOUT_MS = 30000
|
|||||||
* and the others state of running, where it will keep a living running command
|
* and the others state of running, where it will keep a living running command
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export class Daemon {
|
export class Daemon<Manifest extends T.SDKManifest> {
|
||||||
private commandController: CommandController | null = null
|
private commandController: CommandController<Manifest> | null = null
|
||||||
private shouldBeRunning = false
|
private shouldBeRunning = false
|
||||||
constructor(private startCommand: () => Promise<CommandController>) {}
|
constructor(
|
||||||
|
private startCommand: () => Promise<CommandController<Manifest>>,
|
||||||
|
) {}
|
||||||
get subContainerHandle(): undefined | ExecSpawnable {
|
get subContainerHandle(): undefined | ExecSpawnable {
|
||||||
return this.commandController?.subContainerHandle
|
return this.commandController?.subContainerHandle
|
||||||
}
|
}
|
||||||
@@ -25,11 +28,11 @@ export class Daemon {
|
|||||||
imageId: keyof Manifest["images"] & T.ImageId
|
imageId: keyof Manifest["images"] & T.ImageId
|
||||||
sharedRun?: boolean
|
sharedRun?: boolean
|
||||||
}
|
}
|
||||||
| SubContainer,
|
| SubContainer<Manifest>,
|
||||||
command: T.CommandType,
|
command: T.CommandType,
|
||||||
options: {
|
options: {
|
||||||
subcontainerName?: string
|
subcontainerName?: string
|
||||||
mounts?: { mountpoint: string; options: MountOptions }[]
|
mounts: Mounts<Manifest> | null
|
||||||
env?:
|
env?:
|
||||||
| {
|
| {
|
||||||
[variable: string]: string
|
[variable: string]: string
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ type DaemonsParams<
|
|||||||
*/
|
*/
|
||||||
sharedRun?: boolean
|
sharedRun?: boolean
|
||||||
}
|
}
|
||||||
| SubContainer
|
| SubContainer<Manifest>
|
||||||
/** For mounting the necessary volumes. Syntax: sdk.Mounts.of().addVolume() */
|
/** For mounting the necessary volumes. Syntax: sdk.Mounts.of().addVolume() */
|
||||||
mounts: Mounts<Manifest>
|
mounts: Mounts<Manifest>
|
||||||
env?: Record<string, string>
|
env?: Record<string, string>
|
||||||
@@ -113,9 +113,9 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
|||||||
private constructor(
|
private constructor(
|
||||||
readonly effects: T.Effects,
|
readonly effects: T.Effects,
|
||||||
readonly started: (onTerm: () => PromiseLike<void>) => PromiseLike<null>,
|
readonly started: (onTerm: () => PromiseLike<void>) => PromiseLike<null>,
|
||||||
readonly daemons: Promise<Daemon>[],
|
readonly daemons: Promise<Daemon<Manifest>>[],
|
||||||
readonly ids: Ids[],
|
readonly ids: Ids[],
|
||||||
readonly healthDaemons: HealthDaemon[],
|
readonly healthDaemons: HealthDaemon<Manifest>[],
|
||||||
readonly healthChecks: HealthCheck[],
|
readonly healthChecks: HealthCheck[],
|
||||||
) {}
|
) {}
|
||||||
/**
|
/**
|
||||||
@@ -164,7 +164,6 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
|||||||
options.command,
|
options.command,
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
mounts: options.mounts.build(),
|
|
||||||
subcontainerName: id,
|
subcontainerName: id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { HealthCheckResult } from "../health/checkFns"
|
|||||||
import { defaultTrigger } from "../trigger/defaultTrigger"
|
import { defaultTrigger } from "../trigger/defaultTrigger"
|
||||||
import { Ready } from "./Daemons"
|
import { Ready } from "./Daemons"
|
||||||
import { Daemon } from "./Daemon"
|
import { Daemon } from "./Daemon"
|
||||||
import { SetHealth, Effects } from "../../../base/lib/types"
|
import { SetHealth, Effects, SDKManifest } from "../../../base/lib/types"
|
||||||
import { DEFAULT_SIGTERM_TIMEOUT } from "."
|
import { DEFAULT_SIGTERM_TIMEOUT } from "."
|
||||||
import { asError } from "../../../base/lib/util/asError"
|
import { asError } from "../../../base/lib/util/asError"
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ const oncePromise = <T>() => {
|
|||||||
* -- Running: Daemon is running and the status is in the health
|
* -- Running: Daemon is running and the status is in the health
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export class HealthDaemon {
|
export class HealthDaemon<Manifest extends SDKManifest> {
|
||||||
private _health: HealthCheckResult = { result: "starting", message: null }
|
private _health: HealthCheckResult = { result: "starting", message: null }
|
||||||
private healthWatchers: Array<() => unknown> = []
|
private healthWatchers: Array<() => unknown> = []
|
||||||
private running = false
|
private running = false
|
||||||
@@ -29,9 +29,9 @@ export class HealthDaemon {
|
|||||||
private resolveReady: (() => void) | undefined
|
private resolveReady: (() => void) | undefined
|
||||||
private readyPromise: Promise<void>
|
private readyPromise: Promise<void>
|
||||||
constructor(
|
constructor(
|
||||||
private readonly daemon: Promise<Daemon>,
|
private readonly daemon: Promise<Daemon<Manifest>>,
|
||||||
readonly daemonIndex: number,
|
readonly daemonIndex: number,
|
||||||
private readonly dependencies: HealthDaemon[],
|
private readonly dependencies: HealthDaemon<Manifest>[],
|
||||||
readonly id: string,
|
readonly id: string,
|
||||||
readonly ids: string[],
|
readonly ids: string[],
|
||||||
readonly ready: Ready,
|
readonly ready: Ready,
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import { MountOptions } from "../util/SubContainer"
|
|||||||
|
|
||||||
type MountArray = { mountpoint: string; options: MountOptions }[]
|
type MountArray = { mountpoint: string; options: MountOptions }[]
|
||||||
|
|
||||||
export class Mounts<Manifest extends T.SDKManifest> {
|
export class Mounts<
|
||||||
|
Manifest extends T.SDKManifest,
|
||||||
|
Backups extends {
|
||||||
|
subpath: string | null
|
||||||
|
mountpoint: string
|
||||||
|
} = never,
|
||||||
|
> {
|
||||||
private constructor(
|
private constructor(
|
||||||
readonly volumes: {
|
readonly volumes: {
|
||||||
id: Manifest["volumes"][number]
|
id: Manifest["volumes"][number]
|
||||||
@@ -22,10 +28,11 @@ export class Mounts<Manifest extends T.SDKManifest> {
|
|||||||
mountpoint: string
|
mountpoint: string
|
||||||
readonly: boolean
|
readonly: boolean
|
||||||
}[],
|
}[],
|
||||||
|
readonly backups: Backups[],
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
static of<Manifest extends T.SDKManifest>() {
|
static of<Manifest extends T.SDKManifest>() {
|
||||||
return new Mounts<Manifest>([], [], [])
|
return new Mounts<Manifest>([], [], [], [])
|
||||||
}
|
}
|
||||||
|
|
||||||
addVolume(
|
addVolume(
|
||||||
@@ -38,13 +45,20 @@ export class Mounts<Manifest extends T.SDKManifest> {
|
|||||||
/** Whether or not the volume should be readonly for this daemon */
|
/** Whether or not the volume should be readonly for this daemon */
|
||||||
readonly: boolean,
|
readonly: boolean,
|
||||||
) {
|
) {
|
||||||
this.volumes.push({
|
return new Mounts<Manifest, Backups>(
|
||||||
id,
|
[
|
||||||
subpath,
|
...this.volumes,
|
||||||
mountpoint,
|
{
|
||||||
readonly,
|
id,
|
||||||
})
|
subpath,
|
||||||
return this
|
mountpoint,
|
||||||
|
readonly,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[...this.assets],
|
||||||
|
[...this.dependencies],
|
||||||
|
[...this.backups],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
addAssets(
|
addAssets(
|
||||||
@@ -53,11 +67,18 @@ export class Mounts<Manifest extends T.SDKManifest> {
|
|||||||
/** Where to mount the asset. e.g. /asset */
|
/** Where to mount the asset. e.g. /asset */
|
||||||
mountpoint: string,
|
mountpoint: string,
|
||||||
) {
|
) {
|
||||||
this.assets.push({
|
return new Mounts<Manifest, Backups>(
|
||||||
subpath,
|
[...this.volumes],
|
||||||
mountpoint,
|
[
|
||||||
})
|
...this.assets,
|
||||||
return this
|
{
|
||||||
|
subpath,
|
||||||
|
mountpoint,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[...this.dependencies],
|
||||||
|
[...this.backups],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
addDependency<DependencyManifest extends T.SDKManifest>(
|
addDependency<DependencyManifest extends T.SDKManifest>(
|
||||||
@@ -72,14 +93,36 @@ export class Mounts<Manifest extends T.SDKManifest> {
|
|||||||
/** Whether or not the volume should be readonly for this daemon */
|
/** Whether or not the volume should be readonly for this daemon */
|
||||||
readonly: boolean,
|
readonly: boolean,
|
||||||
) {
|
) {
|
||||||
this.dependencies.push({
|
return new Mounts<Manifest, Backups>(
|
||||||
dependencyId,
|
[...this.volumes],
|
||||||
volumeId,
|
[...this.assets],
|
||||||
subpath,
|
[
|
||||||
mountpoint,
|
...this.dependencies,
|
||||||
readonly,
|
{
|
||||||
})
|
dependencyId,
|
||||||
return this
|
volumeId,
|
||||||
|
subpath,
|
||||||
|
mountpoint,
|
||||||
|
readonly,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[...this.backups],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
addBackups(subpath: string | null, mountpoint: string) {
|
||||||
|
return new Mounts<
|
||||||
|
Manifest,
|
||||||
|
{
|
||||||
|
subpath: string | null
|
||||||
|
mountpoint: string
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
[...this.volumes],
|
||||||
|
[...this.assets],
|
||||||
|
[...this.dependencies],
|
||||||
|
[...this.backups, { subpath, mountpoint }],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
build(): MountArray {
|
build(): MountArray {
|
||||||
@@ -130,3 +173,7 @@ export class Mounts<Manifest extends T.SDKManifest> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const a = Mounts.of().addBackups(null, "")
|
||||||
|
// @ts-expect-error
|
||||||
|
const m: Mounts<T.SDKManifest, never> = a
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { promisify } from "util"
|
|||||||
import { Buffer } from "node:buffer"
|
import { Buffer } from "node:buffer"
|
||||||
import { once } from "../../../base/lib/util/once"
|
import { once } from "../../../base/lib/util/once"
|
||||||
import { Drop } from "./Drop"
|
import { Drop } from "./Drop"
|
||||||
|
import { Mounts } from "../mainFn/Mounts"
|
||||||
|
import { BackupEffects } from "../backup/Backups"
|
||||||
|
|
||||||
export const execFile = promisify(cp.execFile)
|
export const execFile = promisify(cp.execFile)
|
||||||
const False = () => false
|
const False = () => false
|
||||||
@@ -46,13 +48,20 @@ export interface ExecSpawnable {
|
|||||||
* Implements:
|
* Implements:
|
||||||
* @see {@link ExecSpawnable}
|
* @see {@link ExecSpawnable}
|
||||||
*/
|
*/
|
||||||
export class SubContainer extends Drop implements ExecSpawnable {
|
export class SubContainer<
|
||||||
|
Manifest extends T.SDKManifest,
|
||||||
|
Effects extends T.Effects = T.Effects,
|
||||||
|
>
|
||||||
|
extends Drop
|
||||||
|
implements ExecSpawnable
|
||||||
|
{
|
||||||
private destroyed = false
|
private destroyed = false
|
||||||
|
|
||||||
private leader: cp.ChildProcess
|
private leader: cp.ChildProcess
|
||||||
private leaderExited: boolean = false
|
private leaderExited: boolean = false
|
||||||
private waitProc: () => Promise<null>
|
private waitProc: () => Promise<null>
|
||||||
private constructor(
|
private constructor(
|
||||||
readonly effects: T.Effects,
|
readonly effects: Effects,
|
||||||
readonly imageId: T.ImageId,
|
readonly imageId: T.ImageId,
|
||||||
readonly rootfs: string,
|
readonly rootfs: string,
|
||||||
readonly guid: T.Guid,
|
readonly guid: T.Guid,
|
||||||
@@ -87,9 +96,23 @@ export class SubContainer extends Drop implements ExecSpawnable {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
static async of(
|
static async of<Manifest extends T.SDKManifest, Effects extends T.Effects>(
|
||||||
effects: T.Effects,
|
effects: Effects,
|
||||||
image: { imageId: T.ImageId; sharedRun?: boolean },
|
image: {
|
||||||
|
imageId: keyof Manifest["images"] & T.ImageId
|
||||||
|
sharedRun?: boolean
|
||||||
|
},
|
||||||
|
mounts:
|
||||||
|
| (Effects extends BackupEffects
|
||||||
|
? Mounts<
|
||||||
|
Manifest,
|
||||||
|
{
|
||||||
|
subpath: string | null
|
||||||
|
mountpoint: string
|
||||||
|
}
|
||||||
|
>
|
||||||
|
: Mounts<Manifest, never>)
|
||||||
|
| null,
|
||||||
name: string,
|
name: string,
|
||||||
) {
|
) {
|
||||||
const { imageId, sharedRun } = image
|
const { imageId, sharedRun } = image
|
||||||
@@ -97,86 +120,121 @@ export class SubContainer extends Drop implements ExecSpawnable {
|
|||||||
imageId,
|
imageId,
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
|
|
||||||
const res = new SubContainer(effects, imageId, rootfs, guid)
|
const res = new SubContainer(effects, imageId, rootfs, guid)
|
||||||
|
|
||||||
const shared = ["dev", "sys"]
|
try {
|
||||||
if (!!sharedRun) {
|
if (mounts) {
|
||||||
shared.push("run")
|
await res.mount(mounts)
|
||||||
|
}
|
||||||
|
const shared = ["dev", "sys"]
|
||||||
|
if (!!sharedRun) {
|
||||||
|
shared.push("run")
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.mkdir(`${rootfs}/etc`, { recursive: true })
|
||||||
|
await fs.copyFile("/etc/resolv.conf", `${rootfs}/etc/resolv.conf`)
|
||||||
|
|
||||||
|
for (const dirPart of shared) {
|
||||||
|
const from = `/${dirPart}`
|
||||||
|
const to = `${rootfs}/${dirPart}`
|
||||||
|
await fs.mkdir(from, { recursive: true })
|
||||||
|
await fs.mkdir(to, { recursive: true })
|
||||||
|
await execFile("mount", ["--rbind", from, to])
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
} finally {
|
||||||
|
await res.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.mkdir(`${rootfs}/etc`, { recursive: true })
|
|
||||||
await fs.copyFile("/etc/resolv.conf", `${rootfs}/etc/resolv.conf`)
|
|
||||||
|
|
||||||
for (const dirPart of shared) {
|
|
||||||
const from = `/${dirPart}`
|
|
||||||
const to = `${rootfs}/${dirPart}`
|
|
||||||
await fs.mkdir(from, { recursive: true })
|
|
||||||
await fs.mkdir(to, { recursive: true })
|
|
||||||
await execFile("mount", ["--rbind", from, to])
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async with<T>(
|
static async with<
|
||||||
effects: T.Effects,
|
Manifest extends T.SDKManifest,
|
||||||
image: { imageId: T.ImageId; sharedRun?: boolean },
|
T,
|
||||||
mounts: { options: MountOptions; mountpoint: string }[],
|
Effects extends T.Effects,
|
||||||
|
>(
|
||||||
|
effects: Effects,
|
||||||
|
image: {
|
||||||
|
imageId: keyof Manifest["images"] & T.ImageId
|
||||||
|
sharedRun?: boolean
|
||||||
|
},
|
||||||
|
mounts:
|
||||||
|
| (Effects extends BackupEffects
|
||||||
|
? Mounts<
|
||||||
|
Manifest,
|
||||||
|
{
|
||||||
|
subpath: string | null
|
||||||
|
mountpoint: string
|
||||||
|
}
|
||||||
|
>
|
||||||
|
: Mounts<Manifest, never>)
|
||||||
|
| null,
|
||||||
name: string,
|
name: string,
|
||||||
fn: (subContainer: SubContainer) => Promise<T>,
|
fn: (subContainer: SubContainer<Manifest, Effects>) => Promise<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const subContainer = await SubContainer.of(effects, image, name)
|
const subContainer = await SubContainer.of(effects, image, mounts, name)
|
||||||
try {
|
try {
|
||||||
for (let mount of mounts) {
|
|
||||||
await subContainer.mount(mount.options, mount.mountpoint)
|
|
||||||
}
|
|
||||||
return await fn(subContainer)
|
return await fn(subContainer)
|
||||||
} finally {
|
} finally {
|
||||||
await subContainer.destroy()
|
await subContainer.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async mount(options: MountOptions, path: string): Promise<SubContainer> {
|
async mount(
|
||||||
path = path.startsWith("/")
|
mounts: Effects extends BackupEffects
|
||||||
? `${this.rootfs}${path}`
|
? Mounts<
|
||||||
: `${this.rootfs}/${path}`
|
Manifest,
|
||||||
if (options.type === "volume") {
|
{
|
||||||
const subpath = options.subpath
|
subpath: string | null
|
||||||
? options.subpath.startsWith("/")
|
mountpoint: string
|
||||||
? options.subpath
|
}
|
||||||
: `/${options.subpath}`
|
>
|
||||||
: "/"
|
: Mounts<Manifest, never>,
|
||||||
const from = `/media/startos/volumes/${options.id}${subpath}`
|
): Promise<SubContainer<Manifest, Effects>> {
|
||||||
|
for (let mount of mounts.build()) {
|
||||||
|
let { options, mountpoint } = mount
|
||||||
|
const path = mountpoint.startsWith("/")
|
||||||
|
? `${this.rootfs}${mountpoint}`
|
||||||
|
: `${this.rootfs}/${mountpoint}`
|
||||||
|
if (options.type === "volume") {
|
||||||
|
const subpath = options.subpath
|
||||||
|
? options.subpath.startsWith("/")
|
||||||
|
? options.subpath
|
||||||
|
: `/${options.subpath}`
|
||||||
|
: "/"
|
||||||
|
const from = `/media/startos/volumes/${options.id}${subpath}`
|
||||||
|
|
||||||
await fs.mkdir(from, { recursive: true })
|
await fs.mkdir(from, { recursive: true })
|
||||||
await fs.mkdir(path, { recursive: true })
|
await fs.mkdir(path, { recursive: true })
|
||||||
await execFile("mount", ["--bind", from, path])
|
await execFile("mount", ["--bind", from, path])
|
||||||
} else if (options.type === "assets") {
|
} else if (options.type === "assets") {
|
||||||
const subpath = options.subpath
|
const subpath = options.subpath
|
||||||
? options.subpath.startsWith("/")
|
? options.subpath.startsWith("/")
|
||||||
? options.subpath
|
? options.subpath
|
||||||
: `/${options.subpath}`
|
: `/${options.subpath}`
|
||||||
: "/"
|
: "/"
|
||||||
const from = `/media/startos/assets/${subpath}`
|
const from = `/media/startos/assets/${subpath}`
|
||||||
|
|
||||||
await fs.mkdir(from, { recursive: true })
|
await fs.mkdir(from, { recursive: true })
|
||||||
await fs.mkdir(path, { recursive: true })
|
await fs.mkdir(path, { recursive: true })
|
||||||
await execFile("mount", ["--bind", from, path])
|
await execFile("mount", ["--bind", from, path])
|
||||||
} else if (options.type === "pointer") {
|
} else if (options.type === "pointer") {
|
||||||
await this.effects.mount({ location: path, target: options })
|
await this.effects.mount({ location: path, target: options })
|
||||||
} else if (options.type === "backup") {
|
} else if (options.type === "backup") {
|
||||||
const subpath = options.subpath
|
const subpath = options.subpath
|
||||||
? options.subpath.startsWith("/")
|
? options.subpath.startsWith("/")
|
||||||
? options.subpath
|
? options.subpath
|
||||||
: `/${options.subpath}`
|
: `/${options.subpath}`
|
||||||
: "/"
|
: "/"
|
||||||
const from = `/media/startos/backup${subpath}`
|
const from = `/media/startos/backup${subpath}`
|
||||||
|
|
||||||
await fs.mkdir(from, { recursive: true })
|
await fs.mkdir(from, { recursive: true })
|
||||||
await fs.mkdir(path, { recursive: true })
|
await fs.mkdir(path, { recursive: true })
|
||||||
await execFile("mount", ["--bind", from, path])
|
await execFile("mount", ["--bind", from, path])
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`unknown type ${(options as any).type}`)
|
throw new Error(`unknown type ${(options as any).type}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
@@ -217,6 +275,13 @@ export class SubContainer extends Drop implements ExecSpawnable {
|
|||||||
this.destroy()
|
this.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description run a command inside this subcontainer
|
||||||
|
* @param commands an array representing the command and args to execute
|
||||||
|
* @param options
|
||||||
|
* @param timeoutMs how long to wait before killing the command in ms
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
async exec(
|
async exec(
|
||||||
command: string[],
|
command: string[],
|
||||||
options?: CommandOptions & ExecOptions,
|
options?: CommandOptions & ExecOptions,
|
||||||
@@ -422,8 +487,17 @@ export class SubContainerHandle implements ExecSpawnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CommandOptions = {
|
export type CommandOptions = {
|
||||||
|
/**
|
||||||
|
* Environment variables to set for this command
|
||||||
|
*/
|
||||||
env?: { [variable: string]: string }
|
env?: { [variable: string]: string }
|
||||||
|
/**
|
||||||
|
* the working directory to run this command in
|
||||||
|
*/
|
||||||
cwd?: string
|
cwd?: string
|
||||||
|
/**
|
||||||
|
* the user to run this command as
|
||||||
|
*/
|
||||||
user?: string
|
user?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user