Files
start-os/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts
Matt Hill b40849f672 Fix/fe bugs 3 (#2943)
* fix typeo in patch db seed

* show all registries in updates tab, fix required dependnecy display in marketplace, update browser tab title desc

* always show pointer for version select

* chore: fix comments

* support html in action desc and marketplace long desc, only show qr in action res if qr is true

* disable save if smtp creds not edited, show better smtp success message

* dont dismiss login spinner until patchDB returns

* feat: redesign of service dashboard and interface (#2946)

* feat: redesign of service dashboard and interface

* chore: comments

* re-add setup complete

* dibale launch UI when not running, re-style things, rename things

* back to 1000

* fix clearnet docs link and require password retype in setup wiz

* faster hint display

* display dependency ID if title not available

* fix migration

* better init progress view

* fix setup success page by providing VERSION and notifications page fixes

* force uninstall from service error page, soft or hard

* handle error state better

* chore: fixed for install and setup wizards

* chore: fix issues (#2949)

* enable and disable kiosk mode

* minor fixes

* fix dependency mounts

* dismissable tasks

* provide replayId

* default if health check success message is null

* look for wifi interface too

* dash for null user agent in sessions

* add disk repair to diagnostic api

---------

Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
2025-05-21 19:04:26 -06:00

179 lines
5.2 KiB
TypeScript

import * as fs from "fs/promises"
import * as cp from "child_process"
import { SubContainer, types as T } from "@start9labs/start-sdk"
import { promisify } from "util"
import { DockerProcedure, VolumeId } from "../../../Models/DockerProcedure"
import { Volume } from "./matchVolume"
import {
CommandOptions,
ExecOptions,
SubContainerOwned,
} 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"
import { Drop } from "@start9labs/start-sdk/package/lib/util"
import { SDKManifest } from "@start9labs/start-sdk/base/lib/types"
export const exec = promisify(cp.exec)
export const execFile = promisify(cp.execFile)
export class DockerProcedureContainer extends Drop {
private constructor(
private readonly subcontainer: SubContainer<SDKManifest>,
) {
super()
}
static async of(
effects: T.Effects,
packageId: string,
data: DockerProcedure,
volumes: { [id: VolumeId]: Volume },
name: string,
options: { subcontainer?: SubContainer<SDKManifest> } = {},
) {
const subcontainer =
options?.subcontainer ??
(await DockerProcedureContainer.createSubContainer(
effects,
packageId,
data,
volumes,
name,
))
return new DockerProcedureContainer(subcontainer)
}
static async createSubContainer(
effects: T.Effects,
packageId: string,
data: DockerProcedure,
volumes: { [id: VolumeId]: Volume },
name: string,
) {
const subcontainer = await SubContainerOwned.of(
effects as BackupEffects,
{ imageId: data.image },
null,
name,
)
if (data.mounts) {
const mounts = data.mounts
for (const mount in mounts) {
const path = mounts[mount].startsWith("/")
? `${subcontainer.rootfs}${mounts[mount]}`
: `${subcontainer.rootfs}/${mounts[mount]}`
await fs.mkdir(path, { recursive: true })
const volumeMount = volumes[mount]
if (volumeMount.type === "data") {
await subcontainer.mount(
Mounts.of().mountVolume({
volumeId: mount,
subpath: null,
mountpoint: mounts[mount],
readonly: false,
}),
)
} else if (volumeMount.type === "assets") {
await subcontainer.mount(
Mounts.of().mountAssets({
subpath: mount,
mountpoint: mounts[mount],
}),
)
} else if (volumeMount.type === "certificate") {
const hostnames = [
`${packageId}.embassy`,
...new Set(
Object.values(
(
await effects.getHostInfo({
hostId: volumeMount["interface-id"],
})
)?.hostnameInfo || {},
)
.flatMap((h) => h)
.flatMap((h) => (h.kind === "onion" ? [h.hostname.value] : [])),
).values(),
]
const certChain = await effects.getSslCertificate({
hostnames,
})
const key = await effects.getSslKey({
hostnames,
})
await fs.writeFile(
`${path}/${volumeMount["interface-id"]}.cert.pem`,
certChain.join("\n"),
)
await fs.writeFile(
`${path}/${volumeMount["interface-id"]}.key.pem`,
key,
)
} else if (volumeMount.type === "pointer") {
await effects.mount({
location: path,
target: {
packageId: volumeMount["package-id"],
subpath: volumeMount.path,
readonly: volumeMount.readonly,
volumeId: volumeMount["volume-id"],
filetype: "directory",
},
})
} else if (volumeMount.type === "backup") {
await subcontainer.mount(
Mounts.of().mountBackups({
subpath: null,
mountpoint: mounts[mount],
}),
)
}
}
}
return subcontainer
}
async exec(
commands: string[],
options?: CommandOptions & ExecOptions,
timeoutMs?: number | null,
) {
try {
return await this.subcontainer.exec(commands, options, timeoutMs)
} finally {
await this.subcontainer.destroy?.()
}
}
async execFail(
commands: string[],
timeoutMs: number | null,
options?: CommandOptions & ExecOptions,
) {
try {
const res = await this.subcontainer.exec(commands, options, timeoutMs)
if (res.exitCode !== 0) {
const codeOrSignal =
res.exitCode !== null
? `code ${res.exitCode}`
: `signal ${res.exitSignal}`
throw new Error(
`Process exited with ${codeOrSignal}: ${res.stderr.toString()}`,
)
}
return res
} finally {
await this.subcontainer.destroy?.()
}
}
// async spawn(commands: string[]): Promise<cp.ChildProcess> {
// return await this.subcontainer.spawn(commands)
// }
onDrop(): void {
this.subcontainer.destroy?.()
}
}