mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
158 lines
4.6 KiB
TypeScript
158 lines
4.6 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,
|
|
ExecSpawnable,
|
|
} from "@start9labs/start-sdk/package/lib/util/SubContainer"
|
|
export const exec = promisify(cp.exec)
|
|
export const execFile = promisify(cp.execFile)
|
|
|
|
export class DockerProcedureContainer {
|
|
private constructor(private readonly subcontainer: ExecSpawnable) {}
|
|
|
|
static async of(
|
|
effects: T.Effects,
|
|
packageId: string,
|
|
data: DockerProcedure,
|
|
volumes: { [id: VolumeId]: Volume },
|
|
name: string,
|
|
options: { subcontainer?: ExecSpawnable } = {},
|
|
) {
|
|
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 SubContainer.of(
|
|
effects,
|
|
{ imageId: data.image },
|
|
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(
|
|
{ type: "volume", id: mount, subpath: null, readonly: false },
|
|
mounts[mount],
|
|
)
|
|
} else if (volumeMount.type === "assets") {
|
|
await subcontainer.mount(
|
|
{ type: "assets", id: mount, subpath: null },
|
|
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"],
|
|
},
|
|
})
|
|
.catch(console.warn)
|
|
} else if (volumeMount.type === "backup") {
|
|
await subcontainer.mount(
|
|
{ type: "backup", subpath: null },
|
|
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)
|
|
}
|
|
}
|