mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
Feature/lxc container runtime (#2514)
* wip: static-server errors * wip: fix wifi * wip: Fix the service_effects * wip: Fix cors in the middleware * wip(chore): Auth clean up the lint. * wip(fix): Vhost * wip: continue manager refactor Co-authored-by: J H <Blu-J@users.noreply.github.com> * wip: service manager refactor * wip: Some fixes * wip(fix): Fix the lib.rs * wip * wip(fix): Logs * wip: bins * wip(innspect): Add in the inspect * wip: config * wip(fix): Diagnostic * wip(fix): Dependencies * wip: context * wip(fix) Sorta auth * wip: warnings * wip(fix): registry/admin * wip(fix) marketplace * wip(fix) Some more converted and fixed with the linter and config * wip: Working on the static server * wip(fix)static server * wip: Remove some asynnc * wip: Something about the request and regular rpc * wip: gut install Co-authored-by: J H <Blu-J@users.noreply.github.com> * wip: Convert the static server into the new system * wip delete file * test * wip(fix) vhost does not need the with safe defaults * wip: Adding in the wifi * wip: Fix the developer and the verify * wip: new install flow Co-authored-by: J H <Blu-J@users.noreply.github.com> * fix middleware * wip * wip: Fix the auth * wip * continue service refactor * feature: Service get_config * feat: Action * wip: Fighting the great fight against the borrow checker * wip: Remove an error in a file that I just need to deel with later * chore: Add in some more lifetime stuff to the services * wip: Install fix on lifetime * cleanup * wip: Deal with the borrow later * more cleanup * resolve borrowchecker errors * wip(feat): add in the handler for the socket, for now * wip(feat): Update the service_effect_handler::action * chore: Add in the changes to make sure the from_service goes to context * chore: Change the * refactor service map * fix references to service map * fill out restore * wip: Before I work on the store stuff * fix backup module * handle some warnings * feat: add in the ui components on the rust side * feature: Update the procedures * chore: Update the js side of the main and a few of the others * chore: Update the rpc listener to match the persistant container * wip: Working on updating some things to have a better name * wip(feat): Try and get the rpc to return the correct shape? * lxc wip * wip(feat): Try and get the rpc to return the correct shape? * build for container runtime wip * remove container-init * fix build * fix error * chore: Update to work I suppose * lxc wip * remove docker module and feature * download alpine squashfs automatically * overlays effect Co-authored-by: Jade <Blu-J@users.noreply.github.com> * chore: Add the overlay effect * feat: Add the mounter in the main * chore: Convert to use the mounts, still need to work with the sandbox * install fixes * fix ssl * fixes from testing * implement tmpfile for upload * wip * misc fixes * cleanup * cleanup * better progress reporting * progress for sideload * return real guid * add devmode script * fix lxc rootfs path * fix percentage bar * fix progress bar styling * fix build for unstable * tweaks * label progress * tweaks * update progress more often * make symlink in rpc_client * make socket dir * fix parent path * add start-cli to container * add echo and gitInfo commands * wip: Add the init + errors * chore: Add in the exit effect for the system * chore: Change the type to null for failure to parse * move sigterm timeout to stopping status * update order * chore: Update the return type * remove dbg * change the map error * chore: Update the thing to capture id * chore add some life changes * chore: Update the loging * chore: Update the package to run module * us From for RpcError * chore: Update to use import instead * chore: update * chore: Use require for the backup * fix a default * update the type that is wrong * chore: Update the type of the manifest * chore: Update to make null * only symlink if not exists * get rid of double result * better debug info for ErrorCollection * chore: Update effects * chore: fix * mount assets and volumes * add exec instead of spawn * fix mounting in image * fix overlay mounts Co-authored-by: Jade <Blu-J@users.noreply.github.com> * misc fixes * feat: Fix two * fix: systemForEmbassy main * chore: Fix small part of main loop * chore: Modify the bundle * merge * fixMain loop" * move tsc to makefile * chore: Update the return types of the health check * fix client * chore: Convert the todo to use tsmatches * add in the fixes for the seen and create the hack to allow demo * chore: Update to include the systemForStartOs * chore UPdate to the latest types from the expected outout * fixes * fix typo * Don't emit if failure on tsc * wip Co-authored-by: Jade <Blu-J@users.noreply.github.com> * add s9pk api * add inspection * add inspect manifest * newline after display serializable * fix squashfs in image name * edit manifest Co-authored-by: Jade <Blu-J@users.noreply.github.com> * wait for response on repl * ignore sig for now * ignore sig for now * re-enable sig verification * fix * wip * env and chroot * add profiling logs * set uid & gid in squashfs to 100000 * set uid of sqfs to 100000 * fix mksquashfs args * add env to compat * fix * re-add docker feature flag * fix docker output format being stupid * here be dragons * chore: Add in the cross compiling for something * fix npm link * extract logs from container on exit * chore: Update for testing * add log capture to drop trait * chore: add in the modifications that I make * chore: Update small things for no updates * chore: Update the types of something * chore: Make main not complain * idmapped mounts * idmapped volumes * re-enable kiosk * chore: Add in some logging for the new system * bring in start-sdk * remove avahi * chore: Update the deps * switch to musl * chore: Update the version of prettier * chore: Organize' * chore: Update some of the headers back to the standard of fetch * fix musl build * fix idmapped mounts * fix cross build * use cross compiler for correct arch * feat: Add in the faked ssl stuff for the effects * @dr_bonez Did a solution here * chore: Something that DrBonez * chore: up * wip: We have a working server!!! * wip * uninstall * wip * tes --------- Co-authored-by: J H <dragondef@gmail.com> Co-authored-by: J H <Blu-J@users.noreply.github.com> Co-authored-by: J H <2364004+Blu-J@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as cp from "child_process"
|
||||
import { Overlay, types as T } from "@start9labs/start-sdk"
|
||||
import { promisify } from "util"
|
||||
import { DockerProcedure, VolumeId } from "../../../Models/DockerProcedure"
|
||||
import { Volume } from "./matchVolume"
|
||||
export const exec = promisify(cp.exec)
|
||||
export const execFile = promisify(cp.execFile)
|
||||
|
||||
export class DockerProcedureContainer {
|
||||
private constructor(readonly overlay: Overlay) {}
|
||||
// static async readonlyOf(data: DockerProcedure) {
|
||||
// return DockerProcedureContainer.of(data, ["-o", "ro"])
|
||||
// }
|
||||
static async of(
|
||||
effects: T.Effects,
|
||||
data: DockerProcedure,
|
||||
volumes: { [id: VolumeId]: Volume },
|
||||
) {
|
||||
const overlay = await Overlay.of(effects, data.image)
|
||||
|
||||
if (data.mounts) {
|
||||
const mounts = data.mounts
|
||||
for (const mount in mounts) {
|
||||
const path = mounts[mount].startsWith("/")
|
||||
? `${overlay.rootfs}${mounts[mount]}`
|
||||
: `${overlay.rootfs}/${mounts[mount]}`
|
||||
await fs.mkdir(path, { recursive: true })
|
||||
const volumeMount = volumes[mount]
|
||||
if (volumeMount.type === "data") {
|
||||
await overlay.mount({ type: "volume", id: mount }, mounts[mount])
|
||||
} else if (volumeMount.type === "assets") {
|
||||
await overlay.mount({ type: "assets", id: mount }, mounts[mount])
|
||||
} else if (volumeMount.type === "certificate") {
|
||||
volumeMount
|
||||
const certChain = await effects.getSslCertificate()
|
||||
const key = await effects.getSslKey()
|
||||
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"],
|
||||
path: volumeMount.path,
|
||||
readonly: volumeMount.readonly,
|
||||
volumeId: volumeMount["volume-id"],
|
||||
},
|
||||
})
|
||||
} else if (volumeMount.type === "backup") {
|
||||
throw new Error("TODO")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new DockerProcedureContainer(overlay)
|
||||
}
|
||||
|
||||
async exec(commands: string[]) {
|
||||
try {
|
||||
return await this.overlay.exec(commands)
|
||||
} finally {
|
||||
await this.overlay.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
async spawn(commands: string[]): Promise<cp.ChildProcessWithoutNullStreams> {
|
||||
return await this.overlay.spawn(commands)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { PolyfillEffects } from "./polyfillEffects"
|
||||
import { DockerProcedureContainer } from "./DockerProcedureContainer"
|
||||
import { SystemForEmbassy } from "."
|
||||
import { HostSystemStartOs } from "../../HostSystemStartOs"
|
||||
import { util, Daemons, types as T } from "@start9labs/start-sdk"
|
||||
|
||||
const EMBASSY_HEALTH_INTERVAL = 15 * 1000
|
||||
const EMBASSY_PROPERTIES_LOOP = 30 * 1000
|
||||
/**
|
||||
* We wanted something to represent what the main loop is doing, and
|
||||
* in this case it used to run the properties, health, and the docker/ js main.
|
||||
* Also, this has an ability to clean itself up too if need be.
|
||||
*/
|
||||
export class MainLoop {
|
||||
private healthLoops:
|
||||
| {
|
||||
name: string
|
||||
interval: NodeJS.Timeout
|
||||
}[]
|
||||
| undefined
|
||||
|
||||
private mainEvent:
|
||||
| Promise<{
|
||||
daemon: T.DaemonReturned
|
||||
wait: Promise<unknown>
|
||||
}>
|
||||
| undefined
|
||||
private propertiesEvent: NodeJS.Timeout | undefined
|
||||
constructor(
|
||||
readonly system: SystemForEmbassy,
|
||||
readonly effects: HostSystemStartOs,
|
||||
readonly runProperties: () => Promise<void>,
|
||||
) {
|
||||
this.healthLoops = this.constructHealthLoops()
|
||||
this.mainEvent = this.constructMainEvent()
|
||||
this.propertiesEvent = this.constructPropertiesEvent()
|
||||
}
|
||||
|
||||
private async constructMainEvent() {
|
||||
const { system, effects } = this
|
||||
const utils = util.createUtils(effects)
|
||||
const currentCommand: [string, ...string[]] = [
|
||||
system.manifest.main.entrypoint,
|
||||
...system.manifest.main.args,
|
||||
]
|
||||
|
||||
await effects.setMainStatus({ status: "running" })
|
||||
const jsMain = (this.system.moduleCode as any)?.jsMain
|
||||
const dockerProcedureContainer = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
this.system.manifest.main,
|
||||
this.system.manifest.volumes,
|
||||
)
|
||||
if (jsMain) {
|
||||
const daemons = Daemons.of({
|
||||
effects,
|
||||
started: async (_) => {},
|
||||
healthReceipts: [],
|
||||
})
|
||||
throw new Error("todo")
|
||||
// return {
|
||||
// daemon,
|
||||
// wait: daemon.wait().finally(() => {
|
||||
// this.clean()
|
||||
// effects.setMainStatus({ status: "stopped" })
|
||||
// }),
|
||||
// }
|
||||
}
|
||||
const daemon = await utils.runDaemon(
|
||||
this.system.manifest.main.image,
|
||||
currentCommand,
|
||||
{
|
||||
overlay: dockerProcedureContainer.overlay,
|
||||
},
|
||||
)
|
||||
return {
|
||||
daemon,
|
||||
wait: daemon.wait().finally(() => {
|
||||
this.clean()
|
||||
effects
|
||||
.setMainStatus({ status: "stopped" })
|
||||
.catch((e) => console.error("Could not set the status to stopped"))
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
public async clean(options?: { timeout?: number }) {
|
||||
const { mainEvent, healthLoops, propertiesEvent } = this
|
||||
delete this.mainEvent
|
||||
delete this.healthLoops
|
||||
delete this.propertiesEvent
|
||||
if (mainEvent) await (await mainEvent).daemon.term()
|
||||
clearInterval(propertiesEvent)
|
||||
if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval))
|
||||
}
|
||||
|
||||
private constructPropertiesEvent() {
|
||||
const { runProperties } = this
|
||||
return setInterval(() => {
|
||||
runProperties()
|
||||
}, EMBASSY_PROPERTIES_LOOP)
|
||||
}
|
||||
|
||||
private constructHealthLoops() {
|
||||
const { manifest } = this.system
|
||||
const effects = this.effects
|
||||
const start = Date.now()
|
||||
return Object.values(manifest["health-checks"]).map((value) => {
|
||||
const name = value.name
|
||||
const interval = setInterval(async () => {
|
||||
const actionProcedure = value
|
||||
const timeChanged = Date.now() - start
|
||||
if (actionProcedure.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
actionProcedure,
|
||||
manifest.volumes,
|
||||
)
|
||||
const executed = await container.exec([
|
||||
actionProcedure.entrypoint,
|
||||
...actionProcedure.args,
|
||||
JSON.stringify(timeChanged),
|
||||
])
|
||||
const stderr = executed.stderr.toString()
|
||||
if (stderr)
|
||||
console.error(`Error running health check ${value.name}: ${stderr}`)
|
||||
return executed.stdout.toString()
|
||||
} else {
|
||||
const moduleCode = await this.system.moduleCode
|
||||
const method = moduleCode.health?.[value.name]
|
||||
if (!method)
|
||||
return console.error(
|
||||
`Expecting that thejs health check ${value.name} exists`,
|
||||
)
|
||||
return (await method(
|
||||
new PolyfillEffects(effects, this.system.manifest),
|
||||
timeChanged,
|
||||
).then((x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x)
|
||||
return console.error("Error getting config: " + x.error)
|
||||
return console.error("Error getting config: " + x["error-code"][1])
|
||||
})) as any
|
||||
}
|
||||
}, EMBASSY_HEALTH_INTERVAL)
|
||||
|
||||
return { name, interval }
|
||||
})
|
||||
}
|
||||
}
|
||||
900
container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts
Normal file
900
container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts
Normal file
@@ -0,0 +1,900 @@
|
||||
import { types as T, util, EmVer } from "@start9labs/start-sdk"
|
||||
import * as fs from "fs/promises"
|
||||
|
||||
import { PolyfillEffects } from "./polyfillEffects"
|
||||
import { ExecuteResult, System } from "../../../Interfaces/System"
|
||||
import { matchManifest, Manifest, Procedure } from "./matchManifest"
|
||||
import { create } from "domain"
|
||||
import * as childProcess from "node:child_process"
|
||||
import { Volume } from "../../../Models/Volume"
|
||||
import { DockerProcedure } from "../../../Models/DockerProcedure"
|
||||
import { DockerProcedureContainer } from "./DockerProcedureContainer"
|
||||
import { promisify } from "node:util"
|
||||
import * as U from "./oldEmbassyTypes"
|
||||
import { MainLoop } from "./MainLoop"
|
||||
import {
|
||||
matches,
|
||||
boolean,
|
||||
dictionary,
|
||||
literal,
|
||||
literals,
|
||||
object,
|
||||
string,
|
||||
unknown,
|
||||
any,
|
||||
tuple,
|
||||
number,
|
||||
} from "ts-matches"
|
||||
import { HostSystemStartOs } from "../../HostSystemStartOs"
|
||||
import { JsonPath, unNestPath } from "../../../Models/JsonPath"
|
||||
import { HostSystem } from "../../../Interfaces/HostSystem"
|
||||
|
||||
type Optional<A> = A | undefined | null
|
||||
function todo(): never {
|
||||
throw new Error("Not implemented")
|
||||
}
|
||||
const execFile = promisify(childProcess.execFile)
|
||||
|
||||
const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json"
|
||||
const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js"
|
||||
const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig"
|
||||
|
||||
export class SystemForEmbassy implements System {
|
||||
currentRunning: MainLoop | undefined
|
||||
static async of(manifestLocation: string = MANIFEST_LOCATION) {
|
||||
const moduleCode = await import(EMBASSY_JS_LOCATION)
|
||||
.catch((_) => require(EMBASSY_JS_LOCATION))
|
||||
.catch(async (_) => {
|
||||
console.error("Could not load the js")
|
||||
console.error({
|
||||
exists: await fs.stat(EMBASSY_JS_LOCATION),
|
||||
})
|
||||
return {}
|
||||
})
|
||||
const manifestData = await fs.readFile(manifestLocation, "utf-8")
|
||||
return new SystemForEmbassy(
|
||||
matchManifest.unsafeCast(JSON.parse(manifestData)),
|
||||
moduleCode,
|
||||
)
|
||||
}
|
||||
constructor(
|
||||
readonly manifest: Manifest,
|
||||
readonly moduleCode: Partial<U.ExpectedExports>,
|
||||
) {}
|
||||
async execute(
|
||||
effects: HostSystemStartOs,
|
||||
options: {
|
||||
procedure: JsonPath
|
||||
input: unknown
|
||||
timeout?: number | undefined
|
||||
},
|
||||
): Promise<ExecuteResult> {
|
||||
return this._execute(effects, options)
|
||||
.then((x) =>
|
||||
matches(x)
|
||||
.when(
|
||||
object({
|
||||
result: any,
|
||||
}),
|
||||
(x) => ({
|
||||
ok: x.result,
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
object({
|
||||
error: string,
|
||||
}),
|
||||
(x) => ({
|
||||
err: {
|
||||
code: 0,
|
||||
message: x.error,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
object({
|
||||
"error-code": tuple(number, string),
|
||||
}),
|
||||
({ "error-code": [code, message] }) => ({
|
||||
err: {
|
||||
code,
|
||||
message,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.defaultTo({ ok: x }),
|
||||
)
|
||||
.catch((error) => ({
|
||||
err: {
|
||||
code: 0,
|
||||
message: "" + error,
|
||||
},
|
||||
}))
|
||||
}
|
||||
async exit(effects: HostSystemStartOs): Promise<void> {
|
||||
if (this.currentRunning) await this.currentRunning.clean()
|
||||
delete this.currentRunning
|
||||
}
|
||||
async _execute(
|
||||
effects: HostSystemStartOs,
|
||||
options: {
|
||||
procedure: JsonPath
|
||||
input: unknown
|
||||
timeout?: number | undefined
|
||||
},
|
||||
): Promise<unknown> {
|
||||
const input = options.input
|
||||
switch (options.procedure) {
|
||||
case "/backup/create":
|
||||
return this.createBackup(effects)
|
||||
case "/backup/restore":
|
||||
return this.restoreBackup(effects)
|
||||
case "/config/get":
|
||||
return this.getConfig(effects)
|
||||
case "/config/set":
|
||||
return this.setConfig(effects, input)
|
||||
case "/actions/metadata":
|
||||
return todo()
|
||||
case "/init":
|
||||
return this.init(effects, string.optional().unsafeCast(input))
|
||||
case "/uninit":
|
||||
return this.uninit(effects, string.optional().unsafeCast(input))
|
||||
case "/main/start":
|
||||
return this.mainStart(effects)
|
||||
case "/main/stop":
|
||||
return this.mainStop(effects)
|
||||
default:
|
||||
const procedures = unNestPath(options.procedure)
|
||||
switch (true) {
|
||||
case procedures[1] === "actions" && procedures[3] === "get":
|
||||
return this.action(effects, procedures[2], input)
|
||||
case procedures[1] === "actions" && procedures[3] === "run":
|
||||
return this.action(effects, procedures[2], input)
|
||||
case procedures[1] === "dependencies" && procedures[3] === "query":
|
||||
return this.dependenciesAutoconfig(effects, procedures[2], input)
|
||||
|
||||
case procedures[1] === "dependencies" && procedures[3] === "update":
|
||||
return this.dependenciesAutoconfig(effects, procedures[2], input)
|
||||
}
|
||||
}
|
||||
}
|
||||
private async init(
|
||||
effects: HostSystemStartOs,
|
||||
previousVersion: Optional<string>,
|
||||
): Promise<void> {
|
||||
console.log("here1")
|
||||
if (previousVersion) await this.migration(effects, previousVersion)
|
||||
console.log("here2")
|
||||
await this.properties(effects)
|
||||
console.log("here3")
|
||||
await effects.setMainStatus({ status: "stopped" })
|
||||
console.log("here4")
|
||||
}
|
||||
private async uninit(
|
||||
effects: HostSystemStartOs,
|
||||
nextVersion: Optional<string>,
|
||||
): Promise<void> {
|
||||
// TODO Do a migration down if the version exists
|
||||
await effects.setMainStatus({ status: "stopped" })
|
||||
}
|
||||
private async mainStart(effects: HostSystemStartOs): Promise<void> {
|
||||
if (!!this.currentRunning) return
|
||||
|
||||
this.currentRunning = new MainLoop(this, effects, () =>
|
||||
this.properties(effects),
|
||||
)
|
||||
}
|
||||
private async mainStop(
|
||||
effects: HostSystemStartOs,
|
||||
options?: { timeout?: number },
|
||||
): Promise<void> {
|
||||
const { currentRunning } = this
|
||||
delete this.currentRunning
|
||||
if (currentRunning) {
|
||||
await currentRunning.clean({
|
||||
timeout: options?.timeout || this.manifest.main["sigterm-timeout"],
|
||||
})
|
||||
}
|
||||
}
|
||||
private async createBackup(effects: HostSystemStartOs): Promise<void> {
|
||||
const backup = this.manifest.backup.create
|
||||
if (backup.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
backup,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
await container.exec([backup.entrypoint, ...backup.args])
|
||||
} else {
|
||||
const moduleCode = await this.moduleCode
|
||||
await moduleCode.createBackup?.(
|
||||
new PolyfillEffects(effects, this.manifest),
|
||||
)
|
||||
}
|
||||
}
|
||||
private async restoreBackup(effects: HostSystemStartOs): Promise<void> {
|
||||
const restoreBackup = this.manifest.backup.restore
|
||||
if (restoreBackup.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
restoreBackup,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
await container.exec([restoreBackup.entrypoint, ...restoreBackup.args])
|
||||
} else {
|
||||
const moduleCode = await this.moduleCode
|
||||
await moduleCode.restoreBackup?.(
|
||||
new PolyfillEffects(effects, this.manifest),
|
||||
)
|
||||
}
|
||||
}
|
||||
private async getConfig(effects: HostSystemStartOs): Promise<T.ConfigRes> {
|
||||
return this.getConfigUncleaned(effects).then(removePointers)
|
||||
}
|
||||
private async getConfigUncleaned(
|
||||
effects: HostSystemStartOs,
|
||||
): Promise<T.ConfigRes> {
|
||||
const config = this.manifest.config?.get
|
||||
if (!config) return { spec: {} }
|
||||
if (config.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
config,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
// TODO: yaml
|
||||
return JSON.parse(
|
||||
(
|
||||
await container.exec([config.entrypoint, ...config.args])
|
||||
).stdout.toString(),
|
||||
)
|
||||
} else {
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.getConfig
|
||||
if (!method) throw new Error("Expecting that the method getConfig exists")
|
||||
return (await method(new PolyfillEffects(effects, this.manifest)).then(
|
||||
(x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
},
|
||||
)) as any
|
||||
}
|
||||
}
|
||||
private async setConfig(
|
||||
effects: HostSystemStartOs,
|
||||
newConfigWithoutPointers: unknown,
|
||||
): Promise<T.SetResult> {
|
||||
const newConfig = structuredClone(newConfigWithoutPointers)
|
||||
await updateConfig(
|
||||
effects,
|
||||
await this.getConfigUncleaned(effects).then((x) => x.spec),
|
||||
newConfig,
|
||||
)
|
||||
const setConfigValue = this.manifest.config?.set
|
||||
if (!setConfigValue) return { signal: "SIGTERM", "depends-on": {} }
|
||||
if (setConfigValue.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
setConfigValue,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
return JSON.parse(
|
||||
(
|
||||
await container.exec([
|
||||
setConfigValue.entrypoint,
|
||||
...setConfigValue.args,
|
||||
JSON.stringify(newConfig),
|
||||
])
|
||||
).stdout.toString(),
|
||||
)
|
||||
} else if (setConfigValue.type === "script") {
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.setConfig
|
||||
if (!method) throw new Error("Expecting that the method setConfig exists")
|
||||
return await method(
|
||||
new PolyfillEffects(effects, this.manifest),
|
||||
newConfig as U.Config,
|
||||
).then((x): T.SetResult => {
|
||||
if ("result" in x)
|
||||
return {
|
||||
"depends-on": x.result["depends-on"],
|
||||
signal: x.result.signal === "SIGEMT" ? "SIGTERM" : x.result.signal,
|
||||
}
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
})
|
||||
} else {
|
||||
return {
|
||||
"depends-on": {},
|
||||
signal: "SIGTERM",
|
||||
}
|
||||
}
|
||||
}
|
||||
private async migration(
|
||||
effects: HostSystemStartOs,
|
||||
fromVersion: string,
|
||||
): Promise<T.MigrationRes> {
|
||||
const fromEmver = EmVer.from(fromVersion)
|
||||
const currentEmver = EmVer.from(this.manifest.version)
|
||||
if (!this.manifest.migrations) return { configured: true }
|
||||
const fromMigration = Object.entries(this.manifest.migrations.from)
|
||||
.map(([version, procedure]) => [EmVer.from(version), procedure] as const)
|
||||
.find(
|
||||
([versionEmver, procedure]) =>
|
||||
versionEmver.greaterThan(fromEmver) &&
|
||||
versionEmver.lessThanOrEqual(currentEmver),
|
||||
)
|
||||
const toMigration = Object.entries(this.manifest.migrations.to)
|
||||
.map(([version, procedure]) => [EmVer.from(version), procedure] as const)
|
||||
.find(
|
||||
([versionEmver, procedure]) =>
|
||||
versionEmver.greaterThan(fromEmver) &&
|
||||
versionEmver.lessThanOrEqual(currentEmver),
|
||||
)
|
||||
|
||||
// prettier-ignore
|
||||
const migration = (
|
||||
fromEmver.greaterThan(currentEmver) ? [toMigration, fromMigration] :
|
||||
[fromMigration, toMigration]).filter(Boolean)[0]
|
||||
|
||||
if (migration) {
|
||||
const [version, procedure] = migration
|
||||
if (procedure.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
procedure,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
return JSON.parse(
|
||||
(
|
||||
await container.exec([
|
||||
procedure.entrypoint,
|
||||
...procedure.args,
|
||||
JSON.stringify(fromVersion),
|
||||
])
|
||||
).stdout.toString(),
|
||||
)
|
||||
} else if (procedure.type === "script") {
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.migration
|
||||
if (!method)
|
||||
throw new Error("Expecting that the method migration exists")
|
||||
return (await method(
|
||||
new PolyfillEffects(effects, this.manifest),
|
||||
fromVersion as string,
|
||||
).then((x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
})) as any
|
||||
}
|
||||
}
|
||||
return { configured: true }
|
||||
}
|
||||
private async properties(effects: HostSystemStartOs): Promise<undefined> {
|
||||
// TODO BLU-J set the properties ever so often
|
||||
const setConfigValue = this.manifest.properties
|
||||
if (!setConfigValue) return
|
||||
if (setConfigValue.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
setConfigValue,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
return JSON.parse(
|
||||
(
|
||||
await container.exec([
|
||||
setConfigValue.entrypoint,
|
||||
...setConfigValue.args,
|
||||
])
|
||||
).stdout.toString(),
|
||||
)
|
||||
} else if (setConfigValue.type === "script") {
|
||||
const moduleCode = this.moduleCode
|
||||
const method = moduleCode.properties
|
||||
if (!method)
|
||||
throw new Error("Expecting that the method properties exists")
|
||||
await method(new PolyfillEffects(effects, this.manifest)).then((x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
})
|
||||
}
|
||||
}
|
||||
private async health(
|
||||
effects: HostSystemStartOs,
|
||||
healthId: string,
|
||||
timeSinceStarted: unknown,
|
||||
): Promise<void> {
|
||||
const healthProcedure = this.manifest["health-checks"][healthId]
|
||||
if (!healthProcedure) return
|
||||
if (healthProcedure.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
healthProcedure,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
return JSON.parse(
|
||||
(
|
||||
await container.exec([
|
||||
healthProcedure.entrypoint,
|
||||
...healthProcedure.args,
|
||||
JSON.stringify(timeSinceStarted),
|
||||
])
|
||||
).stdout.toString(),
|
||||
)
|
||||
} else if (healthProcedure.type === "script") {
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.health?.[healthId]
|
||||
if (!method) throw new Error("Expecting that the method health exists")
|
||||
await method(
|
||||
new PolyfillEffects(effects, this.manifest),
|
||||
Number(timeSinceStarted),
|
||||
).then((x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
})
|
||||
}
|
||||
}
|
||||
private async action(
|
||||
effects: HostSystemStartOs,
|
||||
actionId: string,
|
||||
formData: unknown,
|
||||
): Promise<T.ActionResult> {
|
||||
const actionProcedure = this.manifest.actions?.[actionId]?.implementation
|
||||
if (!actionProcedure) return { message: "Action not found", value: null }
|
||||
if (actionProcedure.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
actionProcedure,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
return JSON.parse(
|
||||
(
|
||||
await container.exec([
|
||||
actionProcedure.entrypoint,
|
||||
...actionProcedure.args,
|
||||
JSON.stringify(formData),
|
||||
])
|
||||
).stdout.toString(),
|
||||
)
|
||||
} else {
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.action?.[actionId]
|
||||
if (!method) throw new Error("Expecting that the method action exists")
|
||||
return (await method(
|
||||
new PolyfillEffects(effects, this.manifest),
|
||||
formData as any,
|
||||
).then((x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
})) as any
|
||||
}
|
||||
}
|
||||
private async dependenciesCheck(
|
||||
effects: HostSystemStartOs,
|
||||
id: string,
|
||||
oldConfig: unknown,
|
||||
): Promise<object> {
|
||||
const actionProcedure = this.manifest.dependencies?.[id]?.config?.check
|
||||
if (!actionProcedure) return { message: "Action not found", value: null }
|
||||
if (actionProcedure.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
actionProcedure,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
return JSON.parse(
|
||||
(
|
||||
await container.exec([
|
||||
actionProcedure.entrypoint,
|
||||
...actionProcedure.args,
|
||||
JSON.stringify(oldConfig),
|
||||
])
|
||||
).stdout.toString(),
|
||||
)
|
||||
} else if (actionProcedure.type === "script") {
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.dependencies?.[id]?.check
|
||||
if (!method)
|
||||
throw new Error(
|
||||
`Expecting that the method dependency check ${id} exists`,
|
||||
)
|
||||
return (await method(
|
||||
new PolyfillEffects(effects, this.manifest),
|
||||
oldConfig as any,
|
||||
).then((x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
})) as any
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
private async dependenciesAutoconfig(
|
||||
effects: HostSystemStartOs,
|
||||
id: string,
|
||||
oldConfig: unknown,
|
||||
): Promise<void> {
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.dependencies?.[id]?.autoConfigure
|
||||
if (!method)
|
||||
throw new Error(
|
||||
`Expecting that the method dependency autoConfigure ${id} exists`,
|
||||
)
|
||||
return (await method(
|
||||
new PolyfillEffects(effects, this.manifest),
|
||||
oldConfig as any,
|
||||
).then((x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
})) as any
|
||||
}
|
||||
// private async sandbox(
|
||||
// effects: HostSystemStartOs,
|
||||
// options: {
|
||||
// procedure:
|
||||
// | "/createBackup"
|
||||
// | "/restoreBackup"
|
||||
// | "/getConfig"
|
||||
// | "/setConfig"
|
||||
// | "migration"
|
||||
// | "/properties"
|
||||
// | `/action/${string}`
|
||||
// | `/dependencies/${string}/check`
|
||||
// | `/dependencies/${string}/autoConfigure`
|
||||
// input: unknown
|
||||
// timeout?: number | undefined
|
||||
// },
|
||||
// ): Promise<unknown> {
|
||||
// const input = options.input
|
||||
// switch (options.procedure) {
|
||||
// case "/createBackup":
|
||||
// return this.roCreateBackup(effects)
|
||||
// case "/restoreBackup":
|
||||
// return this.roRestoreBackup(effects)
|
||||
// case "/getConfig":
|
||||
// return this.roGetConfig(effects)
|
||||
// case "/setConfig":
|
||||
// return this.roSetConfig(effects, input)
|
||||
// case "migration":
|
||||
// return this.roMigration(effects, input)
|
||||
// case "/properties":
|
||||
// return this.roProperties(effects)
|
||||
// default:
|
||||
// const procedure = options.procedure.split("/")
|
||||
// switch (true) {
|
||||
// case options.procedure.startsWith("/action/"):
|
||||
// return this.roAction(effects, procedure[2], input)
|
||||
// case options.procedure.startsWith("/dependencies/") &&
|
||||
// procedure[3] === "check":
|
||||
// return this.roDependenciesCheck(effects, procedure[2], input)
|
||||
|
||||
// case options.procedure.startsWith("/dependencies/") &&
|
||||
// procedure[3] === "autoConfigure":
|
||||
// return this.roDependenciesAutoconfig(effects, procedure[2], input)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// private async roCreateBackup(effects: HostSystemStartOs): Promise<void> {
|
||||
// const backup = this.manifest.backup.create
|
||||
// if (backup.type === "docker") {
|
||||
// const container = await DockerProcedureContainer.readonlyOf(backup)
|
||||
// await container.exec([backup.entrypoint, ...backup.args])
|
||||
// } else {
|
||||
// const moduleCode = await this.moduleCode
|
||||
// await moduleCode.createBackup?.(new PolyfillEffects(effects))
|
||||
// }
|
||||
// }
|
||||
// private async roRestoreBackup(effects: HostSystemStartOs): Promise<void> {
|
||||
// const restoreBackup = this.manifest.backup.restore
|
||||
// if (restoreBackup.type === "docker") {
|
||||
// const container = await DockerProcedureContainer.readonlyOf(restoreBackup)
|
||||
// await container.exec([restoreBackup.entrypoint, ...restoreBackup.args])
|
||||
// } else {
|
||||
// const moduleCode = await this.moduleCode
|
||||
// await moduleCode.restoreBackup?.(new PolyfillEffects(effects))
|
||||
// }
|
||||
// }
|
||||
// private async roGetConfig(effects: HostSystemStartOs): Promise<T.ConfigRes> {
|
||||
// const config = this.manifest.config?.get
|
||||
// if (!config) return { spec: {} }
|
||||
// if (config.type === "docker") {
|
||||
// const container = await DockerProcedureContainer.readonlyOf(config)
|
||||
// return JSON.parse(
|
||||
// (await container.exec([config.entrypoint, ...config.args])).stdout,
|
||||
// )
|
||||
// } else {
|
||||
// const moduleCode = await this.moduleCode
|
||||
// const method = moduleCode.getConfig
|
||||
// if (!method) throw new Error("Expecting that the method getConfig exists")
|
||||
// return (await method(new PolyfillEffects(effects)).then((x) => {
|
||||
// if ("result" in x) return x.result
|
||||
// if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
// throw new Error("Error getting config: " + x["error-code"][1])
|
||||
// })) as any
|
||||
// }
|
||||
// }
|
||||
// private async roSetConfig(
|
||||
// effects: HostSystemStartOs,
|
||||
// newConfig: unknown,
|
||||
// ): Promise<T.SetResult> {
|
||||
// const setConfigValue = this.manifest.config?.set
|
||||
// if (!setConfigValue) return { signal: "SIGTERM", "depends-on": {} }
|
||||
// if (setConfigValue.type === "docker") {
|
||||
// const container = await DockerProcedureContainer.readonlyOf(
|
||||
// setConfigValue,
|
||||
// )
|
||||
// return JSON.parse(
|
||||
// (
|
||||
// await container.exec([
|
||||
// setConfigValue.entrypoint,
|
||||
// ...setConfigValue.args,
|
||||
// JSON.stringify(newConfig),
|
||||
// ])
|
||||
// ).stdout,
|
||||
// )
|
||||
// } else {
|
||||
// const moduleCode = await this.moduleCode
|
||||
// const method = moduleCode.setConfig
|
||||
// if (!method) throw new Error("Expecting that the method setConfig exists")
|
||||
// return await method(
|
||||
// new PolyfillEffects(effects),
|
||||
// newConfig as U.Config,
|
||||
// ).then((x) => {
|
||||
// if ("result" in x) return x.result
|
||||
// if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
// throw new Error("Error getting config: " + x["error-code"][1])
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// private async roMigration(
|
||||
// effects: HostSystemStartOs,
|
||||
// fromVersion: unknown,
|
||||
// ): Promise<T.MigrationRes> {
|
||||
// throw new Error("Migrations should never be ran in the sandbox mode")
|
||||
// }
|
||||
// private async roProperties(effects: HostSystemStartOs): Promise<unknown> {
|
||||
// const setConfigValue = this.manifest.properties
|
||||
// if (!setConfigValue) return {}
|
||||
// if (setConfigValue.type === "docker") {
|
||||
// const container = await DockerProcedureContainer.readonlyOf(
|
||||
// setConfigValue,
|
||||
// )
|
||||
// return JSON.parse(
|
||||
// (
|
||||
// await container.exec([
|
||||
// setConfigValue.entrypoint,
|
||||
// ...setConfigValue.args,
|
||||
// ])
|
||||
// ).stdout,
|
||||
// )
|
||||
// } else {
|
||||
// const moduleCode = await this.moduleCode
|
||||
// const method = moduleCode.properties
|
||||
// if (!method)
|
||||
// throw new Error("Expecting that the method properties exists")
|
||||
// return await method(new PolyfillEffects(effects)).then((x) => {
|
||||
// if ("result" in x) return x.result
|
||||
// if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
// throw new Error("Error getting config: " + x["error-code"][1])
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// private async roHealth(
|
||||
// effects: HostSystemStartOs,
|
||||
// healthId: string,
|
||||
// timeSinceStarted: unknown,
|
||||
// ): Promise<void> {
|
||||
// const healthProcedure = this.manifest["health-checks"][healthId]
|
||||
// if (!healthProcedure) return
|
||||
// if (healthProcedure.type === "docker") {
|
||||
// const container = await DockerProcedureContainer.readonlyOf(
|
||||
// healthProcedure,
|
||||
// )
|
||||
// return JSON.parse(
|
||||
// (
|
||||
// await container.exec([
|
||||
// healthProcedure.entrypoint,
|
||||
// ...healthProcedure.args,
|
||||
// JSON.stringify(timeSinceStarted),
|
||||
// ])
|
||||
// ).stdout,
|
||||
// )
|
||||
// } else {
|
||||
// const moduleCode = await this.moduleCode
|
||||
// const method = moduleCode.health?.[healthId]
|
||||
// if (!method) throw new Error("Expecting that the method health exists")
|
||||
// await method(new PolyfillEffects(effects), Number(timeSinceStarted)).then(
|
||||
// (x) => {
|
||||
// if ("result" in x) return x.result
|
||||
// if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
// throw new Error("Error getting config: " + x["error-code"][1])
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// private async roAction(
|
||||
// effects: HostSystemStartOs,
|
||||
// actionId: string,
|
||||
// formData: unknown,
|
||||
// ): Promise<T.ActionResult> {
|
||||
// const actionProcedure = this.manifest.actions?.[actionId]?.implementation
|
||||
// if (!actionProcedure) return { message: "Action not found", value: null }
|
||||
// if (actionProcedure.type === "docker") {
|
||||
// const container = await DockerProcedureContainer.readonlyOf(
|
||||
// actionProcedure,
|
||||
// )
|
||||
// return JSON.parse(
|
||||
// (
|
||||
// await container.exec([
|
||||
// actionProcedure.entrypoint,
|
||||
// ...actionProcedure.args,
|
||||
// JSON.stringify(formData),
|
||||
// ])
|
||||
// ).stdout,
|
||||
// )
|
||||
// } else {
|
||||
// const moduleCode = await this.moduleCode
|
||||
// const method = moduleCode.action?.[actionId]
|
||||
// if (!method) throw new Error("Expecting that the method action exists")
|
||||
// return (await method(new PolyfillEffects(effects), formData as any).then(
|
||||
// (x) => {
|
||||
// if ("result" in x) return x.result
|
||||
// if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
// throw new Error("Error getting config: " + x["error-code"][1])
|
||||
// },
|
||||
// )) as any
|
||||
// }
|
||||
// }
|
||||
// private async roDependenciesCheck(
|
||||
// effects: HostSystemStartOs,
|
||||
// id: string,
|
||||
// oldConfig: unknown,
|
||||
// ): Promise<object> {
|
||||
// const actionProcedure = this.manifest.dependencies?.[id]?.config?.check
|
||||
// if (!actionProcedure) return { message: "Action not found", value: null }
|
||||
// if (actionProcedure.type === "docker") {
|
||||
// const container = await DockerProcedureContainer.readonlyOf(
|
||||
// actionProcedure,
|
||||
// )
|
||||
// return JSON.parse(
|
||||
// (
|
||||
// await container.exec([
|
||||
// actionProcedure.entrypoint,
|
||||
// ...actionProcedure.args,
|
||||
// JSON.stringify(oldConfig),
|
||||
// ])
|
||||
// ).stdout,
|
||||
// )
|
||||
// } else {
|
||||
// const moduleCode = await this.moduleCode
|
||||
// const method = moduleCode.dependencies?.[id]?.check
|
||||
// if (!method)
|
||||
// throw new Error(
|
||||
// `Expecting that the method dependency check ${id} exists`,
|
||||
// )
|
||||
// return (await method(new PolyfillEffects(effects), oldConfig as any).then(
|
||||
// (x) => {
|
||||
// if ("result" in x) return x.result
|
||||
// if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
// throw new Error("Error getting config: " + x["error-code"][1])
|
||||
// },
|
||||
// )) as any
|
||||
// }
|
||||
// }
|
||||
// private async roDependenciesAutoconfig(
|
||||
// effects: HostSystemStartOs,
|
||||
// id: string,
|
||||
// oldConfig: unknown,
|
||||
// ): Promise<void> {
|
||||
// const moduleCode = await this.moduleCode
|
||||
// const method = moduleCode.dependencies?.[id]?.autoConfigure
|
||||
// if (!method)
|
||||
// throw new Error(
|
||||
// `Expecting that the method dependency autoConfigure ${id} exists`,
|
||||
// )
|
||||
// return (await method(new PolyfillEffects(effects), oldConfig as any).then(
|
||||
// (x) => {
|
||||
// if ("result" in x) return x.result
|
||||
// if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
// throw new Error("Error getting config: " + x["error-code"][1])
|
||||
// },
|
||||
// )) as any
|
||||
// }
|
||||
}
|
||||
async function removePointers(value: T.ConfigRes): Promise<T.ConfigRes> {
|
||||
const startingSpec = structuredClone(value.spec)
|
||||
const spec = cleanSpecOfPointers(startingSpec)
|
||||
|
||||
return { ...value, spec }
|
||||
}
|
||||
|
||||
const matchPointer = object({
|
||||
type: literal("pointer"),
|
||||
})
|
||||
|
||||
const matchPointerPackage = object({
|
||||
subtype: literal("package"),
|
||||
target: literals("tor-key", "tor-address", "lan-address"),
|
||||
"package-id": string,
|
||||
interface: string,
|
||||
})
|
||||
const matchPointerConfig = object({
|
||||
subtype: literal("package"),
|
||||
target: literals("config"),
|
||||
"package-id": string,
|
||||
selector: string,
|
||||
multi: boolean,
|
||||
})
|
||||
const matchSpec = object({
|
||||
spec: object,
|
||||
})
|
||||
const matchVariants = object({ variants: dictionary([string, unknown]) })
|
||||
function cleanSpecOfPointers<T>(mutSpec: T): T {
|
||||
if (!object.test(mutSpec)) return mutSpec
|
||||
for (const key in mutSpec) {
|
||||
const value = mutSpec[key]
|
||||
if (matchSpec.test(value)) value.spec = cleanSpecOfPointers(value.spec)
|
||||
if (matchVariants.test(value))
|
||||
value.variants = Object.fromEntries(
|
||||
Object.entries(value.variants).map(([key, value]) => [
|
||||
key,
|
||||
cleanSpecOfPointers(value),
|
||||
]),
|
||||
)
|
||||
if (!matchPointer.test(value)) continue
|
||||
delete mutSpec[key]
|
||||
// // if (value.target === )
|
||||
}
|
||||
|
||||
return mutSpec
|
||||
}
|
||||
|
||||
async function updateConfig(
|
||||
effects: HostSystemStartOs,
|
||||
spec: unknown,
|
||||
mutConfigValue: unknown,
|
||||
) {
|
||||
if (!dictionary([string, unknown]).test(spec)) return
|
||||
if (!dictionary([string, unknown]).test(mutConfigValue)) return
|
||||
for (const key in spec) {
|
||||
const specValue = spec[key]
|
||||
|
||||
const newConfigValue = mutConfigValue[key]
|
||||
if (matchSpec.test(specValue)) {
|
||||
const updateObject = { spec: null }
|
||||
await updateConfig(effects, { spec: specValue.spec }, updateObject)
|
||||
mutConfigValue[key] = updateObject.spec
|
||||
}
|
||||
if (
|
||||
matchVariants.test(specValue) &&
|
||||
object({ tag: object({ id: string }) }).test(newConfigValue) &&
|
||||
newConfigValue.tag.id in specValue.variants
|
||||
) {
|
||||
// Not going to do anything on the variants...
|
||||
}
|
||||
if (!matchPointer.test(specValue)) continue
|
||||
if (matchPointerConfig.test(specValue)) {
|
||||
const configValue = (await effects.store.get({
|
||||
packageId: specValue["package-id"],
|
||||
callback() {},
|
||||
path: `${EMBASSY_POINTER_PATH_PREFIX}${specValue.selector}` as any,
|
||||
})) as any
|
||||
mutConfigValue[key] = configValue
|
||||
}
|
||||
if (matchPointerPackage.test(specValue)) {
|
||||
mutConfigValue[key] = await effects.embassyGetInterface({
|
||||
target: specValue.target,
|
||||
packageId: specValue["package-id"],
|
||||
interface: specValue["interface"],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
object,
|
||||
literal,
|
||||
string,
|
||||
array,
|
||||
boolean,
|
||||
dictionary,
|
||||
literals,
|
||||
number,
|
||||
unknown,
|
||||
some,
|
||||
every,
|
||||
} from "ts-matches"
|
||||
import { matchVolume } from "./matchVolume"
|
||||
import { matchDockerProcedure } from "../../../Models/DockerProcedure"
|
||||
|
||||
const matchJsProcedure = object(
|
||||
{
|
||||
type: literal("script"),
|
||||
args: array(unknown),
|
||||
},
|
||||
["args"],
|
||||
{
|
||||
args: [],
|
||||
},
|
||||
)
|
||||
|
||||
const matchProcedure = some(matchDockerProcedure, matchJsProcedure)
|
||||
export type Procedure = typeof matchProcedure._TYPE
|
||||
|
||||
const matchAction = object(
|
||||
{
|
||||
name: string,
|
||||
description: string,
|
||||
warning: string,
|
||||
implementation: matchProcedure,
|
||||
"allowed-statuses": array(literals("running", "stopped")),
|
||||
"input-spec": unknown,
|
||||
},
|
||||
["warning", "input-spec", "input-spec"],
|
||||
)
|
||||
export const matchManifest = object(
|
||||
{
|
||||
id: string,
|
||||
version: string,
|
||||
main: matchDockerProcedure,
|
||||
assets: object(
|
||||
{
|
||||
assets: string,
|
||||
scripts: string,
|
||||
},
|
||||
["assets", "scripts"],
|
||||
),
|
||||
"health-checks": dictionary([
|
||||
string,
|
||||
every(
|
||||
matchProcedure,
|
||||
object({
|
||||
name: string,
|
||||
}),
|
||||
),
|
||||
]),
|
||||
config: object({
|
||||
get: matchProcedure,
|
||||
set: matchProcedure,
|
||||
}),
|
||||
properties: matchProcedure,
|
||||
volumes: dictionary([string, matchVolume]),
|
||||
interfaces: dictionary([
|
||||
string,
|
||||
object({
|
||||
name: string,
|
||||
"tor-config": object({}),
|
||||
"lan-config": object({}),
|
||||
ui: boolean,
|
||||
protocols: array(string),
|
||||
}),
|
||||
]),
|
||||
backup: object({
|
||||
create: matchProcedure,
|
||||
restore: matchProcedure,
|
||||
}),
|
||||
migrations: object({
|
||||
to: dictionary([string, matchProcedure]),
|
||||
from: dictionary([string, matchProcedure]),
|
||||
}),
|
||||
dependencies: dictionary([
|
||||
string,
|
||||
object(
|
||||
{
|
||||
version: string,
|
||||
requirement: some(
|
||||
object({
|
||||
type: literal("opt-in"),
|
||||
how: string,
|
||||
}),
|
||||
object({
|
||||
type: literal("opt-out"),
|
||||
how: string,
|
||||
}),
|
||||
object({
|
||||
type: literal("required"),
|
||||
}),
|
||||
),
|
||||
description: string,
|
||||
config: object({
|
||||
check: matchProcedure,
|
||||
"auto-configure": matchProcedure,
|
||||
}),
|
||||
},
|
||||
["description", "config"],
|
||||
),
|
||||
]),
|
||||
|
||||
actions: dictionary([string, matchAction]),
|
||||
},
|
||||
["config", "actions", "properties", "migrations", "dependencies"],
|
||||
)
|
||||
export type Manifest = typeof matchManifest._TYPE
|
||||
@@ -0,0 +1,35 @@
|
||||
import { object, literal, string, boolean, some } from "ts-matches"
|
||||
|
||||
const matchDataVolume = object(
|
||||
{
|
||||
type: literal("data"),
|
||||
readonly: boolean,
|
||||
},
|
||||
["readonly"],
|
||||
)
|
||||
const matchAssetVolume = object({
|
||||
type: literal("assets"),
|
||||
})
|
||||
const matchPointerVolume = object({
|
||||
type: literal("pointer"),
|
||||
"package-id": string,
|
||||
"volume-id": string,
|
||||
path: string,
|
||||
readonly: boolean,
|
||||
})
|
||||
const matchCertificateVolume = object({
|
||||
type: literal("certificate"),
|
||||
"interface-id": string,
|
||||
})
|
||||
const matchBackupVolume = object({
|
||||
type: literal("backup"),
|
||||
readonly: boolean,
|
||||
})
|
||||
export const matchVolume = some(
|
||||
matchDataVolume,
|
||||
matchAssetVolume,
|
||||
matchPointerVolume,
|
||||
matchCertificateVolume,
|
||||
matchBackupVolume,
|
||||
)
|
||||
export type Volume = typeof matchVolume._TYPE
|
||||
@@ -0,0 +1,482 @@
|
||||
// deno-lint-ignore no-namespace
|
||||
export type ExpectedExports = {
|
||||
version: 2
|
||||
/** Set configuration is called after we have modified and saved the configuration in the embassy ui. Use this to make a file for the docker to read from for configuration. */
|
||||
setConfig: (effects: Effects, input: Config) => Promise<ResultType<SetResult>>
|
||||
/** Get configuration returns a shape that describes the format that the embassy ui will generate, and later send to the set config */
|
||||
getConfig: (effects: Effects) => Promise<ResultType<ConfigRes>>
|
||||
/** These are how we make sure the our dependency configurations are valid and if not how to fix them. */
|
||||
dependencies: Dependencies
|
||||
/** For backing up service data though the embassyOS UI */
|
||||
createBackup: (effects: Effects) => Promise<ResultType<unknown>>
|
||||
/** For restoring service data that was previously backed up using the embassyOS UI create backup flow. Backup restores are also triggered via the embassyOS UI, or doing a system restore flow during setup. */
|
||||
restoreBackup: (effects: Effects) => Promise<ResultType<unknown>>
|
||||
/** Properties are used to get values from the docker, like a username + password, what ports we are hosting from */
|
||||
properties: (effects: Effects) => Promise<ResultType<Properties>>
|
||||
health: {
|
||||
/** Should be the health check id */
|
||||
[id: string]: (
|
||||
effects: Effects,
|
||||
dateMs: number,
|
||||
) => Promise<ResultType<unknown>>
|
||||
}
|
||||
migration: (
|
||||
effects: Effects,
|
||||
version: string,
|
||||
...args: unknown[]
|
||||
) => Promise<ResultType<MigrationRes>>
|
||||
action: {
|
||||
[id: string]: (
|
||||
effects: Effects,
|
||||
config?: Config,
|
||||
) => Promise<ResultType<ActionResult>>
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the entrypoint for the main container. Used to start up something like the service that the
|
||||
* package represents, like running a bitcoind in a bitcoind-wrapper.
|
||||
*/
|
||||
main: (effects: Effects) => Promise<ResultType<unknown>>
|
||||
}
|
||||
|
||||
/** Used to reach out from the pure js runtime */
|
||||
export type Effects = {
|
||||
/** Usable when not sandboxed */
|
||||
writeFile(input: {
|
||||
path: string
|
||||
volumeId: string
|
||||
toWrite: string
|
||||
}): Promise<void>
|
||||
readFile(input: { volumeId: string; path: string }): Promise<string>
|
||||
metadata(input: { volumeId: string; path: string }): Promise<Metadata>
|
||||
/** Create a directory. Usable when not sandboxed */
|
||||
createDir(input: { volumeId: string; path: string }): Promise<string>
|
||||
|
||||
readDir(input: { volumeId: string; path: string }): Promise<string[]>
|
||||
/** Remove a directory. Usable when not sandboxed */
|
||||
removeDir(input: { volumeId: string; path: string }): Promise<string>
|
||||
removeFile(input: { volumeId: string; path: string }): Promise<void>
|
||||
|
||||
/** Write a json file into an object. Usable when not sandboxed */
|
||||
writeJsonFile(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
toWrite: Record<string, unknown>
|
||||
}): Promise<void>
|
||||
|
||||
/** Read a json file into an object */
|
||||
readJsonFile(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<Record<string, unknown>>
|
||||
|
||||
runCommand(input: {
|
||||
command: string
|
||||
args?: string[]
|
||||
timeoutMillis?: number
|
||||
}): Promise<ResultType<string>>
|
||||
runDaemon(input: { command: string; args?: string[] }): {
|
||||
wait(): Promise<ResultType<string>>
|
||||
term(): Promise<void>
|
||||
}
|
||||
|
||||
chown(input: { volumeId: string; path: string; uid: string }): Promise<null>
|
||||
chmod(input: { volumeId: string; path: string; mode: string }): Promise<null>
|
||||
|
||||
sleep(timeMs: number): Promise<null>
|
||||
|
||||
/** Log at the trace level */
|
||||
trace(whatToPrint: string): void
|
||||
/** Log at the warn level */
|
||||
warn(whatToPrint: string): void
|
||||
/** Log at the error level */
|
||||
error(whatToPrint: string): void
|
||||
/** Log at the debug level */
|
||||
debug(whatToPrint: string): void
|
||||
/** Log at the info level */
|
||||
info(whatToPrint: string): void
|
||||
|
||||
/** Sandbox mode lets us read but not write */
|
||||
is_sandboxed(): boolean
|
||||
|
||||
exists(input: { volumeId: string; path: string }): Promise<boolean>
|
||||
bindLocal(options: {
|
||||
internalPort: number
|
||||
name: string
|
||||
externalPort: number
|
||||
}): Promise<string>
|
||||
bindTor(options: {
|
||||
internalPort: number
|
||||
name: string
|
||||
externalPort: number
|
||||
}): Promise<string>
|
||||
|
||||
fetch(
|
||||
url: string,
|
||||
options?: {
|
||||
method?: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "PATCH"
|
||||
headers?: Record<string, string>
|
||||
body?: string
|
||||
},
|
||||
): Promise<{
|
||||
method: string
|
||||
ok: boolean
|
||||
status: number
|
||||
headers: Record<string, string>
|
||||
body?: string | null
|
||||
/// Returns the body as a string
|
||||
text(): Promise<string>
|
||||
/// Returns the body as a json
|
||||
json(): Promise<unknown>
|
||||
}>
|
||||
|
||||
runRsync(options: {
|
||||
srcVolume: string
|
||||
dstVolume: string
|
||||
srcPath: string
|
||||
dstPath: string
|
||||
// rsync options: https://linux.die.net/man/1/rsync
|
||||
options: BackupOptions
|
||||
}): {
|
||||
id: () => Promise<string>
|
||||
wait: () => Promise<null>
|
||||
progress: () => Promise<number>
|
||||
}
|
||||
}
|
||||
|
||||
// rsync options: https://linux.die.net/man/1/rsync
|
||||
export type BackupOptions = {
|
||||
delete: boolean
|
||||
force: boolean
|
||||
ignoreExisting: boolean
|
||||
exclude: string[]
|
||||
}
|
||||
export type Metadata = {
|
||||
fileType: string
|
||||
isDir: boolean
|
||||
isFile: boolean
|
||||
isSymlink: boolean
|
||||
len: number
|
||||
modified?: Date
|
||||
accessed?: Date
|
||||
created?: Date
|
||||
readonly: boolean
|
||||
uid: number
|
||||
gid: number
|
||||
mode: number
|
||||
}
|
||||
|
||||
export type MigrationRes = {
|
||||
configured: boolean
|
||||
}
|
||||
|
||||
export type ActionResult = {
|
||||
version: "0"
|
||||
message: string
|
||||
value?: string
|
||||
copyable: boolean
|
||||
qr: boolean
|
||||
}
|
||||
|
||||
export type ConfigRes = {
|
||||
/** This should be the previous config, that way during set config we start with the previous */
|
||||
config?: Config
|
||||
/** Shape that is describing the form in the ui */
|
||||
spec: ConfigSpec
|
||||
}
|
||||
export type Config = {
|
||||
[propertyName: string]: unknown
|
||||
}
|
||||
|
||||
export type ConfigSpec = {
|
||||
/** Given a config value, define what it should render with the following spec */
|
||||
[configValue: string]: ValueSpecAny
|
||||
}
|
||||
export type WithDefault<T, Default> = T & {
|
||||
default: Default
|
||||
}
|
||||
export type WithNullableDefault<T, Default> = T & {
|
||||
default?: Default
|
||||
}
|
||||
|
||||
export type WithDescription<T> = T & {
|
||||
description?: string
|
||||
name: string
|
||||
warning?: string
|
||||
}
|
||||
|
||||
export type WithOptionalDescription<T> = T & {
|
||||
/** @deprecated - optional only for backwards compatibility */
|
||||
description?: string
|
||||
/** @deprecated - optional only for backwards compatibility */
|
||||
name?: string
|
||||
warning?: string
|
||||
}
|
||||
|
||||
export type ListSpec<T> = {
|
||||
spec: T
|
||||
range: string
|
||||
}
|
||||
|
||||
export type Tag<T extends string, V> = V & {
|
||||
type: T
|
||||
}
|
||||
|
||||
export type Subtype<T extends string, V> = V & {
|
||||
subtype: T
|
||||
}
|
||||
|
||||
export type Target<T extends string, V> = V & {
|
||||
target: T
|
||||
}
|
||||
|
||||
export type UniqueBy =
|
||||
| {
|
||||
any: UniqueBy[]
|
||||
}
|
||||
| string
|
||||
| null
|
||||
|
||||
export type WithNullable<T> = T & {
|
||||
nullable: boolean
|
||||
}
|
||||
export type DefaultString =
|
||||
| string
|
||||
| {
|
||||
/** The chars available for the random generation */
|
||||
charset?: string
|
||||
/** Length that we generate to */
|
||||
len: number
|
||||
}
|
||||
|
||||
export type ValueSpecString = // deno-lint-ignore ban-types
|
||||
(
|
||||
| {}
|
||||
| {
|
||||
pattern: string
|
||||
"pattern-description": string
|
||||
}
|
||||
) & {
|
||||
copyable?: boolean
|
||||
masked?: boolean
|
||||
placeholder?: string
|
||||
}
|
||||
export type ValueSpecNumber = {
|
||||
/** Something like [3,6] or [0, *) */
|
||||
range?: string
|
||||
integral?: boolean
|
||||
/** Used a description of the units */
|
||||
units?: string
|
||||
placeholder?: number
|
||||
}
|
||||
export type ValueSpecBoolean = Record<string, unknown>
|
||||
export type ValueSpecAny =
|
||||
| Tag<"boolean", WithDescription<WithDefault<ValueSpecBoolean, boolean>>>
|
||||
| Tag<
|
||||
"string",
|
||||
WithDescription<
|
||||
WithNullableDefault<WithNullable<ValueSpecString>, DefaultString>
|
||||
>
|
||||
>
|
||||
| Tag<
|
||||
"number",
|
||||
WithDescription<
|
||||
WithNullableDefault<WithNullable<ValueSpecNumber>, number>
|
||||
>
|
||||
>
|
||||
| Tag<
|
||||
"enum",
|
||||
WithDescription<
|
||||
WithDefault<
|
||||
{
|
||||
values: readonly string[] | string[]
|
||||
"value-names": {
|
||||
[key: string]: string
|
||||
}
|
||||
},
|
||||
string
|
||||
>
|
||||
>
|
||||
>
|
||||
| Tag<"list", ValueSpecList>
|
||||
| Tag<"object", WithDescription<WithNullableDefault<ValueSpecObject, Config>>>
|
||||
| Tag<"union", WithOptionalDescription<WithDefault<ValueSpecUnion, string>>>
|
||||
| Tag<
|
||||
"pointer",
|
||||
WithDescription<
|
||||
| Subtype<
|
||||
"package",
|
||||
| Target<
|
||||
"tor-key",
|
||||
{
|
||||
"package-id": string
|
||||
interface: string
|
||||
}
|
||||
>
|
||||
| Target<
|
||||
"tor-address",
|
||||
{
|
||||
"package-id": string
|
||||
interface: string
|
||||
}
|
||||
>
|
||||
| Target<
|
||||
"lan-address",
|
||||
{
|
||||
"package-id": string
|
||||
interface: string
|
||||
}
|
||||
>
|
||||
| Target<
|
||||
"config",
|
||||
{
|
||||
"package-id": string
|
||||
selector: string
|
||||
multi: boolean
|
||||
}
|
||||
>
|
||||
>
|
||||
| Subtype<"system", Record<string, unknown>>
|
||||
>
|
||||
>
|
||||
export type ValueSpecUnion = {
|
||||
/** What tag for the specification, for tag unions */
|
||||
tag: {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
"variant-names": {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
/** The possible enum values */
|
||||
variants: {
|
||||
[key: string]: ConfigSpec
|
||||
}
|
||||
"display-as"?: string
|
||||
"unique-by"?: UniqueBy
|
||||
}
|
||||
export type ValueSpecObject = {
|
||||
spec: ConfigSpec
|
||||
"display-as"?: string
|
||||
"unique-by"?: UniqueBy
|
||||
}
|
||||
export type ValueSpecList =
|
||||
| Subtype<
|
||||
"boolean",
|
||||
WithDescription<WithDefault<ListSpec<ValueSpecBoolean>, boolean[]>>
|
||||
>
|
||||
| Subtype<
|
||||
"string",
|
||||
WithDescription<WithDefault<ListSpec<ValueSpecString>, string[]>>
|
||||
>
|
||||
| Subtype<
|
||||
"number",
|
||||
WithDescription<WithDefault<ListSpec<ValueSpecNumber>, number[]>>
|
||||
>
|
||||
| Subtype<
|
||||
"enum",
|
||||
WithDescription<WithDefault<ListSpec<ValueSpecEnum>, string[]>>
|
||||
>
|
||||
| Subtype<
|
||||
"object",
|
||||
WithDescription<
|
||||
WithNullableDefault<
|
||||
ListSpec<ValueSpecObject>,
|
||||
Record<string, unknown>[]
|
||||
>
|
||||
>
|
||||
>
|
||||
| Subtype<
|
||||
"union",
|
||||
WithDescription<WithDefault<ListSpec<ValueSpecUnion>, string[]>>
|
||||
>
|
||||
export type ValueSpecEnum = {
|
||||
values: string[]
|
||||
"value-names": { [key: string]: string }
|
||||
}
|
||||
|
||||
export type SetResult = {
|
||||
/** These are the unix process signals */
|
||||
signal:
|
||||
| "SIGTERM"
|
||||
| "SIGHUP"
|
||||
| "SIGINT"
|
||||
| "SIGQUIT"
|
||||
| "SIGILL"
|
||||
| "SIGTRAP"
|
||||
| "SIGABRT"
|
||||
| "SIGBUS"
|
||||
| "SIGFPE"
|
||||
| "SIGKILL"
|
||||
| "SIGUSR1"
|
||||
| "SIGSEGV"
|
||||
| "SIGUSR2"
|
||||
| "SIGPIPE"
|
||||
| "SIGALRM"
|
||||
| "SIGSTKFLT"
|
||||
| "SIGCHLD"
|
||||
| "SIGCONT"
|
||||
| "SIGSTOP"
|
||||
| "SIGTSTP"
|
||||
| "SIGTTIN"
|
||||
| "SIGTTOU"
|
||||
| "SIGURG"
|
||||
| "SIGXCPU"
|
||||
| "SIGXFSZ"
|
||||
| "SIGVTALRM"
|
||||
| "SIGPROF"
|
||||
| "SIGWINCH"
|
||||
| "SIGIO"
|
||||
| "SIGPWR"
|
||||
| "SIGSYS"
|
||||
| "SIGEMT"
|
||||
| "SIGINFO"
|
||||
"depends-on": DependsOn
|
||||
}
|
||||
|
||||
export type DependsOn = {
|
||||
[packageId: string]: string[]
|
||||
}
|
||||
|
||||
export type KnownError =
|
||||
| { error: string }
|
||||
| {
|
||||
"error-code": [number, string] | readonly [number, string]
|
||||
}
|
||||
export type ResultType<T> = KnownError | { result: T }
|
||||
|
||||
export type PackagePropertiesV2 = {
|
||||
[name: string]: PackagePropertyObject | PackagePropertyString
|
||||
}
|
||||
export type PackagePropertyString = {
|
||||
type: "string"
|
||||
description?: string
|
||||
value: string
|
||||
/** Let's the ui make this copyable button */
|
||||
copyable?: boolean
|
||||
/** Let the ui create a qr for this field */
|
||||
qr?: boolean
|
||||
/** Hiding the value unless toggled off for field */
|
||||
masked?: boolean
|
||||
}
|
||||
export type PackagePropertyObject = {
|
||||
value: PackagePropertiesV2
|
||||
type: "object"
|
||||
description: string
|
||||
}
|
||||
|
||||
export type Properties = {
|
||||
version: 2
|
||||
data: PackagePropertiesV2
|
||||
}
|
||||
|
||||
export type Dependencies = {
|
||||
/** Id is the id of the package, should be the same as the manifest */
|
||||
[id: string]: {
|
||||
/** Checks are called to make sure that our dependency is in the correct shape. If a known error is returned we know that the dependency needs modification */
|
||||
check(effects: Effects, input: Config): Promise<ResultType<void | null>>
|
||||
/** This is called after we know that the dependency package needs a new configuration, this would be a transform for defaults */
|
||||
autoConfigure(effects: Effects, input: Config): Promise<ResultType<Config>>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as oet from "./oldEmbassyTypes"
|
||||
import { Volume } from "../../../Models/Volume"
|
||||
import * as child_process from "child_process"
|
||||
import { promisify } from "util"
|
||||
import { util, Utils } from "@start9labs/start-sdk"
|
||||
import { Manifest } from "./matchManifest"
|
||||
import { HostSystemStartOs } from "../../HostSystemStartOs"
|
||||
import "isomorphic-fetch"
|
||||
|
||||
const { createUtils } = util
|
||||
|
||||
const execFile = promisify(child_process.execFile)
|
||||
|
||||
export class PolyfillEffects implements oet.Effects {
|
||||
private utils: Utils<any, any>
|
||||
constructor(
|
||||
readonly effects: HostSystemStartOs,
|
||||
private manifest: Manifest,
|
||||
) {
|
||||
this.utils = createUtils(effects as any)
|
||||
}
|
||||
async writeFile(input: {
|
||||
path: string
|
||||
volumeId: string
|
||||
toWrite: string
|
||||
}): Promise<void> {
|
||||
await fs.writeFile(
|
||||
new Volume(input.volumeId, input.path).path,
|
||||
input.toWrite,
|
||||
)
|
||||
}
|
||||
async readFile(input: { volumeId: string; path: string }): Promise<string> {
|
||||
return (
|
||||
await fs.readFile(new Volume(input.volumeId, input.path).path)
|
||||
).toString()
|
||||
}
|
||||
async metadata(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<oet.Metadata> {
|
||||
const stats = await fs.stat(new Volume(input.volumeId, input.path).path)
|
||||
return {
|
||||
fileType: stats.isFile() ? "file" : "directory",
|
||||
gid: stats.gid,
|
||||
uid: stats.uid,
|
||||
mode: stats.mode,
|
||||
isDir: stats.isDirectory(),
|
||||
isFile: stats.isFile(),
|
||||
isSymlink: stats.isSymbolicLink(),
|
||||
len: stats.size,
|
||||
readonly: (stats.mode & 0o200) > 0,
|
||||
}
|
||||
}
|
||||
async createDir(input: { volumeId: string; path: string }): Promise<string> {
|
||||
const path = new Volume(input.volumeId, input.path).path
|
||||
await fs.mkdir(path, { recursive: true })
|
||||
return path
|
||||
}
|
||||
async readDir(input: { volumeId: string; path: string }): Promise<string[]> {
|
||||
return fs.readdir(new Volume(input.volumeId, input.path).path)
|
||||
}
|
||||
async removeDir(input: { volumeId: string; path: string }): Promise<string> {
|
||||
const path = new Volume(input.volumeId, input.path).path
|
||||
await fs.rmdir(new Volume(input.volumeId, input.path).path, {
|
||||
recursive: true,
|
||||
})
|
||||
return path
|
||||
}
|
||||
removeFile(input: { volumeId: string; path: string }): Promise<void> {
|
||||
return fs.rm(new Volume(input.volumeId, input.path).path)
|
||||
}
|
||||
async writeJsonFile(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
toWrite: Record<string, unknown>
|
||||
}): Promise<void> {
|
||||
await fs.writeFile(
|
||||
new Volume(input.volumeId, input.path).path,
|
||||
JSON.stringify(input.toWrite),
|
||||
)
|
||||
}
|
||||
async readJsonFile(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<Record<string, unknown>> {
|
||||
return JSON.parse(
|
||||
(
|
||||
await fs.readFile(new Volume(input.volumeId, input.path).path)
|
||||
).toString(),
|
||||
)
|
||||
}
|
||||
runCommand({
|
||||
command,
|
||||
args,
|
||||
timeoutMillis,
|
||||
}: {
|
||||
command: string
|
||||
args?: string[] | undefined
|
||||
timeoutMillis?: number | undefined
|
||||
}): Promise<oet.ResultType<string>> {
|
||||
return this.utils
|
||||
.runCommand(this.manifest.main.image, [command, ...(args || [])], {})
|
||||
.then((x) => ({
|
||||
stderr: x.stderr.toString(),
|
||||
stdout: x.stdout.toString(),
|
||||
}))
|
||||
.then((x) => (!!x.stderr ? { error: x.stderr } : { result: x.stdout }))
|
||||
}
|
||||
runDaemon(input: { command: string; args?: string[] | undefined }): {
|
||||
wait(): Promise<oet.ResultType<string>>
|
||||
term(): Promise<void>
|
||||
} {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
chown(input: { volumeId: string; path: string; uid: string }): Promise<null> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
chmod(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
mode: string
|
||||
}): Promise<null> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
sleep(timeMs: number): Promise<null> {
|
||||
return new Promise((resolve) => setTimeout(resolve, timeMs))
|
||||
}
|
||||
trace(whatToPrint: string): void {
|
||||
console.trace(whatToPrint)
|
||||
}
|
||||
warn(whatToPrint: string): void {
|
||||
console.warn(whatToPrint)
|
||||
}
|
||||
error(whatToPrint: string): void {
|
||||
console.error(whatToPrint)
|
||||
}
|
||||
debug(whatToPrint: string): void {
|
||||
console.debug(whatToPrint)
|
||||
}
|
||||
info(whatToPrint: string): void {
|
||||
console.log(false)
|
||||
}
|
||||
is_sandboxed(): boolean {
|
||||
return false
|
||||
}
|
||||
exists(input: { volumeId: string; path: string }): Promise<boolean> {
|
||||
return this.metadata(input)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
}
|
||||
bindLocal(options: {
|
||||
internalPort: number
|
||||
name: string
|
||||
externalPort: number
|
||||
}): Promise<string> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
bindTor(options: {
|
||||
internalPort: number
|
||||
name: string
|
||||
externalPort: number
|
||||
}): Promise<string> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
async fetch(
|
||||
url: string,
|
||||
options?:
|
||||
| {
|
||||
method?:
|
||||
| "GET"
|
||||
| "POST"
|
||||
| "PUT"
|
||||
| "DELETE"
|
||||
| "HEAD"
|
||||
| "PATCH"
|
||||
| undefined
|
||||
headers?: Record<string, string> | undefined
|
||||
body?: string | undefined
|
||||
}
|
||||
| undefined,
|
||||
): Promise<{
|
||||
method: string
|
||||
ok: boolean
|
||||
status: number
|
||||
headers: Record<string, string>
|
||||
body?: string | null | undefined
|
||||
text(): Promise<string>
|
||||
json(): Promise<unknown>
|
||||
}> {
|
||||
const fetched = await fetch(url, options)
|
||||
return {
|
||||
method: fetched.type,
|
||||
ok: fetched.ok,
|
||||
status: fetched.status,
|
||||
headers: Object.fromEntries(fetched.headers.entries()),
|
||||
body: await fetched.text(),
|
||||
text: () => fetched.text(),
|
||||
json: () => fetched.json(),
|
||||
}
|
||||
}
|
||||
runRsync(options: {
|
||||
srcVolume: string
|
||||
dstVolume: string
|
||||
srcPath: string
|
||||
dstPath: string
|
||||
options: oet.BackupOptions
|
||||
}): {
|
||||
id: () => Promise<string>
|
||||
wait: () => Promise<null>
|
||||
progress: () => Promise<number>
|
||||
} {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
150
container-runtime/src/Adapters/Systems/SystemForStartOs.ts
Normal file
150
container-runtime/src/Adapters/Systems/SystemForStartOs.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { ExecuteResult, System } from "../../Interfaces/System"
|
||||
import { unNestPath } from "../../Models/JsonPath"
|
||||
import { string } from "ts-matches"
|
||||
import { HostSystemStartOs } from "../HostSystemStartOs"
|
||||
import { Effects } from "../../Models/Effects"
|
||||
const LOCATION = "/usr/lib/startos/package/startos"
|
||||
export class SystemForStartOs implements System {
|
||||
private onTerm: (() => Promise<void>) | undefined
|
||||
static of() {
|
||||
return new SystemForStartOs()
|
||||
}
|
||||
constructor() {}
|
||||
async execute(
|
||||
effects: HostSystemStartOs,
|
||||
options: {
|
||||
procedure:
|
||||
| "/init"
|
||||
| "/uninit"
|
||||
| "/main/start"
|
||||
| "/main/stop"
|
||||
| "/config/set"
|
||||
| "/config/get"
|
||||
| "/backup/create"
|
||||
| "/backup/restore"
|
||||
| "/actions/metadata"
|
||||
| `/actions/${string}/get`
|
||||
| `/actions/${string}/run`
|
||||
| `/dependencies/${string}/query`
|
||||
| `/dependencies/${string}/update`
|
||||
input: unknown
|
||||
timeout?: number | undefined
|
||||
},
|
||||
): Promise<ExecuteResult> {
|
||||
return { ok: await this._execute(effects, options) }
|
||||
}
|
||||
async _execute(
|
||||
effects: Effects,
|
||||
options: {
|
||||
procedure:
|
||||
| "/init"
|
||||
| "/uninit"
|
||||
| "/main/start"
|
||||
| "/main/stop"
|
||||
| "/config/set"
|
||||
| "/config/get"
|
||||
| "/backup/create"
|
||||
| "/backup/restore"
|
||||
| "/actions/metadata"
|
||||
| `/actions/${string}/get`
|
||||
| `/actions/${string}/run`
|
||||
| `/dependencies/${string}/query`
|
||||
| `/dependencies/${string}/update`
|
||||
input: unknown
|
||||
timeout?: number | undefined
|
||||
},
|
||||
): Promise<unknown> {
|
||||
switch (options.procedure) {
|
||||
case "/init": {
|
||||
const path = `${LOCATION}/procedures/init`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
const previousVersion = string.optional().unsafeCast(options)
|
||||
return procedure.init({ effects, previousVersion })
|
||||
}
|
||||
case "/uninit": {
|
||||
const path = `${LOCATION}/procedures/init`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
const nextVersion = string.optional().unsafeCast(options)
|
||||
return procedure.uninit({ effects, nextVersion })
|
||||
}
|
||||
case "/main/start": {
|
||||
const path = `${LOCATION}/procedures/main`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
const started = async (onTerm: () => Promise<void>) => {
|
||||
await effects.setMainStatus({ status: "running" })
|
||||
if (this.onTerm) await this.onTerm()
|
||||
this.onTerm = onTerm
|
||||
}
|
||||
return procedure.main({ effects, started })
|
||||
}
|
||||
case "/main/stop": {
|
||||
await effects.setMainStatus({ status: "stopped" })
|
||||
if (this.onTerm) await this.onTerm()
|
||||
delete this.onTerm
|
||||
return
|
||||
}
|
||||
case "/config/set": {
|
||||
const path = `${LOCATION}/procedures/config`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
const input = options.input
|
||||
return procedure.setConfig({ effects, input })
|
||||
}
|
||||
case "/config/get": {
|
||||
const path = `${LOCATION}/procedures/config`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
return procedure.getConfig({ effects })
|
||||
}
|
||||
case "/backup/create":
|
||||
case "/backup/restore":
|
||||
throw new Error("this should be called with the init/unit")
|
||||
case "/actions/metadata": {
|
||||
const path = `${LOCATION}/procedures/actions`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
return procedure.actionsMetadata({ effects })
|
||||
}
|
||||
default:
|
||||
const procedures = unNestPath(options.procedure)
|
||||
const id = procedures[2]
|
||||
switch (true) {
|
||||
case procedures[1] === "actions" && procedures[3] === "get": {
|
||||
const path = `${LOCATION}/procedures/actions`
|
||||
const action: any = (await import(path).catch(() => require(path)))
|
||||
.actions[id]
|
||||
if (!action) throw new Error(`Action ${id} not found`)
|
||||
return action.get({ effects })
|
||||
}
|
||||
case procedures[1] === "actions" && procedures[3] === "run": {
|
||||
const path = `${LOCATION}/procedures/actions`
|
||||
const action: any = (await import(path).catch(() => require(path)))
|
||||
.actions[id]
|
||||
if (!action) throw new Error(`Action ${id} not found`)
|
||||
const input = options.input
|
||||
return action.run({ effects, input })
|
||||
}
|
||||
case procedures[1] === "dependencies" && procedures[3] === "query": {
|
||||
const path = `${LOCATION}/procedures/dependencies`
|
||||
const dependencyConfig: any = (
|
||||
await import(path).catch(() => require(path))
|
||||
).dependencyConfig[id]
|
||||
if (!dependencyConfig)
|
||||
throw new Error(`dependencyConfig ${id} not found`)
|
||||
const localConfig = options.input
|
||||
return dependencyConfig.query({ effects, localConfig })
|
||||
}
|
||||
case procedures[1] === "dependencies" && procedures[3] === "update": {
|
||||
const path = `${LOCATION}/procedures/dependencies`
|
||||
const dependencyConfig: any = (
|
||||
await import(path).catch(() => require(path))
|
||||
).dependencyConfig[id]
|
||||
if (!dependencyConfig)
|
||||
throw new Error(`dependencyConfig ${id} not found`)
|
||||
return dependencyConfig.update(options.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
exit(effects: Effects): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
6
container-runtime/src/Adapters/Systems/index.ts
Normal file
6
container-runtime/src/Adapters/Systems/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { System } from "../../Interfaces/System"
|
||||
import { SystemForEmbassy } from "./SystemForEmbassy"
|
||||
import { SystemForStartOs } from "./SystemForStartOs"
|
||||
export async function getSystem(): Promise<System> {
|
||||
return SystemForEmbassy.of()
|
||||
}
|
||||
Reference in New Issue
Block a user