mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 14:29:45 +00:00
Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major
This commit is contained in:
12
.github/workflows/startos-iso.yaml
vendored
12
.github/workflows/startos-iso.yaml
vendored
@@ -45,7 +45,7 @@ on:
|
|||||||
- next/*
|
- next/*
|
||||||
|
|
||||||
env:
|
env:
|
||||||
NODEJS_VERSION: "18.15.0"
|
NODEJS_VERSION: "20.16.0"
|
||||||
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
|
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -75,6 +75,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODEJS_VERSION }}
|
node-version: ${{ env.NODEJS_VERSION }}
|
||||||
@@ -148,6 +153,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -92,7 +92,7 @@ format:
|
|||||||
test: | test-core test-sdk test-container-runtime
|
test: | test-core test-sdk test-container-runtime
|
||||||
|
|
||||||
test-core: $(CORE_SRC) $(ENVIRONMENT_FILE)
|
test-core: $(CORE_SRC) $(ENVIRONMENT_FILE)
|
||||||
cd core && cargo build --features=test && cargo test --features=test
|
./core/run-tests.sh
|
||||||
|
|
||||||
test-sdk: $(shell git ls-files sdk) sdk/lib/osBindings
|
test-sdk: $(shell git ls-files sdk) sdk/lib/osBindings
|
||||||
cd sdk && make test
|
cd sdk && make test
|
||||||
@@ -231,7 +231,7 @@ sdk/lib/osBindings: core/startos/bindings
|
|||||||
|
|
||||||
core/startos/bindings: $(shell git ls-files core) $(ENVIRONMENT_FILE)
|
core/startos/bindings: $(shell git ls-files core) $(ENVIRONMENT_FILE)
|
||||||
rm -rf core/startos/bindings
|
rm -rf core/startos/bindings
|
||||||
(cd core/ && cargo test --features=test 'export_bindings_')
|
./core/build-ts.sh
|
||||||
touch core/startos/bindings
|
touch core/startos/bindings
|
||||||
|
|
||||||
sdk/dist: $(shell git ls-files sdk) sdk/lib/osBindings
|
sdk/dist: $(shell git ls-files sdk) sdk/lib/osBindings
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ if [ -z "$ARCH" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
DOCKER_PLATFORM="linux/${ARCH}"
|
DOCKER_PLATFORM="linux/${ARCH}"
|
||||||
if [ "$ARCH" = aarch64 ]; then
|
if [ "$ARCH" = aarch64 ] || [ "$ARCH" = arm64 ]; then
|
||||||
DOCKER_PLATFORM="linux/arm64"
|
DOCKER_PLATFORM="linux/arm64"
|
||||||
elif [ "$ARCH" = x86_64 ]; then
|
elif [ "$ARCH" = x86_64 ]; then
|
||||||
DOCKER_PLATFORM="linux/amd64"
|
DOCKER_PLATFORM="linux/amd64"
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ smartmontools
|
|||||||
socat
|
socat
|
||||||
sqlite3
|
sqlite3
|
||||||
squashfs-tools
|
squashfs-tools
|
||||||
|
squashfs-tools-ng
|
||||||
sudo
|
sudo
|
||||||
systemd
|
systemd
|
||||||
systemd-resolved
|
systemd-resolved
|
||||||
|
|||||||
12
container-runtime/package-lock.json
generated
12
container-runtime/package-lock.json
generated
@@ -20,6 +20,7 @@
|
|||||||
"node-fetch": "^3.1.0",
|
"node-fetch": "^3.1.0",
|
||||||
"ts-matches": "^5.5.1",
|
"ts-matches": "^5.5.1",
|
||||||
"tslib": "^2.5.3",
|
"tslib": "^2.5.3",
|
||||||
|
"tslog": "^4.9.3",
|
||||||
"typescript": "^5.1.3",
|
"typescript": "^5.1.3",
|
||||||
"yaml": "^2.3.1"
|
"yaml": "^2.3.1"
|
||||||
},
|
},
|
||||||
@@ -5527,6 +5528,17 @@
|
|||||||
"version": "2.6.3",
|
"version": "2.6.3",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
|
"node_modules/tslog": {
|
||||||
|
"version": "4.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslog/-/tslog-4.9.3.tgz",
|
||||||
|
"integrity": "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fullstack-build/tslog?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { types as T } from "@start9labs/start-sdk"
|
import { types as T, utils } from "@start9labs/start-sdk"
|
||||||
import * as net from "net"
|
import * as net from "net"
|
||||||
import { object, string, number, literals, some, unknown } from "ts-matches"
|
import { object, string, number, literals, some, unknown } from "ts-matches"
|
||||||
import { Effects } from "../Models/Effects"
|
import { Effects } from "../Models/Effects"
|
||||||
@@ -40,7 +40,7 @@ export type EffectContext = {
|
|||||||
|
|
||||||
const rpcRoundFor =
|
const rpcRoundFor =
|
||||||
(procedureId: string | null) =>
|
(procedureId: string | null) =>
|
||||||
<K extends keyof Effects | "getStore" | "setStore" | "clearCallbacks">(
|
<K extends T.EffectMethod | "clearCallbacks">(
|
||||||
method: K,
|
method: K,
|
||||||
params: Record<string, unknown>,
|
params: Record<string, unknown>,
|
||||||
) => {
|
) => {
|
||||||
@@ -65,7 +65,10 @@ const rpcRoundFor =
|
|||||||
)
|
)
|
||||||
if (testRpcError(res)) {
|
if (testRpcError(res)) {
|
||||||
let message = res.error.message
|
let message = res.error.message
|
||||||
console.error("Error in host RPC:", { method, params })
|
console.error(
|
||||||
|
"Error in host RPC:",
|
||||||
|
utils.asError({ method, params }),
|
||||||
|
)
|
||||||
if (string.test(res.error.data)) {
|
if (string.test(res.error.data)) {
|
||||||
message += ": " + res.error.data
|
message += ": " + res.error.data
|
||||||
console.error(`Details: ${res.error.data}`)
|
console.error(`Details: ${res.error.data}`)
|
||||||
@@ -107,65 +110,65 @@ function makeEffects(context: EffectContext): Effects {
|
|||||||
}) as ReturnType<T.Effects["bind"]>
|
}) as ReturnType<T.Effects["bind"]>
|
||||||
},
|
},
|
||||||
clearBindings(...[]: Parameters<T.Effects["clearBindings"]>) {
|
clearBindings(...[]: Parameters<T.Effects["clearBindings"]>) {
|
||||||
return rpcRound("clearBindings", {}) as ReturnType<
|
return rpcRound("clear-bindings", {}) as ReturnType<
|
||||||
T.Effects["clearBindings"]
|
T.Effects["clearBindings"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
clearServiceInterfaces(
|
clearServiceInterfaces(
|
||||||
...[]: Parameters<T.Effects["clearServiceInterfaces"]>
|
...[]: Parameters<T.Effects["clearServiceInterfaces"]>
|
||||||
) {
|
) {
|
||||||
return rpcRound("clearServiceInterfaces", {}) as ReturnType<
|
return rpcRound("clear-service-interfaces", {}) as ReturnType<
|
||||||
T.Effects["clearServiceInterfaces"]
|
T.Effects["clearServiceInterfaces"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
getInstalledPackages(...[]: Parameters<T.Effects["getInstalledPackages"]>) {
|
getInstalledPackages(...[]: Parameters<T.Effects["getInstalledPackages"]>) {
|
||||||
return rpcRound("getInstalledPackages", {}) as ReturnType<
|
return rpcRound("get-installed-packages", {}) as ReturnType<
|
||||||
T.Effects["getInstalledPackages"]
|
T.Effects["getInstalledPackages"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
createOverlayedImage(options: {
|
subcontainer: {
|
||||||
imageId: string
|
createFs(options: { imageId: string }) {
|
||||||
}): Promise<[string, string]> {
|
return rpcRound("subcontainer.create-fs", options) as ReturnType<
|
||||||
return rpcRound("createOverlayedImage", options) as ReturnType<
|
T.Effects["subcontainer"]["createFs"]
|
||||||
T.Effects["createOverlayedImage"]
|
>
|
||||||
>
|
},
|
||||||
},
|
destroyFs(options: { guid: string }): Promise<void> {
|
||||||
destroyOverlayedImage(options: { guid: string }): Promise<void> {
|
return rpcRound("subcontainer.destroy-fs", options) as ReturnType<
|
||||||
return rpcRound("destroyOverlayedImage", options) as ReturnType<
|
T.Effects["subcontainer"]["destroyFs"]
|
||||||
T.Effects["destroyOverlayedImage"]
|
>
|
||||||
>
|
},
|
||||||
},
|
},
|
||||||
executeAction(...[options]: Parameters<T.Effects["executeAction"]>) {
|
executeAction(...[options]: Parameters<T.Effects["executeAction"]>) {
|
||||||
return rpcRound("executeAction", options) as ReturnType<
|
return rpcRound("execute-action", options) as ReturnType<
|
||||||
T.Effects["executeAction"]
|
T.Effects["executeAction"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
exportAction(...[options]: Parameters<T.Effects["exportAction"]>) {
|
exportAction(...[options]: Parameters<T.Effects["exportAction"]>) {
|
||||||
return rpcRound("exportAction", options) as ReturnType<
|
return rpcRound("export-action", options) as ReturnType<
|
||||||
T.Effects["exportAction"]
|
T.Effects["exportAction"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
exportServiceInterface: ((
|
exportServiceInterface: ((
|
||||||
...[options]: Parameters<Effects["exportServiceInterface"]>
|
...[options]: Parameters<Effects["exportServiceInterface"]>
|
||||||
) => {
|
) => {
|
||||||
return rpcRound("exportServiceInterface", options) as ReturnType<
|
return rpcRound("export-service-interface", options) as ReturnType<
|
||||||
T.Effects["exportServiceInterface"]
|
T.Effects["exportServiceInterface"]
|
||||||
>
|
>
|
||||||
}) as Effects["exportServiceInterface"],
|
}) as Effects["exportServiceInterface"],
|
||||||
exposeForDependents(
|
exposeForDependents(
|
||||||
...[options]: Parameters<T.Effects["exposeForDependents"]>
|
...[options]: Parameters<T.Effects["exposeForDependents"]>
|
||||||
) {
|
) {
|
||||||
return rpcRound("exposeForDependents", options) as ReturnType<
|
return rpcRound("expose-for-dependents", options) as ReturnType<
|
||||||
T.Effects["exposeForDependents"]
|
T.Effects["exposeForDependents"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
getConfigured(...[]: Parameters<T.Effects["getConfigured"]>) {
|
getConfigured(...[]: Parameters<T.Effects["getConfigured"]>) {
|
||||||
return rpcRound("getConfigured", {}) as ReturnType<
|
return rpcRound("get-configured", {}) as ReturnType<
|
||||||
T.Effects["getConfigured"]
|
T.Effects["getConfigured"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
getContainerIp(...[]: Parameters<T.Effects["getContainerIp"]>) {
|
getContainerIp(...[]: Parameters<T.Effects["getContainerIp"]>) {
|
||||||
return rpcRound("getContainerIp", {}) as ReturnType<
|
return rpcRound("get-container-ip", {}) as ReturnType<
|
||||||
T.Effects["getContainerIp"]
|
T.Effects["getContainerIp"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
@@ -174,21 +177,21 @@ function makeEffects(context: EffectContext): Effects {
|
|||||||
...allOptions,
|
...allOptions,
|
||||||
callback: context.callbacks?.addCallback(allOptions.callback) || null,
|
callback: context.callbacks?.addCallback(allOptions.callback) || null,
|
||||||
}
|
}
|
||||||
return rpcRound("getHostInfo", options) as ReturnType<
|
return rpcRound("get-host-info", options) as ReturnType<
|
||||||
T.Effects["getHostInfo"]
|
T.Effects["getHostInfo"]
|
||||||
> as any
|
> as any
|
||||||
}) as Effects["getHostInfo"],
|
}) as Effects["getHostInfo"],
|
||||||
getServiceInterface(
|
getServiceInterface(
|
||||||
...[options]: Parameters<T.Effects["getServiceInterface"]>
|
...[options]: Parameters<T.Effects["getServiceInterface"]>
|
||||||
) {
|
) {
|
||||||
return rpcRound("getServiceInterface", {
|
return rpcRound("get-service-interface", {
|
||||||
...options,
|
...options,
|
||||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||||
}) as ReturnType<T.Effects["getServiceInterface"]>
|
}) as ReturnType<T.Effects["getServiceInterface"]>
|
||||||
},
|
},
|
||||||
|
|
||||||
getPrimaryUrl(...[options]: Parameters<T.Effects["getPrimaryUrl"]>) {
|
getPrimaryUrl(...[options]: Parameters<T.Effects["getPrimaryUrl"]>) {
|
||||||
return rpcRound("getPrimaryUrl", {
|
return rpcRound("get-primary-url", {
|
||||||
...options,
|
...options,
|
||||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||||
}) as ReturnType<T.Effects["getPrimaryUrl"]>
|
}) as ReturnType<T.Effects["getPrimaryUrl"]>
|
||||||
@@ -196,22 +199,22 @@ function makeEffects(context: EffectContext): Effects {
|
|||||||
getServicePortForward(
|
getServicePortForward(
|
||||||
...[options]: Parameters<T.Effects["getServicePortForward"]>
|
...[options]: Parameters<T.Effects["getServicePortForward"]>
|
||||||
) {
|
) {
|
||||||
return rpcRound("getServicePortForward", options) as ReturnType<
|
return rpcRound("get-service-port-forward", options) as ReturnType<
|
||||||
T.Effects["getServicePortForward"]
|
T.Effects["getServicePortForward"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
getSslCertificate(options: Parameters<T.Effects["getSslCertificate"]>[0]) {
|
getSslCertificate(options: Parameters<T.Effects["getSslCertificate"]>[0]) {
|
||||||
return rpcRound("getSslCertificate", options) as ReturnType<
|
return rpcRound("get-ssl-certificate", options) as ReturnType<
|
||||||
T.Effects["getSslCertificate"]
|
T.Effects["getSslCertificate"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
getSslKey(options: Parameters<T.Effects["getSslKey"]>[0]) {
|
getSslKey(options: Parameters<T.Effects["getSslKey"]>[0]) {
|
||||||
return rpcRound("getSslKey", options) as ReturnType<
|
return rpcRound("get-ssl-key", options) as ReturnType<
|
||||||
T.Effects["getSslKey"]
|
T.Effects["getSslKey"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
getSystemSmtp(...[options]: Parameters<T.Effects["getSystemSmtp"]>) {
|
getSystemSmtp(...[options]: Parameters<T.Effects["getSystemSmtp"]>) {
|
||||||
return rpcRound("getSystemSmtp", {
|
return rpcRound("get-system-smtp", {
|
||||||
...options,
|
...options,
|
||||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||||
}) as ReturnType<T.Effects["getSystemSmtp"]>
|
}) as ReturnType<T.Effects["getSystemSmtp"]>
|
||||||
@@ -219,7 +222,7 @@ function makeEffects(context: EffectContext): Effects {
|
|||||||
listServiceInterfaces(
|
listServiceInterfaces(
|
||||||
...[options]: Parameters<T.Effects["listServiceInterfaces"]>
|
...[options]: Parameters<T.Effects["listServiceInterfaces"]>
|
||||||
) {
|
) {
|
||||||
return rpcRound("listServiceInterfaces", {
|
return rpcRound("list-service-interfaces", {
|
||||||
...options,
|
...options,
|
||||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||||
}) as ReturnType<T.Effects["listServiceInterfaces"]>
|
}) as ReturnType<T.Effects["listServiceInterfaces"]>
|
||||||
@@ -228,7 +231,7 @@ function makeEffects(context: EffectContext): Effects {
|
|||||||
return rpcRound("mount", options) as ReturnType<T.Effects["mount"]>
|
return rpcRound("mount", options) as ReturnType<T.Effects["mount"]>
|
||||||
},
|
},
|
||||||
clearActions(...[]: Parameters<T.Effects["clearActions"]>) {
|
clearActions(...[]: Parameters<T.Effects["clearActions"]>) {
|
||||||
return rpcRound("clearActions", {}) as ReturnType<
|
return rpcRound("clear-actions", {}) as ReturnType<
|
||||||
T.Effects["clearActions"]
|
T.Effects["clearActions"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
@@ -236,37 +239,39 @@ function makeEffects(context: EffectContext): Effects {
|
|||||||
return rpcRound("restart", {}) as ReturnType<T.Effects["restart"]>
|
return rpcRound("restart", {}) as ReturnType<T.Effects["restart"]>
|
||||||
},
|
},
|
||||||
setConfigured(...[configured]: Parameters<T.Effects["setConfigured"]>) {
|
setConfigured(...[configured]: Parameters<T.Effects["setConfigured"]>) {
|
||||||
return rpcRound("setConfigured", { configured }) as ReturnType<
|
return rpcRound("set-configured", { configured }) as ReturnType<
|
||||||
T.Effects["setConfigured"]
|
T.Effects["setConfigured"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
setDependencies(
|
setDependencies(
|
||||||
dependencies: Parameters<T.Effects["setDependencies"]>[0],
|
dependencies: Parameters<T.Effects["setDependencies"]>[0],
|
||||||
): ReturnType<T.Effects["setDependencies"]> {
|
): ReturnType<T.Effects["setDependencies"]> {
|
||||||
return rpcRound("setDependencies", dependencies) as ReturnType<
|
return rpcRound("set-dependencies", dependencies) as ReturnType<
|
||||||
T.Effects["setDependencies"]
|
T.Effects["setDependencies"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
checkDependencies(
|
checkDependencies(
|
||||||
options: Parameters<T.Effects["checkDependencies"]>[0],
|
options: Parameters<T.Effects["checkDependencies"]>[0],
|
||||||
): ReturnType<T.Effects["checkDependencies"]> {
|
): ReturnType<T.Effects["checkDependencies"]> {
|
||||||
return rpcRound("checkDependencies", options) as ReturnType<
|
return rpcRound("check-dependencies", options) as ReturnType<
|
||||||
T.Effects["checkDependencies"]
|
T.Effects["checkDependencies"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
getDependencies(): ReturnType<T.Effects["getDependencies"]> {
|
getDependencies(): ReturnType<T.Effects["getDependencies"]> {
|
||||||
return rpcRound("getDependencies", {}) as ReturnType<
|
return rpcRound("get-dependencies", {}) as ReturnType<
|
||||||
T.Effects["getDependencies"]
|
T.Effects["getDependencies"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
setHealth(...[options]: Parameters<T.Effects["setHealth"]>) {
|
setHealth(...[options]: Parameters<T.Effects["setHealth"]>) {
|
||||||
return rpcRound("setHealth", options) as ReturnType<
|
return rpcRound("set-health", options) as ReturnType<
|
||||||
T.Effects["setHealth"]
|
T.Effects["setHealth"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
|
|
||||||
setMainStatus(o: { status: "running" | "stopped" }): Promise<void> {
|
setMainStatus(o: { status: "running" | "stopped" }): Promise<void> {
|
||||||
return rpcRound("setMainStatus", o) as ReturnType<T.Effects["setHealth"]>
|
return rpcRound("set-main-status", o) as ReturnType<
|
||||||
|
T.Effects["setHealth"]
|
||||||
|
>
|
||||||
},
|
},
|
||||||
|
|
||||||
shutdown(...[]: Parameters<T.Effects["shutdown"]>) {
|
shutdown(...[]: Parameters<T.Effects["shutdown"]>) {
|
||||||
@@ -274,13 +279,23 @@ function makeEffects(context: EffectContext): Effects {
|
|||||||
},
|
},
|
||||||
store: {
|
store: {
|
||||||
get: async (options: any) =>
|
get: async (options: any) =>
|
||||||
rpcRound("getStore", {
|
rpcRound("store.get", {
|
||||||
...options,
|
...options,
|
||||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||||
}) as any,
|
}) as any,
|
||||||
set: async (options: any) =>
|
set: async (options: any) =>
|
||||||
rpcRound("setStore", options) as ReturnType<T.Effects["store"]["set"]>,
|
rpcRound("store.set", options) as ReturnType<T.Effects["store"]["set"]>,
|
||||||
} as T.Effects["store"],
|
} as T.Effects["store"],
|
||||||
|
getDataVersion() {
|
||||||
|
return rpcRound("get-data-version", {}) as ReturnType<
|
||||||
|
T.Effects["getDataVersion"]
|
||||||
|
>
|
||||||
|
},
|
||||||
|
setDataVersion(...[options]: Parameters<T.Effects["setDataVersion"]>) {
|
||||||
|
return rpcRound("set-data-version", options) as ReturnType<
|
||||||
|
T.Effects["setDataVersion"]
|
||||||
|
>
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import * as fs from "fs"
|
|||||||
|
|
||||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||||
import { AllGetDependencies } from "../Interfaces/AllGetDependencies"
|
import { AllGetDependencies } from "../Interfaces/AllGetDependencies"
|
||||||
import { jsonPath } from "../Models/JsonPath"
|
import { jsonPath, unNestPath } from "../Models/JsonPath"
|
||||||
import { RunningMain, System } from "../Interfaces/System"
|
import { RunningMain, System } from "../Interfaces/System"
|
||||||
import {
|
import {
|
||||||
MakeMainEffects,
|
MakeMainEffects,
|
||||||
@@ -52,6 +52,8 @@ const SOCKET_PARENT = "/media/startos/rpc"
|
|||||||
const SOCKET_PATH = "/media/startos/rpc/service.sock"
|
const SOCKET_PATH = "/media/startos/rpc/service.sock"
|
||||||
const jsonrpc = "2.0" as const
|
const jsonrpc = "2.0" as const
|
||||||
|
|
||||||
|
const isResult = object({ result: any }).test
|
||||||
|
|
||||||
const idType = some(string, number, literal(null))
|
const idType = some(string, number, literal(null))
|
||||||
type IdType = null | string | number
|
type IdType = null | string | number
|
||||||
const runType = object({
|
const runType = object({
|
||||||
@@ -64,7 +66,7 @@ const runType = object({
|
|||||||
input: any,
|
input: any,
|
||||||
timeout: number,
|
timeout: number,
|
||||||
},
|
},
|
||||||
["timeout", "input"],
|
["timeout"],
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
const sandboxRunType = object({
|
const sandboxRunType = object({
|
||||||
@@ -77,7 +79,7 @@ const sandboxRunType = object({
|
|||||||
input: any,
|
input: any,
|
||||||
timeout: number,
|
timeout: number,
|
||||||
},
|
},
|
||||||
["timeout", "input"],
|
["timeout"],
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
const callbackType = object({
|
const callbackType = object({
|
||||||
@@ -226,27 +228,25 @@ export class RpcListener {
|
|||||||
const system = this.system
|
const system = this.system
|
||||||
const procedure = jsonPath.unsafeCast(params.procedure)
|
const procedure = jsonPath.unsafeCast(params.procedure)
|
||||||
const effects = this.getDependencies.makeProcedureEffects()(params.id)
|
const effects = this.getDependencies.makeProcedureEffects()(params.id)
|
||||||
return handleRpc(
|
const input = params.input
|
||||||
id,
|
const timeout = params.timeout
|
||||||
system.execute(effects, {
|
const result = getResult(procedure, system, effects, timeout, input)
|
||||||
procedure,
|
|
||||||
input: params.input,
|
return handleRpc(id, result)
|
||||||
timeout: params.timeout,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.when(sandboxRunType, async ({ id, params }) => {
|
.when(sandboxRunType, async ({ id, params }) => {
|
||||||
const system = this.system
|
const system = this.system
|
||||||
const procedure = jsonPath.unsafeCast(params.procedure)
|
const procedure = jsonPath.unsafeCast(params.procedure)
|
||||||
const effects = this.makeProcedureEffects(params.id)
|
const effects = this.makeProcedureEffects(params.id)
|
||||||
return handleRpc(
|
const result = getResult(
|
||||||
id,
|
procedure,
|
||||||
system.sandbox(effects, {
|
system,
|
||||||
procedure,
|
effects,
|
||||||
input: params.input,
|
params.input,
|
||||||
timeout: params.timeout,
|
params.input,
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return handleRpc(id, result)
|
||||||
})
|
})
|
||||||
.when(callbackType, async ({ params: { callback, args } }) => {
|
.when(callbackType, async ({ params: { callback, args } }) => {
|
||||||
this.system.callCallback(callback, args)
|
this.system.callCallback(callback, args)
|
||||||
@@ -280,7 +280,7 @@ export class RpcListener {
|
|||||||
(async () => {
|
(async () => {
|
||||||
if (!this._system) {
|
if (!this._system) {
|
||||||
const system = await this.getDependencies.system()
|
const system = await this.getDependencies.system()
|
||||||
await system.init()
|
await system.containerInit()
|
||||||
this._system = system
|
this._system = system
|
||||||
}
|
}
|
||||||
})().then((result) => ({ result })),
|
})().then((result) => ({ result })),
|
||||||
@@ -342,3 +342,97 @@ export class RpcListener {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function getResult(
|
||||||
|
procedure: typeof jsonPath._TYPE,
|
||||||
|
system: System,
|
||||||
|
effects: T.Effects,
|
||||||
|
timeout: number | undefined,
|
||||||
|
input: any,
|
||||||
|
) {
|
||||||
|
const ensureResultTypeShape = (
|
||||||
|
result:
|
||||||
|
| void
|
||||||
|
| T.ConfigRes
|
||||||
|
| T.PropertiesReturn
|
||||||
|
| T.ActionMetadata[]
|
||||||
|
| T.ActionResult,
|
||||||
|
): { result: any } => {
|
||||||
|
if (isResult(result)) return result
|
||||||
|
return { result }
|
||||||
|
}
|
||||||
|
return (async () => {
|
||||||
|
switch (procedure) {
|
||||||
|
case "/backup/create":
|
||||||
|
return system.createBackup(effects, timeout || null)
|
||||||
|
case "/backup/restore":
|
||||||
|
return system.restoreBackup(effects, timeout || null)
|
||||||
|
case "/config/get":
|
||||||
|
return system.getConfig(effects, timeout || null)
|
||||||
|
case "/config/set":
|
||||||
|
return system.setConfig(effects, input, timeout || null)
|
||||||
|
case "/properties":
|
||||||
|
return system.properties(effects, timeout || null)
|
||||||
|
case "/actions/metadata":
|
||||||
|
return system.actionsMetadata(effects)
|
||||||
|
case "/init":
|
||||||
|
return system.packageInit(
|
||||||
|
effects,
|
||||||
|
string.optional().unsafeCast(input),
|
||||||
|
timeout || null,
|
||||||
|
)
|
||||||
|
case "/uninit":
|
||||||
|
return system.packageUninit(
|
||||||
|
effects,
|
||||||
|
string.optional().unsafeCast(input),
|
||||||
|
timeout || null,
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
const procedures = unNestPath(procedure)
|
||||||
|
switch (true) {
|
||||||
|
case procedures[1] === "actions" && procedures[3] === "get":
|
||||||
|
return system.action(effects, procedures[2], input, timeout || null)
|
||||||
|
case procedures[1] === "actions" && procedures[3] === "run":
|
||||||
|
return system.action(effects, procedures[2], input, timeout || null)
|
||||||
|
case procedures[1] === "dependencies" && procedures[3] === "query":
|
||||||
|
return system.dependenciesAutoconfig(
|
||||||
|
effects,
|
||||||
|
procedures[2],
|
||||||
|
input,
|
||||||
|
timeout || null,
|
||||||
|
)
|
||||||
|
|
||||||
|
case procedures[1] === "dependencies" && procedures[3] === "update":
|
||||||
|
return system.dependenciesAutoconfig(
|
||||||
|
effects,
|
||||||
|
procedures[2],
|
||||||
|
input,
|
||||||
|
timeout || null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})().then(ensureResultTypeShape, (error) =>
|
||||||
|
matches(error)
|
||||||
|
.when(
|
||||||
|
object(
|
||||||
|
{
|
||||||
|
error: string,
|
||||||
|
code: number,
|
||||||
|
},
|
||||||
|
["code"],
|
||||||
|
{ code: 0 },
|
||||||
|
),
|
||||||
|
(error) => ({
|
||||||
|
error: {
|
||||||
|
code: error.code,
|
||||||
|
message: error.error,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.defaultToLazy(() => ({
|
||||||
|
error: {
|
||||||
|
code: 0,
|
||||||
|
message: String(error),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,40 +1,60 @@
|
|||||||
import * as fs from "fs/promises"
|
import * as fs from "fs/promises"
|
||||||
import * as cp from "child_process"
|
import * as cp from "child_process"
|
||||||
import { Overlay, types as T } from "@start9labs/start-sdk"
|
import { SubContainer, types as T } from "@start9labs/start-sdk"
|
||||||
import { promisify } from "util"
|
import { promisify } from "util"
|
||||||
import { DockerProcedure, VolumeId } from "../../../Models/DockerProcedure"
|
import { DockerProcedure, VolumeId } from "../../../Models/DockerProcedure"
|
||||||
import { Volume } from "./matchVolume"
|
import { Volume } from "./matchVolume"
|
||||||
|
import {
|
||||||
|
CommandOptions,
|
||||||
|
ExecOptions,
|
||||||
|
ExecSpawnable,
|
||||||
|
} from "@start9labs/start-sdk/cjs/lib/util/SubContainer"
|
||||||
export const exec = promisify(cp.exec)
|
export const exec = promisify(cp.exec)
|
||||||
export const execFile = promisify(cp.execFile)
|
export const execFile = promisify(cp.execFile)
|
||||||
|
|
||||||
export class DockerProcedureContainer {
|
export class DockerProcedureContainer {
|
||||||
private constructor(readonly overlay: Overlay) {}
|
private constructor(private readonly subcontainer: ExecSpawnable) {}
|
||||||
// static async readonlyOf(data: DockerProcedure) {
|
|
||||||
// return DockerProcedureContainer.of(data, ["-o", "ro"])
|
|
||||||
// }
|
|
||||||
static async of(
|
static async of(
|
||||||
effects: T.Effects,
|
effects: T.Effects,
|
||||||
packageId: string,
|
packageId: string,
|
||||||
data: DockerProcedure,
|
data: DockerProcedure,
|
||||||
volumes: { [id: VolumeId]: Volume },
|
volumes: { [id: VolumeId]: Volume },
|
||||||
|
options: { subcontainer?: ExecSpawnable } = {},
|
||||||
) {
|
) {
|
||||||
const overlay = await Overlay.of(effects, { id: data.image })
|
const subcontainer =
|
||||||
|
options?.subcontainer ??
|
||||||
|
(await DockerProcedureContainer.createSubContainer(
|
||||||
|
effects,
|
||||||
|
packageId,
|
||||||
|
data,
|
||||||
|
volumes,
|
||||||
|
))
|
||||||
|
return new DockerProcedureContainer(subcontainer)
|
||||||
|
}
|
||||||
|
static async createSubContainer(
|
||||||
|
effects: T.Effects,
|
||||||
|
packageId: string,
|
||||||
|
data: DockerProcedure,
|
||||||
|
volumes: { [id: VolumeId]: Volume },
|
||||||
|
) {
|
||||||
|
const subcontainer = await SubContainer.of(effects, { id: data.image })
|
||||||
|
|
||||||
if (data.mounts) {
|
if (data.mounts) {
|
||||||
const mounts = data.mounts
|
const mounts = data.mounts
|
||||||
for (const mount in mounts) {
|
for (const mount in mounts) {
|
||||||
const path = mounts[mount].startsWith("/")
|
const path = mounts[mount].startsWith("/")
|
||||||
? `${overlay.rootfs}${mounts[mount]}`
|
? `${subcontainer.rootfs}${mounts[mount]}`
|
||||||
: `${overlay.rootfs}/${mounts[mount]}`
|
: `${subcontainer.rootfs}/${mounts[mount]}`
|
||||||
await fs.mkdir(path, { recursive: true })
|
await fs.mkdir(path, { recursive: true })
|
||||||
const volumeMount = volumes[mount]
|
const volumeMount = volumes[mount]
|
||||||
if (volumeMount.type === "data") {
|
if (volumeMount.type === "data") {
|
||||||
await overlay.mount(
|
await subcontainer.mount(
|
||||||
{ type: "volume", id: mount, subpath: null, readonly: false },
|
{ type: "volume", id: mount, subpath: null, readonly: false },
|
||||||
mounts[mount],
|
mounts[mount],
|
||||||
)
|
)
|
||||||
} else if (volumeMount.type === "assets") {
|
} else if (volumeMount.type === "assets") {
|
||||||
await overlay.mount(
|
await subcontainer.mount(
|
||||||
{ type: "assets", id: mount, subpath: null },
|
{ type: "assets", id: mount, subpath: null },
|
||||||
mounts[mount],
|
mounts[mount],
|
||||||
)
|
)
|
||||||
@@ -80,25 +100,35 @@ export class DockerProcedureContainer {
|
|||||||
})
|
})
|
||||||
.catch(console.warn)
|
.catch(console.warn)
|
||||||
} else if (volumeMount.type === "backup") {
|
} else if (volumeMount.type === "backup") {
|
||||||
await overlay.mount({ type: "backup", subpath: null }, mounts[mount])
|
await subcontainer.mount(
|
||||||
|
{ type: "backup", subpath: null },
|
||||||
|
mounts[mount],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return subcontainer
|
||||||
return new DockerProcedureContainer(overlay)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async exec(commands: string[]) {
|
async exec(
|
||||||
|
commands: string[],
|
||||||
|
options?: CommandOptions & ExecOptions,
|
||||||
|
timeoutMs?: number | null,
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
return await this.overlay.exec(commands)
|
return await this.subcontainer.exec(commands, options, timeoutMs)
|
||||||
} finally {
|
} finally {
|
||||||
await this.overlay.destroy()
|
await this.subcontainer.destroy?.()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async execFail(commands: string[], timeoutMs: number | null) {
|
async execFail(
|
||||||
|
commands: string[],
|
||||||
|
timeoutMs: number | null,
|
||||||
|
options?: CommandOptions & ExecOptions,
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const res = await this.overlay.exec(commands, {}, timeoutMs)
|
const res = await this.subcontainer.exec(commands, options, timeoutMs)
|
||||||
if (res.exitCode !== 0) {
|
if (res.exitCode !== 0) {
|
||||||
const codeOrSignal =
|
const codeOrSignal =
|
||||||
res.exitCode !== null
|
res.exitCode !== null
|
||||||
@@ -110,11 +140,11 @@ export class DockerProcedureContainer {
|
|||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
} finally {
|
} finally {
|
||||||
await this.overlay.destroy()
|
await this.subcontainer.destroy?.()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async spawn(commands: string[]): Promise<cp.ChildProcessWithoutNullStreams> {
|
async spawn(commands: string[]): Promise<cp.ChildProcessWithoutNullStreams> {
|
||||||
return await this.overlay.spawn(commands)
|
return await this.subcontainer.spawn(commands)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { T, utils } from "@start9labs/start-sdk"
|
|||||||
import { Daemon } from "@start9labs/start-sdk/cjs/lib/mainFn/Daemon"
|
import { Daemon } from "@start9labs/start-sdk/cjs/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/cjs/lib/mainFn/CommandController"
|
||||||
|
import { asError } from "@start9labs/start-sdk/cjs/lib/util"
|
||||||
|
|
||||||
const EMBASSY_HEALTH_INTERVAL = 15 * 1000
|
const EMBASSY_HEALTH_INTERVAL = 15 * 1000
|
||||||
const EMBASSY_PROPERTIES_LOOP = 30 * 1000
|
const EMBASSY_PROPERTIES_LOOP = 30 * 1000
|
||||||
@@ -14,6 +16,9 @@ const EMBASSY_PROPERTIES_LOOP = 30 * 1000
|
|||||||
* Also, this has an ability to clean itself up too if need be.
|
* Also, this has an ability to clean itself up too if need be.
|
||||||
*/
|
*/
|
||||||
export class MainLoop {
|
export class MainLoop {
|
||||||
|
get mainSubContainerHandle() {
|
||||||
|
return this.mainEvent?.daemon?.subContainerHandle
|
||||||
|
}
|
||||||
private healthLoops?: {
|
private healthLoops?: {
|
||||||
name: string
|
name: string
|
||||||
interval: NodeJS.Timeout
|
interval: NodeJS.Timeout
|
||||||
@@ -48,26 +53,32 @@ export class MainLoop {
|
|||||||
await this.setupInterfaces(effects)
|
await this.setupInterfaces(effects)
|
||||||
await effects.setMainStatus({ status: "running" })
|
await effects.setMainStatus({ status: "running" })
|
||||||
const jsMain = (this.system.moduleCode as any)?.jsMain
|
const jsMain = (this.system.moduleCode as any)?.jsMain
|
||||||
const dockerProcedureContainer = await DockerProcedureContainer.of(
|
|
||||||
effects,
|
|
||||||
this.system.manifest.id,
|
|
||||||
this.system.manifest.main,
|
|
||||||
this.system.manifest.volumes,
|
|
||||||
)
|
|
||||||
if (jsMain) {
|
if (jsMain) {
|
||||||
throw new Error("Unreachable")
|
throw new Error("Unreachable")
|
||||||
}
|
}
|
||||||
const daemon = await Daemon.of()(
|
const daemon = new Daemon(async () => {
|
||||||
this.effects,
|
const subcontainer = await DockerProcedureContainer.createSubContainer(
|
||||||
{ id: this.system.manifest.main.image },
|
effects,
|
||||||
currentCommand,
|
this.system.manifest.id,
|
||||||
{
|
this.system.manifest.main,
|
||||||
overlay: dockerProcedureContainer.overlay,
|
this.system.manifest.volumes,
|
||||||
sigtermTimeout: utils.inMs(
|
)
|
||||||
this.system.manifest.main["sigterm-timeout"],
|
return CommandController.of()(
|
||||||
),
|
this.effects,
|
||||||
},
|
subcontainer,
|
||||||
)
|
currentCommand,
|
||||||
|
{
|
||||||
|
runAsInit: true,
|
||||||
|
env: {
|
||||||
|
TINI_SUBREAPER: "true",
|
||||||
|
},
|
||||||
|
sigtermTimeout: utils.inMs(
|
||||||
|
this.system.manifest.main["sigterm-timeout"],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
daemon.start()
|
daemon.start()
|
||||||
return {
|
return {
|
||||||
daemon,
|
daemon,
|
||||||
@@ -123,7 +134,9 @@ export class MainLoop {
|
|||||||
const main = await mainEvent
|
const main = await mainEvent
|
||||||
delete this.mainEvent
|
delete this.mainEvent
|
||||||
delete this.healthLoops
|
delete this.healthLoops
|
||||||
await main?.daemon.stop().catch((e) => console.error(e))
|
await main?.daemon
|
||||||
|
.stop()
|
||||||
|
.catch((e) => console.error(`Main loop error`, utils.asError(e)))
|
||||||
this.effects.setMainStatus({ status: "stopped" })
|
this.effects.setMainStatus({ status: "stopped" })
|
||||||
if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval))
|
if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval))
|
||||||
}
|
}
|
||||||
@@ -134,27 +147,42 @@ export class MainLoop {
|
|||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
return Object.entries(manifest["health-checks"]).map(
|
return Object.entries(manifest["health-checks"]).map(
|
||||||
([healthId, value]) => {
|
([healthId, value]) => {
|
||||||
|
effects
|
||||||
|
.setHealth({
|
||||||
|
id: healthId,
|
||||||
|
name: value.name,
|
||||||
|
result: "starting",
|
||||||
|
message: null,
|
||||||
|
})
|
||||||
|
.catch((e) => console.error(asError(e)))
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
const actionProcedure = value
|
const actionProcedure = value
|
||||||
const timeChanged = Date.now() - start
|
const timeChanged = Date.now() - start
|
||||||
if (actionProcedure.type === "docker") {
|
if (actionProcedure.type === "docker") {
|
||||||
const container = await DockerProcedureContainer.of(
|
const subcontainer = actionProcedure.inject
|
||||||
effects,
|
? this.mainSubContainerHandle
|
||||||
manifest.id,
|
: undefined
|
||||||
actionProcedure,
|
// prettier-ignore
|
||||||
manifest.volumes,
|
const container =
|
||||||
|
await DockerProcedureContainer.of(
|
||||||
|
effects,
|
||||||
|
manifest.id,
|
||||||
|
actionProcedure,
|
||||||
|
manifest.volumes,
|
||||||
|
{
|
||||||
|
subcontainer,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const executed = await container.exec(
|
||||||
|
[actionProcedure.entrypoint, ...actionProcedure.args],
|
||||||
|
{ input: JSON.stringify(timeChanged) },
|
||||||
)
|
)
|
||||||
const executed = await container.exec([
|
|
||||||
actionProcedure.entrypoint,
|
|
||||||
...actionProcedure.args,
|
|
||||||
JSON.stringify(timeChanged),
|
|
||||||
])
|
|
||||||
if (executed.exitCode === 0) {
|
if (executed.exitCode === 0) {
|
||||||
await effects.setHealth({
|
await effects.setHealth({
|
||||||
id: healthId,
|
id: healthId,
|
||||||
name: value.name,
|
name: value.name,
|
||||||
result: "success",
|
result: "success",
|
||||||
message: actionProcedure["success-message"],
|
message: actionProcedure["success-message"] ?? null,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
export default {
|
||||||
|
"eos-version": "0.3.5.1",
|
||||||
|
id: "gitea",
|
||||||
|
"git-hash": "91fada3edf30357a2e75c281d32f8888c87fcc2d\n",
|
||||||
|
title: "Gitea",
|
||||||
|
version: "1.22.0",
|
||||||
|
description: {
|
||||||
|
short: "A painless self-hosted Git service.",
|
||||||
|
long: "Gitea is a community managed lightweight code hosting solution written in Go. It is published under the MIT license.\n",
|
||||||
|
},
|
||||||
|
assets: {
|
||||||
|
license: "LICENSE",
|
||||||
|
instructions: "instructions.md",
|
||||||
|
icon: "icon.png",
|
||||||
|
"docker-images": null,
|
||||||
|
assets: null,
|
||||||
|
scripts: null,
|
||||||
|
},
|
||||||
|
build: ["make"],
|
||||||
|
"release-notes":
|
||||||
|
"* Upstream code update\n* Fix deprecated config options\n* Full list of upstream changes available [here](https://github.com/go-gitea/gitea/compare/v1.21.8...v1.22.0)\n",
|
||||||
|
license: "MIT",
|
||||||
|
"wrapper-repo": "https://github.com/Start9Labs/gitea-startos",
|
||||||
|
"upstream-repo": "https://github.com/go-gitea/gitea",
|
||||||
|
"support-site": "https://docs.gitea.io/en-us/",
|
||||||
|
"marketing-site": "https://gitea.io/en-us/",
|
||||||
|
"donation-url": null,
|
||||||
|
alerts: {
|
||||||
|
install: null,
|
||||||
|
uninstall: null,
|
||||||
|
restore: null,
|
||||||
|
start: null,
|
||||||
|
stop: null,
|
||||||
|
},
|
||||||
|
main: {
|
||||||
|
type: "docker",
|
||||||
|
image: "main",
|
||||||
|
system: false,
|
||||||
|
entrypoint: "/usr/local/bin/docker_entrypoint.sh",
|
||||||
|
args: [],
|
||||||
|
inject: false,
|
||||||
|
mounts: { main: "/data" },
|
||||||
|
"io-format": null,
|
||||||
|
"sigterm-timeout": null,
|
||||||
|
"shm-size-mb": null,
|
||||||
|
"gpu-acceleration": false,
|
||||||
|
},
|
||||||
|
"health-checks": {
|
||||||
|
"user-signups-off": {
|
||||||
|
name: "User Signups Off",
|
||||||
|
"success-message": null,
|
||||||
|
type: "script",
|
||||||
|
args: [],
|
||||||
|
timeout: null,
|
||||||
|
},
|
||||||
|
web: {
|
||||||
|
name: "Web & Git HTTP Tor Interfaces",
|
||||||
|
"success-message":
|
||||||
|
"Gitea is ready to be visited in a web browser and git can be used with SSH over TOR.",
|
||||||
|
type: "script",
|
||||||
|
args: [],
|
||||||
|
timeout: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
get: { type: "script", args: [] },
|
||||||
|
set: { type: "script", args: [] },
|
||||||
|
},
|
||||||
|
properties: { type: "script", args: [] },
|
||||||
|
volumes: { main: { type: "data" } },
|
||||||
|
interfaces: {
|
||||||
|
main: {
|
||||||
|
name: "Web UI / Git HTTPS/SSH",
|
||||||
|
description:
|
||||||
|
"Port 80: Browser Interface and HTTP Git Interface / Port 22: Git SSH Interface",
|
||||||
|
"tor-config": { "port-mapping": { "22": "22", "80": "3000" } },
|
||||||
|
"lan-config": { "443": { ssl: true, internal: 3000 } },
|
||||||
|
ui: true,
|
||||||
|
protocols: ["tcp", "http", "ssh", "git"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
backup: {
|
||||||
|
create: {
|
||||||
|
type: "docker",
|
||||||
|
image: "compat",
|
||||||
|
system: true,
|
||||||
|
entrypoint: "compat",
|
||||||
|
args: ["duplicity", "create", "/mnt/backup", "/root/data"],
|
||||||
|
inject: false,
|
||||||
|
mounts: { BACKUP: "/mnt/backup", main: "/root/data" },
|
||||||
|
"io-format": "yaml",
|
||||||
|
"sigterm-timeout": null,
|
||||||
|
"shm-size-mb": null,
|
||||||
|
"gpu-acceleration": false,
|
||||||
|
},
|
||||||
|
restore: {
|
||||||
|
type: "docker",
|
||||||
|
image: "compat",
|
||||||
|
system: true,
|
||||||
|
entrypoint: "compat",
|
||||||
|
args: ["duplicity", "restore", "/mnt/backup", "/root/data"],
|
||||||
|
inject: false,
|
||||||
|
mounts: { BACKUP: "/mnt/backup", main: "/root/data" },
|
||||||
|
"io-format": "yaml",
|
||||||
|
"sigterm-timeout": null,
|
||||||
|
"shm-size-mb": null,
|
||||||
|
"gpu-acceleration": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
from: { "*": { type: "script", args: ["from"] } },
|
||||||
|
to: { "*": { type: "script", args: ["to"] } },
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
dependencies: {},
|
||||||
|
containers: null,
|
||||||
|
replaces: [],
|
||||||
|
"hardware-requirements": {
|
||||||
|
device: {},
|
||||||
|
ram: null,
|
||||||
|
arch: ["x86_64", "aarch64"],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
export default {
|
||||||
|
"tor-address": {
|
||||||
|
name: "Tor Address",
|
||||||
|
description: "The Tor address for the websocket server.",
|
||||||
|
type: "pointer",
|
||||||
|
subtype: "package",
|
||||||
|
"package-id": "nostr",
|
||||||
|
target: "tor-address",
|
||||||
|
interface: "websocket",
|
||||||
|
},
|
||||||
|
"lan-address": {
|
||||||
|
name: "Tor Address",
|
||||||
|
description: "The LAN address for the websocket server.",
|
||||||
|
type: "pointer",
|
||||||
|
subtype: "package",
|
||||||
|
"package-id": "nostr",
|
||||||
|
target: "lan-address",
|
||||||
|
interface: "websocket",
|
||||||
|
},
|
||||||
|
"relay-type": {
|
||||||
|
type: "union",
|
||||||
|
name: "Relay Type",
|
||||||
|
warning:
|
||||||
|
"Running a public relay carries risk. Your relay can be spammed, resulting in large amounts of disk usage.",
|
||||||
|
tag: {
|
||||||
|
id: "type",
|
||||||
|
name: "Relay Type",
|
||||||
|
description:
|
||||||
|
"Private or public. A private relay (highly recommended) restricts write access to specific pubkeys. Anyone can write to a public relay.",
|
||||||
|
"variant-names": { private: "Private", public: "Public" },
|
||||||
|
},
|
||||||
|
default: "private",
|
||||||
|
variants: {
|
||||||
|
private: {
|
||||||
|
pubkey_whitelist: {
|
||||||
|
name: "Pubkey Whitelist (hex)",
|
||||||
|
description:
|
||||||
|
"A list of pubkeys that are permitted to publish through your relay. A minimum, you need to enter your own Nostr hex (not npub) pubkey. Go to https://damus.io/key/ to convert from npub to hex.",
|
||||||
|
type: "list",
|
||||||
|
range: "[1,*)",
|
||||||
|
subtype: "string",
|
||||||
|
spec: {
|
||||||
|
placeholder: "hex (not npub) pubkey",
|
||||||
|
pattern: "[0-9a-fA-F]{64}",
|
||||||
|
"pattern-description":
|
||||||
|
"Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.",
|
||||||
|
},
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
public: {
|
||||||
|
info: {
|
||||||
|
name: "Relay Info",
|
||||||
|
description: "General public info about your relay",
|
||||||
|
type: "object",
|
||||||
|
spec: {
|
||||||
|
name: {
|
||||||
|
name: "Relay Name",
|
||||||
|
description: "Your relay's human-readable identifier",
|
||||||
|
type: "string",
|
||||||
|
nullable: true,
|
||||||
|
placeholder: "Bob's Public Relay",
|
||||||
|
pattern: ".{3,32}",
|
||||||
|
"pattern-description":
|
||||||
|
"Must be at least 3 character and no more than 32 characters",
|
||||||
|
masked: false,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
name: "Relay Description",
|
||||||
|
description: "A more detailed description for your relay",
|
||||||
|
type: "string",
|
||||||
|
nullable: true,
|
||||||
|
placeholder: "The best relay in town",
|
||||||
|
pattern: ".{6,256}",
|
||||||
|
"pattern-description":
|
||||||
|
"Must be at least 6 character and no more than 256 characters",
|
||||||
|
masked: false,
|
||||||
|
},
|
||||||
|
pubkey: {
|
||||||
|
name: "Admin contact pubkey (hex)",
|
||||||
|
description:
|
||||||
|
"The Nostr hex (not npub) pubkey of the relay administrator",
|
||||||
|
type: "string",
|
||||||
|
nullable: true,
|
||||||
|
placeholder: "hex (not npub) pubkey",
|
||||||
|
pattern: "[0-9a-fA-F]{64}",
|
||||||
|
"pattern-description":
|
||||||
|
"Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.",
|
||||||
|
masked: false,
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
name: "Admin contact email",
|
||||||
|
description: "The email address of the relay administrator",
|
||||||
|
type: "string",
|
||||||
|
nullable: true,
|
||||||
|
pattern: "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+",
|
||||||
|
"pattern-description": "Must be a valid email address.",
|
||||||
|
masked: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
name: "Limits",
|
||||||
|
description:
|
||||||
|
"Data limits to protect your relay from using too many resources",
|
||||||
|
type: "object",
|
||||||
|
spec: {
|
||||||
|
messages_per_sec: {
|
||||||
|
name: "Messages Per Second Limit",
|
||||||
|
description:
|
||||||
|
"Limit events created per second, averaged over one minute. Note: this is for the server as a whole, not per connection.",
|
||||||
|
type: "number",
|
||||||
|
nullable: false,
|
||||||
|
range: "[1,*)",
|
||||||
|
integral: true,
|
||||||
|
default: 2,
|
||||||
|
units: "messages/sec",
|
||||||
|
},
|
||||||
|
subscriptions_per_min: {
|
||||||
|
name: "Subscriptions Per Minute Limit",
|
||||||
|
description:
|
||||||
|
"Limit client subscriptions created per second, averaged over one minute. Strongly recommended to set this to a low value such as 10 to ensure fair service.",
|
||||||
|
type: "number",
|
||||||
|
nullable: false,
|
||||||
|
range: "[1,*)",
|
||||||
|
integral: true,
|
||||||
|
default: 10,
|
||||||
|
units: "subscriptions",
|
||||||
|
},
|
||||||
|
max_blocking_threads: {
|
||||||
|
name: "Max Blocking Threads",
|
||||||
|
description:
|
||||||
|
"Maximum number of blocking threads used for database connections.",
|
||||||
|
type: "number",
|
||||||
|
nullable: false,
|
||||||
|
range: "[0,*)",
|
||||||
|
integral: true,
|
||||||
|
units: "threads",
|
||||||
|
default: 16,
|
||||||
|
},
|
||||||
|
max_event_bytes: {
|
||||||
|
name: "Max Event Size",
|
||||||
|
description:
|
||||||
|
"Limit the maximum size of an EVENT message. Set to 0 for unlimited",
|
||||||
|
type: "number",
|
||||||
|
nullable: false,
|
||||||
|
range: "[0,*)",
|
||||||
|
integral: true,
|
||||||
|
units: "bytes",
|
||||||
|
default: 131072,
|
||||||
|
},
|
||||||
|
max_ws_message_bytes: {
|
||||||
|
name: "Max Websocket Message Size",
|
||||||
|
description: "Maximum WebSocket message in bytes.",
|
||||||
|
type: "number",
|
||||||
|
nullable: false,
|
||||||
|
range: "[0,*)",
|
||||||
|
integral: true,
|
||||||
|
units: "bytes",
|
||||||
|
default: 131072,
|
||||||
|
},
|
||||||
|
max_ws_frame_bytes: {
|
||||||
|
name: "Max Websocket Frame Size",
|
||||||
|
description: "Maximum WebSocket frame size in bytes.",
|
||||||
|
type: "number",
|
||||||
|
nullable: false,
|
||||||
|
range: "[0,*)",
|
||||||
|
integral: true,
|
||||||
|
units: "bytes",
|
||||||
|
default: 131072,
|
||||||
|
},
|
||||||
|
event_kind_blacklist: {
|
||||||
|
name: "Event Kind Blacklist",
|
||||||
|
description:
|
||||||
|
"Events with these kinds will be discarded. For a list of event kinds, see here: https://github.com/nostr-protocol/nips#event-kinds",
|
||||||
|
type: "list",
|
||||||
|
range: "[0,*)",
|
||||||
|
subtype: "number",
|
||||||
|
spec: { integral: true, placeholder: 30023, range: "(0,100000]" },
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
export default {
|
||||||
|
id: "synapse",
|
||||||
|
title: "Synapse",
|
||||||
|
version: "1.98.0",
|
||||||
|
"release-notes":
|
||||||
|
"* Upstream code update\n* Synapse Admin updated to the latest version - ([full changelog](https://github.com/Awesome-Technologies/synapse-admin/compare/0.8.7...0.9.1))\n* Instructions update\n* Updated package and upstream repositories links\n* Full list of upstream changes available [here](https://github.com/element-hq/synapse/compare/v1.95.1...v1.98.0)\n",
|
||||||
|
license: "Apache-2.0",
|
||||||
|
"wrapper-repo": "https://github.com/Start9Labs/synapse-startos",
|
||||||
|
"upstream-repo": "https://github.com/element-hq/synapse",
|
||||||
|
"support-site": "https://github.com/element-hq/synapse/issues",
|
||||||
|
"marketing-site": "https://matrix.org/",
|
||||||
|
build: ["make"],
|
||||||
|
description: {
|
||||||
|
short:
|
||||||
|
"Synapse is a battle-tested implementation of the Matrix protocol, the killer of all messaging apps.",
|
||||||
|
long: "Synapse is the battle-tested, reference implementation of the Matrix protocol. Matrix is a next-generation, federated, full-featured, encrypted, independent messaging system. There are no trusted third parties involved. (see matrix.org for details).",
|
||||||
|
},
|
||||||
|
assets: {
|
||||||
|
license: "LICENSE",
|
||||||
|
icon: "icon.png",
|
||||||
|
instructions: "instructions.md",
|
||||||
|
},
|
||||||
|
main: {
|
||||||
|
type: "docker",
|
||||||
|
image: "main",
|
||||||
|
entrypoint: "docker_entrypoint.sh",
|
||||||
|
args: [],
|
||||||
|
mounts: {
|
||||||
|
main: "/data",
|
||||||
|
cert: "/mnt/cert",
|
||||||
|
"admin-cert": "/mnt/admin-cert",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"health-checks": {
|
||||||
|
federation: {
|
||||||
|
name: "Federation",
|
||||||
|
type: "docker",
|
||||||
|
image: "main",
|
||||||
|
system: false,
|
||||||
|
entrypoint: "check-federation.sh",
|
||||||
|
args: [],
|
||||||
|
mounts: {},
|
||||||
|
"io-format": "json",
|
||||||
|
inject: true,
|
||||||
|
},
|
||||||
|
"synapse-admin": {
|
||||||
|
name: "Admin interface",
|
||||||
|
"success-message":
|
||||||
|
"Synapse Admin is ready to be visited in a web browser.",
|
||||||
|
type: "docker",
|
||||||
|
image: "main",
|
||||||
|
system: false,
|
||||||
|
entrypoint: "check-ui.sh",
|
||||||
|
args: [],
|
||||||
|
mounts: {},
|
||||||
|
"io-format": "yaml",
|
||||||
|
inject: true,
|
||||||
|
},
|
||||||
|
"user-signups-off": {
|
||||||
|
name: "User Signups Off",
|
||||||
|
type: "docker",
|
||||||
|
image: "main",
|
||||||
|
system: false,
|
||||||
|
entrypoint: "user-signups-off.sh",
|
||||||
|
args: [],
|
||||||
|
mounts: {},
|
||||||
|
"io-format": "yaml",
|
||||||
|
inject: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
get: {
|
||||||
|
type: "script",
|
||||||
|
},
|
||||||
|
set: {
|
||||||
|
type: "script",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
type: "script",
|
||||||
|
},
|
||||||
|
volumes: {
|
||||||
|
main: {
|
||||||
|
type: "data",
|
||||||
|
},
|
||||||
|
cert: {
|
||||||
|
type: "certificate",
|
||||||
|
"interface-id": "main",
|
||||||
|
},
|
||||||
|
"admin-cert": {
|
||||||
|
type: "certificate",
|
||||||
|
"interface-id": "admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
alerts: {
|
||||||
|
start:
|
||||||
|
"After your first run, Synapse needs a little time to establish a stable TOR connection over federation. We kindly ask for your patience during this process. Remember, great things take time! 🕒",
|
||||||
|
},
|
||||||
|
interfaces: {
|
||||||
|
main: {
|
||||||
|
name: "Homeserver Address",
|
||||||
|
description:
|
||||||
|
"Used by clients and other servers to connect with your homeserver",
|
||||||
|
"tor-config": {
|
||||||
|
"port-mapping": {
|
||||||
|
"80": "80",
|
||||||
|
"443": "443",
|
||||||
|
"8448": "8448",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ui: false,
|
||||||
|
protocols: ["tcp", "http", "matrix"],
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
name: "Admin Portal",
|
||||||
|
description: "A web application for administering your Synapse server",
|
||||||
|
"tor-config": {
|
||||||
|
"port-mapping": {
|
||||||
|
"80": "8080",
|
||||||
|
"443": "4433",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"lan-config": {
|
||||||
|
"443": {
|
||||||
|
ssl: true,
|
||||||
|
internal: 8080,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ui: true,
|
||||||
|
protocols: ["tcp", "http"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dependencies: {},
|
||||||
|
backup: {
|
||||||
|
create: {
|
||||||
|
type: "docker",
|
||||||
|
image: "compat",
|
||||||
|
system: true,
|
||||||
|
entrypoint: "compat",
|
||||||
|
args: ["duplicity", "create", "/mnt/backup", "/data"],
|
||||||
|
mounts: {
|
||||||
|
BACKUP: "/mnt/backup",
|
||||||
|
main: "/data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
restore: {
|
||||||
|
type: "docker",
|
||||||
|
image: "compat",
|
||||||
|
system: true,
|
||||||
|
entrypoint: "compat",
|
||||||
|
args: ["duplicity", "restore", "/mnt/backup", "/data"],
|
||||||
|
mounts: {
|
||||||
|
BACKUP: "/mnt/backup",
|
||||||
|
main: "/data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
"reset-first-user": {
|
||||||
|
name: "Reset First User",
|
||||||
|
description:
|
||||||
|
"This action will reset the password of the first user in your database to a random value.",
|
||||||
|
"allowed-statuses": ["stopped"],
|
||||||
|
implementation: {
|
||||||
|
type: "docker",
|
||||||
|
image: "main",
|
||||||
|
system: false,
|
||||||
|
entrypoint: "docker_entrypoint.sh",
|
||||||
|
args: ["reset-first-user"],
|
||||||
|
mounts: {
|
||||||
|
main: "/data",
|
||||||
|
},
|
||||||
|
"io-format": "json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
from: {
|
||||||
|
"*": {
|
||||||
|
type: "script",
|
||||||
|
args: ["from"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
"*": {
|
||||||
|
type: "script",
|
||||||
|
args: ["to"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -750,6 +750,283 @@ exports[`transformConfigSpec transformConfigSpec(nostr) 1`] = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`transformConfigSpec transformConfigSpec(nostr2) 1`] = `
|
||||||
|
{
|
||||||
|
"relay-type": {
|
||||||
|
"default": "private",
|
||||||
|
"description": "Private or public. A private relay (highly recommended) restricts write access to specific pubkeys. Anyone can write to a public relay.",
|
||||||
|
"disabled": false,
|
||||||
|
"immutable": false,
|
||||||
|
"name": "Relay Type",
|
||||||
|
"required": true,
|
||||||
|
"type": "union",
|
||||||
|
"variants": {
|
||||||
|
"private": {
|
||||||
|
"name": "Private",
|
||||||
|
"spec": {
|
||||||
|
"pubkey_whitelist": {
|
||||||
|
"default": [],
|
||||||
|
"description": "A list of pubkeys that are permitted to publish through your relay. A minimum, you need to enter your own Nostr hex (not npub) pubkey. Go to https://damus.io/key/ to convert from npub to hex.",
|
||||||
|
"disabled": false,
|
||||||
|
"maxLength": null,
|
||||||
|
"minLength": 1,
|
||||||
|
"name": "Pubkey Whitelist (hex)",
|
||||||
|
"spec": {
|
||||||
|
"generate": null,
|
||||||
|
"inputmode": "text",
|
||||||
|
"masked": false,
|
||||||
|
"maxLength": null,
|
||||||
|
"minLength": null,
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"description": "Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.",
|
||||||
|
"regex": "[0-9a-fA-F]{64}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"placeholder": "hex (not npub) pubkey",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
"type": "list",
|
||||||
|
"warning": null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"public": {
|
||||||
|
"name": "Public",
|
||||||
|
"spec": {
|
||||||
|
"info": {
|
||||||
|
"description": "General public info about your relay",
|
||||||
|
"name": "Relay Info",
|
||||||
|
"spec": {
|
||||||
|
"contact": {
|
||||||
|
"default": null,
|
||||||
|
"description": "The email address of the relay administrator",
|
||||||
|
"disabled": false,
|
||||||
|
"generate": null,
|
||||||
|
"immutable": false,
|
||||||
|
"inputmode": "text",
|
||||||
|
"masked": false,
|
||||||
|
"maxLength": null,
|
||||||
|
"minLength": null,
|
||||||
|
"name": "Admin contact email",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"description": "Must be a valid email address.",
|
||||||
|
"regex": "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"placeholder": null,
|
||||||
|
"required": false,
|
||||||
|
"type": "text",
|
||||||
|
"warning": null,
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"default": null,
|
||||||
|
"description": "A more detailed description for your relay",
|
||||||
|
"disabled": false,
|
||||||
|
"generate": null,
|
||||||
|
"immutable": false,
|
||||||
|
"inputmode": "text",
|
||||||
|
"masked": false,
|
||||||
|
"maxLength": null,
|
||||||
|
"minLength": null,
|
||||||
|
"name": "Relay Description",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"description": "Must be at least 6 character and no more than 256 characters",
|
||||||
|
"regex": ".{6,256}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"placeholder": "The best relay in town",
|
||||||
|
"required": false,
|
||||||
|
"type": "text",
|
||||||
|
"warning": null,
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"default": null,
|
||||||
|
"description": "Your relay's human-readable identifier",
|
||||||
|
"disabled": false,
|
||||||
|
"generate": null,
|
||||||
|
"immutable": false,
|
||||||
|
"inputmode": "text",
|
||||||
|
"masked": false,
|
||||||
|
"maxLength": null,
|
||||||
|
"minLength": null,
|
||||||
|
"name": "Relay Name",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"description": "Must be at least 3 character and no more than 32 characters",
|
||||||
|
"regex": ".{3,32}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"placeholder": "Bob's Public Relay",
|
||||||
|
"required": false,
|
||||||
|
"type": "text",
|
||||||
|
"warning": null,
|
||||||
|
},
|
||||||
|
"pubkey": {
|
||||||
|
"default": null,
|
||||||
|
"description": "The Nostr hex (not npub) pubkey of the relay administrator",
|
||||||
|
"disabled": false,
|
||||||
|
"generate": null,
|
||||||
|
"immutable": false,
|
||||||
|
"inputmode": "text",
|
||||||
|
"masked": false,
|
||||||
|
"maxLength": null,
|
||||||
|
"minLength": null,
|
||||||
|
"name": "Admin contact pubkey (hex)",
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"description": "Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.",
|
||||||
|
"regex": "[0-9a-fA-F]{64}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"placeholder": "hex (not npub) pubkey",
|
||||||
|
"required": false,
|
||||||
|
"type": "text",
|
||||||
|
"warning": null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"warning": null,
|
||||||
|
},
|
||||||
|
"limits": {
|
||||||
|
"description": "Data limits to protect your relay from using too many resources",
|
||||||
|
"name": "Limits",
|
||||||
|
"spec": {
|
||||||
|
"event_kind_blacklist": {
|
||||||
|
"default": [],
|
||||||
|
"description": "Events with these kinds will be discarded. For a list of event kinds, see here: https://github.com/nostr-protocol/nips#event-kinds",
|
||||||
|
"disabled": false,
|
||||||
|
"maxLength": null,
|
||||||
|
"minLength": null,
|
||||||
|
"name": "Event Kind Blacklist",
|
||||||
|
"spec": {
|
||||||
|
"generate": null,
|
||||||
|
"inputmode": "text",
|
||||||
|
"masked": false,
|
||||||
|
"maxLength": null,
|
||||||
|
"minLength": null,
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"description": "Integral number type",
|
||||||
|
"regex": "[0-9]+",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"placeholder": "30023",
|
||||||
|
"type": "text",
|
||||||
|
},
|
||||||
|
"type": "list",
|
||||||
|
"warning": null,
|
||||||
|
},
|
||||||
|
"max_blocking_threads": {
|
||||||
|
"default": 16,
|
||||||
|
"description": "Maximum number of blocking threads used for database connections.",
|
||||||
|
"disabled": false,
|
||||||
|
"immutable": false,
|
||||||
|
"integer": true,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "Max Blocking Threads",
|
||||||
|
"placeholder": null,
|
||||||
|
"required": true,
|
||||||
|
"step": null,
|
||||||
|
"type": "number",
|
||||||
|
"units": "threads",
|
||||||
|
"warning": null,
|
||||||
|
},
|
||||||
|
"max_event_bytes": {
|
||||||
|
"default": 131072,
|
||||||
|
"description": "Limit the maximum size of an EVENT message. Set to 0 for unlimited",
|
||||||
|
"disabled": false,
|
||||||
|
"immutable": false,
|
||||||
|
"integer": true,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "Max Event Size",
|
||||||
|
"placeholder": null,
|
||||||
|
"required": true,
|
||||||
|
"step": null,
|
||||||
|
"type": "number",
|
||||||
|
"units": "bytes",
|
||||||
|
"warning": null,
|
||||||
|
},
|
||||||
|
"max_ws_frame_bytes": {
|
||||||
|
"default": 131072,
|
||||||
|
"description": "Maximum WebSocket frame size in bytes.",
|
||||||
|
"disabled": false,
|
||||||
|
"immutable": false,
|
||||||
|
"integer": true,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "Max Websocket Frame Size",
|
||||||
|
"placeholder": null,
|
||||||
|
"required": true,
|
||||||
|
"step": null,
|
||||||
|
"type": "number",
|
||||||
|
"units": "bytes",
|
||||||
|
"warning": null,
|
||||||
|
},
|
||||||
|
"max_ws_message_bytes": {
|
||||||
|
"default": 131072,
|
||||||
|
"description": "Maximum WebSocket message in bytes.",
|
||||||
|
"disabled": false,
|
||||||
|
"immutable": false,
|
||||||
|
"integer": true,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "Max Websocket Message Size",
|
||||||
|
"placeholder": null,
|
||||||
|
"required": true,
|
||||||
|
"step": null,
|
||||||
|
"type": "number",
|
||||||
|
"units": "bytes",
|
||||||
|
"warning": null,
|
||||||
|
},
|
||||||
|
"messages_per_sec": {
|
||||||
|
"default": 2,
|
||||||
|
"description": "Limit events created per second, averaged over one minute. Note: this is for the server as a whole, not per connection.",
|
||||||
|
"disabled": false,
|
||||||
|
"immutable": false,
|
||||||
|
"integer": true,
|
||||||
|
"max": null,
|
||||||
|
"min": 1,
|
||||||
|
"name": "Messages Per Second Limit",
|
||||||
|
"placeholder": null,
|
||||||
|
"required": true,
|
||||||
|
"step": null,
|
||||||
|
"type": "number",
|
||||||
|
"units": "messages/sec",
|
||||||
|
"warning": null,
|
||||||
|
},
|
||||||
|
"subscriptions_per_min": {
|
||||||
|
"default": 10,
|
||||||
|
"description": "Limit client subscriptions created per second, averaged over one minute. Strongly recommended to set this to a low value such as 10 to ensure fair service.",
|
||||||
|
"disabled": false,
|
||||||
|
"immutable": false,
|
||||||
|
"integer": true,
|
||||||
|
"max": null,
|
||||||
|
"min": 1,
|
||||||
|
"name": "Subscriptions Per Minute Limit",
|
||||||
|
"placeholder": null,
|
||||||
|
"required": true,
|
||||||
|
"step": null,
|
||||||
|
"type": "number",
|
||||||
|
"units": "subscriptions",
|
||||||
|
"warning": null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"warning": null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"warning": null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`transformConfigSpec transformConfigSpec(searNXG) 1`] = `
|
exports[`transformConfigSpec transformConfigSpec(searNXG) 1`] = `
|
||||||
{
|
{
|
||||||
"enable-metrics": {
|
"enable-metrics": {
|
||||||
|
|||||||
@@ -61,6 +61,42 @@ const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json"
|
|||||||
export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js"
|
export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js"
|
||||||
const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig" as StorePath
|
const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig" as StorePath
|
||||||
|
|
||||||
|
const matchResult = object({
|
||||||
|
result: any,
|
||||||
|
})
|
||||||
|
const matchError = object({
|
||||||
|
error: string,
|
||||||
|
})
|
||||||
|
const matchErrorCode = object<{
|
||||||
|
"error-code": [number, string] | readonly [number, string]
|
||||||
|
}>({
|
||||||
|
"error-code": tuple(number, string),
|
||||||
|
})
|
||||||
|
|
||||||
|
const assertNever = (
|
||||||
|
x: never,
|
||||||
|
message = "Not expecting to get here: ",
|
||||||
|
): never => {
|
||||||
|
throw new Error(message + JSON.stringify(x))
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
Should be changing the type for specific properties, and this is mostly a transformation for the old return types to the newer one.
|
||||||
|
*/
|
||||||
|
const fromReturnType = <A>(a: U.ResultType<A>): A => {
|
||||||
|
if (matchResult.test(a)) {
|
||||||
|
return a.result
|
||||||
|
}
|
||||||
|
if (matchError.test(a)) {
|
||||||
|
console.info({ passedErrorStack: new Error().stack, error: a.error })
|
||||||
|
throw { error: a.error }
|
||||||
|
}
|
||||||
|
if (matchErrorCode.test(a)) {
|
||||||
|
const [code, message] = a["error-code"]
|
||||||
|
throw { error: message, code }
|
||||||
|
}
|
||||||
|
return assertNever(a)
|
||||||
|
}
|
||||||
|
|
||||||
const matchSetResult = object(
|
const matchSetResult = object(
|
||||||
{
|
{
|
||||||
"depends-on": dictionary([string, array(string)]),
|
"depends-on": dictionary([string, array(string)]),
|
||||||
@@ -194,7 +230,7 @@ export class SystemForEmbassy implements System {
|
|||||||
const moduleCode = await import(EMBASSY_JS_LOCATION)
|
const moduleCode = await import(EMBASSY_JS_LOCATION)
|
||||||
.catch((_) => require(EMBASSY_JS_LOCATION))
|
.catch((_) => require(EMBASSY_JS_LOCATION))
|
||||||
.catch(async (_) => {
|
.catch(async (_) => {
|
||||||
console.error("Could not load the js")
|
console.error(utils.asError("Could not load the js"))
|
||||||
console.error({
|
console.error({
|
||||||
exists: await fs.stat(EMBASSY_JS_LOCATION),
|
exists: await fs.stat(EMBASSY_JS_LOCATION),
|
||||||
})
|
})
|
||||||
@@ -206,12 +242,49 @@ export class SystemForEmbassy implements System {
|
|||||||
moduleCode,
|
moduleCode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly manifest: Manifest,
|
readonly manifest: Manifest,
|
||||||
readonly moduleCode: Partial<U.ExpectedExports>,
|
readonly moduleCode: Partial<U.ExpectedExports>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init(): Promise<void> {}
|
async actionsMetadata(effects: T.Effects): Promise<T.ActionMetadata[]> {
|
||||||
|
const actions = Object.entries(this.manifest.actions ?? {})
|
||||||
|
return Promise.all(
|
||||||
|
actions.map(async ([actionId, action]): Promise<T.ActionMetadata> => {
|
||||||
|
const name = action.name ?? actionId
|
||||||
|
const description = action.description
|
||||||
|
const warning = action.warning ?? null
|
||||||
|
const disabled = false
|
||||||
|
const input = (await convertToNewConfig(action["input-spec"] as any))
|
||||||
|
.spec
|
||||||
|
const hasRunning = !!action["allowed-statuses"].find(
|
||||||
|
(x) => x === "running",
|
||||||
|
)
|
||||||
|
const hasStopped = !!action["allowed-statuses"].find(
|
||||||
|
(x) => x === "stopped",
|
||||||
|
)
|
||||||
|
// prettier-ignore
|
||||||
|
const allowedStatuses =
|
||||||
|
hasRunning && hasStopped ? "any":
|
||||||
|
hasRunning ? "onlyRunning" :
|
||||||
|
"onlyStopped"
|
||||||
|
|
||||||
|
const group = null
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
warning,
|
||||||
|
disabled,
|
||||||
|
allowedStatuses,
|
||||||
|
group,
|
||||||
|
input,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async containerInit(): Promise<void> {}
|
||||||
|
|
||||||
async exit(): Promise<void> {
|
async exit(): Promise<void> {
|
||||||
if (this.currentRunning) await this.currentRunning.clean()
|
if (this.currentRunning) await this.currentRunning.clean()
|
||||||
@@ -235,141 +308,7 @@ export class SystemForEmbassy implements System {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(
|
async packageInit(
|
||||||
effects: Effects,
|
|
||||||
options: {
|
|
||||||
procedure: JsonPath
|
|
||||||
input?: unknown
|
|
||||||
timeout?: number | undefined
|
|
||||||
},
|
|
||||||
): Promise<RpcResult> {
|
|
||||||
return this._execute(effects, options)
|
|
||||||
.then((x) =>
|
|
||||||
matches(x)
|
|
||||||
.when(
|
|
||||||
object({
|
|
||||||
result: any,
|
|
||||||
}),
|
|
||||||
(x) => x,
|
|
||||||
)
|
|
||||||
.when(
|
|
||||||
object({
|
|
||||||
error: string,
|
|
||||||
}),
|
|
||||||
(x) => ({
|
|
||||||
error: {
|
|
||||||
code: 0,
|
|
||||||
message: x.error,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.when(
|
|
||||||
object({
|
|
||||||
"error-code": tuple(number, string),
|
|
||||||
}),
|
|
||||||
({ "error-code": [code, message] }) => ({
|
|
||||||
error: {
|
|
||||||
code,
|
|
||||||
message,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.defaultTo({ result: x }),
|
|
||||||
)
|
|
||||||
.catch((error: unknown) => {
|
|
||||||
if (error instanceof Error)
|
|
||||||
return {
|
|
||||||
error: {
|
|
||||||
code: 0,
|
|
||||||
message: error.name,
|
|
||||||
data: {
|
|
||||||
details: error.message,
|
|
||||||
debug: `${error?.cause ?? "[noCause]"}:${error?.stack ?? "[noStack]"}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if (matchRpcResult.test(error)) return error
|
|
||||||
return {
|
|
||||||
error: {
|
|
||||||
code: 0,
|
|
||||||
message: String(error),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
async _execute(
|
|
||||||
effects: Effects,
|
|
||||||
options: {
|
|
||||||
procedure: JsonPath
|
|
||||||
input?: unknown
|
|
||||||
timeout?: number | undefined
|
|
||||||
},
|
|
||||||
): Promise<unknown> {
|
|
||||||
const input = options.input
|
|
||||||
switch (options.procedure) {
|
|
||||||
case "/backup/create":
|
|
||||||
return this.createBackup(effects, options.timeout || null)
|
|
||||||
case "/backup/restore":
|
|
||||||
return this.restoreBackup(effects, options.timeout || null)
|
|
||||||
case "/config/get":
|
|
||||||
return this.getConfig(effects, options.timeout || null)
|
|
||||||
case "/config/set":
|
|
||||||
return this.setConfig(effects, input, options.timeout || null)
|
|
||||||
case "/properties":
|
|
||||||
return this.properties(effects, options.timeout || null)
|
|
||||||
case "/actions/metadata":
|
|
||||||
return todo()
|
|
||||||
case "/init":
|
|
||||||
return this.initProcedure(
|
|
||||||
effects,
|
|
||||||
string.optional().unsafeCast(input),
|
|
||||||
options.timeout || null,
|
|
||||||
)
|
|
||||||
case "/uninit":
|
|
||||||
return this.uninit(
|
|
||||||
effects,
|
|
||||||
string.optional().unsafeCast(input),
|
|
||||||
options.timeout || null,
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
const procedures = unNestPath(options.procedure)
|
|
||||||
switch (true) {
|
|
||||||
case procedures[1] === "actions" && procedures[3] === "get":
|
|
||||||
return this.action(
|
|
||||||
effects,
|
|
||||||
procedures[2],
|
|
||||||
input,
|
|
||||||
options.timeout || null,
|
|
||||||
)
|
|
||||||
case procedures[1] === "actions" && procedures[3] === "run":
|
|
||||||
return this.action(
|
|
||||||
effects,
|
|
||||||
procedures[2],
|
|
||||||
input,
|
|
||||||
options.timeout || null,
|
|
||||||
)
|
|
||||||
case procedures[1] === "dependencies" && procedures[3] === "query":
|
|
||||||
return null
|
|
||||||
|
|
||||||
case procedures[1] === "dependencies" && procedures[3] === "update":
|
|
||||||
return this.dependenciesAutoconfig(
|
|
||||||
effects,
|
|
||||||
procedures[2],
|
|
||||||
input,
|
|
||||||
options.timeout || null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error(`Could not find the path for ${options.procedure}`)
|
|
||||||
}
|
|
||||||
async sandbox(
|
|
||||||
effects: Effects,
|
|
||||||
options: { procedure: Procedure; input: unknown; timeout?: number },
|
|
||||||
): Promise<RpcResult> {
|
|
||||||
return this.execute(effects, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
private async initProcedure(
|
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
previousVersion: Optional<string>,
|
previousVersion: Optional<string>,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
@@ -445,7 +384,6 @@ export class SystemForEmbassy implements System {
|
|||||||
id: `${id}-${internal}`,
|
id: `${id}-${internal}`,
|
||||||
description: interfaceValue.description,
|
description: interfaceValue.description,
|
||||||
hasPrimary: false,
|
hasPrimary: false,
|
||||||
disabled: false,
|
|
||||||
type:
|
type:
|
||||||
interfaceValue.ui &&
|
interfaceValue.ui &&
|
||||||
(origin.scheme === "http" || origin.sslScheme === "https")
|
(origin.scheme === "http" || origin.sslScheme === "https")
|
||||||
@@ -490,7 +428,7 @@ export class SystemForEmbassy implements System {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private async uninit(
|
async packageUninit(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
nextVersion: Optional<string>,
|
nextVersion: Optional<string>,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
@@ -499,7 +437,7 @@ export class SystemForEmbassy implements System {
|
|||||||
await effects.setMainStatus({ status: "stopped" })
|
await effects.setMainStatus({ status: "stopped" })
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createBackup(
|
async createBackup(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -520,7 +458,7 @@ export class SystemForEmbassy implements System {
|
|||||||
await moduleCode.createBackup?.(polyfillEffects(effects, this.manifest))
|
await moduleCode.createBackup?.(polyfillEffects(effects, this.manifest))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private async restoreBackup(
|
async restoreBackup(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -544,7 +482,7 @@ export class SystemForEmbassy implements System {
|
|||||||
await moduleCode.restoreBackup?.(polyfillEffects(effects, this.manifest))
|
await moduleCode.restoreBackup?.(polyfillEffects(effects, this.manifest))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private async getConfig(
|
async getConfig(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
): Promise<T.ConfigRes> {
|
): Promise<T.ConfigRes> {
|
||||||
@@ -585,7 +523,7 @@ export class SystemForEmbassy implements System {
|
|||||||
)) as any
|
)) as any
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private async setConfig(
|
async setConfig(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
newConfigWithoutPointers: unknown,
|
newConfigWithoutPointers: unknown,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
@@ -677,7 +615,7 @@ export class SystemForEmbassy implements System {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async migration(
|
async migration(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
fromVersion: string,
|
fromVersion: string,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
@@ -749,10 +687,10 @@ export class SystemForEmbassy implements System {
|
|||||||
}
|
}
|
||||||
return { configured: true }
|
return { configured: true }
|
||||||
}
|
}
|
||||||
private async properties(
|
async properties(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
): Promise<ReturnType<T.ExpectedExports.properties>> {
|
): Promise<T.PropertiesReturn> {
|
||||||
// TODO BLU-J set the properties ever so often
|
// TODO BLU-J set the properties ever so often
|
||||||
const setConfigValue = this.manifest.properties
|
const setConfigValue = this.manifest.properties
|
||||||
if (!setConfigValue) throw new Error("There is no properties")
|
if (!setConfigValue) throw new Error("There is no properties")
|
||||||
@@ -780,23 +718,80 @@ export class SystemForEmbassy implements System {
|
|||||||
if (!method)
|
if (!method)
|
||||||
throw new Error("Expecting that the method properties exists")
|
throw new Error("Expecting that the method properties exists")
|
||||||
const properties = matchProperties.unsafeCast(
|
const properties = matchProperties.unsafeCast(
|
||||||
await method(polyfillEffects(effects, this.manifest)).then((x) => {
|
await method(polyfillEffects(effects, this.manifest)).then(
|
||||||
if ("result" in x) return x.result
|
fromReturnType,
|
||||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
),
|
||||||
throw new Error("Error getting config: " + x["error-code"][1])
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
return asProperty(properties.data)
|
return asProperty(properties.data)
|
||||||
}
|
}
|
||||||
throw new Error(`Unknown type in the fetch properties: ${setConfigValue}`)
|
throw new Error(`Unknown type in the fetch properties: ${setConfigValue}`)
|
||||||
}
|
}
|
||||||
private async action(
|
async action(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
actionId: string,
|
actionId: string,
|
||||||
formData: unknown,
|
formData: unknown,
|
||||||
timeoutMs: number | null,
|
timeoutMs: number | null,
|
||||||
): Promise<T.ActionResult> {
|
): Promise<T.ActionResult> {
|
||||||
const actionProcedure = this.manifest.actions?.[actionId]?.implementation
|
const actionProcedure = this.manifest.actions?.[actionId]?.implementation
|
||||||
|
const toActionResult = ({
|
||||||
|
message,
|
||||||
|
value = "",
|
||||||
|
copyable,
|
||||||
|
qr,
|
||||||
|
}: U.ActionResult): T.ActionResult => ({
|
||||||
|
version: "0",
|
||||||
|
message,
|
||||||
|
value,
|
||||||
|
copyable,
|
||||||
|
qr,
|
||||||
|
})
|
||||||
|
if (!actionProcedure) throw Error("Action not found")
|
||||||
|
if (actionProcedure.type === "docker") {
|
||||||
|
const subcontainer = actionProcedure.inject
|
||||||
|
? this.currentRunning?.mainSubContainerHandle
|
||||||
|
: undefined
|
||||||
|
const container = await DockerProcedureContainer.of(
|
||||||
|
effects,
|
||||||
|
this.manifest.id,
|
||||||
|
actionProcedure,
|
||||||
|
this.manifest.volumes,
|
||||||
|
{
|
||||||
|
subcontainer,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return toActionResult(
|
||||||
|
JSON.parse(
|
||||||
|
(
|
||||||
|
await container.execFail(
|
||||||
|
[
|
||||||
|
actionProcedure.entrypoint,
|
||||||
|
...actionProcedure.args,
|
||||||
|
JSON.stringify(formData),
|
||||||
|
],
|
||||||
|
timeoutMs,
|
||||||
|
)
|
||||||
|
).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(
|
||||||
|
polyfillEffects(effects, this.manifest),
|
||||||
|
formData as any,
|
||||||
|
)
|
||||||
|
.then(fromReturnType)
|
||||||
|
.then(toActionResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async dependenciesCheck(
|
||||||
|
effects: Effects,
|
||||||
|
id: string,
|
||||||
|
oldConfig: unknown,
|
||||||
|
timeoutMs: number | null,
|
||||||
|
): Promise<object> {
|
||||||
|
const actionProcedure = this.manifest.dependencies?.[id]?.config?.check
|
||||||
if (!actionProcedure) return { message: "Action not found", value: null }
|
if (!actionProcedure) return { message: "Action not found", value: null }
|
||||||
if (actionProcedure.type === "docker") {
|
if (actionProcedure.type === "docker") {
|
||||||
const container = await DockerProcedureContainer.of(
|
const container = await DockerProcedureContainer.of(
|
||||||
@@ -811,27 +806,32 @@ export class SystemForEmbassy implements System {
|
|||||||
[
|
[
|
||||||
actionProcedure.entrypoint,
|
actionProcedure.entrypoint,
|
||||||
...actionProcedure.args,
|
...actionProcedure.args,
|
||||||
JSON.stringify(formData),
|
JSON.stringify(oldConfig),
|
||||||
],
|
],
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
)
|
)
|
||||||
).stdout.toString(),
|
).stdout.toString(),
|
||||||
)
|
)
|
||||||
} else {
|
} else if (actionProcedure.type === "script") {
|
||||||
const moduleCode = await this.moduleCode
|
const moduleCode = await this.moduleCode
|
||||||
const method = moduleCode.action?.[actionId]
|
const method = moduleCode.dependencies?.[id]?.check
|
||||||
if (!method) throw new Error("Expecting that the method action exists")
|
if (!method)
|
||||||
|
throw new Error(
|
||||||
|
`Expecting that the method dependency check ${id} exists`,
|
||||||
|
)
|
||||||
return (await method(
|
return (await method(
|
||||||
polyfillEffects(effects, this.manifest),
|
polyfillEffects(effects, this.manifest),
|
||||||
formData as any,
|
oldConfig as any,
|
||||||
).then((x) => {
|
).then((x) => {
|
||||||
if ("result" in x) return x.result
|
if ("result" in x) return x.result
|
||||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||||
throw new Error("Error getting config: " + x["error-code"][1])
|
throw new Error("Error getting config: " + x["error-code"][1])
|
||||||
})) as any
|
})) as any
|
||||||
|
} else {
|
||||||
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private async dependenciesAutoconfig(
|
async dependenciesAutoconfig(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
id: string,
|
id: string,
|
||||||
input: unknown,
|
input: unknown,
|
||||||
@@ -982,7 +982,10 @@ async function updateConfig(
|
|||||||
})
|
})
|
||||||
.once()
|
.once()
|
||||||
.catch((x) => {
|
.catch((x) => {
|
||||||
console.error("Could not get the service interface", x)
|
console.error(
|
||||||
|
"Could not get the service interface",
|
||||||
|
utils.asError(x),
|
||||||
|
)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
const catchFn = <X>(fn: () => X) => {
|
const catchFn = <X>(fn: () => X) => {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { matchManifest } from "./matchManifest"
|
||||||
|
import giteaManifest from "./__fixtures__/giteaManifest"
|
||||||
|
import synapseManifest from "./__fixtures__/synapseManifest"
|
||||||
|
|
||||||
|
describe("matchManifest", () => {
|
||||||
|
test("gittea", () => {
|
||||||
|
matchManifest.unsafeCast(giteaManifest)
|
||||||
|
})
|
||||||
|
test("synapse", () => {
|
||||||
|
matchManifest.unsafeCast(synapseManifest)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -55,10 +55,13 @@ export const matchManifest = object(
|
|||||||
string,
|
string,
|
||||||
every(
|
every(
|
||||||
matchProcedure,
|
matchProcedure,
|
||||||
object({
|
object(
|
||||||
name: string,
|
{
|
||||||
["success-message"]: string,
|
name: string,
|
||||||
}),
|
["success-message"]: string,
|
||||||
|
},
|
||||||
|
["success-message"],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
config: object({
|
config: object({
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as oet from "./oldEmbassyTypes"
|
|||||||
import { Volume } from "../../../Models/Volume"
|
import { Volume } from "../../../Models/Volume"
|
||||||
import * as child_process from "child_process"
|
import * as child_process from "child_process"
|
||||||
import { promisify } from "util"
|
import { promisify } from "util"
|
||||||
import { daemons, startSdk, T } from "@start9labs/start-sdk"
|
import { daemons, startSdk, T, utils } from "@start9labs/start-sdk"
|
||||||
import "isomorphic-fetch"
|
import "isomorphic-fetch"
|
||||||
import { Manifest } from "./matchManifest"
|
import { Manifest } from "./matchManifest"
|
||||||
import { DockerProcedureContainer } from "./DockerProcedureContainer"
|
import { DockerProcedureContainer } from "./DockerProcedureContainer"
|
||||||
@@ -124,20 +124,18 @@ export const polyfillEffects = (
|
|||||||
wait(): Promise<oet.ResultType<string>>
|
wait(): Promise<oet.ResultType<string>>
|
||||||
term(): Promise<void>
|
term(): Promise<void>
|
||||||
} {
|
} {
|
||||||
const dockerProcedureContainer = DockerProcedureContainer.of(
|
const promiseSubcontainer = DockerProcedureContainer.createSubContainer(
|
||||||
effects,
|
effects,
|
||||||
manifest.id,
|
manifest.id,
|
||||||
manifest.main,
|
manifest.main,
|
||||||
manifest.volumes,
|
manifest.volumes,
|
||||||
)
|
)
|
||||||
const daemon = dockerProcedureContainer.then((dockerProcedureContainer) =>
|
const daemon = promiseSubcontainer.then((subcontainer) =>
|
||||||
daemons.runCommand()(
|
daemons.runCommand()(
|
||||||
effects,
|
effects,
|
||||||
{ id: manifest.main.image },
|
subcontainer,
|
||||||
[input.command, ...(input.args || [])],
|
[input.command, ...(input.args || [])],
|
||||||
{
|
{},
|
||||||
overlay: dockerProcedureContainer.overlay,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
@@ -224,16 +222,16 @@ export const polyfillEffects = (
|
|||||||
return new Promise((resolve) => setTimeout(resolve, timeMs))
|
return new Promise((resolve) => setTimeout(resolve, timeMs))
|
||||||
},
|
},
|
||||||
trace(whatToPrint: string): void {
|
trace(whatToPrint: string): void {
|
||||||
console.trace(whatToPrint)
|
console.trace(utils.asError(whatToPrint))
|
||||||
},
|
},
|
||||||
warn(whatToPrint: string): void {
|
warn(whatToPrint: string): void {
|
||||||
console.warn(whatToPrint)
|
console.warn(utils.asError(whatToPrint))
|
||||||
},
|
},
|
||||||
error(whatToPrint: string): void {
|
error(whatToPrint: string): void {
|
||||||
console.error(whatToPrint)
|
console.error(utils.asError(whatToPrint))
|
||||||
},
|
},
|
||||||
debug(whatToPrint: string): void {
|
debug(whatToPrint: string): void {
|
||||||
console.debug(whatToPrint)
|
console.debug(utils.asError(whatToPrint))
|
||||||
},
|
},
|
||||||
info(whatToPrint: string): void {
|
info(whatToPrint: string): void {
|
||||||
console.log(false)
|
console.log(false)
|
||||||
@@ -357,7 +355,7 @@ export const polyfillEffects = (
|
|||||||
})
|
})
|
||||||
|
|
||||||
spawned.stderr.on("data", (data: unknown) => {
|
spawned.stderr.on("data", (data: unknown) => {
|
||||||
console.error(String(data))
|
console.error(`polyfill.runAsync`, utils.asError(data))
|
||||||
})
|
})
|
||||||
|
|
||||||
const id = async () => {
|
const id = async () => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import fixtureEmbasyPagesConfig from "./__fixtures__/embasyPagesConfig"
|
|||||||
import searNXG from "./__fixtures__/searNXG"
|
import searNXG from "./__fixtures__/searNXG"
|
||||||
import bitcoind from "./__fixtures__/bitcoind"
|
import bitcoind from "./__fixtures__/bitcoind"
|
||||||
import nostr from "./__fixtures__/nostr"
|
import nostr from "./__fixtures__/nostr"
|
||||||
|
import nostrConfig2 from "./__fixtures__/nostrConfig2"
|
||||||
|
|
||||||
describe("transformConfigSpec", () => {
|
describe("transformConfigSpec", () => {
|
||||||
test("matchOldConfigSpec(embassyPages.homepage.variants[web-page])", () => {
|
test("matchOldConfigSpec(embassyPages.homepage.variants[web-page])", () => {
|
||||||
@@ -30,4 +31,8 @@ describe("transformConfigSpec", () => {
|
|||||||
const spec = matchOldConfigSpec.unsafeCast(nostr)
|
const spec = matchOldConfigSpec.unsafeCast(nostr)
|
||||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
test("transformConfigSpec(nostr2)", () => {
|
||||||
|
const spec = matchOldConfigSpec.unsafeCast(nostrConfig2)
|
||||||
|
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec {
|
|||||||
integer: oldVal.integral,
|
integer: oldVal.integral,
|
||||||
step: null,
|
step: null,
|
||||||
units: oldVal.units || null,
|
units: oldVal.units || null,
|
||||||
placeholder: oldVal.placeholder || null,
|
placeholder: oldVal.placeholder ? String(oldVal.placeholder) : null,
|
||||||
}
|
}
|
||||||
} else if (oldVal.type === "object") {
|
} else if (oldVal.type === "object") {
|
||||||
newVal = {
|
newVal = {
|
||||||
@@ -267,6 +267,31 @@ function getListSpec(
|
|||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
} else if (isNumberList(oldVal)) {
|
||||||
|
return {
|
||||||
|
...partial,
|
||||||
|
type: "list",
|
||||||
|
default: oldVal.default.map(String) as string[],
|
||||||
|
spec: {
|
||||||
|
type: "text",
|
||||||
|
patterns: oldVal.spec.integral
|
||||||
|
? [{ regex: "[0-9]+", description: "Integral number type" }]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
regex: "[-+]?[0-9]*\\.?[0-9]+",
|
||||||
|
description: "Number type",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
minLength: null,
|
||||||
|
maxLength: null,
|
||||||
|
masked: false,
|
||||||
|
generate: null,
|
||||||
|
inputmode: "text",
|
||||||
|
placeholder: oldVal.spec.placeholder
|
||||||
|
? String(oldVal.spec.placeholder)
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
} else if (isStringList(oldVal)) {
|
} else if (isStringList(oldVal)) {
|
||||||
return {
|
return {
|
||||||
...partial,
|
...partial,
|
||||||
@@ -337,11 +362,16 @@ function isStringList(
|
|||||||
): val is OldValueSpecList & { subtype: "string" } {
|
): val is OldValueSpecList & { subtype: "string" } {
|
||||||
return val.subtype === "string"
|
return val.subtype === "string"
|
||||||
}
|
}
|
||||||
|
function isNumberList(
|
||||||
|
val: OldValueSpecList,
|
||||||
|
): val is OldValueSpecList & { subtype: "number" } {
|
||||||
|
return val.subtype === "number"
|
||||||
|
}
|
||||||
|
|
||||||
function isObjectList(
|
function isObjectList(
|
||||||
val: OldValueSpecList,
|
val: OldValueSpecList,
|
||||||
): val is OldValueSpecList & { subtype: "object" } {
|
): val is OldValueSpecList & { subtype: "object" } {
|
||||||
if (["number", "union"].includes(val.subtype)) {
|
if (["union"].includes(val.subtype)) {
|
||||||
throw new Error("Invalid list subtype. enum, string, and object permitted.")
|
throw new Error("Invalid list subtype. enum, string, and object permitted.")
|
||||||
}
|
}
|
||||||
return val.subtype === "object"
|
return val.subtype === "object"
|
||||||
@@ -398,7 +428,7 @@ export const matchOldValueSpecNumber = object(
|
|||||||
description: string,
|
description: string,
|
||||||
warning: string,
|
warning: string,
|
||||||
units: string,
|
units: string,
|
||||||
placeholder: string,
|
placeholder: anyOf(number, string),
|
||||||
},
|
},
|
||||||
["default", "description", "warning", "units", "placeholder"],
|
["default", "description", "warning", "units", "placeholder"],
|
||||||
)
|
)
|
||||||
@@ -499,6 +529,15 @@ const matchOldListValueSpecEnum = object({
|
|||||||
values: array(string),
|
values: array(string),
|
||||||
"value-names": dictionary([string, string]),
|
"value-names": dictionary([string, string]),
|
||||||
})
|
})
|
||||||
|
const matchOldListValueSpecNumber = object(
|
||||||
|
{
|
||||||
|
range: string,
|
||||||
|
integral: boolean,
|
||||||
|
units: string,
|
||||||
|
placeholder: anyOf(number, string),
|
||||||
|
},
|
||||||
|
["units", "placeholder"],
|
||||||
|
)
|
||||||
|
|
||||||
// represents a spec for a list
|
// represents a spec for a list
|
||||||
const matchOldValueSpecList = every(
|
const matchOldValueSpecList = every(
|
||||||
@@ -531,6 +570,10 @@ const matchOldValueSpecList = every(
|
|||||||
subtype: literals("object"),
|
subtype: literals("object"),
|
||||||
spec: matchOldListValueSpecObject,
|
spec: matchOldListValueSpecObject,
|
||||||
}),
|
}),
|
||||||
|
object({
|
||||||
|
subtype: literals("number"),
|
||||||
|
spec: matchOldListValueSpecNumber,
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
type OldValueSpecList = typeof matchOldValueSpecList._TYPE
|
type OldValueSpecList = typeof matchOldValueSpecList._TYPE
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import matches, { any, number, object, string, tuple } from "ts-matches"
|
|||||||
import { Effects } from "../../Models/Effects"
|
import { Effects } from "../../Models/Effects"
|
||||||
import { RpcResult, matchRpcResult } from "../RpcListener"
|
import { RpcResult, matchRpcResult } from "../RpcListener"
|
||||||
import { duration } from "../../Models/Duration"
|
import { duration } from "../../Models/Duration"
|
||||||
import { T } from "@start9labs/start-sdk"
|
import { T, utils } from "@start9labs/start-sdk"
|
||||||
import { Volume } from "../../Models/Volume"
|
import { Volume } from "../../Models/Volume"
|
||||||
import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk"
|
import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk"
|
||||||
import { CallbackHolder } from "../../Models/CallbackHolder"
|
import { CallbackHolder } from "../../Models/CallbackHolder"
|
||||||
|
import { Optional } from "ts-matches/lib/parsers/interfaces"
|
||||||
|
|
||||||
export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js"
|
export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js"
|
||||||
|
|
||||||
@@ -25,6 +26,107 @@ export class SystemForStartOs implements System {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(readonly abi: T.ABI) {}
|
constructor(readonly abi: T.ABI) {}
|
||||||
|
containerInit(): Promise<void> {
|
||||||
|
throw new Error("Method not implemented.")
|
||||||
|
}
|
||||||
|
async packageInit(
|
||||||
|
effects: Effects,
|
||||||
|
previousVersion: Optional<string> = null,
|
||||||
|
timeoutMs: number | null = null,
|
||||||
|
): Promise<void> {
|
||||||
|
return void (await this.abi.init({ effects }))
|
||||||
|
}
|
||||||
|
async packageUninit(
|
||||||
|
effects: Effects,
|
||||||
|
nextVersion: Optional<string> = null,
|
||||||
|
timeoutMs: number | null = null,
|
||||||
|
): Promise<void> {
|
||||||
|
return void (await this.abi.uninit({ effects, nextVersion }))
|
||||||
|
}
|
||||||
|
async createBackup(
|
||||||
|
effects: T.Effects,
|
||||||
|
timeoutMs: number | null,
|
||||||
|
): Promise<void> {
|
||||||
|
return void (await this.abi.createBackup({
|
||||||
|
effects,
|
||||||
|
pathMaker: ((options) =>
|
||||||
|
new Volume(options.volume, options.path).path) as T.PathMaker,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
async restoreBackup(
|
||||||
|
effects: T.Effects,
|
||||||
|
timeoutMs: number | null,
|
||||||
|
): Promise<void> {
|
||||||
|
return void (await this.abi.restoreBackup({
|
||||||
|
effects,
|
||||||
|
pathMaker: ((options) =>
|
||||||
|
new Volume(options.volume, options.path).path) as T.PathMaker,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
getConfig(
|
||||||
|
effects: T.Effects,
|
||||||
|
timeoutMs: number | null,
|
||||||
|
): Promise<T.ConfigRes> {
|
||||||
|
return this.abi.getConfig({ effects })
|
||||||
|
}
|
||||||
|
async setConfig(
|
||||||
|
effects: Effects,
|
||||||
|
input: { effects: Effects; input: Record<string, unknown> },
|
||||||
|
timeoutMs: number | null,
|
||||||
|
): Promise<void> {
|
||||||
|
const _: unknown = await this.abi.setConfig({ effects, input })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
migration(
|
||||||
|
effects: Effects,
|
||||||
|
fromVersion: string,
|
||||||
|
timeoutMs: number | null,
|
||||||
|
): Promise<T.MigrationRes> {
|
||||||
|
throw new Error("Method not implemented.")
|
||||||
|
}
|
||||||
|
properties(
|
||||||
|
effects: Effects,
|
||||||
|
timeoutMs: number | null,
|
||||||
|
): Promise<T.PropertiesReturn> {
|
||||||
|
throw new Error("Method not implemented.")
|
||||||
|
}
|
||||||
|
async action(
|
||||||
|
effects: Effects,
|
||||||
|
id: string,
|
||||||
|
formData: unknown,
|
||||||
|
timeoutMs: number | null,
|
||||||
|
): Promise<T.ActionResult> {
|
||||||
|
const action = (await this.abi.actions({ effects }))[id]
|
||||||
|
if (!action) throw new Error(`Action ${id} not found`)
|
||||||
|
return action.run({ effects })
|
||||||
|
}
|
||||||
|
dependenciesCheck(
|
||||||
|
effects: Effects,
|
||||||
|
id: string,
|
||||||
|
oldConfig: unknown,
|
||||||
|
timeoutMs: number | null,
|
||||||
|
): Promise<any> {
|
||||||
|
const dependencyConfig = this.abi.dependencyConfig[id]
|
||||||
|
if (!dependencyConfig) throw new Error(`dependencyConfig ${id} not found`)
|
||||||
|
return dependencyConfig.query({ effects })
|
||||||
|
}
|
||||||
|
async dependenciesAutoconfig(
|
||||||
|
effects: Effects,
|
||||||
|
id: string,
|
||||||
|
remoteConfig: unknown,
|
||||||
|
timeoutMs: number | null,
|
||||||
|
): Promise<void> {
|
||||||
|
const dependencyConfig = this.abi.dependencyConfig[id]
|
||||||
|
if (!dependencyConfig) throw new Error(`dependencyConfig ${id} not found`)
|
||||||
|
const queryResults = await this.getConfig(effects, timeoutMs)
|
||||||
|
return void (await dependencyConfig.update({
|
||||||
|
queryResults,
|
||||||
|
remoteConfig,
|
||||||
|
})) // TODO
|
||||||
|
}
|
||||||
|
async actionsMetadata(effects: T.Effects): Promise<T.ActionMetadata[]> {
|
||||||
|
return this.abi.actionsMetadata({ effects })
|
||||||
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {}
|
async init(): Promise<void> {}
|
||||||
|
|
||||||
@@ -57,7 +159,9 @@ export class SystemForStartOs implements System {
|
|||||||
if (this.runningMain) {
|
if (this.runningMain) {
|
||||||
this.runningMain.callbacks
|
this.runningMain.callbacks
|
||||||
.callCallback(callback, args)
|
.callCallback(callback, args)
|
||||||
.catch((error) => console.error(`callback ${callback} failed`, error))
|
.catch((error) =>
|
||||||
|
console.error(`callback ${callback} failed`, utils.asError(error)),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
console.warn(`callback ${callback} ignored because system is not running`)
|
console.warn(`callback ${callback} ignored because system is not running`)
|
||||||
}
|
}
|
||||||
@@ -70,157 +174,4 @@ export class SystemForStartOs implements System {
|
|||||||
this.runningMain = undefined
|
this.runningMain = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(
|
|
||||||
effects: Effects,
|
|
||||||
options: {
|
|
||||||
procedure: Procedure
|
|
||||||
input?: unknown
|
|
||||||
timeout?: number | undefined
|
|
||||||
},
|
|
||||||
): Promise<RpcResult> {
|
|
||||||
return this._execute(effects, options)
|
|
||||||
.then((x) =>
|
|
||||||
matches(x)
|
|
||||||
.when(
|
|
||||||
object({
|
|
||||||
result: any,
|
|
||||||
}),
|
|
||||||
(x) => x,
|
|
||||||
)
|
|
||||||
.when(
|
|
||||||
object({
|
|
||||||
error: string,
|
|
||||||
}),
|
|
||||||
(x) => ({
|
|
||||||
error: {
|
|
||||||
code: 0,
|
|
||||||
message: x.error,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.when(
|
|
||||||
object({
|
|
||||||
"error-code": tuple(number, string),
|
|
||||||
}),
|
|
||||||
({ "error-code": [code, message] }) => ({
|
|
||||||
error: {
|
|
||||||
code,
|
|
||||||
message,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.defaultTo({ result: x }),
|
|
||||||
)
|
|
||||||
.catch((error: unknown) => {
|
|
||||||
if (error instanceof Error)
|
|
||||||
return {
|
|
||||||
error: {
|
|
||||||
code: 0,
|
|
||||||
message: error.name,
|
|
||||||
data: {
|
|
||||||
details: error.message,
|
|
||||||
debug: `${error?.cause ?? "[noCause]"}:${error?.stack ?? "[noStack]"}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if (matchRpcResult.test(error)) return error
|
|
||||||
return {
|
|
||||||
error: {
|
|
||||||
code: 0,
|
|
||||||
message: String(error),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
async _execute(
|
|
||||||
effects: Effects | MainEffects,
|
|
||||||
options: {
|
|
||||||
procedure: Procedure
|
|
||||||
input?: unknown
|
|
||||||
timeout?: number | undefined
|
|
||||||
},
|
|
||||||
): Promise<unknown> {
|
|
||||||
switch (options.procedure) {
|
|
||||||
case "/init": {
|
|
||||||
const previousVersion =
|
|
||||||
string.optional().unsafeCast(options.input) || null
|
|
||||||
return this.abi.init({ effects, previousVersion })
|
|
||||||
}
|
|
||||||
case "/uninit": {
|
|
||||||
const nextVersion = string.optional().unsafeCast(options.input) || null
|
|
||||||
return this.abi.uninit({ effects, nextVersion })
|
|
||||||
}
|
|
||||||
// case "/main/start": {
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
// case "/main/stop": {
|
|
||||||
// if (this.onTerm) await this.onTerm()
|
|
||||||
// await effects.setMainStatus({ status: "stopped" })
|
|
||||||
// delete this.onTerm
|
|
||||||
// return duration(30, "s")
|
|
||||||
// }
|
|
||||||
case "/config/set": {
|
|
||||||
const input = options.input as any // TODO
|
|
||||||
return this.abi.setConfig({ effects, input })
|
|
||||||
}
|
|
||||||
case "/config/get": {
|
|
||||||
return this.abi.getConfig({ effects })
|
|
||||||
}
|
|
||||||
case "/backup/create":
|
|
||||||
return this.abi.createBackup({
|
|
||||||
effects,
|
|
||||||
pathMaker: ((options) =>
|
|
||||||
new Volume(options.volume, options.path).path) as T.PathMaker,
|
|
||||||
})
|
|
||||||
case "/backup/restore":
|
|
||||||
return this.abi.restoreBackup({
|
|
||||||
effects,
|
|
||||||
pathMaker: ((options) =>
|
|
||||||
new Volume(options.volume, options.path).path) as T.PathMaker,
|
|
||||||
})
|
|
||||||
case "/actions/metadata": {
|
|
||||||
return this.abi.actionsMetadata({ effects })
|
|
||||||
}
|
|
||||||
case "/properties": {
|
|
||||||
throw new Error("TODO")
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
const procedures = unNestPath(options.procedure)
|
|
||||||
const id = procedures[2]
|
|
||||||
switch (true) {
|
|
||||||
case procedures[1] === "actions" && procedures[3] === "get": {
|
|
||||||
const action = (await this.abi.actions({ effects }))[id]
|
|
||||||
if (!action) throw new Error(`Action ${id} not found`)
|
|
||||||
return action.getConfig({ effects })
|
|
||||||
}
|
|
||||||
case procedures[1] === "actions" && procedures[3] === "run": {
|
|
||||||
const action = (await this.abi.actions({ effects }))[id]
|
|
||||||
if (!action) throw new Error(`Action ${id} not found`)
|
|
||||||
return action.run({ effects, input: options.input as any }) // TODO
|
|
||||||
}
|
|
||||||
case procedures[1] === "dependencies" && procedures[3] === "query": {
|
|
||||||
const dependencyConfig = this.abi.dependencyConfig[id]
|
|
||||||
if (!dependencyConfig)
|
|
||||||
throw new Error(`dependencyConfig ${id} not found`)
|
|
||||||
const localConfig = options.input
|
|
||||||
return dependencyConfig.query({ effects })
|
|
||||||
}
|
|
||||||
case procedures[1] === "dependencies" && procedures[3] === "update": {
|
|
||||||
const dependencyConfig = this.abi.dependencyConfig[id]
|
|
||||||
if (!dependencyConfig)
|
|
||||||
throw new Error(`dependencyConfig ${id} not found`)
|
|
||||||
return dependencyConfig.update(options.input as any) // TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sandbox(
|
|
||||||
effects: Effects,
|
|
||||||
options: { procedure: Procedure; input?: unknown; timeout?: number },
|
|
||||||
): Promise<RpcResult> {
|
|
||||||
return this.execute(effects, options)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { RpcResult } from "../Adapters/RpcListener"
|
|||||||
import { Effects } from "../Models/Effects"
|
import { Effects } from "../Models/Effects"
|
||||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||||
import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk"
|
import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk"
|
||||||
|
import { Optional } from "ts-matches/lib/parsers/interfaces"
|
||||||
|
|
||||||
export type Procedure =
|
export type Procedure =
|
||||||
| "/init"
|
| "/init"
|
||||||
@@ -22,28 +23,60 @@ export type ExecuteResult =
|
|||||||
| { ok: unknown }
|
| { ok: unknown }
|
||||||
| { err: { code: number; message: string } }
|
| { err: { code: number; message: string } }
|
||||||
export type System = {
|
export type System = {
|
||||||
init(): Promise<void>
|
containerInit(): Promise<void>
|
||||||
|
|
||||||
start(effects: MainEffects): Promise<void>
|
start(effects: MainEffects): Promise<void>
|
||||||
callCallback(callback: number, args: any[]): void
|
callCallback(callback: number, args: any[]): void
|
||||||
stop(): Promise<void>
|
stop(): Promise<void>
|
||||||
|
|
||||||
execute(
|
packageInit(
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
options: {
|
previousVersion: Optional<string>,
|
||||||
procedure: Procedure
|
timeoutMs: number | null,
|
||||||
input: unknown
|
): Promise<void>
|
||||||
timeout?: number
|
packageUninit(
|
||||||
},
|
|
||||||
): Promise<RpcResult>
|
|
||||||
sandbox(
|
|
||||||
effects: Effects,
|
effects: Effects,
|
||||||
options: {
|
nextVersion: Optional<string>,
|
||||||
procedure: Procedure
|
timeoutMs: number | null,
|
||||||
input: unknown
|
): Promise<void>
|
||||||
timeout?: number
|
|
||||||
},
|
createBackup(effects: T.Effects, timeoutMs: number | null): Promise<void>
|
||||||
): Promise<RpcResult>
|
restoreBackup(effects: T.Effects, timeoutMs: number | null): Promise<void>
|
||||||
|
getConfig(effects: T.Effects, timeoutMs: number | null): Promise<T.ConfigRes>
|
||||||
|
setConfig(
|
||||||
|
effects: Effects,
|
||||||
|
input: { effects: Effects; input: Record<string, unknown> },
|
||||||
|
timeoutMs: number | null,
|
||||||
|
): Promise<void>
|
||||||
|
migration(
|
||||||
|
effects: Effects,
|
||||||
|
fromVersion: string,
|
||||||
|
timeoutMs: number | null,
|
||||||
|
): Promise<T.MigrationRes>
|
||||||
|
properties(
|
||||||
|
effects: Effects,
|
||||||
|
timeoutMs: number | null,
|
||||||
|
): Promise<T.PropertiesReturn>
|
||||||
|
action(
|
||||||
|
effects: Effects,
|
||||||
|
actionId: string,
|
||||||
|
formData: unknown,
|
||||||
|
timeoutMs: number | null,
|
||||||
|
): Promise<T.ActionResult>
|
||||||
|
|
||||||
|
dependenciesCheck(
|
||||||
|
effects: Effects,
|
||||||
|
id: string,
|
||||||
|
oldConfig: unknown,
|
||||||
|
timeoutMs: number | null,
|
||||||
|
): Promise<any>
|
||||||
|
dependenciesAutoconfig(
|
||||||
|
effects: Effects,
|
||||||
|
id: string,
|
||||||
|
oldConfig: unknown,
|
||||||
|
timeoutMs: number | null,
|
||||||
|
): Promise<void>
|
||||||
|
actionsMetadata(effects: T.Effects): Promise<T.ActionMetadata[]>
|
||||||
|
|
||||||
exit(): Promise<void>
|
exit(): Promise<void>
|
||||||
}
|
}
|
||||||
|
|||||||
467
core/Cargo.lock
generated
467
core/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -2,13 +2,17 @@
|
|||||||
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
set -e
|
set -ea
|
||||||
shopt -s expand_aliases
|
shopt -s expand_aliases
|
||||||
|
|
||||||
if [ -z "$ARCH" ]; then
|
if [ -z "$ARCH" ]; then
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$ARCH" = "arm64" ]; then
|
||||||
|
ARCH="aarch64"
|
||||||
|
fi
|
||||||
|
|
||||||
USE_TTY=
|
USE_TTY=
|
||||||
if tty -s; then
|
if tty -s; then
|
||||||
USE_TTY="-it"
|
USE_TTY="-it"
|
||||||
@@ -24,16 +28,9 @@ fi
|
|||||||
|
|
||||||
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
||||||
|
|
||||||
set +e
|
|
||||||
fail=
|
|
||||||
echo "FEATURES=\"$FEATURES\""
|
echo "FEATURES=\"$FEATURES\""
|
||||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||||
if ! rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features container-runtime,$FEATURES --locked --bin containerbox --target=$ARCH-unknown-linux-musl && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"; then
|
rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features container-runtime,$FEATURES --locked --bin containerbox --target=$ARCH-unknown-linux-musl"
|
||||||
fail=true
|
if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/release/containerbox | awk '{ print $3 }')" != "$UID" ]; then
|
||||||
fi
|
rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"
|
||||||
set -e
|
fi
|
||||||
cd core
|
|
||||||
|
|
||||||
if [ -n "$fail" ]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -2,13 +2,17 @@
|
|||||||
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
set -e
|
set -ea
|
||||||
shopt -s expand_aliases
|
shopt -s expand_aliases
|
||||||
|
|
||||||
if [ -z "$ARCH" ]; then
|
if [ -z "$ARCH" ]; then
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$ARCH" = "arm64" ]; then
|
||||||
|
ARCH="aarch64"
|
||||||
|
fi
|
||||||
|
|
||||||
USE_TTY=
|
USE_TTY=
|
||||||
if tty -s; then
|
if tty -s; then
|
||||||
USE_TTY="-it"
|
USE_TTY="-it"
|
||||||
@@ -24,16 +28,9 @@ fi
|
|||||||
|
|
||||||
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
||||||
|
|
||||||
set +e
|
|
||||||
fail=
|
|
||||||
echo "FEATURES=\"$FEATURES\""
|
echo "FEATURES=\"$FEATURES\""
|
||||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||||
if ! rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli,registry,$FEATURES --locked --bin registrybox --target=$ARCH-unknown-linux-musl && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"; then
|
rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli,registry,$FEATURES --locked --bin registrybox --target=$ARCH-unknown-linux-musl"
|
||||||
fail=true
|
if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/release/registrybox | awk '{ print $3 }')" != "$UID" ]; then
|
||||||
fi
|
rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"
|
||||||
set -e
|
|
||||||
cd core
|
|
||||||
|
|
||||||
if [ -n "$fail" ]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -2,13 +2,17 @@
|
|||||||
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
set -e
|
set -ea
|
||||||
shopt -s expand_aliases
|
shopt -s expand_aliases
|
||||||
|
|
||||||
if [ -z "$ARCH" ]; then
|
if [ -z "$ARCH" ]; then
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$ARCH" = "arm64" ]; then
|
||||||
|
ARCH="aarch64"
|
||||||
|
fi
|
||||||
|
|
||||||
USE_TTY=
|
USE_TTY=
|
||||||
if tty -s; then
|
if tty -s; then
|
||||||
USE_TTY="-it"
|
USE_TTY="-it"
|
||||||
@@ -24,16 +28,9 @@ fi
|
|||||||
|
|
||||||
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
||||||
|
|
||||||
set +e
|
|
||||||
fail=
|
|
||||||
echo "FEATURES=\"$FEATURES\""
|
echo "FEATURES=\"$FEATURES\""
|
||||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||||
if ! rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli,daemon,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-musl && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"; then
|
rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli,daemon,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-musl"
|
||||||
fail=true
|
if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/release/startbox | awk '{ print $3 }')" != "$UID" ]; then
|
||||||
fi
|
rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"
|
||||||
set -e
|
fi
|
||||||
cd core
|
|
||||||
|
|
||||||
if [ -n "$fail" ]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
36
core/build-ts.sh
Executable file
36
core/build-ts.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
|
set -ea
|
||||||
|
shopt -s expand_aliases
|
||||||
|
|
||||||
|
if [ -z "$ARCH" ]; then
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$ARCH" = "arm64" ]; then
|
||||||
|
ARCH="aarch64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
USE_TTY=
|
||||||
|
if tty -s; then
|
||||||
|
USE_TTY="-it"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
|
||||||
|
RUSTFLAGS=""
|
||||||
|
|
||||||
|
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||||
|
RUSTFLAGS="--cfg tokio_unstable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
||||||
|
|
||||||
|
echo "FEATURES=\"$FEATURES\""
|
||||||
|
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||||
|
rust-musl-builder sh -c "cd core && cargo test --release --features=test,$FEATURES 'export_bindings_' && chown \$UID:\$UID startos/bindings"
|
||||||
|
if [ "$(ls -nd core/startos/bindings | awk '{ print $3 }')" != "$UID" ]; then
|
||||||
|
rust-musl-builder sh -c "cd core && chown -R $UID:$UID startos/bindings && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"
|
||||||
|
fi
|
||||||
@@ -2,14 +2,18 @@
|
|||||||
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
set -e
|
set -ea
|
||||||
shopt -s expand_aliases
|
shopt -s expand_aliases
|
||||||
|
|
||||||
web="../web/dist/static"
|
web="../web/dist/static"
|
||||||
[ -d "$web" ] || mkdir -p "$web"
|
[ -d "$web" ] || mkdir -p "$web"
|
||||||
|
|
||||||
if [ -z "$PLATFORM" ]; then
|
if [ -z "$PLATFORM" ]; then
|
||||||
export PLATFORM=$(uname -m)
|
PLATFORM=$(uname -m)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$PLATFORM" = "arm64" ]; then
|
||||||
|
PLATFORM="aarch64"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cargo install --path=./startos --no-default-features --features=cli,docker,registry --bin start-cli --locked
|
cargo install --path=./startos --no-default-features --features=cli,docker,registry --bin start-cli --locked
|
||||||
|
|||||||
36
core/run-tests.sh
Executable file
36
core/run-tests.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
|
set -ea
|
||||||
|
shopt -s expand_aliases
|
||||||
|
|
||||||
|
if [ -z "$ARCH" ]; then
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$ARCH" = "arm64" ]; then
|
||||||
|
ARCH="aarch64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
USE_TTY=
|
||||||
|
if tty -s; then
|
||||||
|
USE_TTY="-it"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
|
||||||
|
RUSTFLAGS=""
|
||||||
|
|
||||||
|
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||||
|
RUSTFLAGS="--cfg tokio_unstable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
||||||
|
|
||||||
|
echo "FEATURES=\"$FEATURES\""
|
||||||
|
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||||
|
rust-musl-builder sh -c "apt-get update && apt-get install -y rsync && cd core && cargo test --release --features=test,$FEATURES --workspace --locked --target=$ARCH-unknown-linux-musl -- --skip export_bindings_ && chown \$UID:\$UID target"
|
||||||
|
if [ "$(ls -nd core/target | awk '{ print $3 }')" != "$UID" ]; then
|
||||||
|
rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"
|
||||||
|
fi
|
||||||
@@ -14,7 +14,7 @@ keywords = [
|
|||||||
name = "start-os"
|
name = "start-os"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/Start9Labs/start-os"
|
repository = "https://github.com/Start9Labs/start-os"
|
||||||
version = "0.3.6-alpha.3"
|
version = "0.3.6-alpha.5"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
@@ -39,7 +39,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
cli = []
|
cli = []
|
||||||
container-runtime = []
|
container-runtime = ["procfs", "unshare"]
|
||||||
daemon = []
|
daemon = []
|
||||||
registry = []
|
registry = []
|
||||||
default = ["cli", "daemon"]
|
default = ["cli", "daemon"]
|
||||||
@@ -130,7 +130,14 @@ log = "0.4.20"
|
|||||||
mbrman = "0.5.2"
|
mbrman = "0.5.2"
|
||||||
models = { version = "*", path = "../models" }
|
models = { version = "*", path = "../models" }
|
||||||
new_mime_guess = "4"
|
new_mime_guess = "4"
|
||||||
nix = { version = "0.29.0", features = ["user", "process", "signal", "fs"] }
|
nix = { version = "0.29.0", features = [
|
||||||
|
"fs",
|
||||||
|
"mount",
|
||||||
|
"process",
|
||||||
|
"sched",
|
||||||
|
"signal",
|
||||||
|
"user",
|
||||||
|
] }
|
||||||
nom = "7.1.3"
|
nom = "7.1.3"
|
||||||
num = "0.4.1"
|
num = "0.4.1"
|
||||||
num_enum = "0.7.0"
|
num_enum = "0.7.0"
|
||||||
@@ -146,6 +153,7 @@ pbkdf2 = "0.12.2"
|
|||||||
pin-project = "1.1.3"
|
pin-project = "1.1.3"
|
||||||
pkcs8 = { version = "0.10.2", features = ["std"] }
|
pkcs8 = { version = "0.10.2", features = ["std"] }
|
||||||
prettytable-rs = "0.10.0"
|
prettytable-rs = "0.10.0"
|
||||||
|
procfs = { version = "0.16.0", optional = true }
|
||||||
proptest = "1.3.1"
|
proptest = "1.3.1"
|
||||||
proptest-derive = "0.5.0"
|
proptest-derive = "0.5.0"
|
||||||
rand = { version = "0.8.5", features = ["std"] }
|
rand = { version = "0.8.5", features = ["std"] }
|
||||||
@@ -166,6 +174,7 @@ 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" }
|
||||||
sha2 = "0.10.2"
|
sha2 = "0.10.2"
|
||||||
shell-words = "1"
|
shell-words = "1"
|
||||||
|
signal-hook = "0.3.17"
|
||||||
simple-logging = "2.0.2"
|
simple-logging = "2.0.2"
|
||||||
socket2 = "0.5.7"
|
socket2 = "0.5.7"
|
||||||
sqlx = { version = "0.7.2", features = [
|
sqlx = { version = "0.7.2", features = [
|
||||||
@@ -197,6 +206,9 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
|||||||
trust-dns-server = "0.23.1"
|
trust-dns-server = "0.23.1"
|
||||||
ts-rs = { git = "https://github.com/dr-bonez/ts-rs.git", branch = "feature/top-level-as" } # "8.1.0"
|
ts-rs = { git = "https://github.com/dr-bonez/ts-rs.git", branch = "feature/top-level-as" } # "8.1.0"
|
||||||
typed-builder = "0.18.0"
|
typed-builder = "0.18.0"
|
||||||
|
which = "6.0.3"
|
||||||
|
unix-named-pipe = "0.2.0"
|
||||||
|
unshare = { version = "0.7.0", optional = true }
|
||||||
url = { version = "2.4.1", features = ["serde"] }
|
url = { version = "2.4.1", features = ["serde"] }
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
uuid = { version = "1.4.1", features = ["v4"] }
|
uuid = { version = "1.4.1", features = ["v4"] }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::future::Future;
|
||||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -6,6 +7,8 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use chrono::{TimeDelta, Utc};
|
||||||
|
use helpers::NonDetachingJoinHandle;
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
use josekit::jwk::Jwk;
|
use josekit::jwk::Jwk;
|
||||||
use reqwest::{Client, Proxy};
|
use reqwest::{Client, Proxy};
|
||||||
@@ -29,7 +32,7 @@ use crate::net::utils::{find_eth_iface, find_wifi_iface};
|
|||||||
use crate::net::wifi::WpaCli;
|
use crate::net::wifi::WpaCli;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle};
|
use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle};
|
||||||
use crate::rpc_continuations::{OpenAuthedContinuations, RpcContinuations};
|
use crate::rpc_continuations::{Guid, OpenAuthedContinuations, RpcContinuations};
|
||||||
use crate::service::effects::callbacks::ServiceCallbacks;
|
use crate::service::effects::callbacks::ServiceCallbacks;
|
||||||
use crate::service::ServiceMap;
|
use crate::service::ServiceMap;
|
||||||
use crate::shutdown::Shutdown;
|
use crate::shutdown::Shutdown;
|
||||||
@@ -63,6 +66,7 @@ pub struct RpcContextSeed {
|
|||||||
pub client: Client,
|
pub client: Client,
|
||||||
pub hardware: Hardware,
|
pub hardware: Hardware,
|
||||||
pub start_time: Instant,
|
pub start_time: Instant,
|
||||||
|
pub crons: SyncMutex<BTreeMap<Guid, NonDetachingJoinHandle<()>>>,
|
||||||
#[cfg(feature = "dev")]
|
#[cfg(feature = "dev")]
|
||||||
pub dev: Dev,
|
pub dev: Dev,
|
||||||
}
|
}
|
||||||
@@ -94,12 +98,14 @@ impl InitRpcContextPhases {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct CleanupInitPhases {
|
pub struct CleanupInitPhases {
|
||||||
|
cleanup_sessions: PhaseProgressTrackerHandle,
|
||||||
init_services: PhaseProgressTrackerHandle,
|
init_services: PhaseProgressTrackerHandle,
|
||||||
check_dependencies: PhaseProgressTrackerHandle,
|
check_dependencies: PhaseProgressTrackerHandle,
|
||||||
}
|
}
|
||||||
impl CleanupInitPhases {
|
impl CleanupInitPhases {
|
||||||
pub fn new(handle: &FullProgressTracker) -> Self {
|
pub fn new(handle: &FullProgressTracker) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
cleanup_sessions: handle.add_phase("Cleaning up sessions".into(), Some(1)),
|
||||||
init_services: handle.add_phase("Initializing services".into(), Some(10)),
|
init_services: handle.add_phase("Initializing services".into(), Some(10)),
|
||||||
check_dependencies: handle.add_phase("Checking dependencies".into(), Some(1)),
|
check_dependencies: handle.add_phase("Checking dependencies".into(), Some(1)),
|
||||||
}
|
}
|
||||||
@@ -174,6 +180,8 @@ impl RpcContext {
|
|||||||
let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024;
|
let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024;
|
||||||
read_device_info.complete();
|
read_device_info.complete();
|
||||||
|
|
||||||
|
let crons = SyncMutex::new(BTreeMap::new());
|
||||||
|
|
||||||
if !db
|
if !db
|
||||||
.peek()
|
.peek()
|
||||||
.await
|
.await
|
||||||
@@ -183,18 +191,24 @@ impl RpcContext {
|
|||||||
.de()?
|
.de()?
|
||||||
{
|
{
|
||||||
let db = db.clone();
|
let db = db.clone();
|
||||||
tokio::spawn(async move {
|
crons.mutate(|c| {
|
||||||
while !check_time_is_synchronized().await.unwrap() {
|
c.insert(
|
||||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
Guid::new(),
|
||||||
}
|
tokio::spawn(async move {
|
||||||
db.mutate(|v| {
|
while !check_time_is_synchronized().await.unwrap() {
|
||||||
v.as_public_mut()
|
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||||
.as_server_info_mut()
|
}
|
||||||
.as_ntp_synced_mut()
|
db.mutate(|v| {
|
||||||
.ser(&true)
|
v.as_public_mut()
|
||||||
})
|
.as_server_info_mut()
|
||||||
.await
|
.as_ntp_synced_mut()
|
||||||
.unwrap()
|
.ser(&true)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,6 +273,7 @@ impl RpcContext {
|
|||||||
.with_kind(crate::ErrorKind::ParseUrl)?,
|
.with_kind(crate::ErrorKind::ParseUrl)?,
|
||||||
hardware: Hardware { devices, ram },
|
hardware: Hardware { devices, ram },
|
||||||
start_time: Instant::now(),
|
start_time: Instant::now(),
|
||||||
|
crons,
|
||||||
#[cfg(feature = "dev")]
|
#[cfg(feature = "dev")]
|
||||||
dev: Dev {
|
dev: Dev {
|
||||||
lxc: Mutex::new(BTreeMap::new()),
|
lxc: Mutex::new(BTreeMap::new()),
|
||||||
@@ -273,6 +288,7 @@ impl RpcContext {
|
|||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn shutdown(self) -> Result<(), Error> {
|
pub async fn shutdown(self) -> Result<(), Error> {
|
||||||
|
self.crons.mutate(|c| std::mem::take(c));
|
||||||
self.services.shutdown_all().await?;
|
self.services.shutdown_all().await?;
|
||||||
self.is_closed.store(true, Ordering::SeqCst);
|
self.is_closed.store(true, Ordering::SeqCst);
|
||||||
tracing::info!("RPC Context is shutdown");
|
tracing::info!("RPC Context is shutdown");
|
||||||
@@ -280,14 +296,75 @@ impl RpcContext {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_cron<F: Future<Output = ()> + Send + 'static>(&self, fut: F) -> Guid {
|
||||||
|
let guid = Guid::new();
|
||||||
|
self.crons
|
||||||
|
.mutate(|c| c.insert(guid.clone(), tokio::spawn(fut).into()));
|
||||||
|
guid
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn cleanup_and_initialize(
|
pub async fn cleanup_and_initialize(
|
||||||
&self,
|
&self,
|
||||||
CleanupInitPhases {
|
CleanupInitPhases {
|
||||||
|
mut cleanup_sessions,
|
||||||
init_services,
|
init_services,
|
||||||
mut check_dependencies,
|
mut check_dependencies,
|
||||||
}: CleanupInitPhases,
|
}: CleanupInitPhases,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
cleanup_sessions.start();
|
||||||
|
self.db
|
||||||
|
.mutate(|db| {
|
||||||
|
if db.as_public().as_server_info().as_ntp_synced().de()? {
|
||||||
|
for id in db.as_private().as_sessions().keys()? {
|
||||||
|
if Utc::now()
|
||||||
|
- db.as_private()
|
||||||
|
.as_sessions()
|
||||||
|
.as_idx(&id)
|
||||||
|
.unwrap()
|
||||||
|
.de()?
|
||||||
|
.last_active
|
||||||
|
> TimeDelta::days(30)
|
||||||
|
{
|
||||||
|
db.as_private_mut().as_sessions_mut().remove(&id)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let db = self.db.clone();
|
||||||
|
self.add_cron(async move {
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(Duration::from_secs(86400)).await;
|
||||||
|
if let Err(e) = db
|
||||||
|
.mutate(|db| {
|
||||||
|
if db.as_public().as_server_info().as_ntp_synced().de()? {
|
||||||
|
for id in db.as_private().as_sessions().keys()? {
|
||||||
|
if Utc::now()
|
||||||
|
- db.as_private()
|
||||||
|
.as_sessions()
|
||||||
|
.as_idx(&id)
|
||||||
|
.unwrap()
|
||||||
|
.de()?
|
||||||
|
.last_active
|
||||||
|
> TimeDelta::days(30)
|
||||||
|
{
|
||||||
|
db.as_private_mut().as_sessions_mut().remove(&id)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::error!("Error in session cleanup cron: {e}");
|
||||||
|
tracing::debug!("{e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cleanup_sessions.complete();
|
||||||
|
|
||||||
self.services.init(&self, init_services).await?;
|
self.services.init(&self, init_services).await?;
|
||||||
tracing::info!("Initialized Package Managers");
|
tracing::info!("Initialized Package Managers");
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use exver::VersionRange;
|
use exver::VersionRange;
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
use models::{ActionId, DataUrl, HealthCheckId, HostId, PackageId, ServiceInterfaceId};
|
use models::{
|
||||||
|
ActionId, DataUrl, HealthCheckId, HostId, PackageId, ServiceInterfaceId, VersionString,
|
||||||
|
};
|
||||||
use patch_db::json_ptr::JsonPointer;
|
use patch_db::json_ptr::JsonPointer;
|
||||||
use patch_db::HasModel;
|
use patch_db::HasModel;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
@@ -335,6 +337,7 @@ pub struct ActionMetadata {
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct PackageDataEntry {
|
pub struct PackageDataEntry {
|
||||||
pub state_info: PackageState,
|
pub state_info: PackageState,
|
||||||
|
pub data_version: Option<VersionString>,
|
||||||
pub status: Status,
|
pub status: Status,
|
||||||
#[ts(type = "string | null")]
|
#[ts(type = "string | null")]
|
||||||
pub registry: Option<Url>,
|
pub registry: Option<Url>,
|
||||||
|
|||||||
@@ -268,9 +268,10 @@ impl LxcContainer {
|
|||||||
.invoke(ErrorKind::Docker)
|
.invoke(ErrorKind::Docker)
|
||||||
.await?,
|
.await?,
|
||||||
)?;
|
)?;
|
||||||
let out_str = output.trim();
|
for line in output.lines() {
|
||||||
if !out_str.is_empty() {
|
if let Ok(ip) = line.trim().parse() {
|
||||||
return Ok(out_str.parse()?);
|
return Ok(ip);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if start.elapsed() > CONTAINER_DHCP_TIMEOUT {
|
if start.elapsed() > CONTAINER_DHCP_TIMEOUT {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ pub struct ServiceInterface {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub has_primary: bool,
|
pub has_primary: bool,
|
||||||
pub disabled: bool,
|
|
||||||
pub masked: bool,
|
pub masked: bool,
|
||||||
pub address_info: AddressInfo,
|
pub address_info: AddressInfo,
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub struct Manifest {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub git_hash: Option<GitHash>,
|
pub git_hash: Option<GitHash>,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub version: exver::emver::Version,
|
pub version: String,
|
||||||
pub description: Description,
|
pub description: Description,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub assets: Assets,
|
pub assets: Assets,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use exver::ExtendedVersion;
|
use exver::{ExtendedVersion, VersionRange};
|
||||||
use models::ImageId;
|
use models::ImageId;
|
||||||
use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt};
|
use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
@@ -44,9 +45,9 @@ impl S9pk<TmpSource<PackSource>> {
|
|||||||
// manifest.json
|
// manifest.json
|
||||||
let manifest_raw = reader.manifest().await?;
|
let manifest_raw = reader.manifest().await?;
|
||||||
let manifest = from_value::<ManifestV1>(manifest_raw.clone())?;
|
let manifest = from_value::<ManifestV1>(manifest_raw.clone())?;
|
||||||
let mut new_manifest = Manifest::from(manifest.clone());
|
let mut new_manifest = Manifest::try_from(manifest.clone())?;
|
||||||
|
|
||||||
let images: BTreeMap<ImageId, bool> = manifest
|
let images: BTreeSet<(ImageId, bool)> = manifest
|
||||||
.package_procedures()
|
.package_procedures()
|
||||||
.filter_map(|p| {
|
.filter_map(|p| {
|
||||||
if let PackageProcedure::Docker(p) = p {
|
if let PackageProcedure::Docker(p) = p {
|
||||||
@@ -89,8 +90,6 @@ impl S9pk<TmpSource<PackSource>> {
|
|||||||
|
|
||||||
// images
|
// images
|
||||||
for arch in reader.docker_arches().await? {
|
for arch in reader.docker_arches().await? {
|
||||||
let images_dir = tmp_dir.join("images").join(&arch);
|
|
||||||
tokio::fs::create_dir_all(&images_dir).await?;
|
|
||||||
Command::new(CONTAINER_TOOL)
|
Command::new(CONTAINER_TOOL)
|
||||||
.arg("load")
|
.arg("load")
|
||||||
.input(Some(&mut reader.docker_images(&arch).await?))
|
.input(Some(&mut reader.docker_images(&arch).await?))
|
||||||
@@ -194,15 +193,22 @@ impl S9pk<TmpSource<PackSource>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ManifestV1> for Manifest {
|
impl TryFrom<ManifestV1> for Manifest {
|
||||||
fn from(value: ManifestV1) -> Self {
|
type Error = Error;
|
||||||
|
fn try_from(value: ManifestV1) -> Result<Self, Self::Error> {
|
||||||
let default_url = value.upstream_repo.clone();
|
let default_url = value.upstream_repo.clone();
|
||||||
Self {
|
Ok(Self {
|
||||||
id: value.id,
|
id: value.id,
|
||||||
title: value.title.into(),
|
title: value.title.into(),
|
||||||
version: ExtendedVersion::from(value.version).into(),
|
version: ExtendedVersion::from(
|
||||||
|
exver::emver::Version::from_str(&value.version)
|
||||||
|
.with_kind(ErrorKind::Deserialization)?,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
satisfies: BTreeSet::new(),
|
satisfies: BTreeSet::new(),
|
||||||
release_notes: value.release_notes,
|
release_notes: value.release_notes,
|
||||||
|
can_migrate_from: VersionRange::any(),
|
||||||
|
can_migrate_to: VersionRange::none(),
|
||||||
license: value.license.into(),
|
license: value.license.into(),
|
||||||
wrapper_repo: value.wrapper_repo,
|
wrapper_repo: value.wrapper_repo,
|
||||||
upstream_repo: value.upstream_repo,
|
upstream_repo: value.upstream_repo,
|
||||||
@@ -244,6 +250,6 @@ impl From<ManifestV1> for Manifest {
|
|||||||
git_hash: value.git_hash,
|
git_hash: value.git_hash,
|
||||||
os_version: value.eos_version,
|
os_version: value.eos_version,
|
||||||
has_config: value.config.is_some(),
|
has_config: value.config.is_some(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
use exver::Version;
|
use exver::{Version, VersionRange};
|
||||||
use helpers::const_true;
|
use helpers::const_true;
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
pub use models::PackageId;
|
pub use models::PackageId;
|
||||||
@@ -37,6 +37,10 @@ pub struct Manifest {
|
|||||||
pub satisfies: BTreeSet<VersionString>,
|
pub satisfies: BTreeSet<VersionString>,
|
||||||
pub release_notes: String,
|
pub release_notes: String,
|
||||||
#[ts(type = "string")]
|
#[ts(type = "string")]
|
||||||
|
pub can_migrate_to: VersionRange,
|
||||||
|
#[ts(type = "string")]
|
||||||
|
pub can_migrate_from: VersionRange,
|
||||||
|
#[ts(type = "string")]
|
||||||
pub license: InternedString, // type of license
|
pub license: InternedString, // type of license
|
||||||
#[ts(type = "string")]
|
#[ts(type = "string")]
|
||||||
pub wrapper_repo: Url,
|
pub wrapper_repo: Url,
|
||||||
@@ -159,8 +163,8 @@ impl Manifest {
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct HardwareRequirements {
|
pub struct HardwareRequirements {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[ts(type = "{ device?: string, processor?: string }")]
|
#[ts(type = "{ display?: string, processor?: string }")]
|
||||||
pub device: BTreeMap<String, Regex>,
|
pub device: BTreeMap<String, Regex>, // TODO: array
|
||||||
#[ts(type = "number | null")]
|
#[ts(type = "number | null")]
|
||||||
pub ram: Option<u64>,
|
pub ram: Option<u64>,
|
||||||
#[ts(type = "string[] | null")]
|
#[ts(type = "string[] | null")]
|
||||||
|
|||||||
@@ -60,14 +60,20 @@ impl SqfsDir {
|
|||||||
.get_or_try_init(|| async move {
|
.get_or_try_init(|| async move {
|
||||||
let guid = Guid::new();
|
let guid = Guid::new();
|
||||||
let path = self.tmpdir.join(guid.as_ref()).with_extension("squashfs");
|
let path = self.tmpdir.join(guid.as_ref()).with_extension("squashfs");
|
||||||
let mut cmd = Command::new("mksquashfs");
|
|
||||||
if self.path.extension().and_then(|s| s.to_str()) == Some("tar") {
|
if self.path.extension().and_then(|s| s.to_str()) == Some("tar") {
|
||||||
cmd.arg("-tar");
|
Command::new("tar2sqfs")
|
||||||
|
.arg(&path)
|
||||||
|
.input(Some(&mut open_file(&self.path).await?))
|
||||||
|
.invoke(ErrorKind::Filesystem)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
Command::new("mksquashfs")
|
||||||
|
.arg(&self.path)
|
||||||
|
.arg(&path)
|
||||||
|
.invoke(ErrorKind::Filesystem)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
cmd.arg(&self.path)
|
|
||||||
.arg(&path)
|
|
||||||
.invoke(ErrorKind::Filesystem)
|
|
||||||
.await?;
|
|
||||||
Ok(MultiCursorFile::from(
|
Ok(MultiCursorFile::from(
|
||||||
open_file(&path)
|
open_file(&path)
|
||||||
.await
|
.await
|
||||||
@@ -507,7 +513,7 @@ impl ImageSource {
|
|||||||
Command::new(CONTAINER_TOOL)
|
Command::new(CONTAINER_TOOL)
|
||||||
.arg("export")
|
.arg("export")
|
||||||
.arg(container.trim())
|
.arg(container.trim())
|
||||||
.pipe(Command::new("mksquashfs").arg("-").arg(&dest).arg("-tar"))
|
.pipe(Command::new("tar2sqfs").arg(&dest))
|
||||||
.capture(false)
|
.capture(false)
|
||||||
.invoke(ErrorKind::Docker)
|
.invoke(ErrorKind::Docker)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
use clap::builder::ValueParserFactory;
|
use clap::builder::ValueParserFactory;
|
||||||
use exver::VersionRange;
|
use exver::VersionRange;
|
||||||
|
use imbl::OrdMap;
|
||||||
|
use imbl_value::InternedString;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use models::{HealthCheckId, PackageId, VolumeId};
|
use models::{HealthCheckId, PackageId, VersionString, VolumeId};
|
||||||
use patch_db::json_ptr::JsonPointer;
|
use patch_db::json_ptr::JsonPointer;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
@@ -17,7 +19,7 @@ use crate::disk::mount::filesystem::idmapped::IdMapped;
|
|||||||
use crate::disk::mount::filesystem::{FileSystem, MountType};
|
use crate::disk::mount::filesystem::{FileSystem, MountType};
|
||||||
use crate::rpc_continuations::Guid;
|
use crate::rpc_continuations::Guid;
|
||||||
use crate::service::effects::prelude::*;
|
use crate::service::effects::prelude::*;
|
||||||
use crate::status::health_check::HealthCheckResult;
|
use crate::status::health_check::NamedHealthCheckResult;
|
||||||
use crate::util::clap::FromStrParser;
|
use crate::util::clap::FromStrParser;
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
use crate::volume::data_dir;
|
use crate::volume::data_dir;
|
||||||
@@ -316,12 +318,16 @@ pub struct CheckDependenciesParam {
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct CheckDependenciesResult {
|
pub struct CheckDependenciesResult {
|
||||||
package_id: PackageId,
|
package_id: PackageId,
|
||||||
is_installed: bool,
|
#[ts(type = "string | null")]
|
||||||
|
title: Option<InternedString>,
|
||||||
|
#[ts(type = "string | null")]
|
||||||
|
installed_version: Option<exver::ExtendedVersion>,
|
||||||
|
#[ts(type = "string[]")]
|
||||||
|
satisfies: BTreeSet<VersionString>,
|
||||||
is_running: bool,
|
is_running: bool,
|
||||||
config_satisfied: bool,
|
config_satisfied: bool,
|
||||||
health_checks: BTreeMap<HealthCheckId, HealthCheckResult>,
|
#[ts(as = "BTreeMap::<HealthCheckId, NamedHealthCheckResult>")]
|
||||||
#[ts(type = "string | null")]
|
health_checks: OrdMap<HealthCheckId, NamedHealthCheckResult>,
|
||||||
version: Option<exver::ExtendedVersion>,
|
|
||||||
}
|
}
|
||||||
pub async fn check_dependencies(
|
pub async fn check_dependencies(
|
||||||
context: EffectContext,
|
context: EffectContext,
|
||||||
@@ -347,36 +353,23 @@ pub async fn check_dependencies(
|
|||||||
let mut results = Vec::with_capacity(package_ids.len());
|
let mut results = Vec::with_capacity(package_ids.len());
|
||||||
|
|
||||||
for (package_id, dependency_info) in package_ids {
|
for (package_id, dependency_info) in package_ids {
|
||||||
|
let title = dependency_info.title.clone();
|
||||||
let Some(package) = db.as_public().as_package_data().as_idx(&package_id) else {
|
let Some(package) = db.as_public().as_package_data().as_idx(&package_id) else {
|
||||||
results.push(CheckDependenciesResult {
|
results.push(CheckDependenciesResult {
|
||||||
package_id,
|
package_id,
|
||||||
is_installed: false,
|
title,
|
||||||
|
installed_version: None,
|
||||||
|
satisfies: BTreeSet::new(),
|
||||||
is_running: false,
|
is_running: false,
|
||||||
config_satisfied: false,
|
config_satisfied: false,
|
||||||
health_checks: Default::default(),
|
health_checks: Default::default(),
|
||||||
version: None,
|
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let manifest = package.as_state_info().as_manifest(ManifestPreference::New);
|
let manifest = package.as_state_info().as_manifest(ManifestPreference::New);
|
||||||
let installed_version = manifest.as_version().de()?.into_version();
|
let installed_version = manifest.as_version().de()?.into_version();
|
||||||
let satisfies = manifest.as_satisfies().de()?;
|
let satisfies = manifest.as_satisfies().de()?;
|
||||||
let version = Some(installed_version.clone());
|
let installed_version = Some(installed_version.clone());
|
||||||
if ![installed_version]
|
|
||||||
.into_iter()
|
|
||||||
.chain(satisfies.into_iter().map(|v| v.into_version()))
|
|
||||||
.any(|v| v.satisfies(&dependency_info.version_range))
|
|
||||||
{
|
|
||||||
results.push(CheckDependenciesResult {
|
|
||||||
package_id,
|
|
||||||
is_installed: false,
|
|
||||||
is_running: false,
|
|
||||||
config_satisfied: false,
|
|
||||||
health_checks: Default::default(),
|
|
||||||
version,
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let is_installed = true;
|
let is_installed = true;
|
||||||
let status = package.as_status().as_main().de()?;
|
let status = package.as_status().as_main().de()?;
|
||||||
let is_running = if is_installed {
|
let is_running = if is_installed {
|
||||||
@@ -384,25 +377,15 @@ pub async fn check_dependencies(
|
|||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
let health_checks =
|
let health_checks = status.health().cloned().unwrap_or_default();
|
||||||
if let CurrentDependencyKind::Running { health_checks } = &dependency_info.kind {
|
|
||||||
status
|
|
||||||
.health()
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|(id, _)| health_checks.contains(id))
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
Default::default()
|
|
||||||
};
|
|
||||||
results.push(CheckDependenciesResult {
|
results.push(CheckDependenciesResult {
|
||||||
package_id,
|
package_id,
|
||||||
is_installed,
|
title,
|
||||||
|
installed_version,
|
||||||
|
satisfies,
|
||||||
is_running,
|
is_running,
|
||||||
config_satisfied: dependency_info.config_satisfied,
|
config_satisfied: dependency_info.config_satisfied,
|
||||||
health_checks,
|
health_checks,
|
||||||
version,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(results)
|
Ok(results)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use models::HealthCheckId;
|
use models::HealthCheckId;
|
||||||
|
|
||||||
use crate::service::effects::prelude::*;
|
use crate::service::effects::prelude::*;
|
||||||
use crate::status::health_check::HealthCheckResult;
|
use crate::status::health_check::NamedHealthCheckResult;
|
||||||
use crate::status::MainStatus;
|
use crate::status::MainStatus;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
@@ -10,7 +10,7 @@ use crate::status::MainStatus;
|
|||||||
pub struct SetHealth {
|
pub struct SetHealth {
|
||||||
id: HealthCheckId,
|
id: HealthCheckId,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
result: HealthCheckResult,
|
result: NamedHealthCheckResult,
|
||||||
}
|
}
|
||||||
pub async fn set_health(
|
pub async fn set_health(
|
||||||
context: EffectContext,
|
context: EffectContext,
|
||||||
@@ -32,8 +32,8 @@ pub async fn set_health(
|
|||||||
.as_main_mut()
|
.as_main_mut()
|
||||||
.mutate(|main| {
|
.mutate(|main| {
|
||||||
match main {
|
match main {
|
||||||
&mut MainStatus::Running { ref mut health, .. }
|
MainStatus::Running { ref mut health, .. }
|
||||||
| &mut MainStatus::BackingUp { ref mut health, .. } => {
|
| MainStatus::Starting { ref mut health } => {
|
||||||
health.insert(id, result);
|
health.insert(id, result);
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use rpc_toolkit::{from_fn, from_fn_async, Context, HandlerExt, ParentHandler};
|
use rpc_toolkit::{from_fn, from_fn_async, from_fn_blocking, Context, HandlerExt, ParentHandler};
|
||||||
|
|
||||||
use crate::echo;
|
use crate::echo;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
@@ -12,44 +12,44 @@ pub mod context;
|
|||||||
mod control;
|
mod control;
|
||||||
mod dependency;
|
mod dependency;
|
||||||
mod health;
|
mod health;
|
||||||
mod image;
|
|
||||||
mod net;
|
mod net;
|
||||||
mod prelude;
|
mod prelude;
|
||||||
mod store;
|
mod store;
|
||||||
|
mod subcontainer;
|
||||||
mod system;
|
mod system;
|
||||||
|
|
||||||
pub fn handler<C: Context>() -> ParentHandler<C> {
|
pub fn handler<C: Context>() -> ParentHandler<C> {
|
||||||
ParentHandler::new()
|
ParentHandler::new()
|
||||||
.subcommand("gitInfo", from_fn(|_: C| crate::version::git_info()))
|
.subcommand("git-info", from_fn(|_: C| crate::version::git_info()))
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"echo",
|
"echo",
|
||||||
from_fn(echo::<EffectContext>).with_call_remote::<ContainerCliContext>(),
|
from_fn(echo::<EffectContext>).with_call_remote::<ContainerCliContext>(),
|
||||||
)
|
)
|
||||||
// action
|
// action
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"executeAction",
|
"execute-action",
|
||||||
from_fn_async(action::execute_action).no_cli(),
|
from_fn_async(action::execute_action).no_cli(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"exportAction",
|
"export-action",
|
||||||
from_fn_async(action::export_action).no_cli(),
|
from_fn_async(action::export_action).no_cli(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"clearActions",
|
"clear-actions",
|
||||||
from_fn_async(action::clear_actions).no_cli(),
|
from_fn_async(action::clear_actions).no_cli(),
|
||||||
)
|
)
|
||||||
// callbacks
|
// callbacks
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"clearCallbacks",
|
"clear-callbacks",
|
||||||
from_fn(callbacks::clear_callbacks).no_cli(),
|
from_fn(callbacks::clear_callbacks).no_cli(),
|
||||||
)
|
)
|
||||||
// config
|
// config
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"getConfigured",
|
"get-configured",
|
||||||
from_fn_async(config::get_configured).no_cli(),
|
from_fn_async(config::get_configured).no_cli(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"setConfigured",
|
"set-configured",
|
||||||
from_fn_async(config::set_configured)
|
from_fn_async(config::set_configured)
|
||||||
.no_display()
|
.no_display()
|
||||||
.with_call_remote::<ContainerCliContext>(),
|
.with_call_remote::<ContainerCliContext>(),
|
||||||
@@ -68,105 +68,143 @@ pub fn handler<C: Context>() -> ParentHandler<C> {
|
|||||||
.with_call_remote::<ContainerCliContext>(),
|
.with_call_remote::<ContainerCliContext>(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"setMainStatus",
|
"set-main-status",
|
||||||
from_fn_async(control::set_main_status)
|
from_fn_async(control::set_main_status)
|
||||||
.no_display()
|
.no_display()
|
||||||
.with_call_remote::<ContainerCliContext>(),
|
.with_call_remote::<ContainerCliContext>(),
|
||||||
)
|
)
|
||||||
// dependency
|
// dependency
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"setDependencies",
|
"set-dependencies",
|
||||||
from_fn_async(dependency::set_dependencies)
|
from_fn_async(dependency::set_dependencies)
|
||||||
.no_display()
|
.no_display()
|
||||||
.with_call_remote::<ContainerCliContext>(),
|
.with_call_remote::<ContainerCliContext>(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"getDependencies",
|
"get-dependencies",
|
||||||
from_fn_async(dependency::get_dependencies)
|
from_fn_async(dependency::get_dependencies)
|
||||||
.no_display()
|
.no_display()
|
||||||
.with_call_remote::<ContainerCliContext>(),
|
.with_call_remote::<ContainerCliContext>(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"checkDependencies",
|
"check-dependencies",
|
||||||
from_fn_async(dependency::check_dependencies)
|
from_fn_async(dependency::check_dependencies)
|
||||||
.no_display()
|
.no_display()
|
||||||
.with_call_remote::<ContainerCliContext>(),
|
.with_call_remote::<ContainerCliContext>(),
|
||||||
)
|
)
|
||||||
.subcommand("mount", from_fn_async(dependency::mount).no_cli())
|
.subcommand("mount", from_fn_async(dependency::mount).no_cli())
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"getInstalledPackages",
|
"get-installed-packages",
|
||||||
from_fn_async(dependency::get_installed_packages).no_cli(),
|
from_fn_async(dependency::get_installed_packages).no_cli(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"exposeForDependents",
|
"expose-for-dependents",
|
||||||
from_fn_async(dependency::expose_for_dependents).no_cli(),
|
from_fn_async(dependency::expose_for_dependents).no_cli(),
|
||||||
)
|
)
|
||||||
// health
|
// health
|
||||||
.subcommand("setHealth", from_fn_async(health::set_health).no_cli())
|
.subcommand("set-health", from_fn_async(health::set_health).no_cli())
|
||||||
// image
|
// subcontainer
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"chroot",
|
"subcontainer",
|
||||||
from_fn(image::chroot::<ContainerCliContext>).no_display(),
|
ParentHandler::<C>::new()
|
||||||
)
|
.subcommand(
|
||||||
.subcommand(
|
"launch",
|
||||||
"createOverlayedImage",
|
from_fn_blocking(subcontainer::launch).no_display(),
|
||||||
from_fn_async(image::create_overlayed_image)
|
)
|
||||||
.with_custom_display_fn(|_, (path, _)| Ok(println!("{}", path.display())))
|
.subcommand(
|
||||||
.with_call_remote::<ContainerCliContext>(),
|
"launch-init",
|
||||||
)
|
from_fn_blocking(subcontainer::launch_init).no_display(),
|
||||||
.subcommand(
|
)
|
||||||
"destroyOverlayedImage",
|
.subcommand("exec", from_fn_blocking(subcontainer::exec).no_display())
|
||||||
from_fn_async(image::destroy_overlayed_image).no_cli(),
|
.subcommand(
|
||||||
|
"exec-command",
|
||||||
|
from_fn_blocking(subcontainer::exec_command).no_display(),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
"create-fs",
|
||||||
|
from_fn_async(subcontainer::create_subcontainer_fs)
|
||||||
|
.with_custom_display_fn(|_, (path, _)| Ok(println!("{}", path.display())))
|
||||||
|
.with_call_remote::<ContainerCliContext>(),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
"destroy-fs",
|
||||||
|
from_fn_async(subcontainer::destroy_subcontainer_fs)
|
||||||
|
.no_display()
|
||||||
|
.with_call_remote::<ContainerCliContext>(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
// net
|
// net
|
||||||
.subcommand("bind", from_fn_async(net::bind::bind).no_cli())
|
.subcommand("bind", from_fn_async(net::bind::bind).no_cli())
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"getServicePortForward",
|
"get-service-port-forward",
|
||||||
from_fn_async(net::bind::get_service_port_forward).no_cli(),
|
from_fn_async(net::bind::get_service_port_forward).no_cli(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"clearBindings",
|
"clear-bindings",
|
||||||
from_fn_async(net::bind::clear_bindings).no_cli(),
|
from_fn_async(net::bind::clear_bindings).no_cli(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"getHostInfo",
|
"get-host-info",
|
||||||
from_fn_async(net::host::get_host_info).no_cli(),
|
from_fn_async(net::host::get_host_info).no_cli(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"getPrimaryUrl",
|
"get-primary-url",
|
||||||
from_fn_async(net::host::get_primary_url).no_cli(),
|
from_fn_async(net::host::get_primary_url).no_cli(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"getContainerIp",
|
"get-container-ip",
|
||||||
from_fn_async(net::info::get_container_ip).no_cli(),
|
from_fn_async(net::info::get_container_ip).no_cli(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"exportServiceInterface",
|
"export-service-interface",
|
||||||
from_fn_async(net::interface::export_service_interface).no_cli(),
|
from_fn_async(net::interface::export_service_interface).no_cli(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"getServiceInterface",
|
"get-service-interface",
|
||||||
from_fn_async(net::interface::get_service_interface).no_cli(),
|
from_fn_async(net::interface::get_service_interface).no_cli(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"listServiceInterfaces",
|
"list-service-interfaces",
|
||||||
from_fn_async(net::interface::list_service_interfaces).no_cli(),
|
from_fn_async(net::interface::list_service_interfaces).no_cli(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"clearServiceInterfaces",
|
"clear-service-interfaces",
|
||||||
from_fn_async(net::interface::clear_service_interfaces).no_cli(),
|
from_fn_async(net::interface::clear_service_interfaces).no_cli(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"getSslCertificate",
|
"get-ssl-certificate",
|
||||||
from_fn_async(net::ssl::get_ssl_certificate).no_cli(),
|
from_fn_async(net::ssl::get_ssl_certificate).no_cli(),
|
||||||
)
|
)
|
||||||
.subcommand("getSslKey", from_fn_async(net::ssl::get_ssl_key).no_cli())
|
.subcommand("get-ssl-key", from_fn_async(net::ssl::get_ssl_key).no_cli())
|
||||||
// store
|
// store
|
||||||
.subcommand("getStore", from_fn_async(store::get_store).no_cli())
|
.subcommand(
|
||||||
.subcommand("setStore", from_fn_async(store::set_store).no_cli())
|
"store",
|
||||||
|
ParentHandler::<C>::new()
|
||||||
|
.subcommand("get", from_fn_async(store::get_store).no_cli())
|
||||||
|
.subcommand("set", from_fn_async(store::set_store).no_cli()),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
"set-data-version",
|
||||||
|
from_fn_async(store::set_data_version)
|
||||||
|
.no_display()
|
||||||
|
.with_call_remote::<ContainerCliContext>(),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
"get-data-version",
|
||||||
|
from_fn_async(store::get_data_version)
|
||||||
|
.with_custom_display_fn(|_, v| {
|
||||||
|
if let Some(v) = v {
|
||||||
|
println!("{v}")
|
||||||
|
} else {
|
||||||
|
println!("N/A")
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.with_call_remote::<ContainerCliContext>(),
|
||||||
|
)
|
||||||
// system
|
// system
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"getSystemSmtp",
|
"get-system-smtp",
|
||||||
from_fn_async(system::get_system_smtp).no_cli(),
|
from_fn_async(system::get_system_smtp).no_cli(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ pub struct ExportServiceInterfaceParams {
|
|||||||
name: String,
|
name: String,
|
||||||
description: String,
|
description: String,
|
||||||
has_primary: bool,
|
has_primary: bool,
|
||||||
disabled: bool,
|
|
||||||
masked: bool,
|
masked: bool,
|
||||||
address_info: AddressInfo,
|
address_info: AddressInfo,
|
||||||
r#type: ServiceInterfaceType,
|
r#type: ServiceInterfaceType,
|
||||||
@@ -28,7 +27,6 @@ pub async fn export_service_interface(
|
|||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
has_primary,
|
has_primary,
|
||||||
disabled,
|
|
||||||
masked,
|
masked,
|
||||||
address_info,
|
address_info,
|
||||||
r#type,
|
r#type,
|
||||||
@@ -42,7 +40,6 @@ pub async fn export_service_interface(
|
|||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
has_primary,
|
has_primary,
|
||||||
disabled,
|
|
||||||
masked,
|
masked,
|
||||||
address_info,
|
address_info,
|
||||||
interface_type: r#type,
|
interface_type: r#type,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use imbl::vector;
|
use imbl::vector;
|
||||||
use imbl_value::json;
|
use imbl_value::json;
|
||||||
use models::PackageId;
|
use models::{PackageId, VersionString};
|
||||||
use patch_db::json_ptr::JsonPointer;
|
use patch_db::json_ptr::JsonPointer;
|
||||||
|
|
||||||
use crate::service::effects::callbacks::CallbackHandler;
|
use crate::service::effects::callbacks::CallbackHandler;
|
||||||
@@ -91,3 +91,50 @@ pub async fn set_store(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct SetDataVersionParams {
|
||||||
|
#[ts(type = "string")]
|
||||||
|
version: VersionString,
|
||||||
|
}
|
||||||
|
pub async fn set_data_version(
|
||||||
|
context: EffectContext,
|
||||||
|
SetDataVersionParams { version }: SetDataVersionParams,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let context = context.deref()?;
|
||||||
|
let package_id = &context.seed.id;
|
||||||
|
context
|
||||||
|
.seed
|
||||||
|
.ctx
|
||||||
|
.db
|
||||||
|
.mutate(|db| {
|
||||||
|
db.as_public_mut()
|
||||||
|
.as_package_data_mut()
|
||||||
|
.as_idx_mut(package_id)
|
||||||
|
.or_not_found(package_id)?
|
||||||
|
.as_data_version_mut()
|
||||||
|
.ser(&Some(version))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_data_version(context: EffectContext) -> Result<Option<VersionString>, Error> {
|
||||||
|
let context = context.deref()?;
|
||||||
|
let package_id = &context.seed.id;
|
||||||
|
context
|
||||||
|
.seed
|
||||||
|
.ctx
|
||||||
|
.db
|
||||||
|
.peek()
|
||||||
|
.await
|
||||||
|
.as_public()
|
||||||
|
.as_package_data()
|
||||||
|
.as_idx(package_id)
|
||||||
|
.or_not_found(package_id)?
|
||||||
|
.as_data_version()
|
||||||
|
.de()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
use std::ffi::OsString;
|
|
||||||
use std::os::unix::process::CommandExt;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use models::ImageId;
|
use models::ImageId;
|
||||||
use rpc_toolkit::Context;
|
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
use crate::disk::mount::filesystem::overlayfs::OverlayGuard;
|
use crate::disk::mount::filesystem::overlayfs::OverlayGuard;
|
||||||
@@ -11,89 +8,39 @@ use crate::rpc_continuations::Guid;
|
|||||||
use crate::service::effects::prelude::*;
|
use crate::service::effects::prelude::*;
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Parser)]
|
#[cfg(feature = "container-runtime")]
|
||||||
pub struct ChrootParams {
|
mod sync;
|
||||||
#[arg(short = 'e', long = "env")]
|
|
||||||
env: Option<PathBuf>,
|
#[cfg(not(feature = "container-runtime"))]
|
||||||
#[arg(short = 'w', long = "workdir")]
|
mod sync_dummy;
|
||||||
workdir: Option<PathBuf>,
|
|
||||||
#[arg(short = 'u', long = "user")]
|
pub use sync::*;
|
||||||
user: Option<String>,
|
#[cfg(not(feature = "container-runtime"))]
|
||||||
path: PathBuf,
|
use sync_dummy as sync;
|
||||||
command: OsString,
|
|
||||||
args: Vec<OsString>,
|
|
||||||
}
|
|
||||||
pub fn chroot<C: Context>(
|
|
||||||
_: C,
|
|
||||||
ChrootParams {
|
|
||||||
env,
|
|
||||||
workdir,
|
|
||||||
user,
|
|
||||||
path,
|
|
||||||
command,
|
|
||||||
args,
|
|
||||||
}: ChrootParams,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let mut cmd = std::process::Command::new(command);
|
|
||||||
if let Some(env) = env {
|
|
||||||
for (k, v) in std::fs::read_to_string(env)?
|
|
||||||
.lines()
|
|
||||||
.map(|l| l.trim())
|
|
||||||
.filter_map(|l| l.split_once("="))
|
|
||||||
{
|
|
||||||
cmd.env(k, v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nix::unistd::setsid().ok(); // https://stackoverflow.com/questions/25701333/os-setsid-operation-not-permitted
|
|
||||||
std::os::unix::fs::chroot(path)?;
|
|
||||||
if let Some(uid) = user.as_deref().and_then(|u| u.parse::<u32>().ok()) {
|
|
||||||
cmd.uid(uid);
|
|
||||||
} else if let Some(user) = user {
|
|
||||||
let (uid, gid) = std::fs::read_to_string("/etc/passwd")?
|
|
||||||
.lines()
|
|
||||||
.find_map(|l| {
|
|
||||||
let mut split = l.trim().split(":");
|
|
||||||
if user != split.next()? {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
split.next(); // throw away x
|
|
||||||
Some((split.next()?.parse().ok()?, split.next()?.parse().ok()?))
|
|
||||||
// uid gid
|
|
||||||
})
|
|
||||||
.or_not_found(lazy_format!("{user} in /etc/passwd"))?;
|
|
||||||
cmd.uid(uid);
|
|
||||||
cmd.gid(gid);
|
|
||||||
};
|
|
||||||
if let Some(workdir) = workdir {
|
|
||||||
cmd.current_dir(workdir);
|
|
||||||
}
|
|
||||||
cmd.args(args);
|
|
||||||
Err(cmd.exec().into())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct DestroyOverlayedImageParams {
|
pub struct DestroySubcontainerFsParams {
|
||||||
guid: Guid,
|
guid: Guid,
|
||||||
}
|
}
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn destroy_overlayed_image(
|
pub async fn destroy_subcontainer_fs(
|
||||||
context: EffectContext,
|
context: EffectContext,
|
||||||
DestroyOverlayedImageParams { guid }: DestroyOverlayedImageParams,
|
DestroySubcontainerFsParams { guid }: DestroySubcontainerFsParams,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let context = context.deref()?;
|
let context = context.deref()?;
|
||||||
if let Some(overlay) = context
|
if let Some(overlay) = context
|
||||||
.seed
|
.seed
|
||||||
.persistent_container
|
.persistent_container
|
||||||
.overlays
|
.subcontainers
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.remove(&guid)
|
.remove(&guid)
|
||||||
{
|
{
|
||||||
overlay.unmount(true).await?;
|
overlay.unmount(true).await?;
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("Could not find a guard to remove on the destroy overlayed image; assumming that it already is removed and will be skipping");
|
tracing::warn!("Could not find a subcontainer fs to destroy; assumming that it already is destroyed and will be skipping");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -101,13 +48,13 @@ pub async fn destroy_overlayed_image(
|
|||||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct CreateOverlayedImageParams {
|
pub struct CreateSubcontainerFsParams {
|
||||||
image_id: ImageId,
|
image_id: ImageId,
|
||||||
}
|
}
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn create_overlayed_image(
|
pub async fn create_subcontainer_fs(
|
||||||
context: EffectContext,
|
context: EffectContext,
|
||||||
CreateOverlayedImageParams { image_id }: CreateOverlayedImageParams,
|
CreateSubcontainerFsParams { image_id }: CreateSubcontainerFsParams,
|
||||||
) -> Result<(PathBuf, Guid), Error> {
|
) -> Result<(PathBuf, Guid), Error> {
|
||||||
let context = context.deref()?;
|
let context = context.deref()?;
|
||||||
if let Some(image) = context
|
if let Some(image) = context
|
||||||
@@ -131,7 +78,7 @@ pub async fn create_overlayed_image(
|
|||||||
})?
|
})?
|
||||||
.rootfs_dir();
|
.rootfs_dir();
|
||||||
let mountpoint = rootfs_dir
|
let mountpoint = rootfs_dir
|
||||||
.join("media/startos/overlays")
|
.join("media/startos/subcontainers")
|
||||||
.join(guid.as_ref());
|
.join(guid.as_ref());
|
||||||
tokio::fs::create_dir_all(&mountpoint).await?;
|
tokio::fs::create_dir_all(&mountpoint).await?;
|
||||||
let container_mountpoint = Path::new("/").join(
|
let container_mountpoint = Path::new("/").join(
|
||||||
@@ -150,7 +97,7 @@ pub async fn create_overlayed_image(
|
|||||||
context
|
context
|
||||||
.seed
|
.seed
|
||||||
.persistent_container
|
.persistent_container
|
||||||
.overlays
|
.subcontainers
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.insert(guid.clone(), guard);
|
.insert(guid.clone(), guard);
|
||||||
392
core/startos/src/service/effects/subcontainer/sync.rs
Normal file
392
core/startos/src/service/effects/subcontainer/sync.rs
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::ffi::{c_int, OsStr, OsString};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Command as StdCommand, Stdio};
|
||||||
|
|
||||||
|
use nix::sched::CloneFlags;
|
||||||
|
use nix::unistd::Pid;
|
||||||
|
use rpc_toolkit::Context;
|
||||||
|
use signal_hook::consts::signal::*;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
use unshare::Command as NSCommand;
|
||||||
|
|
||||||
|
use crate::service::effects::prelude::*;
|
||||||
|
use crate::service::effects::ContainerCliContext;
|
||||||
|
|
||||||
|
const FWD_SIGNALS: &[c_int] = &[
|
||||||
|
SIGABRT, SIGALRM, SIGCONT, SIGHUP, SIGINT, SIGIO, SIGPIPE, SIGPROF, SIGQUIT, SIGTERM, SIGTRAP,
|
||||||
|
SIGTSTP, SIGTTIN, SIGTTOU, SIGURG, SIGUSR1, SIGUSR2, SIGVTALRM,
|
||||||
|
];
|
||||||
|
|
||||||
|
struct NSPid(Vec<i32>);
|
||||||
|
impl procfs::FromBufRead for NSPid {
|
||||||
|
fn from_buf_read<R: std::io::BufRead>(r: R) -> procfs::ProcResult<Self> {
|
||||||
|
for line in r.lines() {
|
||||||
|
let line = line?;
|
||||||
|
if let Some(row) = line.trim().strip_prefix("NSpid") {
|
||||||
|
return Ok(Self(
|
||||||
|
row.split_ascii_whitespace()
|
||||||
|
.map(|pid| pid.parse::<i32>())
|
||||||
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(procfs::ProcError::Incomplete(None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_file_read(path: impl AsRef<Path>) -> Result<File, Error> {
|
||||||
|
File::open(&path).with_ctx(|_| {
|
||||||
|
(
|
||||||
|
ErrorKind::Filesystem,
|
||||||
|
lazy_format!("open r {}", path.as_ref().display()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Parser)]
|
||||||
|
pub struct ExecParams {
|
||||||
|
#[arg(short = 'e', long = "env")]
|
||||||
|
env: Option<PathBuf>,
|
||||||
|
#[arg(short = 'w', long = "workdir")]
|
||||||
|
workdir: Option<PathBuf>,
|
||||||
|
#[arg(short = 'u', long = "user")]
|
||||||
|
user: Option<String>,
|
||||||
|
chroot: PathBuf,
|
||||||
|
#[arg(trailing_var_arg = true)]
|
||||||
|
command: Vec<OsString>,
|
||||||
|
}
|
||||||
|
impl ExecParams {
|
||||||
|
fn exec(&self) -> Result<(), Error> {
|
||||||
|
let ExecParams {
|
||||||
|
env,
|
||||||
|
workdir,
|
||||||
|
user,
|
||||||
|
chroot,
|
||||||
|
command,
|
||||||
|
} = self;
|
||||||
|
let Some(([command], args)) = command.split_at_checked(1) else {
|
||||||
|
return Err(Error::new(
|
||||||
|
eyre!("command cannot be empty"),
|
||||||
|
ErrorKind::InvalidRequest,
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let env_string = if let Some(env) = &env {
|
||||||
|
std::fs::read_to_string(env)
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("read {env:?}")))?
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
};
|
||||||
|
let env = env_string
|
||||||
|
.lines()
|
||||||
|
.map(|l| l.trim())
|
||||||
|
.filter_map(|l| l.split_once("="))
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
std::os::unix::fs::chroot(chroot)
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("chroot {chroot:?}")))?;
|
||||||
|
let command = which::which_in(
|
||||||
|
command,
|
||||||
|
env.get("PATH")
|
||||||
|
.copied()
|
||||||
|
.map(Cow::Borrowed)
|
||||||
|
.or_else(|| std::env::var("PATH").ok().map(Cow::Owned))
|
||||||
|
.as_deref(),
|
||||||
|
workdir.as_deref().unwrap_or(Path::new("/")),
|
||||||
|
)
|
||||||
|
.with_kind(ErrorKind::Filesystem)?;
|
||||||
|
let mut cmd = StdCommand::new(command);
|
||||||
|
cmd.args(args);
|
||||||
|
for (k, v) in env {
|
||||||
|
cmd.env(k, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(uid) = user.as_deref().and_then(|u| u.parse::<u32>().ok()) {
|
||||||
|
cmd.uid(uid);
|
||||||
|
} else if let Some(user) = user {
|
||||||
|
let (uid, gid) = std::fs::read_to_string("/etc/passwd")
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, "read /etc/passwd"))?
|
||||||
|
.lines()
|
||||||
|
.find_map(|l| {
|
||||||
|
let mut split = l.trim().split(":");
|
||||||
|
if user != split.next()? {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
split.next(); // throw away x
|
||||||
|
Some((split.next()?.parse().ok()?, split.next()?.parse().ok()?))
|
||||||
|
// uid gid
|
||||||
|
})
|
||||||
|
.or_not_found(lazy_format!("{user} in /etc/passwd"))?;
|
||||||
|
cmd.uid(uid);
|
||||||
|
cmd.gid(gid);
|
||||||
|
};
|
||||||
|
if let Some(workdir) = workdir {
|
||||||
|
cmd.current_dir(workdir);
|
||||||
|
} else {
|
||||||
|
cmd.current_dir("/");
|
||||||
|
}
|
||||||
|
Err(cmd.exec().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn launch(
|
||||||
|
_: ContainerCliContext,
|
||||||
|
ExecParams {
|
||||||
|
env,
|
||||||
|
workdir,
|
||||||
|
user,
|
||||||
|
chroot,
|
||||||
|
command,
|
||||||
|
}: ExecParams,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
use unshare::{Namespace, Stdio};
|
||||||
|
|
||||||
|
use crate::service::cli::ContainerCliContext;
|
||||||
|
let mut sig = signal_hook::iterator::Signals::new(FWD_SIGNALS)?;
|
||||||
|
let mut cmd = NSCommand::new("/usr/bin/start-cli");
|
||||||
|
cmd.arg("subcontainer").arg("launch-init");
|
||||||
|
if let Some(env) = env {
|
||||||
|
cmd.arg("--env").arg(env);
|
||||||
|
}
|
||||||
|
if let Some(workdir) = workdir {
|
||||||
|
cmd.arg("--workdir").arg(workdir);
|
||||||
|
}
|
||||||
|
if let Some(user) = user {
|
||||||
|
cmd.arg("--user").arg(user);
|
||||||
|
}
|
||||||
|
cmd.arg(&chroot);
|
||||||
|
cmd.args(&command);
|
||||||
|
cmd.unshare(&[Namespace::Pid, Namespace::Cgroup, Namespace::Ipc]);
|
||||||
|
cmd.stdin(Stdio::piped());
|
||||||
|
cmd.stdout(Stdio::piped());
|
||||||
|
cmd.stderr(Stdio::piped());
|
||||||
|
let (stdin_send, stdin_recv) = oneshot::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Ok(mut stdin) = stdin_recv.blocking_recv() {
|
||||||
|
std::io::copy(&mut std::io::stdin(), &mut stdin).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let (stdout_send, stdout_recv) = oneshot::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Ok(mut stdout) = stdout_recv.blocking_recv() {
|
||||||
|
std::io::copy(&mut stdout, &mut std::io::stdout()).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let (stderr_send, stderr_recv) = oneshot::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Ok(mut stderr) = stderr_recv.blocking_recv() {
|
||||||
|
std::io::copy(&mut stderr, &mut std::io::stderr()).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if chroot.join("proc/1").exists() {
|
||||||
|
let ns_id = procfs::process::Process::new_with_root(chroot.join("proc"))
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, "open subcontainer procfs"))?
|
||||||
|
.namespaces()
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, "read subcontainer pid 1 ns"))?
|
||||||
|
.0
|
||||||
|
.get(OsStr::new("pid"))
|
||||||
|
.or_not_found("pid namespace")?
|
||||||
|
.identifier;
|
||||||
|
for proc in
|
||||||
|
procfs::process::all_processes().with_ctx(|_| (ErrorKind::Filesystem, "open procfs"))?
|
||||||
|
{
|
||||||
|
let proc = proc.with_ctx(|_| (ErrorKind::Filesystem, "read single process details"))?;
|
||||||
|
let pid = proc.pid();
|
||||||
|
if proc
|
||||||
|
.namespaces()
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("read pid {} ns", pid)))?
|
||||||
|
.0
|
||||||
|
.get(OsStr::new("pid"))
|
||||||
|
.map_or(false, |ns| ns.identifier == ns_id)
|
||||||
|
{
|
||||||
|
let pids = proc.read::<NSPid>("status").with_ctx(|_| {
|
||||||
|
(
|
||||||
|
ErrorKind::Filesystem,
|
||||||
|
lazy_format!("read pid {} NSpid", pid),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if pids.0.len() == 2 && pids.0[1] == 1 {
|
||||||
|
nix::sys::signal::kill(Pid::from_raw(pid), nix::sys::signal::SIGKILL)
|
||||||
|
.with_ctx(|_| {
|
||||||
|
(
|
||||||
|
ErrorKind::Filesystem,
|
||||||
|
lazy_format!(
|
||||||
|
"kill pid {} (determined to be pid 1 in subcontainer)",
|
||||||
|
pid
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nix::mount::umount(&chroot.join("proc"))
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, "unmounting subcontainer procfs"))?;
|
||||||
|
}
|
||||||
|
let mut child = cmd
|
||||||
|
.spawn()
|
||||||
|
.map_err(color_eyre::eyre::Report::msg)
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, "spawning child process"))?;
|
||||||
|
let pid = child.pid();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
for sig in sig.forever() {
|
||||||
|
nix::sys::signal::kill(
|
||||||
|
Pid::from_raw(pid),
|
||||||
|
Some(nix::sys::signal::Signal::try_from(sig).unwrap()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stdin_send
|
||||||
|
.send(child.stdin.take().unwrap())
|
||||||
|
.unwrap_or_default();
|
||||||
|
stdout_send
|
||||||
|
.send(child.stdout.take().unwrap())
|
||||||
|
.unwrap_or_default();
|
||||||
|
stderr_send
|
||||||
|
.send(child.stderr.take().unwrap())
|
||||||
|
.unwrap_or_default();
|
||||||
|
// TODO: subreaping, signal handling
|
||||||
|
let exit = child
|
||||||
|
.wait()
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, "waiting on child process"))?;
|
||||||
|
if let Some(code) = exit.code() {
|
||||||
|
std::process::exit(code);
|
||||||
|
} else {
|
||||||
|
if exit.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::new(
|
||||||
|
color_eyre::eyre::Report::msg(exit),
|
||||||
|
ErrorKind::Unknown,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn launch_init(_: ContainerCliContext, params: ExecParams) -> Result<(), Error> {
|
||||||
|
nix::mount::mount(
|
||||||
|
Some("proc"),
|
||||||
|
¶ms.chroot.join("proc"),
|
||||||
|
Some("proc"),
|
||||||
|
nix::mount::MsFlags::empty(),
|
||||||
|
None::<&str>,
|
||||||
|
)
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, "mount procfs"))?;
|
||||||
|
if params.command.is_empty() {
|
||||||
|
signal_hook::iterator::Signals::new(signal_hook::consts::TERM_SIGNALS)?
|
||||||
|
.forever()
|
||||||
|
.next();
|
||||||
|
std::process::exit(0)
|
||||||
|
} else {
|
||||||
|
params.exec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exec(
|
||||||
|
_: ContainerCliContext,
|
||||||
|
ExecParams {
|
||||||
|
env,
|
||||||
|
workdir,
|
||||||
|
user,
|
||||||
|
chroot,
|
||||||
|
command,
|
||||||
|
}: ExecParams,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut sig = signal_hook::iterator::Signals::new(FWD_SIGNALS)?;
|
||||||
|
let (send_pid, recv_pid) = oneshot::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Ok(pid) = recv_pid.blocking_recv() {
|
||||||
|
for sig in sig.forever() {
|
||||||
|
nix::sys::signal::kill(
|
||||||
|
Pid::from_raw(pid),
|
||||||
|
Some(nix::sys::signal::Signal::try_from(sig).unwrap()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut cmd = StdCommand::new("/usr/bin/start-cli");
|
||||||
|
cmd.arg("subcontainer").arg("exec-command");
|
||||||
|
if let Some(env) = env {
|
||||||
|
cmd.arg("--env").arg(env);
|
||||||
|
}
|
||||||
|
if let Some(workdir) = workdir {
|
||||||
|
cmd.arg("--workdir").arg(workdir);
|
||||||
|
}
|
||||||
|
if let Some(user) = user {
|
||||||
|
cmd.arg("--user").arg(user);
|
||||||
|
}
|
||||||
|
cmd.arg(&chroot);
|
||||||
|
cmd.args(&command);
|
||||||
|
cmd.stdin(Stdio::piped());
|
||||||
|
cmd.stdout(Stdio::piped());
|
||||||
|
cmd.stderr(Stdio::piped());
|
||||||
|
let (stdin_send, stdin_recv) = oneshot::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Ok(mut stdin) = stdin_recv.blocking_recv() {
|
||||||
|
std::io::copy(&mut std::io::stdin(), &mut stdin).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let (stdout_send, stdout_recv) = oneshot::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Ok(mut stdout) = stdout_recv.blocking_recv() {
|
||||||
|
std::io::copy(&mut stdout, &mut std::io::stdout()).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let (stderr_send, stderr_recv) = oneshot::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Ok(mut stderr) = stderr_recv.blocking_recv() {
|
||||||
|
std::io::copy(&mut stderr, &mut std::io::stderr()).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
nix::sched::setns(
|
||||||
|
open_file_read(chroot.join("proc/1/ns/pid"))?,
|
||||||
|
CloneFlags::CLONE_NEWPID,
|
||||||
|
)
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, "set pid ns"))?;
|
||||||
|
nix::sched::setns(
|
||||||
|
open_file_read(chroot.join("proc/1/ns/cgroup"))?,
|
||||||
|
CloneFlags::CLONE_NEWCGROUP,
|
||||||
|
)
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, "set cgroup ns"))?;
|
||||||
|
nix::sched::setns(
|
||||||
|
open_file_read(chroot.join("proc/1/ns/ipc"))?,
|
||||||
|
CloneFlags::CLONE_NEWIPC,
|
||||||
|
)
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, "set ipc ns"))?;
|
||||||
|
let mut child = cmd
|
||||||
|
.spawn()
|
||||||
|
.map_err(color_eyre::eyre::Report::msg)
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, "spawning child process"))?;
|
||||||
|
send_pid.send(child.id() as i32).unwrap_or_default();
|
||||||
|
stdin_send
|
||||||
|
.send(child.stdin.take().unwrap())
|
||||||
|
.unwrap_or_default();
|
||||||
|
stdout_send
|
||||||
|
.send(child.stdout.take().unwrap())
|
||||||
|
.unwrap_or_default();
|
||||||
|
stderr_send
|
||||||
|
.send(child.stderr.take().unwrap())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let exit = child
|
||||||
|
.wait()
|
||||||
|
.with_ctx(|_| (ErrorKind::Filesystem, "waiting on child process"))?;
|
||||||
|
if let Some(code) = exit.code() {
|
||||||
|
std::process::exit(code);
|
||||||
|
} else {
|
||||||
|
if exit.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::new(
|
||||||
|
color_eyre::eyre::Report::msg(exit),
|
||||||
|
ErrorKind::Unknown,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exec_command(_: ContainerCliContext, params: ExecParams) -> Result<(), Error> {
|
||||||
|
params.exec()
|
||||||
|
}
|
||||||
30
core/startos/src/service/effects/subcontainer/sync_dummy.rs
Normal file
30
core/startos/src/service/effects/subcontainer/sync_dummy.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
use crate::service::effects::prelude::*;
|
||||||
|
use crate::service::effects::ContainerCliContext;
|
||||||
|
|
||||||
|
pub fn launch(_: ContainerCliContext) -> Result<(), Error> {
|
||||||
|
Err(Error::new(
|
||||||
|
eyre!("requires feature container-runtime"),
|
||||||
|
ErrorKind::InvalidRequest,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn launch_init(_: ContainerCliContext) -> Result<(), Error> {
|
||||||
|
Err(Error::new(
|
||||||
|
eyre!("requires feature container-runtime"),
|
||||||
|
ErrorKind::InvalidRequest,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exec(_: ContainerCliContext) -> Result<(), Error> {
|
||||||
|
Err(Error::new(
|
||||||
|
eyre!("requires feature container-runtime"),
|
||||||
|
ErrorKind::InvalidRequest,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exec_command(_: ContainerCliContext) -> Result<(), Error> {
|
||||||
|
Err(Error::new(
|
||||||
|
eyre!("requires feature container-runtime"),
|
||||||
|
ErrorKind::InvalidRequest,
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ use crate::progress::{NamedProgress, Progress};
|
|||||||
use crate::rpc_continuations::Guid;
|
use crate::rpc_continuations::Guid;
|
||||||
use crate::s9pk::S9pk;
|
use crate::s9pk::S9pk;
|
||||||
use crate::service::service_map::InstallProgressHandles;
|
use crate::service::service_map::InstallProgressHandles;
|
||||||
use crate::status::health_check::HealthCheckResult;
|
use crate::status::health_check::NamedHealthCheckResult;
|
||||||
use crate::util::actor::concurrent::ConcurrentActor;
|
use crate::util::actor::concurrent::ConcurrentActor;
|
||||||
use crate::util::io::create_file;
|
use crate::util::io::create_file;
|
||||||
use crate::util::serde::{NoOutput, Pem};
|
use crate::util::serde::{NoOutput, Pem};
|
||||||
@@ -45,7 +45,7 @@ mod properties;
|
|||||||
mod rpc;
|
mod rpc;
|
||||||
mod service_actor;
|
mod service_actor;
|
||||||
pub mod service_map;
|
pub mod service_map;
|
||||||
mod start_stop;
|
pub mod start_stop;
|
||||||
mod transition;
|
mod transition;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
@@ -493,7 +493,6 @@ impl Service {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RunningStatus {
|
pub struct RunningStatus {
|
||||||
health: OrdMap<HealthCheckId, HealthCheckResult>,
|
|
||||||
started: DateTime<Utc>,
|
started: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,7 +515,6 @@ impl ServiceActorSeed {
|
|||||||
.running_status
|
.running_status
|
||||||
.take()
|
.take()
|
||||||
.unwrap_or_else(|| RunningStatus {
|
.unwrap_or_else(|| RunningStatus {
|
||||||
health: Default::default(),
|
|
||||||
started: Utc::now(),
|
started: Utc::now(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ pub struct PersistentContainer {
|
|||||||
volumes: BTreeMap<VolumeId, MountGuard>,
|
volumes: BTreeMap<VolumeId, MountGuard>,
|
||||||
assets: BTreeMap<VolumeId, MountGuard>,
|
assets: BTreeMap<VolumeId, MountGuard>,
|
||||||
pub(super) images: BTreeMap<ImageId, Arc<MountGuard>>,
|
pub(super) images: BTreeMap<ImageId, Arc<MountGuard>>,
|
||||||
pub(super) overlays: Arc<Mutex<BTreeMap<Guid, OverlayGuard<Arc<MountGuard>>>>>,
|
pub(super) subcontainers: Arc<Mutex<BTreeMap<Guid, OverlayGuard<Arc<MountGuard>>>>>,
|
||||||
pub(super) state: Arc<watch::Sender<ServiceState>>,
|
pub(super) state: Arc<watch::Sender<ServiceState>>,
|
||||||
pub(super) net_service: Mutex<NetService>,
|
pub(super) net_service: Mutex<NetService>,
|
||||||
destroyed: bool,
|
destroyed: bool,
|
||||||
@@ -273,7 +273,7 @@ impl PersistentContainer {
|
|||||||
volumes,
|
volumes,
|
||||||
assets,
|
assets,
|
||||||
images,
|
images,
|
||||||
overlays: Arc::new(Mutex::new(BTreeMap::new())),
|
subcontainers: Arc::new(Mutex::new(BTreeMap::new())),
|
||||||
state: Arc::new(watch::channel(ServiceState::new(start)).0),
|
state: Arc::new(watch::channel(ServiceState::new(start)).0),
|
||||||
net_service: Mutex::new(net_service),
|
net_service: Mutex::new(net_service),
|
||||||
destroyed: false,
|
destroyed: false,
|
||||||
@@ -388,7 +388,7 @@ impl PersistentContainer {
|
|||||||
let volumes = std::mem::take(&mut self.volumes);
|
let volumes = std::mem::take(&mut self.volumes);
|
||||||
let assets = std::mem::take(&mut self.assets);
|
let assets = std::mem::take(&mut self.assets);
|
||||||
let images = std::mem::take(&mut self.images);
|
let images = std::mem::take(&mut self.images);
|
||||||
let overlays = self.overlays.clone();
|
let subcontainers = self.subcontainers.clone();
|
||||||
let lxc_container = self.lxc_container.take();
|
let lxc_container = self.lxc_container.take();
|
||||||
self.destroyed = true;
|
self.destroyed = true;
|
||||||
Some(async move {
|
Some(async move {
|
||||||
@@ -404,7 +404,7 @@ impl PersistentContainer {
|
|||||||
for (_, assets) in assets {
|
for (_, assets) in assets {
|
||||||
errs.handle(assets.unmount(true).await);
|
errs.handle(assets.unmount(true).await);
|
||||||
}
|
}
|
||||||
for (_, overlay) in std::mem::take(&mut *overlays.lock().await) {
|
for (_, overlay) in std::mem::take(&mut *subcontainers.lock().await) {
|
||||||
errs.handle(overlay.unmount(true).await);
|
errs.handle(overlay.unmount(true).await);
|
||||||
}
|
}
|
||||||
for (_, images) in images {
|
for (_, images) in images {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use imbl::OrdMap;
|
|
||||||
|
|
||||||
use super::start_stop::StartStop;
|
use super::start_stop::StartStop;
|
||||||
use super::ServiceActorSeed;
|
use super::ServiceActorSeed;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::service::persistent_container::ServiceStateKinds;
|
||||||
use crate::service::transition::TransitionKind;
|
use crate::service::transition::TransitionKind;
|
||||||
use crate::service::SYNC_RETRY_COOLDOWN_SECONDS;
|
use crate::service::SYNC_RETRY_COOLDOWN_SECONDS;
|
||||||
use crate::status::MainStatus;
|
use crate::status::MainStatus;
|
||||||
@@ -46,96 +45,77 @@ async fn service_actor_loop(
|
|||||||
let id = &seed.id;
|
let id = &seed.id;
|
||||||
let kinds = current.borrow().kinds();
|
let kinds = current.borrow().kinds();
|
||||||
if let Err(e) = async {
|
if let Err(e) = async {
|
||||||
let main_status = match (
|
|
||||||
kinds.transition_state,
|
|
||||||
kinds.desired_state,
|
|
||||||
kinds.running_status,
|
|
||||||
) {
|
|
||||||
(Some(TransitionKind::Restarting), StartStop::Stop, Some(_)) => {
|
|
||||||
seed.persistent_container.stop().await?;
|
|
||||||
MainStatus::Restarting
|
|
||||||
}
|
|
||||||
(Some(TransitionKind::Restarting), StartStop::Start, _) => {
|
|
||||||
seed.persistent_container.start().await?;
|
|
||||||
MainStatus::Restarting
|
|
||||||
}
|
|
||||||
(Some(TransitionKind::Restarting), _, _) => MainStatus::Restarting,
|
|
||||||
(Some(TransitionKind::Restoring), _, _) => MainStatus::Restoring,
|
|
||||||
(Some(TransitionKind::BackingUp), StartStop::Stop, Some(status)) => {
|
|
||||||
seed.persistent_container.stop().await?;
|
|
||||||
MainStatus::BackingUp {
|
|
||||||
started: Some(status.started),
|
|
||||||
health: status.health.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Some(TransitionKind::BackingUp), StartStop::Start, _) => {
|
|
||||||
seed.persistent_container.start().await?;
|
|
||||||
MainStatus::BackingUp {
|
|
||||||
started: None,
|
|
||||||
health: OrdMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Some(TransitionKind::BackingUp), _, _) => MainStatus::BackingUp {
|
|
||||||
started: None,
|
|
||||||
health: OrdMap::new(),
|
|
||||||
},
|
|
||||||
(None, StartStop::Stop, None) => MainStatus::Stopped,
|
|
||||||
(None, StartStop::Stop, Some(_)) => {
|
|
||||||
let task_seed = seed.clone();
|
|
||||||
seed.ctx
|
|
||||||
.db
|
|
||||||
.mutate(|d| {
|
|
||||||
if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id) {
|
|
||||||
i.as_status_mut().as_main_mut().ser(&MainStatus::Stopping)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
task_seed.persistent_container.stop().await?;
|
|
||||||
MainStatus::Stopped
|
|
||||||
}
|
|
||||||
(None, StartStop::Start, Some(status)) => MainStatus::Running {
|
|
||||||
started: status.started,
|
|
||||||
health: status.health.clone(),
|
|
||||||
},
|
|
||||||
(None, StartStop::Start, None) => {
|
|
||||||
seed.persistent_container.start().await?;
|
|
||||||
MainStatus::Starting
|
|
||||||
}
|
|
||||||
};
|
|
||||||
seed.ctx
|
seed.ctx
|
||||||
.db
|
.db
|
||||||
.mutate(|d| {
|
.mutate(|d| {
|
||||||
if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id) {
|
if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id) {
|
||||||
let previous = i.as_status().as_main().de()?;
|
let previous = i.as_status().as_main().de()?;
|
||||||
let previous_health = previous.health();
|
let main_status = match &kinds {
|
||||||
let previous_started = previous.started();
|
ServiceStateKinds {
|
||||||
let mut main_status = main_status;
|
transition_state: Some(TransitionKind::Restarting),
|
||||||
match &mut main_status {
|
..
|
||||||
&mut MainStatus::Running { ref mut health, .. }
|
} => MainStatus::Restarting,
|
||||||
| &mut MainStatus::BackingUp { ref mut health, .. } => {
|
ServiceStateKinds {
|
||||||
*health = previous_health.unwrap_or(health).clone();
|
transition_state: Some(TransitionKind::Restoring),
|
||||||
}
|
..
|
||||||
_ => (),
|
} => MainStatus::Restoring,
|
||||||
};
|
ServiceStateKinds {
|
||||||
match &mut main_status {
|
transition_state: Some(TransitionKind::BackingUp),
|
||||||
MainStatus::Running {
|
..
|
||||||
ref mut started, ..
|
} => previous.backing_up(),
|
||||||
} => {
|
ServiceStateKinds {
|
||||||
*started = previous_started.unwrap_or(*started);
|
running_status: Some(status),
|
||||||
}
|
desired_state: StartStop::Start,
|
||||||
MainStatus::BackingUp {
|
..
|
||||||
ref mut started, ..
|
} => MainStatus::Running {
|
||||||
} => {
|
started: status.started,
|
||||||
*started = previous_started.map(Some).unwrap_or(*started);
|
health: previous.health().cloned().unwrap_or_default(),
|
||||||
}
|
},
|
||||||
_ => (),
|
ServiceStateKinds {
|
||||||
|
running_status: None,
|
||||||
|
desired_state: StartStop::Start,
|
||||||
|
..
|
||||||
|
} => MainStatus::Starting {
|
||||||
|
health: previous.health().cloned().unwrap_or_default(),
|
||||||
|
},
|
||||||
|
ServiceStateKinds {
|
||||||
|
running_status: Some(_),
|
||||||
|
desired_state: StartStop::Stop,
|
||||||
|
..
|
||||||
|
} => MainStatus::Stopping,
|
||||||
|
ServiceStateKinds {
|
||||||
|
running_status: None,
|
||||||
|
desired_state: StartStop::Stop,
|
||||||
|
..
|
||||||
|
} => MainStatus::Stopped,
|
||||||
};
|
};
|
||||||
i.as_status_mut().as_main_mut().ser(&main_status)?;
|
i.as_status_mut().as_main_mut().ser(&main_status)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
seed.synchronized.notify_waiters();
|
||||||
|
|
||||||
|
match kinds {
|
||||||
|
ServiceStateKinds {
|
||||||
|
running_status: None,
|
||||||
|
desired_state: StartStop::Start,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
seed.persistent_container.start().await?;
|
||||||
|
}
|
||||||
|
ServiceStateKinds {
|
||||||
|
running_status: Some(_),
|
||||||
|
desired_state: StartStop::Stop,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
seed.persistent_container.stop().await?;
|
||||||
|
seed.persistent_container
|
||||||
|
.state
|
||||||
|
.send_if_modified(|s| s.running_status.take().is_some());
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
|
||||||
Ok::<_, Error>(())
|
Ok::<_, Error>(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ impl ServiceMap {
|
|||||||
} else {
|
} else {
|
||||||
PackageState::Installing(installing)
|
PackageState::Installing(installing)
|
||||||
},
|
},
|
||||||
|
data_version: None,
|
||||||
status: Status {
|
status: Status {
|
||||||
configured: false,
|
configured: false,
|
||||||
main: MainStatus::Stopped,
|
main: MainStatus::Stopped,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::status::MainStatus;
|
use crate::status::MainStatus;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum StartStop {
|
pub enum StartStop {
|
||||||
Start,
|
Start,
|
||||||
Stop,
|
Stop,
|
||||||
@@ -11,23 +15,19 @@ impl StartStop {
|
|||||||
matches!(self, StartStop::Start)
|
matches!(self, StartStop::Start)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl From<MainStatus> for StartStop {
|
// impl From<MainStatus> for StartStop {
|
||||||
fn from(value: MainStatus) -> Self {
|
// fn from(value: MainStatus) -> Self {
|
||||||
match value {
|
// match value {
|
||||||
MainStatus::Stopped => StartStop::Stop,
|
// MainStatus::Stopped => StartStop::Stop,
|
||||||
MainStatus::Restoring => StartStop::Stop,
|
// MainStatus::Restoring => StartStop::Stop,
|
||||||
MainStatus::Restarting => StartStop::Start,
|
// MainStatus::Restarting => StartStop::Start,
|
||||||
MainStatus::Stopping { .. } => StartStop::Stop,
|
// MainStatus::Stopping { .. } => StartStop::Stop,
|
||||||
MainStatus::Starting => StartStop::Start,
|
// MainStatus::Starting => StartStop::Start,
|
||||||
MainStatus::Running {
|
// MainStatus::Running {
|
||||||
started: _,
|
// started: _,
|
||||||
health: _,
|
// health: _,
|
||||||
} => StartStop::Start,
|
// } => StartStop::Start,
|
||||||
MainStatus::BackingUp { started, health: _ } if started.is_some() => StartStop::Start,
|
// MainStatus::BackingUp { on_complete } => on_complete,
|
||||||
MainStatus::BackingUp {
|
// }
|
||||||
started: _,
|
// }
|
||||||
health: _,
|
// }
|
||||||
} => StartStop::Stop,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,25 +9,25 @@ use crate::util::clap::FromStrParser;
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, TS)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct HealthCheckResult {
|
pub struct NamedHealthCheckResult {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub kind: HealthCheckResultKind,
|
pub kind: NamedHealthCheckResultKind,
|
||||||
}
|
}
|
||||||
// healthCheckName:kind:message OR healthCheckName:kind
|
// healthCheckName:kind:message OR healthCheckName:kind
|
||||||
impl FromStr for HealthCheckResult {
|
impl FromStr for NamedHealthCheckResult {
|
||||||
type Err = color_eyre::eyre::Report;
|
type Err = color_eyre::eyre::Report;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
let from_parts = |name: &str, kind: &str, message: Option<&str>| {
|
let from_parts = |name: &str, kind: &str, message: Option<&str>| {
|
||||||
let message = message.map(|x| x.to_string());
|
let message = message.map(|x| x.to_string());
|
||||||
let kind = match kind {
|
let kind = match kind {
|
||||||
"success" => HealthCheckResultKind::Success { message },
|
"success" => NamedHealthCheckResultKind::Success { message },
|
||||||
"disabled" => HealthCheckResultKind::Disabled { message },
|
"disabled" => NamedHealthCheckResultKind::Disabled { message },
|
||||||
"starting" => HealthCheckResultKind::Starting { message },
|
"starting" => NamedHealthCheckResultKind::Starting { message },
|
||||||
"loading" => HealthCheckResultKind::Loading {
|
"loading" => NamedHealthCheckResultKind::Loading {
|
||||||
message: message.unwrap_or_default(),
|
message: message.unwrap_or_default(),
|
||||||
},
|
},
|
||||||
"failure" => HealthCheckResultKind::Failure {
|
"failure" => NamedHealthCheckResultKind::Failure {
|
||||||
message: message.unwrap_or_default(),
|
message: message.unwrap_or_default(),
|
||||||
},
|
},
|
||||||
_ => return Err(color_eyre::eyre::eyre!("Invalid health check kind")),
|
_ => return Err(color_eyre::eyre::eyre!("Invalid health check kind")),
|
||||||
@@ -47,7 +47,7 @@ impl FromStr for HealthCheckResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl ValueParserFactory for HealthCheckResult {
|
impl ValueParserFactory for NamedHealthCheckResult {
|
||||||
type Parser = FromStrParser<Self>;
|
type Parser = FromStrParser<Self>;
|
||||||
fn value_parser() -> Self::Parser {
|
fn value_parser() -> Self::Parser {
|
||||||
FromStrParser::new()
|
FromStrParser::new()
|
||||||
@@ -57,40 +57,44 @@ impl ValueParserFactory for HealthCheckResult {
|
|||||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, TS)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[serde(tag = "result")]
|
#[serde(tag = "result")]
|
||||||
pub enum HealthCheckResultKind {
|
pub enum NamedHealthCheckResultKind {
|
||||||
Success { message: Option<String> },
|
Success { message: Option<String> },
|
||||||
Disabled { message: Option<String> },
|
Disabled { message: Option<String> },
|
||||||
Starting { message: Option<String> },
|
Starting { message: Option<String> },
|
||||||
Loading { message: String },
|
Loading { message: String },
|
||||||
Failure { message: String },
|
Failure { message: String },
|
||||||
}
|
}
|
||||||
impl std::fmt::Display for HealthCheckResult {
|
impl std::fmt::Display for NamedHealthCheckResult {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let name = &self.name;
|
let name = &self.name;
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
HealthCheckResultKind::Success { message } => {
|
NamedHealthCheckResultKind::Success { message } => {
|
||||||
if let Some(message) = message {
|
if let Some(message) = message {
|
||||||
write!(f, "{name}: Succeeded ({message})")
|
write!(f, "{name}: Succeeded ({message})")
|
||||||
} else {
|
} else {
|
||||||
write!(f, "{name}: Succeeded")
|
write!(f, "{name}: Succeeded")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HealthCheckResultKind::Disabled { message } => {
|
NamedHealthCheckResultKind::Disabled { message } => {
|
||||||
if let Some(message) = message {
|
if let Some(message) = message {
|
||||||
write!(f, "{name}: Disabled ({message})")
|
write!(f, "{name}: Disabled ({message})")
|
||||||
} else {
|
} else {
|
||||||
write!(f, "{name}: Disabled")
|
write!(f, "{name}: Disabled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HealthCheckResultKind::Starting { message } => {
|
NamedHealthCheckResultKind::Starting { message } => {
|
||||||
if let Some(message) = message {
|
if let Some(message) = message {
|
||||||
write!(f, "{name}: Starting ({message})")
|
write!(f, "{name}: Starting ({message})")
|
||||||
} else {
|
} else {
|
||||||
write!(f, "{name}: Starting")
|
write!(f, "{name}: Starting")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HealthCheckResultKind::Loading { message } => write!(f, "{name}: Loading ({message})"),
|
NamedHealthCheckResultKind::Loading { message } => {
|
||||||
HealthCheckResultKind::Failure { message } => write!(f, "{name}: Failed ({message})"),
|
write!(f, "{name}: Loading ({message})")
|
||||||
|
}
|
||||||
|
NamedHealthCheckResultKind::Failure { message } => {
|
||||||
|
write!(f, "{name}: Failed ({message})")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::{collections::BTreeMap, sync::Arc};
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use imbl::OrdMap;
|
use imbl::OrdMap;
|
||||||
@@ -6,8 +6,9 @@ use serde::{Deserialize, Serialize};
|
|||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
use self::health_check::HealthCheckId;
|
use self::health_check::HealthCheckId;
|
||||||
use crate::status::health_check::HealthCheckResult;
|
use crate::prelude::*;
|
||||||
use crate::{prelude::*, util::GeneralGuard};
|
use crate::service::start_stop::StartStop;
|
||||||
|
use crate::status::health_check::NamedHealthCheckResult;
|
||||||
|
|
||||||
pub mod health_check;
|
pub mod health_check;
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)]
|
#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)]
|
||||||
@@ -22,25 +23,24 @@ pub struct Status {
|
|||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, TS)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, TS)]
|
||||||
#[serde(tag = "status")]
|
#[serde(tag = "status")]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[serde(rename_all_fields = "camelCase")]
|
||||||
pub enum MainStatus {
|
pub enum MainStatus {
|
||||||
Stopped,
|
Stopped,
|
||||||
Restarting,
|
Restarting,
|
||||||
Restoring,
|
Restoring,
|
||||||
Stopping,
|
Stopping,
|
||||||
Starting,
|
Starting {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[ts(as = "BTreeMap<HealthCheckId, NamedHealthCheckResult>")]
|
||||||
|
health: OrdMap<HealthCheckId, NamedHealthCheckResult>,
|
||||||
|
},
|
||||||
Running {
|
Running {
|
||||||
#[ts(type = "string")]
|
#[ts(type = "string")]
|
||||||
started: DateTime<Utc>,
|
started: DateTime<Utc>,
|
||||||
#[ts(as = "BTreeMap<HealthCheckId, HealthCheckResult>")]
|
#[ts(as = "BTreeMap<HealthCheckId, NamedHealthCheckResult>")]
|
||||||
health: OrdMap<HealthCheckId, HealthCheckResult>,
|
health: OrdMap<HealthCheckId, NamedHealthCheckResult>,
|
||||||
},
|
},
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
BackingUp {
|
BackingUp {
|
||||||
#[ts(type = "string | null")]
|
on_complete: StartStop,
|
||||||
started: Option<DateTime<Utc>>,
|
|
||||||
#[ts(as = "BTreeMap<HealthCheckId, HealthCheckResult>")]
|
|
||||||
health: OrdMap<HealthCheckId, HealthCheckResult>,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
impl MainStatus {
|
impl MainStatus {
|
||||||
@@ -48,60 +48,37 @@ impl MainStatus {
|
|||||||
match self {
|
match self {
|
||||||
MainStatus::Starting { .. }
|
MainStatus::Starting { .. }
|
||||||
| MainStatus::Running { .. }
|
| MainStatus::Running { .. }
|
||||||
|
| MainStatus::Restarting
|
||||||
| MainStatus::BackingUp {
|
| MainStatus::BackingUp {
|
||||||
started: Some(_), ..
|
on_complete: StartStop::Start,
|
||||||
} => true,
|
} => true,
|
||||||
MainStatus::Stopped
|
MainStatus::Stopped
|
||||||
| MainStatus::Restoring
|
| MainStatus::Restoring
|
||||||
| MainStatus::Stopping { .. }
|
| MainStatus::Stopping { .. }
|
||||||
| MainStatus::Restarting
|
| MainStatus::BackingUp {
|
||||||
| MainStatus::BackingUp { started: None, .. } => false,
|
on_complete: StartStop::Stop,
|
||||||
|
} => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// pub fn stop(&mut self) {
|
|
||||||
// match self {
|
|
||||||
// MainStatus::Starting { .. } | MainStatus::Running { .. } => {
|
|
||||||
// *self = MainStatus::Stopping;
|
|
||||||
// }
|
|
||||||
// MainStatus::BackingUp { started, .. } => {
|
|
||||||
// *started = None;
|
|
||||||
// }
|
|
||||||
// MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => (),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
pub fn started(&self) -> Option<DateTime<Utc>> {
|
|
||||||
match self {
|
|
||||||
MainStatus::Running { started, .. } => Some(*started),
|
|
||||||
MainStatus::BackingUp { started, .. } => *started,
|
|
||||||
MainStatus::Stopped => None,
|
|
||||||
MainStatus::Restoring => None,
|
|
||||||
MainStatus::Restarting => None,
|
|
||||||
MainStatus::Stopping { .. } => None,
|
|
||||||
MainStatus::Starting { .. } => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn backing_up(&self) -> Self {
|
|
||||||
let (started, health) = match self {
|
|
||||||
MainStatus::Starting { .. } => (Some(Utc::now()), Default::default()),
|
|
||||||
MainStatus::Running { started, health } => (Some(started.clone()), health.clone()),
|
|
||||||
MainStatus::Stopped
|
|
||||||
| MainStatus::Stopping { .. }
|
|
||||||
| MainStatus::Restoring
|
|
||||||
| MainStatus::Restarting => (None, Default::default()),
|
|
||||||
MainStatus::BackingUp { .. } => return self.clone(),
|
|
||||||
};
|
|
||||||
MainStatus::BackingUp { started, health }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn health(&self) -> Option<&OrdMap<HealthCheckId, HealthCheckResult>> {
|
pub fn backing_up(self) -> Self {
|
||||||
|
MainStatus::BackingUp {
|
||||||
|
on_complete: if self.running() {
|
||||||
|
StartStop::Start
|
||||||
|
} else {
|
||||||
|
StartStop::Stop
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn health(&self) -> Option<&OrdMap<HealthCheckId, NamedHealthCheckResult>> {
|
||||||
match self {
|
match self {
|
||||||
MainStatus::Running { health, .. } => Some(health),
|
MainStatus::Running { health, .. } | MainStatus::Starting { health } => Some(health),
|
||||||
MainStatus::BackingUp { health, .. } => Some(health),
|
MainStatus::BackingUp { .. }
|
||||||
MainStatus::Stopped
|
| MainStatus::Stopped
|
||||||
| MainStatus::Restoring
|
| MainStatus::Restoring
|
||||||
| MainStatus::Stopping { .. }
|
| MainStatus::Stopping { .. }
|
||||||
| MainStatus::Restarting => None,
|
| MainStatus::Restarting => None,
|
||||||
MainStatus::Starting { .. } => None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ mod v0_3_6_alpha_5;
|
|||||||
mod v0_3_6_alpha_6;
|
mod v0_3_6_alpha_6;
|
||||||
mod v0_3_6_alpha_7;
|
mod v0_3_6_alpha_7;
|
||||||
|
|
||||||
pub type Current = v0_3_6_alpha_3::Version; // VERSION_BUMP
|
pub type Current = v0_3_6_alpha_5::Version; // VERSION_BUMP
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
|
|||||||
1
debian/postinst
vendored
1
debian/postinst
vendored
@@ -79,6 +79,7 @@ sed -i '/\(^\|#\)SystemMaxUse=/c\SystemMaxUse=1G' /etc/systemd/journald.conf
|
|||||||
sed -i '/\(^\|#\)ForwardToSyslog=/c\ForwardToSyslog=no' /etc/systemd/journald.conf
|
sed -i '/\(^\|#\)ForwardToSyslog=/c\ForwardToSyslog=no' /etc/systemd/journald.conf
|
||||||
sed -i '/^\s*#\?\s*issue_discards\s*=\s*/c\issue_discards = 1' /etc/lvm/lvm.conf
|
sed -i '/^\s*#\?\s*issue_discards\s*=\s*/c\issue_discards = 1' /etc/lvm/lvm.conf
|
||||||
sed -i '/\(^\|#\)\s*unqualified-search-registries\s*=\s*/c\unqualified-search-registries = ["docker.io"]' /etc/containers/registries.conf
|
sed -i '/\(^\|#\)\s*unqualified-search-registries\s*=\s*/c\unqualified-search-registries = ["docker.io"]' /etc/containers/registries.conf
|
||||||
|
sed -i 's/\(#\|\^\)\s*\([^=]\+\)=\(suspend\|hibernate\)\s*$/\2=ignore/g' /etc/systemd/logind.conf
|
||||||
|
|
||||||
mkdir -p /etc/nginx/ssl
|
mkdir -p /etc/nginx/ssl
|
||||||
|
|
||||||
|
|||||||
@@ -30,17 +30,11 @@ import { healthCheck, HealthCheckParams } from "./health/HealthCheck"
|
|||||||
import { checkPortListening } from "./health/checkFns/checkPortListening"
|
import { checkPortListening } from "./health/checkFns/checkPortListening"
|
||||||
import { checkWebUrl, runHealthScript } from "./health/checkFns"
|
import { checkWebUrl, runHealthScript } from "./health/checkFns"
|
||||||
import { List } from "./config/builder/list"
|
import { List } from "./config/builder/list"
|
||||||
import { Migration } from "./inits/migrations/Migration"
|
|
||||||
import { Install, InstallFn } from "./inits/setupInstall"
|
import { Install, InstallFn } from "./inits/setupInstall"
|
||||||
import { setupActions } from "./actions/setupActions"
|
import { setupActions } from "./actions/setupActions"
|
||||||
import { setupDependencyConfig } from "./dependencies/setupDependencyConfig"
|
import { setupDependencyConfig } from "./dependencies/setupDependencyConfig"
|
||||||
import { SetupBackupsParams, setupBackups } from "./backup/setupBackups"
|
import { SetupBackupsParams, setupBackups } from "./backup/setupBackups"
|
||||||
import { setupInit } from "./inits/setupInit"
|
import { setupInit } from "./inits/setupInit"
|
||||||
import {
|
|
||||||
EnsureUniqueId,
|
|
||||||
Migrations,
|
|
||||||
setupMigrations,
|
|
||||||
} from "./inits/migrations/setupMigrations"
|
|
||||||
import { Uninstall, UninstallFn, setupUninstall } from "./inits/setupUninstall"
|
import { Uninstall, UninstallFn, setupUninstall } from "./inits/setupUninstall"
|
||||||
import { setupMain } from "./mainFn"
|
import { setupMain } from "./mainFn"
|
||||||
import { defaultTrigger } from "./trigger/defaultTrigger"
|
import { defaultTrigger } from "./trigger/defaultTrigger"
|
||||||
@@ -67,7 +61,7 @@ import {
|
|||||||
} from "./util/getServiceInterface"
|
} from "./util/getServiceInterface"
|
||||||
import { getServiceInterfaces } from "./util/getServiceInterfaces"
|
import { getServiceInterfaces } from "./util/getServiceInterfaces"
|
||||||
import { getStore } from "./store/getStore"
|
import { getStore } from "./store/getStore"
|
||||||
import { CommandOptions, MountOptions, Overlay } from "./util/Overlay"
|
import { CommandOptions, MountOptions, SubContainer } from "./util/SubContainer"
|
||||||
import { splitCommand } from "./util/splitCommand"
|
import { splitCommand } from "./util/splitCommand"
|
||||||
import { Mounts } from "./mainFn/Mounts"
|
import { Mounts } from "./mainFn/Mounts"
|
||||||
import { Dependency } from "./Dependency"
|
import { Dependency } from "./Dependency"
|
||||||
@@ -75,9 +69,13 @@ import * as T from "./types"
|
|||||||
import { testTypeVersion, ValidateExVer } from "./exver"
|
import { testTypeVersion, ValidateExVer } from "./exver"
|
||||||
import { ExposedStorePaths } from "./store/setupExposeStore"
|
import { ExposedStorePaths } from "./store/setupExposeStore"
|
||||||
import { PathBuilder, extractJsonPath, pathBuilder } from "./store/PathBuilder"
|
import { PathBuilder, extractJsonPath, pathBuilder } from "./store/PathBuilder"
|
||||||
import { checkAllDependencies } from "./dependencies/dependencies"
|
import {
|
||||||
|
CheckDependencies,
|
||||||
|
checkDependencies,
|
||||||
|
} from "./dependencies/dependencies"
|
||||||
import { health } from "."
|
import { health } from "."
|
||||||
import { GetSslCertificate } from "./util/GetSslCertificate"
|
import { GetSslCertificate } from "./util/GetSslCertificate"
|
||||||
|
import { VersionGraph } from "./version"
|
||||||
|
|
||||||
export const SDKVersion = testTypeVersion("0.3.6")
|
export const SDKVersion = testTypeVersion("0.3.6")
|
||||||
|
|
||||||
@@ -141,8 +139,58 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
|||||||
}]?: Dependency
|
}]?: Dependency
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NestedEffects = "subcontainer" | "store"
|
||||||
|
type InterfaceEffects =
|
||||||
|
| "getServiceInterface"
|
||||||
|
| "listServiceInterfaces"
|
||||||
|
| "exportServiceInterface"
|
||||||
|
| "clearServiceInterfaces"
|
||||||
|
| "bind"
|
||||||
|
| "getHostInfo"
|
||||||
|
| "getPrimaryUrl"
|
||||||
|
type MainUsedEffects = "setMainStatus" | "setHealth"
|
||||||
|
type AlreadyExposed = "getSslCertificate" | "getSystemSmtp"
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
type StartSdkEffectWrapper = {
|
||||||
|
[K in keyof Omit<Effects, NestedEffects | InterfaceEffects | MainUsedEffects| AlreadyExposed>]: (effects: Effects, ...args: Parameters<Effects[K]>) => ReturnType<Effects[K]>
|
||||||
|
}
|
||||||
|
const startSdkEffectWrapper: StartSdkEffectWrapper = {
|
||||||
|
executeAction: (effects, ...args) => effects.executeAction(...args),
|
||||||
|
exportAction: (effects, ...args) => effects.exportAction(...args),
|
||||||
|
clearActions: (effects, ...args) => effects.clearActions(...args),
|
||||||
|
getConfigured: (effects, ...args) => effects.getConfigured(...args),
|
||||||
|
setConfigured: (effects, ...args) => effects.setConfigured(...args),
|
||||||
|
restart: (effects, ...args) => effects.restart(...args),
|
||||||
|
setDependencies: (effects, ...args) => effects.setDependencies(...args),
|
||||||
|
checkDependencies: (effects, ...args) =>
|
||||||
|
effects.checkDependencies(...args),
|
||||||
|
mount: (effects, ...args) => effects.mount(...args),
|
||||||
|
getInstalledPackages: (effects, ...args) =>
|
||||||
|
effects.getInstalledPackages(...args),
|
||||||
|
exposeForDependents: (effects, ...args) =>
|
||||||
|
effects.exposeForDependents(...args),
|
||||||
|
getServicePortForward: (effects, ...args) =>
|
||||||
|
effects.getServicePortForward(...args),
|
||||||
|
clearBindings: (effects, ...args) => effects.clearBindings(...args),
|
||||||
|
getContainerIp: (effects, ...args) => effects.getContainerIp(...args),
|
||||||
|
getSslKey: (effects, ...args) => effects.getSslKey(...args),
|
||||||
|
setDataVersion: (effects, ...args) => effects.setDataVersion(...args),
|
||||||
|
getDataVersion: (effects, ...args) => effects.getDataVersion(...args),
|
||||||
|
shutdown: (effects, ...args) => effects.shutdown(...args),
|
||||||
|
getDependencies: (effects, ...args) => effects.getDependencies(...args),
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
checkAllDependencies,
|
...startSdkEffectWrapper,
|
||||||
|
|
||||||
|
checkDependencies: checkDependencies as <
|
||||||
|
DependencyId extends keyof Manifest["dependencies"] &
|
||||||
|
PackageId = keyof Manifest["dependencies"] & PackageId,
|
||||||
|
>(
|
||||||
|
effects: Effects,
|
||||||
|
packageIds?: DependencyId[],
|
||||||
|
) => Promise<CheckDependencies<DependencyId>>,
|
||||||
serviceInterface: {
|
serviceInterface: {
|
||||||
getOwn: <E extends Effects>(effects: E, id: ServiceInterfaceId) =>
|
getOwn: <E extends Effects>(effects: E, id: ServiceInterfaceId) =>
|
||||||
removeCallbackTypes<E>(effects)(
|
removeCallbackTypes<E>(effects)(
|
||||||
@@ -247,7 +295,6 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
|||||||
id: string
|
id: string
|
||||||
description: string
|
description: string
|
||||||
hasPrimary: boolean
|
hasPrimary: boolean
|
||||||
disabled: boolean
|
|
||||||
type: ServiceInterfaceType
|
type: ServiceInterfaceType
|
||||||
username: null | string
|
username: null | string
|
||||||
path: string
|
path: string
|
||||||
@@ -293,8 +340,8 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
HealthCheck: {
|
HealthCheck: {
|
||||||
of(o: HealthCheckParams<Manifest>) {
|
of(o: HealthCheckParams) {
|
||||||
return healthCheck<Manifest>(o)
|
return healthCheck(o)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Dependency: {
|
Dependency: {
|
||||||
@@ -311,7 +358,7 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
|||||||
setupActions: (...createdActions: CreatedAction<any, any, any>[]) =>
|
setupActions: (...createdActions: CreatedAction<any, any, any>[]) =>
|
||||||
setupActions<Manifest, Store>(...createdActions),
|
setupActions<Manifest, Store>(...createdActions),
|
||||||
setupBackups: (...args: SetupBackupsParams<Manifest>) =>
|
setupBackups: (...args: SetupBackupsParams<Manifest>) =>
|
||||||
setupBackups<Manifest>(...args),
|
setupBackups<Manifest>(this.manifest, ...args),
|
||||||
setupConfig: <
|
setupConfig: <
|
||||||
ConfigType extends Config<any, Store> | Config<any, never>,
|
ConfigType extends Config<any, Store> | Config<any, never>,
|
||||||
Type extends Record<string, any> = ExtractConfigType<ConfigType>,
|
Type extends Record<string, any> = ExtractConfigType<ConfigType>,
|
||||||
@@ -380,7 +427,7 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
setupInit: (
|
setupInit: (
|
||||||
migrations: Migrations<Manifest, Store>,
|
versions: VersionGraph<Manifest["version"]>,
|
||||||
install: Install<Manifest, Store>,
|
install: Install<Manifest, Store>,
|
||||||
uninstall: Uninstall<Manifest, Store>,
|
uninstall: Uninstall<Manifest, Store>,
|
||||||
setInterfaces: SetInterfaces<Manifest, Store, any, any>,
|
setInterfaces: SetInterfaces<Manifest, Store, any, any>,
|
||||||
@@ -391,7 +438,7 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
|||||||
exposedStore: ExposedStorePaths,
|
exposedStore: ExposedStorePaths,
|
||||||
) =>
|
) =>
|
||||||
setupInit<Manifest, Store>(
|
setupInit<Manifest, Store>(
|
||||||
migrations,
|
versions,
|
||||||
install,
|
install,
|
||||||
uninstall,
|
uninstall,
|
||||||
setInterfaces,
|
setInterfaces,
|
||||||
@@ -412,15 +459,6 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
|||||||
started(onTerm: () => PromiseLike<void>): PromiseLike<void>
|
started(onTerm: () => PromiseLike<void>): PromiseLike<void>
|
||||||
}) => Promise<Daemons<Manifest, any>>,
|
}) => Promise<Daemons<Manifest, any>>,
|
||||||
) => setupMain<Manifest, Store>(fn),
|
) => setupMain<Manifest, Store>(fn),
|
||||||
setupMigrations: <
|
|
||||||
Migrations extends Array<Migration<Manifest, Store, any>>,
|
|
||||||
>(
|
|
||||||
...migrations: EnsureUniqueId<Migrations>
|
|
||||||
) =>
|
|
||||||
setupMigrations<Manifest, Store, Migrations>(
|
|
||||||
this.manifest,
|
|
||||||
...migrations,
|
|
||||||
),
|
|
||||||
setupProperties:
|
setupProperties:
|
||||||
(
|
(
|
||||||
fn: (options: { effects: Effects }) => Promise<T.SdkPropertiesReturn>,
|
fn: (options: { effects: Effects }) => Promise<T.SdkPropertiesReturn>,
|
||||||
@@ -541,13 +579,6 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
|||||||
>,
|
>,
|
||||||
) => List.dynamicText<Store>(getA),
|
) => List.dynamicText<Store>(getA),
|
||||||
},
|
},
|
||||||
Migration: {
|
|
||||||
of: <Version extends string>(options: {
|
|
||||||
version: Version & ValidateExVer<Version>
|
|
||||||
up: (opts: { effects: Effects }) => Promise<void>
|
|
||||||
down: (opts: { effects: Effects }) => Promise<void>
|
|
||||||
}) => Migration.of<Manifest, Store, Version>(options),
|
|
||||||
},
|
|
||||||
StorePath: pathBuilder<Store>(),
|
StorePath: pathBuilder<Store>(),
|
||||||
Value: {
|
Value: {
|
||||||
toggle: Value.toggle,
|
toggle: Value.toggle,
|
||||||
@@ -747,15 +778,12 @@ export async function runCommand<Manifest extends T.Manifest>(
|
|||||||
},
|
},
|
||||||
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> {
|
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> {
|
||||||
const commands = splitCommand(command)
|
const commands = splitCommand(command)
|
||||||
const overlay = await Overlay.of(effects, image)
|
return SubContainer.with(
|
||||||
try {
|
effects,
|
||||||
for (let mount of options.mounts || []) {
|
image,
|
||||||
await overlay.mount(mount.options, mount.path)
|
options.mounts || [],
|
||||||
}
|
(subcontainer) => subcontainer.exec(commands),
|
||||||
return await overlay.exec(commands)
|
)
|
||||||
} finally {
|
|
||||||
await overlay.destroy()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function nullifyProperties(value: T.SdkPropertiesReturn): T.PropertiesReturn {
|
function nullifyProperties(value: T.SdkPropertiesReturn): T.PropertiesReturn {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as T from "../types"
|
|||||||
|
|
||||||
import * as child_process from "child_process"
|
import * as child_process from "child_process"
|
||||||
import { promises as fsPromises } from "fs"
|
import { promises as fsPromises } from "fs"
|
||||||
|
import { asError } from "../util"
|
||||||
|
|
||||||
export type BACKUP = "BACKUP"
|
export type BACKUP = "BACKUP"
|
||||||
export const DEFAULT_OPTIONS: T.BackupOptions = {
|
export const DEFAULT_OPTIONS: T.BackupOptions = {
|
||||||
@@ -183,7 +184,7 @@ async function runRsync(
|
|||||||
})
|
})
|
||||||
|
|
||||||
spawned.stderr.on("data", (data: unknown) => {
|
spawned.stderr.on("data", (data: unknown) => {
|
||||||
console.error(String(data))
|
console.error(`Backups.runAsync`, asError(data))
|
||||||
})
|
})
|
||||||
|
|
||||||
const id = async () => {
|
const id = async () => {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type SetupBackupsParams<M extends T.Manifest> = Array<
|
|||||||
>
|
>
|
||||||
|
|
||||||
export function setupBackups<M extends T.Manifest>(
|
export function setupBackups<M extends T.Manifest>(
|
||||||
|
manifest: M,
|
||||||
...args: _<SetupBackupsParams<M>>
|
...args: _<SetupBackupsParams<M>>
|
||||||
) {
|
) {
|
||||||
const backups = Array<Backups<M>>()
|
const backups = Array<Backups<M>>()
|
||||||
@@ -36,6 +37,7 @@ export function setupBackups<M extends T.Manifest>(
|
|||||||
for (const backup of backups) {
|
for (const backup of backups) {
|
||||||
await backup.build(options.pathMaker).restoreBackup(options)
|
await backup.build(options.pathMaker).restoreBackup(options)
|
||||||
}
|
}
|
||||||
|
await options.effects.setDataVersion({ version: manifest.version })
|
||||||
}) as T.ExpectedExports.restoreBackup
|
}) as T.ExpectedExports.restoreBackup
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ export function setupConfig<
|
|||||||
return {
|
return {
|
||||||
setConfig: (async ({ effects, input }) => {
|
setConfig: (async ({ effects, input }) => {
|
||||||
if (!validator.test(input)) {
|
if (!validator.test(input)) {
|
||||||
await console.error(String(validator.errorMessage(input)))
|
await console.error(
|
||||||
|
new Error(validator.errorMessage(input)?.toString()),
|
||||||
|
)
|
||||||
return { error: "Set config type error for config" }
|
return { error: "Set config type error for config" }
|
||||||
}
|
}
|
||||||
await effects.clearBindings()
|
await effects.clearBindings()
|
||||||
|
|||||||
@@ -1,131 +1,206 @@
|
|||||||
|
import { ExtendedVersion, VersionRange } from "../exver"
|
||||||
import {
|
import {
|
||||||
Effects,
|
Effects,
|
||||||
PackageId,
|
PackageId,
|
||||||
DependencyRequirement,
|
DependencyRequirement,
|
||||||
SetHealth,
|
SetHealth,
|
||||||
CheckDependenciesResult,
|
CheckDependenciesResult,
|
||||||
|
HealthCheckId,
|
||||||
} from "../types"
|
} from "../types"
|
||||||
|
|
||||||
export type CheckAllDependencies = {
|
export type CheckDependencies<DependencyId extends PackageId = PackageId> = {
|
||||||
notInstalled: () => Promise<CheckDependenciesResult[]>
|
installedSatisfied: (packageId: DependencyId) => boolean
|
||||||
notRunning: () => Promise<CheckDependenciesResult[]>
|
installedVersionSatisfied: (packageId: DependencyId) => boolean
|
||||||
configNotSatisfied: () => Promise<CheckDependenciesResult[]>
|
runningSatisfied: (packageId: DependencyId) => boolean
|
||||||
healthErrors: () => Promise<{ [id: string]: SetHealth[] }>
|
configSatisfied: (packageId: DependencyId) => boolean
|
||||||
|
healthCheckSatisfied: (
|
||||||
|
packageId: DependencyId,
|
||||||
|
healthCheckId: HealthCheckId,
|
||||||
|
) => boolean
|
||||||
|
satisfied: () => boolean
|
||||||
|
|
||||||
isValid: () => Promise<boolean>
|
throwIfInstalledNotSatisfied: (packageId: DependencyId) => void
|
||||||
|
throwIfInstalledVersionNotSatisfied: (packageId: DependencyId) => void
|
||||||
throwIfNotRunning: () => Promise<void>
|
throwIfRunningNotSatisfied: (packageId: DependencyId) => void
|
||||||
throwIfNotInstalled: () => Promise<void>
|
throwIfConfigNotSatisfied: (packageId: DependencyId) => void
|
||||||
throwIfConfigNotSatisfied: () => Promise<void>
|
throwIfHealthNotSatisfied: (
|
||||||
throwIfHealthError: () => Promise<void>
|
packageId: DependencyId,
|
||||||
|
healthCheckId?: HealthCheckId,
|
||||||
throwIfNotValid: () => Promise<void>
|
) => void
|
||||||
|
throwIfNotSatisfied: (packageId?: DependencyId) => void
|
||||||
}
|
}
|
||||||
export function checkAllDependencies(effects: Effects): CheckAllDependencies {
|
export async function checkDependencies<
|
||||||
const dependenciesPromise = effects.getDependencies()
|
DependencyId extends PackageId = PackageId,
|
||||||
const resultsPromise = dependenciesPromise.then((dependencies) =>
|
>(
|
||||||
|
effects: Effects,
|
||||||
|
packageIds?: DependencyId[],
|
||||||
|
): Promise<CheckDependencies<DependencyId>> {
|
||||||
|
let [dependencies, results] = await Promise.all([
|
||||||
|
effects.getDependencies(),
|
||||||
effects.checkDependencies({
|
effects.checkDependencies({
|
||||||
packageIds: dependencies.map((dep) => dep.id),
|
packageIds,
|
||||||
}),
|
}),
|
||||||
)
|
])
|
||||||
|
if (packageIds) {
|
||||||
const dependenciesByIdPromise = dependenciesPromise.then((d) =>
|
dependencies = dependencies.filter((d) =>
|
||||||
d.reduce(
|
(packageIds as PackageId[]).includes(d.id),
|
||||||
(acc, dep) => {
|
|
||||||
acc[dep.id] = dep
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{} as { [id: PackageId]: DependencyRequirement },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const healthErrors = async () => {
|
|
||||||
const results = await resultsPromise
|
|
||||||
const dependenciesById = await dependenciesByIdPromise
|
|
||||||
const answer: { [id: PackageId]: SetHealth[] } = {}
|
|
||||||
for (const result of results) {
|
|
||||||
const dependency = dependenciesById[result.packageId]
|
|
||||||
if (!dependency) continue
|
|
||||||
if (dependency.kind !== "running") continue
|
|
||||||
|
|
||||||
const healthChecks = Object.entries(result.healthChecks)
|
|
||||||
.map(([id, hc]) => ({ ...hc, id }))
|
|
||||||
.filter((x) => !!x.message)
|
|
||||||
if (healthChecks.length === 0) continue
|
|
||||||
answer[result.packageId] = healthChecks
|
|
||||||
}
|
|
||||||
return answer
|
|
||||||
}
|
|
||||||
const configNotSatisfied = () =>
|
|
||||||
resultsPromise.then((x) => x.filter((x) => !x.configSatisfied))
|
|
||||||
const notInstalled = () =>
|
|
||||||
resultsPromise.then((x) => x.filter((x) => !x.isInstalled))
|
|
||||||
const notRunning = async () => {
|
|
||||||
const results = await resultsPromise
|
|
||||||
const dependenciesById = await dependenciesByIdPromise
|
|
||||||
return results.filter((x) => {
|
|
||||||
const dependency = dependenciesById[x.packageId]
|
|
||||||
if (!dependency) return false
|
|
||||||
if (dependency.kind !== "running") return false
|
|
||||||
return !x.isRunning
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const entries = <B>(x: { [k: string]: B }) => Object.entries(x)
|
|
||||||
const first = <A>(x: A[]): A | undefined => x[0]
|
|
||||||
const sinkVoid = <A>(x: A) => void 0
|
|
||||||
const throwIfHealthError = () =>
|
|
||||||
healthErrors()
|
|
||||||
.then(entries)
|
|
||||||
.then(first)
|
|
||||||
.then((x) => {
|
|
||||||
if (!x) return
|
|
||||||
const [id, healthChecks] = x
|
|
||||||
if (healthChecks.length > 0)
|
|
||||||
throw `Package ${id} has the following errors: ${healthChecks.map((x) => x.message).join(", ")}`
|
|
||||||
})
|
|
||||||
|
|
||||||
const throwIfConfigNotSatisfied = () =>
|
|
||||||
configNotSatisfied().then((results) => {
|
|
||||||
throw new Error(
|
|
||||||
`Package ${results[0].packageId} does not have a valid configuration`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const throwIfNotRunning = () =>
|
|
||||||
notRunning().then((results) => {
|
|
||||||
if (results[0])
|
|
||||||
throw new Error(`Package ${results[0].packageId} is not running`)
|
|
||||||
})
|
|
||||||
|
|
||||||
const throwIfNotInstalled = () =>
|
|
||||||
notInstalled().then((results) => {
|
|
||||||
if (results[0])
|
|
||||||
throw new Error(`Package ${results[0].packageId} is not installed`)
|
|
||||||
})
|
|
||||||
const throwIfNotValid = async () =>
|
|
||||||
Promise.all([
|
|
||||||
throwIfNotRunning(),
|
|
||||||
throwIfNotInstalled(),
|
|
||||||
throwIfConfigNotSatisfied(),
|
|
||||||
throwIfHealthError(),
|
|
||||||
]).then(sinkVoid)
|
|
||||||
|
|
||||||
const isValid = () =>
|
|
||||||
throwIfNotValid().then(
|
|
||||||
() => true,
|
|
||||||
() => false,
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const find = (packageId: DependencyId) => {
|
||||||
|
const dependencyRequirement = dependencies.find((d) => d.id === packageId)
|
||||||
|
const dependencyResult = results.find((d) => d.packageId === packageId)
|
||||||
|
if (!dependencyRequirement || !dependencyResult) {
|
||||||
|
throw new Error(`Unknown DependencyId ${packageId}`)
|
||||||
|
}
|
||||||
|
return { requirement: dependencyRequirement, result: dependencyResult }
|
||||||
|
}
|
||||||
|
|
||||||
|
const installedSatisfied = (packageId: DependencyId) =>
|
||||||
|
!!find(packageId).result.installedVersion
|
||||||
|
const installedVersionSatisfied = (packageId: DependencyId) => {
|
||||||
|
const dep = find(packageId)
|
||||||
|
return (
|
||||||
|
!!dep.result.installedVersion &&
|
||||||
|
ExtendedVersion.parse(dep.result.installedVersion).satisfies(
|
||||||
|
VersionRange.parse(dep.requirement.versionRange),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const runningSatisfied = (packageId: DependencyId) => {
|
||||||
|
const dep = find(packageId)
|
||||||
|
return dep.requirement.kind !== "running" || dep.result.isRunning
|
||||||
|
}
|
||||||
|
const configSatisfied = (packageId: DependencyId) =>
|
||||||
|
find(packageId).result.configSatisfied
|
||||||
|
const healthCheckSatisfied = (
|
||||||
|
packageId: DependencyId,
|
||||||
|
healthCheckId?: HealthCheckId,
|
||||||
|
) => {
|
||||||
|
const dep = find(packageId)
|
||||||
|
if (
|
||||||
|
healthCheckId &&
|
||||||
|
(dep.requirement.kind !== "running" ||
|
||||||
|
!dep.requirement.healthChecks.includes(healthCheckId))
|
||||||
|
) {
|
||||||
|
throw new Error(`Unknown HealthCheckId ${healthCheckId}`)
|
||||||
|
}
|
||||||
|
const errors = Object.entries(dep.result.healthChecks)
|
||||||
|
.filter(([id, _]) => (healthCheckId ? id === healthCheckId : true))
|
||||||
|
.filter(([_, res]) => res.result !== "success")
|
||||||
|
return errors.length === 0
|
||||||
|
}
|
||||||
|
const pkgSatisfied = (packageId: DependencyId) =>
|
||||||
|
installedSatisfied(packageId) &&
|
||||||
|
installedVersionSatisfied(packageId) &&
|
||||||
|
runningSatisfied(packageId) &&
|
||||||
|
configSatisfied(packageId) &&
|
||||||
|
healthCheckSatisfied(packageId)
|
||||||
|
const satisfied = (packageId?: DependencyId) =>
|
||||||
|
packageId
|
||||||
|
? pkgSatisfied(packageId)
|
||||||
|
: dependencies.every((d) => pkgSatisfied(d.id as DependencyId))
|
||||||
|
|
||||||
|
const throwIfInstalledNotSatisfied = (packageId: DependencyId) => {
|
||||||
|
const dep = find(packageId)
|
||||||
|
if (!dep.result.installedVersion) {
|
||||||
|
throw new Error(`${dep.result.title || packageId} is not installed`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const throwIfInstalledVersionNotSatisfied = (packageId: DependencyId) => {
|
||||||
|
const dep = find(packageId)
|
||||||
|
if (!dep.result.installedVersion) {
|
||||||
|
throw new Error(`${dep.result.title || packageId} is not installed`)
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
![dep.result.installedVersion, ...dep.result.satisfies].find((v) =>
|
||||||
|
ExtendedVersion.parse(v).satisfies(
|
||||||
|
VersionRange.parse(dep.requirement.versionRange),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Installed version ${dep.result.installedVersion} of ${dep.result.title || packageId} does not match expected version range ${dep.requirement.versionRange}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const throwIfRunningNotSatisfied = (packageId: DependencyId) => {
|
||||||
|
const dep = find(packageId)
|
||||||
|
if (dep.requirement.kind === "running" && !dep.result.isRunning) {
|
||||||
|
throw new Error(`${dep.result.title || packageId} is not running`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const throwIfConfigNotSatisfied = (packageId: DependencyId) => {
|
||||||
|
const dep = find(packageId)
|
||||||
|
if (!dep.result.configSatisfied) {
|
||||||
|
throw new Error(
|
||||||
|
`${dep.result.title || packageId}'s configuration does not satisfy requirements`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const throwIfHealthNotSatisfied = (
|
||||||
|
packageId: DependencyId,
|
||||||
|
healthCheckId?: HealthCheckId,
|
||||||
|
) => {
|
||||||
|
const dep = find(packageId)
|
||||||
|
if (
|
||||||
|
healthCheckId &&
|
||||||
|
(dep.requirement.kind !== "running" ||
|
||||||
|
!dep.requirement.healthChecks.includes(healthCheckId))
|
||||||
|
) {
|
||||||
|
throw new Error(`Unknown HealthCheckId ${healthCheckId}`)
|
||||||
|
}
|
||||||
|
const errors = Object.entries(dep.result.healthChecks)
|
||||||
|
.filter(([id, _]) => (healthCheckId ? id === healthCheckId : true))
|
||||||
|
.filter(([_, res]) => res.result !== "success")
|
||||||
|
if (errors.length) {
|
||||||
|
throw new Error(
|
||||||
|
errors
|
||||||
|
.map(
|
||||||
|
([_, e]) =>
|
||||||
|
`Health Check ${e.name} of ${dep.result.title || packageId} failed with status ${e.result}${e.message ? `: ${e.message}` : ""}`,
|
||||||
|
)
|
||||||
|
.join("; "),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const throwIfPkgNotSatisfied = (packageId: DependencyId) => {
|
||||||
|
throwIfInstalledNotSatisfied(packageId)
|
||||||
|
throwIfInstalledVersionNotSatisfied(packageId)
|
||||||
|
throwIfRunningNotSatisfied(packageId)
|
||||||
|
throwIfConfigNotSatisfied(packageId)
|
||||||
|
throwIfHealthNotSatisfied(packageId)
|
||||||
|
}
|
||||||
|
const throwIfNotSatisfied = (packageId?: DependencyId) =>
|
||||||
|
packageId
|
||||||
|
? throwIfPkgNotSatisfied(packageId)
|
||||||
|
: (() => {
|
||||||
|
const err = dependencies.flatMap((d) => {
|
||||||
|
try {
|
||||||
|
throwIfPkgNotSatisfied(d.id as DependencyId)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) return [e.message]
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
if (err.length) {
|
||||||
|
throw new Error(err.join("; "))
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
notRunning,
|
installedSatisfied,
|
||||||
notInstalled,
|
installedVersionSatisfied,
|
||||||
configNotSatisfied,
|
runningSatisfied,
|
||||||
healthErrors,
|
configSatisfied,
|
||||||
throwIfNotRunning,
|
healthCheckSatisfied,
|
||||||
|
satisfied,
|
||||||
|
throwIfInstalledNotSatisfied,
|
||||||
|
throwIfInstalledVersionNotSatisfied,
|
||||||
|
throwIfRunningNotSatisfied,
|
||||||
throwIfConfigNotSatisfied,
|
throwIfConfigNotSatisfied,
|
||||||
throwIfNotValid,
|
throwIfHealthNotSatisfied,
|
||||||
throwIfNotInstalled,
|
throwIfNotSatisfied,
|
||||||
throwIfHealthError,
|
|
||||||
isValid,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as P from "./exver"
|
|||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
export type ValidateVersion<T extends String> =
|
export type ValidateVersion<T extends String> =
|
||||||
T extends `-${infer A}` ? never :
|
T extends `-${infer A}` ? never :
|
||||||
T extends `${infer A}-${infer B}` ? ValidateVersion<A> & ValidateVersion<B> :
|
T extends `${infer A}-${string}` ? ValidateVersion<A> :
|
||||||
T extends `${bigint}` ? unknown :
|
T extends `${bigint}` ? unknown :
|
||||||
T extends `${bigint}.${infer A}` ? ValidateVersion<A> :
|
T extends `${bigint}.${infer A}` ? ValidateVersion<A> :
|
||||||
never
|
never
|
||||||
@@ -16,9 +16,9 @@ export type ValidateExVer<T extends string> =
|
|||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
export type ValidateExVers<T> =
|
export type ValidateExVers<T> =
|
||||||
T extends [] ? unknown :
|
T extends [] ? unknown[] :
|
||||||
T extends [infer A, ...infer B] ? ValidateExVer<A & string> & ValidateExVers<B> :
|
T extends [infer A, ...infer B] ? ValidateExVer<A & string> & ValidateExVers<B> :
|
||||||
never
|
never[]
|
||||||
|
|
||||||
type Anchor = {
|
type Anchor = {
|
||||||
type: "Anchor"
|
type: "Anchor"
|
||||||
@@ -44,7 +44,7 @@ type Not = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class VersionRange {
|
export class VersionRange {
|
||||||
private constructor(private atom: Anchor | And | Or | Not | P.Any | P.None) {}
|
private constructor(public atom: Anchor | And | Or | Not | P.Any | P.None) {}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
switch (this.atom.type) {
|
switch (this.atom.type) {
|
||||||
@@ -63,67 +63,6 @@ export class VersionRange {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a boolean indicating whether a given version satisfies the VersionRange
|
|
||||||
* !( >= 1:1 <= 2:2) || <=#bitcoin:1.2.0-alpha:0
|
|
||||||
*/
|
|
||||||
satisfiedBy(version: ExtendedVersion): boolean {
|
|
||||||
switch (this.atom.type) {
|
|
||||||
case "Anchor":
|
|
||||||
const otherVersion = this.atom.version
|
|
||||||
switch (this.atom.operator) {
|
|
||||||
case "=":
|
|
||||||
return version.equals(otherVersion)
|
|
||||||
case ">":
|
|
||||||
return version.greaterThan(otherVersion)
|
|
||||||
case "<":
|
|
||||||
return version.lessThan(otherVersion)
|
|
||||||
case ">=":
|
|
||||||
return version.greaterThanOrEqual(otherVersion)
|
|
||||||
case "<=":
|
|
||||||
return version.lessThanOrEqual(otherVersion)
|
|
||||||
case "!=":
|
|
||||||
return !version.equals(otherVersion)
|
|
||||||
case "^":
|
|
||||||
const nextMajor = this.atom.version.incrementMajor()
|
|
||||||
if (
|
|
||||||
version.greaterThanOrEqual(otherVersion) &&
|
|
||||||
version.lessThan(nextMajor)
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case "~":
|
|
||||||
const nextMinor = this.atom.version.incrementMinor()
|
|
||||||
if (
|
|
||||||
version.greaterThanOrEqual(otherVersion) &&
|
|
||||||
version.lessThan(nextMinor)
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "And":
|
|
||||||
return (
|
|
||||||
this.atom.left.satisfiedBy(version) &&
|
|
||||||
this.atom.right.satisfiedBy(version)
|
|
||||||
)
|
|
||||||
case "Or":
|
|
||||||
return (
|
|
||||||
this.atom.left.satisfiedBy(version) ||
|
|
||||||
this.atom.right.satisfiedBy(version)
|
|
||||||
)
|
|
||||||
case "Not":
|
|
||||||
return !this.atom.value.satisfiedBy(version)
|
|
||||||
case "Any":
|
|
||||||
return true
|
|
||||||
case "None":
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static parseAtom(atom: P.VersionRangeAtom): VersionRange {
|
private static parseAtom(atom: P.VersionRangeAtom): VersionRange {
|
||||||
switch (atom.type) {
|
switch (atom.type) {
|
||||||
case "Not":
|
case "Not":
|
||||||
@@ -207,6 +146,10 @@ export class VersionRange {
|
|||||||
static none() {
|
static none() {
|
||||||
return new VersionRange({ type: "None" })
|
return new VersionRange({ type: "None" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
satisfiedBy(version: Version | ExtendedVersion) {
|
||||||
|
return version.satisfies(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Version {
|
export class Version {
|
||||||
@@ -266,6 +209,12 @@ export class Version {
|
|||||||
const parsed = P.parse(version, { startRule: "Version" })
|
const parsed = P.parse(version, { startRule: "Version" })
|
||||||
return new Version(parsed.number, parsed.prerelease)
|
return new Version(parsed.number, parsed.prerelease)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
satisfies(versionRange: VersionRange): boolean {
|
||||||
|
return new ExtendedVersion(null, this, new Version([0], [])).satisfies(
|
||||||
|
versionRange,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// #flavor:0.1.2-beta.1:0
|
// #flavor:0.1.2-beta.1:0
|
||||||
@@ -404,6 +353,67 @@ export class ExtendedVersion {
|
|||||||
updatedDownstream,
|
updatedDownstream,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a boolean indicating whether a given version satisfies the VersionRange
|
||||||
|
* !( >= 1:1 <= 2:2) || <=#bitcoin:1.2.0-alpha:0
|
||||||
|
*/
|
||||||
|
satisfies(versionRange: VersionRange): boolean {
|
||||||
|
switch (versionRange.atom.type) {
|
||||||
|
case "Anchor":
|
||||||
|
const otherVersion = versionRange.atom.version
|
||||||
|
switch (versionRange.atom.operator) {
|
||||||
|
case "=":
|
||||||
|
return this.equals(otherVersion)
|
||||||
|
case ">":
|
||||||
|
return this.greaterThan(otherVersion)
|
||||||
|
case "<":
|
||||||
|
return this.lessThan(otherVersion)
|
||||||
|
case ">=":
|
||||||
|
return this.greaterThanOrEqual(otherVersion)
|
||||||
|
case "<=":
|
||||||
|
return this.lessThanOrEqual(otherVersion)
|
||||||
|
case "!=":
|
||||||
|
return !this.equals(otherVersion)
|
||||||
|
case "^":
|
||||||
|
const nextMajor = versionRange.atom.version.incrementMajor()
|
||||||
|
if (
|
||||||
|
this.greaterThanOrEqual(otherVersion) &&
|
||||||
|
this.lessThan(nextMajor)
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case "~":
|
||||||
|
const nextMinor = versionRange.atom.version.incrementMinor()
|
||||||
|
if (
|
||||||
|
this.greaterThanOrEqual(otherVersion) &&
|
||||||
|
this.lessThan(nextMinor)
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "And":
|
||||||
|
return (
|
||||||
|
this.satisfies(versionRange.atom.left) &&
|
||||||
|
this.satisfies(versionRange.atom.right)
|
||||||
|
)
|
||||||
|
case "Or":
|
||||||
|
return (
|
||||||
|
this.satisfies(versionRange.atom.left) ||
|
||||||
|
this.satisfies(versionRange.atom.right)
|
||||||
|
)
|
||||||
|
case "Not":
|
||||||
|
return !this.satisfies(versionRange.atom.value)
|
||||||
|
case "Any":
|
||||||
|
return true
|
||||||
|
case "None":
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const testTypeExVer = <T extends string>(t: T & ValidateExVer<T>) => t
|
export const testTypeExVer = <T extends string>(t: T & ValidateExVer<T>) => t
|
||||||
@@ -416,6 +426,7 @@ function tests() {
|
|||||||
testTypeVersion("12.34.56")
|
testTypeVersion("12.34.56")
|
||||||
testTypeVersion("1.2-3")
|
testTypeVersion("1.2-3")
|
||||||
testTypeVersion("1-3")
|
testTypeVersion("1-3")
|
||||||
|
testTypeVersion("1-alpha")
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
testTypeVersion("-3")
|
testTypeVersion("-3")
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
|
|||||||
@@ -1,74 +1,61 @@
|
|||||||
import { Effects } from "../types"
|
import { Effects } from "../types"
|
||||||
import { CheckResult } from "./checkFns/CheckResult"
|
import { HealthCheckResult } from "./checkFns/HealthCheckResult"
|
||||||
import { HealthReceipt } from "./HealthReceipt"
|
import { HealthReceipt } from "./HealthReceipt"
|
||||||
import { Trigger } from "../trigger"
|
import { Trigger } from "../trigger"
|
||||||
import { TriggerInput } from "../trigger/TriggerInput"
|
import { TriggerInput } from "../trigger/TriggerInput"
|
||||||
import { defaultTrigger } from "../trigger/defaultTrigger"
|
import { defaultTrigger } from "../trigger/defaultTrigger"
|
||||||
import { once } from "../util/once"
|
import { once } from "../util/once"
|
||||||
import { Overlay } from "../util/Overlay"
|
import { SubContainer } from "../util/SubContainer"
|
||||||
import { object, unknown } from "ts-matches"
|
import { object, unknown } from "ts-matches"
|
||||||
import * as T from "../types"
|
import * as T from "../types"
|
||||||
|
import { asError } from "../util/asError"
|
||||||
|
|
||||||
export type HealthCheckParams<Manifest extends T.Manifest> = {
|
export type HealthCheckParams = {
|
||||||
effects: Effects
|
effects: Effects
|
||||||
name: string
|
name: string
|
||||||
image: {
|
|
||||||
id: keyof Manifest["images"] & T.ImageId
|
|
||||||
sharedRun?: boolean
|
|
||||||
}
|
|
||||||
trigger?: Trigger
|
trigger?: Trigger
|
||||||
fn(overlay: Overlay): Promise<CheckResult> | CheckResult
|
fn(): Promise<HealthCheckResult> | HealthCheckResult
|
||||||
onFirstSuccess?: () => unknown | Promise<unknown>
|
onFirstSuccess?: () => unknown | Promise<unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function healthCheck<Manifest extends T.Manifest>(
|
export function healthCheck(o: HealthCheckParams) {
|
||||||
o: HealthCheckParams<Manifest>,
|
|
||||||
) {
|
|
||||||
new Promise(async () => {
|
new Promise(async () => {
|
||||||
const overlay = await Overlay.of(o.effects, o.image)
|
let currentValue: TriggerInput = {}
|
||||||
try {
|
const getCurrentValue = () => currentValue
|
||||||
let currentValue: TriggerInput = {
|
const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue)
|
||||||
hadSuccess: false,
|
const triggerFirstSuccess = once(() =>
|
||||||
|
Promise.resolve(
|
||||||
|
"onFirstSuccess" in o && o.onFirstSuccess
|
||||||
|
? o.onFirstSuccess()
|
||||||
|
: undefined,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for (
|
||||||
|
let res = await trigger.next();
|
||||||
|
!res.done;
|
||||||
|
res = await trigger.next()
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { result, message } = await o.fn()
|
||||||
|
await o.effects.setHealth({
|
||||||
|
name: o.name,
|
||||||
|
id: o.name,
|
||||||
|
result,
|
||||||
|
message: message || "",
|
||||||
|
})
|
||||||
|
currentValue.lastResult = result
|
||||||
|
await triggerFirstSuccess().catch((err) => {
|
||||||
|
console.error(asError(err))
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
await o.effects.setHealth({
|
||||||
|
name: o.name,
|
||||||
|
id: o.name,
|
||||||
|
result: "failure",
|
||||||
|
message: asMessage(e) || "",
|
||||||
|
})
|
||||||
|
currentValue.lastResult = "failure"
|
||||||
}
|
}
|
||||||
const getCurrentValue = () => currentValue
|
|
||||||
const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue)
|
|
||||||
const triggerFirstSuccess = once(() =>
|
|
||||||
Promise.resolve(
|
|
||||||
"onFirstSuccess" in o && o.onFirstSuccess
|
|
||||||
? o.onFirstSuccess()
|
|
||||||
: undefined,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for (
|
|
||||||
let res = await trigger.next();
|
|
||||||
!res.done;
|
|
||||||
res = await trigger.next()
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { status, message } = await o.fn(overlay)
|
|
||||||
await o.effects.setHealth({
|
|
||||||
name: o.name,
|
|
||||||
id: o.name,
|
|
||||||
result: status,
|
|
||||||
message: message || "",
|
|
||||||
})
|
|
||||||
currentValue.hadSuccess = true
|
|
||||||
currentValue.lastResult = "success"
|
|
||||||
await triggerFirstSuccess().catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
await o.effects.setHealth({
|
|
||||||
name: o.name,
|
|
||||||
id: o.name,
|
|
||||||
result: "failure",
|
|
||||||
message: asMessage(e) || "",
|
|
||||||
})
|
|
||||||
currentValue.lastResult = "failure"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await overlay.destroy()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return {} as HealthReceipt
|
return {} as HealthReceipt
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { HealthStatus } from "../../types"
|
|
||||||
|
|
||||||
export type CheckResult = {
|
|
||||||
status: HealthStatus
|
|
||||||
message: string | null
|
|
||||||
}
|
|
||||||
3
sdk/lib/health/checkFns/HealthCheckResult.ts
Normal file
3
sdk/lib/health/checkFns/HealthCheckResult.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { T } from "../.."
|
||||||
|
|
||||||
|
export type HealthCheckResult = Omit<T.NamedHealthCheckResult, "name">
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Effects } from "../../types"
|
import { Effects } from "../../types"
|
||||||
import { stringFromStdErrOut } from "../../util/stringFromStdErrOut"
|
import { stringFromStdErrOut } from "../../util/stringFromStdErrOut"
|
||||||
import { CheckResult } from "./CheckResult"
|
import { HealthCheckResult } from "./HealthCheckResult"
|
||||||
|
|
||||||
import { promisify } from "node:util"
|
import { promisify } from "node:util"
|
||||||
import * as CP from "node:child_process"
|
import * as CP from "node:child_process"
|
||||||
@@ -32,8 +32,8 @@ export async function checkPortListening(
|
|||||||
timeoutMessage?: string
|
timeoutMessage?: string
|
||||||
timeout?: number
|
timeout?: number
|
||||||
},
|
},
|
||||||
): Promise<CheckResult> {
|
): Promise<HealthCheckResult> {
|
||||||
return Promise.race<CheckResult>([
|
return Promise.race<HealthCheckResult>([
|
||||||
Promise.resolve().then(async () => {
|
Promise.resolve().then(async () => {
|
||||||
const hasAddress =
|
const hasAddress =
|
||||||
containsAddress(
|
containsAddress(
|
||||||
@@ -45,10 +45,10 @@ export async function checkPortListening(
|
|||||||
port,
|
port,
|
||||||
)
|
)
|
||||||
if (hasAddress) {
|
if (hasAddress) {
|
||||||
return { status: "success", message: options.successMessage }
|
return { result: "success", message: options.successMessage }
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
status: "failure",
|
result: "failure",
|
||||||
message: options.errorMessage,
|
message: options.errorMessage,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -56,7 +56,7 @@ export async function checkPortListening(
|
|||||||
setTimeout(
|
setTimeout(
|
||||||
() =>
|
() =>
|
||||||
resolve({
|
resolve({
|
||||||
status: "failure",
|
result: "failure",
|
||||||
message:
|
message:
|
||||||
options.timeoutMessage || `Timeout trying to check port ${port}`,
|
options.timeoutMessage || `Timeout trying to check port ${port}`,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Effects } from "../../types"
|
import { Effects } from "../../types"
|
||||||
import { CheckResult } from "./CheckResult"
|
import { asError } from "../../util/asError"
|
||||||
|
import { HealthCheckResult } from "./HealthCheckResult"
|
||||||
import { timeoutPromise } from "./index"
|
import { timeoutPromise } from "./index"
|
||||||
import "isomorphic-fetch"
|
import "isomorphic-fetch"
|
||||||
|
|
||||||
@@ -17,19 +18,19 @@ export const checkWebUrl = async (
|
|||||||
successMessage = `Reached ${url}`,
|
successMessage = `Reached ${url}`,
|
||||||
errorMessage = `Error while fetching URL: ${url}`,
|
errorMessage = `Error while fetching URL: ${url}`,
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<CheckResult> => {
|
): Promise<HealthCheckResult> => {
|
||||||
return Promise.race([fetch(url), timeoutPromise(timeout)])
|
return Promise.race([fetch(url), timeoutPromise(timeout)])
|
||||||
.then(
|
.then(
|
||||||
(x) =>
|
(x) =>
|
||||||
({
|
({
|
||||||
status: "success",
|
result: "success",
|
||||||
message: successMessage,
|
message: successMessage,
|
||||||
}) as const,
|
}) as const,
|
||||||
)
|
)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.warn(`Error while fetching URL: ${url}`)
|
console.warn(`Error while fetching URL: ${url}`)
|
||||||
console.error(JSON.stringify(e))
|
console.error(JSON.stringify(e))
|
||||||
console.error(e.toString())
|
console.error(asError(e))
|
||||||
return { status: "failure" as const, message: errorMessage }
|
return { result: "failure" as const, message: errorMessage }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { runHealthScript } from "./runHealthScript"
|
import { runHealthScript } from "./runHealthScript"
|
||||||
export { checkPortListening } from "./checkPortListening"
|
export { checkPortListening } from "./checkPortListening"
|
||||||
export { CheckResult } from "./CheckResult"
|
export { HealthCheckResult } from "./HealthCheckResult"
|
||||||
export { checkWebUrl } from "./checkWebUrl"
|
export { checkWebUrl } from "./checkWebUrl"
|
||||||
|
|
||||||
export function timeoutPromise(ms: number, { message = "Timed out" } = {}) {
|
export function timeoutPromise(ms: number, { message = "Timed out" } = {}) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Effects } from "../../types"
|
import { Effects } from "../../types"
|
||||||
import { Overlay } from "../../util/Overlay"
|
import { SubContainer } from "../../util/SubContainer"
|
||||||
import { stringFromStdErrOut } from "../../util/stringFromStdErrOut"
|
import { stringFromStdErrOut } from "../../util/stringFromStdErrOut"
|
||||||
import { CheckResult } from "./CheckResult"
|
import { HealthCheckResult } from "./HealthCheckResult"
|
||||||
import { timeoutPromise } from "./index"
|
import { timeoutPromise } from "./index"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12,27 +12,26 @@ import { timeoutPromise } from "./index"
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const runHealthScript = async (
|
export const runHealthScript = async (
|
||||||
effects: Effects,
|
|
||||||
runCommand: string[],
|
runCommand: string[],
|
||||||
overlay: Overlay,
|
subcontainer: SubContainer,
|
||||||
{
|
{
|
||||||
timeout = 30000,
|
timeout = 30000,
|
||||||
errorMessage = `Error while running command: ${runCommand}`,
|
errorMessage = `Error while running command: ${runCommand}`,
|
||||||
message = (res: string) =>
|
message = (res: string) =>
|
||||||
`Have ran script ${runCommand} and the result: ${res}`,
|
`Have ran script ${runCommand} and the result: ${res}`,
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<CheckResult> => {
|
): Promise<HealthCheckResult> => {
|
||||||
const res = await Promise.race([
|
const res = await Promise.race([
|
||||||
overlay.exec(runCommand),
|
subcontainer.exec(runCommand),
|
||||||
timeoutPromise(timeout),
|
timeoutPromise(timeout),
|
||||||
]).catch((e) => {
|
]).catch((e) => {
|
||||||
console.warn(errorMessage)
|
console.warn(errorMessage)
|
||||||
console.warn(JSON.stringify(e))
|
console.warn(JSON.stringify(e))
|
||||||
console.warn(e.toString())
|
console.warn(e.toString())
|
||||||
throw { status: "failure", message: errorMessage } as CheckResult
|
throw { result: "failure", message: errorMessage } as HealthCheckResult
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
status: "success",
|
result: "success",
|
||||||
message: message(res.stdout.toString()),
|
message: message(res.stdout.toString()),
|
||||||
} as CheckResult
|
} as HealthCheckResult
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export { Daemons } from "./mainFn/Daemons"
|
export { Daemons } from "./mainFn/Daemons"
|
||||||
export { Overlay } from "./util/Overlay"
|
export { SubContainer } from "./util/SubContainer"
|
||||||
export { StartSdk } from "./StartSdk"
|
export { StartSdk } from "./StartSdk"
|
||||||
export { setupManifest } from "./manifest/setupManifest"
|
export { setupManifest } from "./manifest/setupManifest"
|
||||||
export { FileHelper } from "./util/fileHelper"
|
export { FileHelper } from "./util/fileHelper"
|
||||||
@@ -29,3 +29,4 @@ export * as utils from "./util"
|
|||||||
export * as matches from "ts-matches"
|
export * as matches from "ts-matches"
|
||||||
export * as YAML from "yaml"
|
export * as YAML from "yaml"
|
||||||
export * as TOML from "@iarna/toml"
|
export * as TOML from "@iarna/toml"
|
||||||
|
export * from "./version"
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { ValidateExVer } from "../../exver"
|
|
||||||
import * as T from "../../types"
|
|
||||||
|
|
||||||
export class Migration<
|
|
||||||
Manifest extends T.Manifest,
|
|
||||||
Store,
|
|
||||||
Version extends string,
|
|
||||||
> {
|
|
||||||
constructor(
|
|
||||||
readonly options: {
|
|
||||||
version: Version & ValidateExVer<Version>
|
|
||||||
up: (opts: { effects: T.Effects }) => Promise<void>
|
|
||||||
down: (opts: { effects: T.Effects }) => Promise<void>
|
|
||||||
},
|
|
||||||
) {}
|
|
||||||
static of<
|
|
||||||
Manifest extends T.Manifest,
|
|
||||||
Store,
|
|
||||||
Version extends string,
|
|
||||||
>(options: {
|
|
||||||
version: Version & ValidateExVer<Version>
|
|
||||||
up: (opts: { effects: T.Effects }) => Promise<void>
|
|
||||||
down: (opts: { effects: T.Effects }) => Promise<void>
|
|
||||||
}) {
|
|
||||||
return new Migration<Manifest, Store, Version>(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
async up(opts: { effects: T.Effects }) {
|
|
||||||
this.up(opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
async down(opts: { effects: T.Effects }) {
|
|
||||||
this.down(opts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { ExtendedVersion } from "../../exver"
|
|
||||||
|
|
||||||
import * as T from "../../types"
|
|
||||||
import { once } from "../../util/once"
|
|
||||||
import { Migration } from "./Migration"
|
|
||||||
|
|
||||||
export class Migrations<Manifest extends T.Manifest, Store> {
|
|
||||||
private constructor(
|
|
||||||
readonly manifest: T.Manifest,
|
|
||||||
readonly migrations: Array<Migration<Manifest, Store, any>>,
|
|
||||||
) {}
|
|
||||||
private sortedMigrations = once(() => {
|
|
||||||
const migrationsAsVersions = (
|
|
||||||
this.migrations as Array<Migration<Manifest, Store, any>>
|
|
||||||
)
|
|
||||||
.map((x) => [ExtendedVersion.parse(x.options.version), x] as const)
|
|
||||||
.filter(([v, _]) => v.flavor === this.currentVersion().flavor)
|
|
||||||
migrationsAsVersions.sort((a, b) => a[0].compareForSort(b[0]))
|
|
||||||
return migrationsAsVersions
|
|
||||||
})
|
|
||||||
private currentVersion = once(() =>
|
|
||||||
ExtendedVersion.parse(this.manifest.version),
|
|
||||||
)
|
|
||||||
static of<
|
|
||||||
Manifest extends T.Manifest,
|
|
||||||
Store,
|
|
||||||
Migrations extends Array<Migration<Manifest, Store, any>>,
|
|
||||||
>(manifest: T.Manifest, ...migrations: EnsureUniqueId<Migrations>) {
|
|
||||||
return new Migrations(
|
|
||||||
manifest,
|
|
||||||
migrations as Array<Migration<Manifest, Store, any>>,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
async init({
|
|
||||||
effects,
|
|
||||||
previousVersion,
|
|
||||||
}: Parameters<T.ExpectedExports.init>[0]) {
|
|
||||||
if (!!previousVersion) {
|
|
||||||
const previousVersionExVer = ExtendedVersion.parse(previousVersion)
|
|
||||||
for (const [_, migration] of this.sortedMigrations()
|
|
||||||
.filter((x) => x[0].greaterThan(previousVersionExVer))
|
|
||||||
.filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) {
|
|
||||||
await migration.up({ effects })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async uninit({
|
|
||||||
effects,
|
|
||||||
nextVersion,
|
|
||||||
}: Parameters<T.ExpectedExports.uninit>[0]) {
|
|
||||||
if (!!nextVersion) {
|
|
||||||
const nextVersionExVer = ExtendedVersion.parse(nextVersion)
|
|
||||||
const reversed = [...this.sortedMigrations()].reverse()
|
|
||||||
for (const [_, migration] of reversed
|
|
||||||
.filter((x) => x[0].greaterThan(nextVersionExVer))
|
|
||||||
.filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) {
|
|
||||||
await migration.down({ effects })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setupMigrations<
|
|
||||||
Manifest extends T.Manifest,
|
|
||||||
Store,
|
|
||||||
Migrations extends Array<Migration<Manifest, Store, any>>,
|
|
||||||
>(manifest: T.Manifest, ...migrations: EnsureUniqueId<Migrations>) {
|
|
||||||
return Migrations.of<Manifest, Store, Migrations>(manifest, ...migrations)
|
|
||||||
}
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
export type EnsureUniqueId<A, B = A, ids = never> =
|
|
||||||
B extends [] ? A :
|
|
||||||
B extends [Migration<any, any, infer id>, ...infer Rest] ? (
|
|
||||||
id extends ids ? "One of the ids are not unique"[] :
|
|
||||||
EnsureUniqueId<A, Rest, id | ids>
|
|
||||||
) : "There exists a migration that is not a Migration"[]
|
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import { DependenciesReceipt } from "../config/setupConfig"
|
import { DependenciesReceipt } from "../config/setupConfig"
|
||||||
|
import { ExtendedVersion, VersionRange } from "../exver"
|
||||||
import { SetInterfaces } from "../interfaces/setupInterfaces"
|
import { SetInterfaces } from "../interfaces/setupInterfaces"
|
||||||
|
|
||||||
import { ExposedStorePaths } from "../store/setupExposeStore"
|
import { ExposedStorePaths } from "../store/setupExposeStore"
|
||||||
import * as T from "../types"
|
import * as T from "../types"
|
||||||
import { Migrations } from "./migrations/setupMigrations"
|
import { VersionGraph } from "../version/VersionGraph"
|
||||||
import { Install } from "./setupInstall"
|
import { Install } from "./setupInstall"
|
||||||
import { Uninstall } from "./setupUninstall"
|
import { Uninstall } from "./setupUninstall"
|
||||||
|
|
||||||
export function setupInit<Manifest extends T.Manifest, Store>(
|
export function setupInit<Manifest extends T.Manifest, Store>(
|
||||||
migrations: Migrations<Manifest, Store>,
|
versions: VersionGraph<Manifest["version"]>,
|
||||||
install: Install<Manifest, Store>,
|
install: Install<Manifest, Store>,
|
||||||
uninstall: Uninstall<Manifest, Store>,
|
uninstall: Uninstall<Manifest, Store>,
|
||||||
setInterfaces: SetInterfaces<Manifest, Store, any, any>,
|
setInterfaces: SetInterfaces<Manifest, Store, any, any>,
|
||||||
@@ -23,8 +24,19 @@ export function setupInit<Manifest extends T.Manifest, Store>(
|
|||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
init: async (opts) => {
|
init: async (opts) => {
|
||||||
await migrations.init(opts)
|
const prev = await opts.effects.getDataVersion()
|
||||||
await install.init(opts)
|
if (prev) {
|
||||||
|
await versions.migrate({
|
||||||
|
effects: opts.effects,
|
||||||
|
from: ExtendedVersion.parse(prev),
|
||||||
|
to: versions.currentVersion(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await install.install(opts)
|
||||||
|
await opts.effects.setDataVersion({
|
||||||
|
version: versions.current.options.version,
|
||||||
|
})
|
||||||
|
}
|
||||||
await setInterfaces({
|
await setInterfaces({
|
||||||
...opts,
|
...opts,
|
||||||
input: null,
|
input: null,
|
||||||
@@ -33,8 +45,18 @@ export function setupInit<Manifest extends T.Manifest, Store>(
|
|||||||
await setDependencies({ effects: opts.effects, input: null })
|
await setDependencies({ effects: opts.effects, input: null })
|
||||||
},
|
},
|
||||||
uninit: async (opts) => {
|
uninit: async (opts) => {
|
||||||
await migrations.uninit(opts)
|
if (opts.nextVersion) {
|
||||||
await uninstall.uninit(opts)
|
const prev = await opts.effects.getDataVersion()
|
||||||
|
if (prev) {
|
||||||
|
await versions.migrate({
|
||||||
|
effects: opts.effects,
|
||||||
|
from: ExtendedVersion.parse(prev),
|
||||||
|
to: ExtendedVersion.parse(opts.nextVersion),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await uninstall.uninstall(opts)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,10 @@ export class Install<Manifest extends T.Manifest, Store> {
|
|||||||
return new Install(fn)
|
return new Install(fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
async init({
|
async install({ effects }: Parameters<T.ExpectedExports.init>[0]) {
|
||||||
effects,
|
await this.fn({
|
||||||
previousVersion,
|
effects,
|
||||||
}: Parameters<T.ExpectedExports.init>[0]) {
|
})
|
||||||
if (!previousVersion)
|
|
||||||
await this.fn({
|
|
||||||
effects,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export class Uninstall<Manifest extends T.Manifest, Store> {
|
|||||||
return new Uninstall(fn)
|
return new Uninstall(fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
async uninit({
|
async uninstall({
|
||||||
effects,
|
effects,
|
||||||
nextVersion,
|
nextVersion,
|
||||||
}: Parameters<T.ExpectedExports.uninit>[0]) {
|
}: Parameters<T.ExpectedExports.uninit>[0]) {
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export class Origin<T extends Host> {
|
|||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
hasPrimary,
|
hasPrimary,
|
||||||
disabled,
|
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
username,
|
username,
|
||||||
@@ -69,7 +68,6 @@ export class Origin<T extends Host> {
|
|||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
hasPrimary,
|
hasPrimary,
|
||||||
disabled,
|
|
||||||
addressInfo,
|
addressInfo,
|
||||||
type,
|
type,
|
||||||
masked,
|
masked,
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export class ServiceInterfaceBuilder {
|
|||||||
id: string
|
id: string
|
||||||
description: string
|
description: string
|
||||||
hasPrimary: boolean
|
hasPrimary: boolean
|
||||||
disabled: boolean
|
|
||||||
type: ServiceInterfaceType
|
type: ServiceInterfaceType
|
||||||
username: string | null
|
username: string | null
|
||||||
path: string
|
path: string
|
||||||
|
|||||||
@@ -2,30 +2,39 @@ import { DEFAULT_SIGTERM_TIMEOUT } from "."
|
|||||||
import { NO_TIMEOUT, SIGKILL, SIGTERM } from "../StartSdk"
|
import { NO_TIMEOUT, SIGKILL, SIGTERM } from "../StartSdk"
|
||||||
|
|
||||||
import * as T from "../types"
|
import * as T from "../types"
|
||||||
import { MountOptions, Overlay } from "../util/Overlay"
|
import { asError } from "../util/asError"
|
||||||
|
import {
|
||||||
|
ExecSpawnable,
|
||||||
|
MountOptions,
|
||||||
|
SubContainerHandle,
|
||||||
|
SubContainer,
|
||||||
|
} from "../util/SubContainer"
|
||||||
import { splitCommand } from "../util/splitCommand"
|
import { splitCommand } from "../util/splitCommand"
|
||||||
import { cpExecFile, cpExec } from "./Daemons"
|
import * as cp from "child_process"
|
||||||
|
|
||||||
export class CommandController {
|
export class CommandController {
|
||||||
private constructor(
|
private constructor(
|
||||||
readonly runningAnswer: Promise<unknown>,
|
readonly runningAnswer: Promise<unknown>,
|
||||||
readonly overlay: Overlay,
|
private state: { exited: boolean },
|
||||||
readonly pid: number | undefined,
|
private readonly subcontainer: SubContainer,
|
||||||
|
private process: cp.ChildProcessWithoutNullStreams,
|
||||||
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
|
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
|
||||||
) {}
|
) {}
|
||||||
static of<Manifest extends T.Manifest>() {
|
static of<Manifest extends T.Manifest>() {
|
||||||
return async <A extends string>(
|
return async <A extends string>(
|
||||||
effects: T.Effects,
|
effects: T.Effects,
|
||||||
imageId: {
|
subcontainer:
|
||||||
id: keyof Manifest["images"] & T.ImageId
|
| {
|
||||||
sharedRun?: boolean
|
id: keyof Manifest["images"] & T.ImageId
|
||||||
},
|
sharedRun?: boolean
|
||||||
|
}
|
||||||
|
| SubContainer,
|
||||||
command: T.CommandType,
|
command: T.CommandType,
|
||||||
options: {
|
options: {
|
||||||
// Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms
|
// Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms
|
||||||
sigtermTimeout?: number
|
sigtermTimeout?: number
|
||||||
mounts?: { path: string; options: MountOptions }[]
|
mounts?: { path: string; options: MountOptions }[]
|
||||||
overlay?: Overlay
|
runAsInit?: boolean
|
||||||
env?:
|
env?:
|
||||||
| {
|
| {
|
||||||
[variable: string]: string
|
[variable: string]: string
|
||||||
@@ -38,43 +47,62 @@ export class CommandController {
|
|||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const commands = splitCommand(command)
|
const commands = splitCommand(command)
|
||||||
const overlay = options.overlay || (await Overlay.of(effects, imageId))
|
const subc =
|
||||||
for (let mount of options.mounts || []) {
|
subcontainer instanceof SubContainer
|
||||||
await overlay.mount(mount.options, mount.path)
|
? subcontainer
|
||||||
|
: await (async () => {
|
||||||
|
const subc = await SubContainer.of(effects, subcontainer)
|
||||||
|
for (let mount of options.mounts || []) {
|
||||||
|
await subc.mount(mount.options, mount.path)
|
||||||
|
}
|
||||||
|
return subc
|
||||||
|
})()
|
||||||
|
let childProcess: cp.ChildProcessWithoutNullStreams
|
||||||
|
if (options.runAsInit) {
|
||||||
|
childProcess = await subc.launch(commands, {
|
||||||
|
env: options.env,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
childProcess = await subc.spawn(commands, {
|
||||||
|
env: options.env,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const childProcess = await overlay.spawn(commands, {
|
const state = { exited: false }
|
||||||
env: options.env,
|
|
||||||
})
|
|
||||||
const answer = new Promise<null>((resolve, reject) => {
|
const answer = new Promise<null>((resolve, reject) => {
|
||||||
childProcess.stdout.on(
|
childProcess.on("exit", (code) => {
|
||||||
"data",
|
state.exited = true
|
||||||
options.onStdout ??
|
if (
|
||||||
((data: any) => {
|
code === 0 ||
|
||||||
console.log(data.toString())
|
code === 143 ||
|
||||||
}),
|
(code === null && childProcess.signalCode == "SIGTERM")
|
||||||
)
|
) {
|
||||||
childProcess.stderr.on(
|
|
||||||
"data",
|
|
||||||
options.onStderr ??
|
|
||||||
((data: any) => {
|
|
||||||
console.error(data.toString())
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
childProcess.on("exit", (code: any) => {
|
|
||||||
if (code === 0) {
|
|
||||||
return resolve(null)
|
return resolve(null)
|
||||||
}
|
}
|
||||||
return reject(new Error(`${commands[0]} exited with code ${code}`))
|
if (code) {
|
||||||
|
return reject(new Error(`${commands[0]} exited with code ${code}`))
|
||||||
|
} else {
|
||||||
|
return reject(
|
||||||
|
new Error(
|
||||||
|
`${commands[0]} exited with signal ${childProcess.signalCode}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const pid = childProcess.pid
|
return new CommandController(
|
||||||
|
answer,
|
||||||
return new CommandController(answer, overlay, pid, options.sigtermTimeout)
|
state,
|
||||||
|
subc,
|
||||||
|
childProcess,
|
||||||
|
options.sigtermTimeout,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async wait(timeout: number = NO_TIMEOUT) {
|
get subContainerHandle() {
|
||||||
|
return new SubContainerHandle(this.subcontainer)
|
||||||
|
}
|
||||||
|
async wait({ timeout = NO_TIMEOUT } = {}) {
|
||||||
if (timeout > 0)
|
if (timeout > 0)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.term()
|
this.term()
|
||||||
@@ -82,75 +110,30 @@ export class CommandController {
|
|||||||
try {
|
try {
|
||||||
return await this.runningAnswer
|
return await this.runningAnswer
|
||||||
} finally {
|
} finally {
|
||||||
if (this.pid !== undefined) {
|
if (!this.state.exited) {
|
||||||
await cpExecFile("pkill", ["-9", "-s", String(this.pid)]).catch(
|
this.process.kill("SIGKILL")
|
||||||
(_) => {},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
await this.overlay.destroy().catch((_) => {})
|
await this.subcontainer.destroy?.().catch((_) => {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) {
|
async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) {
|
||||||
if (this.pid === undefined) return
|
|
||||||
try {
|
try {
|
||||||
await cpExecFile("pkill", [
|
if (!this.state.exited) {
|
||||||
`-${signal.replace("SIG", "")}`,
|
if (!this.process.kill(signal)) {
|
||||||
"-s",
|
console.error(
|
||||||
String(this.pid),
|
`failed to send signal ${signal} to pid ${this.process.pid}`,
|
||||||
])
|
)
|
||||||
|
}
|
||||||
const didTimeout = await waitSession(this.pid, timeout)
|
|
||||||
if (didTimeout) {
|
|
||||||
await cpExecFile("pkill", [`-9`, "-s", String(this.pid)]).catch(
|
|
||||||
(_) => {},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
await this.overlay.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function waitSession(
|
if (signal !== "SIGKILL") {
|
||||||
sid: number,
|
setTimeout(() => {
|
||||||
timeout = NO_TIMEOUT,
|
this.process.kill("SIGKILL")
|
||||||
interval = 100,
|
}, timeout)
|
||||||
): Promise<boolean> {
|
}
|
||||||
let nextInterval = interval * 2
|
await this.runningAnswer
|
||||||
if (timeout >= 0 && timeout < nextInterval) {
|
} finally {
|
||||||
nextInterval = timeout
|
await this.subcontainer.destroy?.()
|
||||||
}
|
|
||||||
let nextTimeout = timeout
|
|
||||||
if (timeout > 0) {
|
|
||||||
if (timeout >= interval) {
|
|
||||||
nextTimeout -= interval
|
|
||||||
} else {
|
|
||||||
nextTimeout = 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let next: NodeJS.Timeout | null = null
|
|
||||||
if (timeout !== 0) {
|
|
||||||
next = setTimeout(() => {
|
|
||||||
waitSession(sid, nextTimeout, nextInterval).then(resolve, reject)
|
|
||||||
}, interval)
|
|
||||||
}
|
|
||||||
cpExecFile("ps", [`--sid=${sid}`, "-o", "--pid="]).then(
|
|
||||||
(_) => {
|
|
||||||
if (timeout === 0) {
|
|
||||||
resolve(true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(e) => {
|
|
||||||
if (next) {
|
|
||||||
clearTimeout(next)
|
|
||||||
}
|
|
||||||
if (typeof e === "object" && e && "code" in e && e.code) {
|
|
||||||
resolve(false)
|
|
||||||
} else {
|
|
||||||
reject(e)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as T from "../types"
|
import * as T from "../types"
|
||||||
import { MountOptions, Overlay } from "../util/Overlay"
|
import { asError } from "../util/asError"
|
||||||
|
import { ExecSpawnable, MountOptions, SubContainer } from "../util/SubContainer"
|
||||||
import { CommandController } from "./CommandController"
|
import { CommandController } from "./CommandController"
|
||||||
|
|
||||||
const TIMEOUT_INCREMENT_MS = 1000
|
const TIMEOUT_INCREMENT_MS = 1000
|
||||||
@@ -12,18 +13,22 @@ const MAX_TIMEOUT_MS = 30000
|
|||||||
export class Daemon {
|
export class Daemon {
|
||||||
private commandController: CommandController | null = null
|
private commandController: CommandController | null = null
|
||||||
private shouldBeRunning = false
|
private shouldBeRunning = false
|
||||||
private constructor(private startCommand: () => Promise<CommandController>) {}
|
constructor(private startCommand: () => Promise<CommandController>) {}
|
||||||
|
get subContainerHandle(): undefined | ExecSpawnable {
|
||||||
|
return this.commandController?.subContainerHandle
|
||||||
|
}
|
||||||
static of<Manifest extends T.Manifest>() {
|
static of<Manifest extends T.Manifest>() {
|
||||||
return async <A extends string>(
|
return async <A extends string>(
|
||||||
effects: T.Effects,
|
effects: T.Effects,
|
||||||
imageId: {
|
subcontainer:
|
||||||
id: keyof Manifest["images"] & T.ImageId
|
| {
|
||||||
sharedRun?: boolean
|
id: keyof Manifest["images"] & T.ImageId
|
||||||
},
|
sharedRun?: boolean
|
||||||
|
}
|
||||||
|
| SubContainer,
|
||||||
command: T.CommandType,
|
command: T.CommandType,
|
||||||
options: {
|
options: {
|
||||||
mounts?: { path: string; options: MountOptions }[]
|
mounts?: { path: string; options: MountOptions }[]
|
||||||
overlay?: Overlay
|
|
||||||
env?:
|
env?:
|
||||||
| {
|
| {
|
||||||
[variable: string]: string
|
[variable: string]: string
|
||||||
@@ -37,11 +42,15 @@ export class Daemon {
|
|||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const startCommand = () =>
|
const startCommand = () =>
|
||||||
CommandController.of<Manifest>()(effects, imageId, command, options)
|
CommandController.of<Manifest>()(
|
||||||
|
effects,
|
||||||
|
subcontainer,
|
||||||
|
command,
|
||||||
|
options,
|
||||||
|
)
|
||||||
return new Daemon(startCommand)
|
return new Daemon(startCommand)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
if (this.commandController) {
|
if (this.commandController) {
|
||||||
return
|
return
|
||||||
@@ -57,7 +66,7 @@ export class Daemon {
|
|||||||
timeoutCounter = Math.max(MAX_TIMEOUT_MS, timeoutCounter)
|
timeoutCounter = Math.max(MAX_TIMEOUT_MS, timeoutCounter)
|
||||||
}
|
}
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error(err)
|
console.error(asError(err))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
async term(termOptions?: {
|
async term(termOptions?: {
|
||||||
@@ -72,8 +81,8 @@ export class Daemon {
|
|||||||
}) {
|
}) {
|
||||||
this.shouldBeRunning = false
|
this.shouldBeRunning = false
|
||||||
await this.commandController
|
await this.commandController
|
||||||
?.term(termOptions)
|
?.term({ ...termOptions })
|
||||||
.catch((e) => console.error(e))
|
.catch((e) => console.error(asError(e)))
|
||||||
this.commandController = null
|
this.commandController = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { NO_TIMEOUT, SIGKILL, SIGTERM, Signals } from "../StartSdk"
|
import { NO_TIMEOUT, SIGKILL, SIGTERM, Signals } from "../StartSdk"
|
||||||
import { HealthReceipt } from "../health/HealthReceipt"
|
import { HealthReceipt } from "../health/HealthReceipt"
|
||||||
import { CheckResult } from "../health/checkFns"
|
import { HealthCheckResult } from "../health/checkFns"
|
||||||
|
|
||||||
import { Trigger } from "../trigger"
|
import { Trigger } from "../trigger"
|
||||||
import { TriggerInput } from "../trigger/TriggerInput"
|
import { TriggerInput } from "../trigger/TriggerInput"
|
||||||
import { defaultTrigger } from "../trigger/defaultTrigger"
|
import { defaultTrigger } from "../trigger/defaultTrigger"
|
||||||
import * as T from "../types"
|
import * as T from "../types"
|
||||||
import { Mounts } from "./Mounts"
|
import { Mounts } from "./Mounts"
|
||||||
import { CommandOptions, MountOptions, Overlay } from "../util/Overlay"
|
import {
|
||||||
|
CommandOptions,
|
||||||
|
ExecSpawnable,
|
||||||
|
MountOptions,
|
||||||
|
SubContainer,
|
||||||
|
} from "../util/SubContainer"
|
||||||
import { splitCommand } from "../util/splitCommand"
|
import { splitCommand } from "../util/splitCommand"
|
||||||
|
|
||||||
import { promisify } from "node:util"
|
import { promisify } from "node:util"
|
||||||
@@ -23,7 +28,9 @@ export const cpExec = promisify(CP.exec)
|
|||||||
export const cpExecFile = promisify(CP.execFile)
|
export const cpExecFile = promisify(CP.execFile)
|
||||||
export type Ready = {
|
export type Ready = {
|
||||||
display: string | null
|
display: string | null
|
||||||
fn: () => Promise<CheckResult> | CheckResult
|
fn: (
|
||||||
|
spawnable: ExecSpawnable,
|
||||||
|
) => Promise<HealthCheckResult> | HealthCheckResult
|
||||||
trigger?: Trigger
|
trigger?: Trigger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { CheckResult } from "../health/checkFns"
|
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 { Effects } from "../types"
|
import { Effects, SetHealth } from "../types"
|
||||||
import { DEFAULT_SIGTERM_TIMEOUT } from "."
|
import { DEFAULT_SIGTERM_TIMEOUT } from "."
|
||||||
|
import { asError } from "../util/asError"
|
||||||
|
|
||||||
const oncePromise = <T>() => {
|
const oncePromise = <T>() => {
|
||||||
let resolve: (value: T) => void
|
let resolve: (value: T) => void
|
||||||
@@ -21,14 +22,13 @@ const oncePromise = <T>() => {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export class HealthDaemon {
|
export class HealthDaemon {
|
||||||
#health: CheckResult = { status: "starting", message: null }
|
private _health: HealthCheckResult = { result: "starting", message: null }
|
||||||
#healthWatchers: Array<() => unknown> = []
|
private healthWatchers: Array<() => unknown> = []
|
||||||
#running = false
|
private running = false
|
||||||
#hadSuccess = false
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly daemon: Promise<Daemon>,
|
private readonly daemon: Promise<Daemon>,
|
||||||
readonly daemonIndex: number,
|
readonly daemonIndex: number,
|
||||||
readonly dependencies: HealthDaemon[],
|
private readonly dependencies: HealthDaemon[],
|
||||||
readonly id: string,
|
readonly id: string,
|
||||||
readonly ids: string[],
|
readonly ids: string[],
|
||||||
readonly ready: Ready,
|
readonly ready: Ready,
|
||||||
@@ -44,12 +44,12 @@ export class HealthDaemon {
|
|||||||
signal?: NodeJS.Signals | undefined
|
signal?: NodeJS.Signals | undefined
|
||||||
timeout?: number | undefined
|
timeout?: number | undefined
|
||||||
}) {
|
}) {
|
||||||
this.#healthWatchers = []
|
this.healthWatchers = []
|
||||||
this.#running = false
|
this.running = false
|
||||||
this.#healthCheckCleanup?.()
|
this.healthCheckCleanup?.()
|
||||||
|
|
||||||
await this.daemon.then((d) =>
|
await this.daemon.then((d) =>
|
||||||
d.stop({
|
d.term({
|
||||||
timeout: this.sigtermTimeout,
|
timeout: this.sigtermTimeout,
|
||||||
...termOptions,
|
...termOptions,
|
||||||
}),
|
}),
|
||||||
@@ -58,17 +58,17 @@ export class HealthDaemon {
|
|||||||
|
|
||||||
/** Want to add another notifier that the health might have changed */
|
/** Want to add another notifier that the health might have changed */
|
||||||
addWatcher(watcher: () => unknown) {
|
addWatcher(watcher: () => unknown) {
|
||||||
this.#healthWatchers.push(watcher)
|
this.healthWatchers.push(watcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
get health() {
|
get health() {
|
||||||
return Object.freeze(this.#health)
|
return Object.freeze(this._health)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async changeRunning(newStatus: boolean) {
|
private async changeRunning(newStatus: boolean) {
|
||||||
if (this.#running === newStatus) return
|
if (this.running === newStatus) return
|
||||||
|
|
||||||
this.#running = newStatus
|
this.running = newStatus
|
||||||
|
|
||||||
if (newStatus) {
|
if (newStatus) {
|
||||||
;(await this.daemon).start()
|
;(await this.daemon).start()
|
||||||
@@ -77,19 +77,18 @@ export class HealthDaemon {
|
|||||||
;(await this.daemon).stop()
|
;(await this.daemon).stop()
|
||||||
this.turnOffHealthCheck()
|
this.turnOffHealthCheck()
|
||||||
|
|
||||||
this.setHealth({ status: "starting", message: null })
|
this.setHealth({ result: "starting", message: null })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#healthCheckCleanup: (() => void) | null = null
|
private healthCheckCleanup: (() => void) | null = null
|
||||||
private turnOffHealthCheck() {
|
private turnOffHealthCheck() {
|
||||||
this.#healthCheckCleanup?.()
|
this.healthCheckCleanup?.()
|
||||||
}
|
}
|
||||||
private async setupHealthCheck() {
|
private async setupHealthCheck() {
|
||||||
if (this.#healthCheckCleanup) return
|
if (this.healthCheckCleanup) return
|
||||||
const trigger = (this.ready.trigger ?? defaultTrigger)(() => ({
|
const trigger = (this.ready.trigger ?? defaultTrigger)(() => ({
|
||||||
hadSuccess: this.#hadSuccess,
|
lastResult: this._health.result,
|
||||||
lastResult: this.#health.status,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const { promise: status, resolve: setStatus } = oncePromise<{
|
const { promise: status, resolve: setStatus } = oncePromise<{
|
||||||
@@ -101,59 +100,51 @@ export class HealthDaemon {
|
|||||||
!res.done;
|
!res.done;
|
||||||
res = await Promise.race([status, trigger.next()])
|
res = await Promise.race([status, trigger.next()])
|
||||||
) {
|
) {
|
||||||
const response: CheckResult = await Promise.resolve(
|
const handle = (await this.daemon).subContainerHandle
|
||||||
this.ready.fn(),
|
|
||||||
).catch((err) => {
|
if (handle) {
|
||||||
console.error(err)
|
const response: HealthCheckResult = await Promise.resolve(
|
||||||
return {
|
this.ready.fn(handle),
|
||||||
status: "failure",
|
).catch((err) => {
|
||||||
message: "message" in err ? err.message : String(err),
|
console.error(asError(err))
|
||||||
}
|
return {
|
||||||
})
|
result: "failure",
|
||||||
this.setHealth(response)
|
message: "message" in err ? err.message : String(err),
|
||||||
if (response.status === "success") {
|
}
|
||||||
this.#hadSuccess = true
|
})
|
||||||
|
await this.setHealth(response)
|
||||||
|
} else {
|
||||||
|
await this.setHealth({
|
||||||
|
result: "failure",
|
||||||
|
message: "Daemon not running",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).catch((err) => console.error(`Daemon ${this.id} failed: ${err}`))
|
}).catch((err) => console.error(`Daemon ${this.id} failed: ${err}`))
|
||||||
|
|
||||||
this.#healthCheckCleanup = () => {
|
this.healthCheckCleanup = () => {
|
||||||
setStatus({ done: true })
|
setStatus({ done: true })
|
||||||
this.#healthCheckCleanup = null
|
this.healthCheckCleanup = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setHealth(health: CheckResult) {
|
private async setHealth(health: HealthCheckResult) {
|
||||||
this.#health = health
|
this._health = health
|
||||||
this.#healthWatchers.forEach((watcher) => watcher())
|
this.healthWatchers.forEach((watcher) => watcher())
|
||||||
const display = this.ready.display
|
const display = this.ready.display
|
||||||
const status = health.status
|
const result = health.result
|
||||||
if (!display) {
|
if (!display) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (
|
await this.effects.setHealth({
|
||||||
status === "success" ||
|
...health,
|
||||||
status === "disabled" ||
|
id: this.id,
|
||||||
status === "starting"
|
name: display,
|
||||||
) {
|
} as SetHealth)
|
||||||
this.effects.setHealth({
|
|
||||||
result: status,
|
|
||||||
message: health.message,
|
|
||||||
id: this.id,
|
|
||||||
name: display,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.effects.setHealth({
|
|
||||||
result: health.status,
|
|
||||||
message: health.message || "",
|
|
||||||
id: this.id,
|
|
||||||
name: display,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateStatus() {
|
private async updateStatus() {
|
||||||
const healths = this.dependencies.map((d) => d.#health)
|
const healths = this.dependencies.map((d) => d._health)
|
||||||
this.changeRunning(healths.every((x) => x.status === "success"))
|
this.changeRunning(healths.every((x) => x.result === "success"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as T from "../types"
|
import * as T from "../types"
|
||||||
import { MountOptions } from "../util/Overlay"
|
import { MountOptions } from "../util/SubContainer"
|
||||||
|
|
||||||
type MountArray = { path: string; options: MountOptions }[]
|
type MountArray = { path: string; options: MountOptions }[]
|
||||||
|
|
||||||
|
|||||||
@@ -7,22 +7,11 @@ import {
|
|||||||
ImageSource,
|
ImageSource,
|
||||||
} from "../types"
|
} from "../types"
|
||||||
|
|
||||||
export type SDKManifest<
|
export type SDKManifest = {
|
||||||
Version extends string,
|
|
||||||
Satisfies extends string[] = [],
|
|
||||||
> = {
|
|
||||||
/** The package identifier used by the OS. This must be unique amongst all other known packages */
|
/** The package identifier used by the OS. This must be unique amongst all other known packages */
|
||||||
readonly id: string
|
readonly id: string
|
||||||
/** A human readable service title */
|
/** A human readable service title */
|
||||||
readonly title: string
|
readonly title: string
|
||||||
/** Service version - accepts up to four digits, where the last confirms to revisions necessary for StartOs
|
|
||||||
* - see documentation: https://github.com/Start9Labs/emver-rs. This value will change with each release of
|
|
||||||
* the service
|
|
||||||
*/
|
|
||||||
readonly version: Version & ValidateExVer<Version>
|
|
||||||
readonly satisfies?: Satisfies & ValidateExVers<Satisfies>
|
|
||||||
/** Release notes for the update - can be a string, paragraph or URL */
|
|
||||||
readonly releaseNotes: string
|
|
||||||
/** The type of license for the project. Include the LICENSE in the root of the project directory. A license is required for a Start9 package.*/
|
/** The type of license for the project. Include the LICENSE in the root of the project directory. A license is required for a Start9 package.*/
|
||||||
readonly license: string // name of license
|
readonly license: string // name of license
|
||||||
/** The Start9 wrapper repository URL for the package. This repo contains the manifest file (this),
|
/** The Start9 wrapper repository URL for the package. This repo contains the manifest file (this),
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import * as T from "../types"
|
|||||||
import { ImageConfig, ImageId, VolumeId } from "../osBindings"
|
import { ImageConfig, ImageId, VolumeId } from "../osBindings"
|
||||||
import { SDKManifest, SDKImageConfig } from "./ManifestTypes"
|
import { SDKManifest, SDKImageConfig } from "./ManifestTypes"
|
||||||
import { SDKVersion } from "../StartSdk"
|
import { SDKVersion } from "../StartSdk"
|
||||||
|
import { VersionGraph } from "../version/VersionGraph"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an example of a function that takes a manifest and returns a new manifest with additional properties
|
||||||
|
* @param manifest Manifests are the description of the package
|
||||||
|
* @returns The manifest with additional properties
|
||||||
|
*/
|
||||||
export function setupManifest<
|
export function setupManifest<
|
||||||
Id extends string,
|
Id extends string,
|
||||||
Version extends string,
|
Version extends string,
|
||||||
@@ -10,7 +16,7 @@ export function setupManifest<
|
|||||||
VolumesTypes extends VolumeId,
|
VolumesTypes extends VolumeId,
|
||||||
AssetTypes extends VolumeId,
|
AssetTypes extends VolumeId,
|
||||||
ImagesTypes extends ImageId,
|
ImagesTypes extends ImageId,
|
||||||
Manifest extends SDKManifest<Version, Satisfies> & {
|
Manifest extends {
|
||||||
dependencies: Dependencies
|
dependencies: Dependencies
|
||||||
id: Id
|
id: Id
|
||||||
assets: AssetTypes[]
|
assets: AssetTypes[]
|
||||||
@@ -18,7 +24,10 @@ export function setupManifest<
|
|||||||
volumes: VolumesTypes[]
|
volumes: VolumesTypes[]
|
||||||
},
|
},
|
||||||
Satisfies extends string[] = [],
|
Satisfies extends string[] = [],
|
||||||
>(manifest: Manifest & { version: Version }): Manifest & T.Manifest {
|
>(
|
||||||
|
versions: VersionGraph<Version>,
|
||||||
|
manifest: SDKManifest & Manifest,
|
||||||
|
): Manifest & T.Manifest {
|
||||||
const images = Object.entries(manifest.images).reduce(
|
const images = Object.entries(manifest.images).reduce(
|
||||||
(images, [k, v]) => {
|
(images, [k, v]) => {
|
||||||
v.arch = v.arch || ["aarch64", "x86_64"]
|
v.arch = v.arch || ["aarch64", "x86_64"]
|
||||||
@@ -33,7 +42,11 @@ export function setupManifest<
|
|||||||
...manifest,
|
...manifest,
|
||||||
gitHash: null,
|
gitHash: null,
|
||||||
osVersion: SDKVersion,
|
osVersion: SDKVersion,
|
||||||
satisfies: manifest.satisfies || [],
|
version: versions.current.options.version,
|
||||||
|
releaseNotes: versions.current.options.releaseNotes,
|
||||||
|
satisfies: versions.current.options.satisfies || [],
|
||||||
|
canMigrateTo: versions.canMigrateTo().toString(),
|
||||||
|
canMigrateFrom: versions.canMigrateFrom().toString(),
|
||||||
images,
|
images,
|
||||||
alerts: {
|
alerts: {
|
||||||
install: manifest.alerts?.install || null,
|
install: manifest.alerts?.install || null,
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
import type { HealthCheckId } from "./HealthCheckId"
|
import type { HealthCheckId } from "./HealthCheckId"
|
||||||
import type { HealthCheckResult } from "./HealthCheckResult"
|
import type { NamedHealthCheckResult } from "./NamedHealthCheckResult"
|
||||||
import type { PackageId } from "./PackageId"
|
import type { PackageId } from "./PackageId"
|
||||||
|
|
||||||
export type CheckDependenciesResult = {
|
export type CheckDependenciesResult = {
|
||||||
packageId: PackageId
|
packageId: PackageId
|
||||||
isInstalled: boolean
|
title: string | null
|
||||||
|
installedVersion: string | null
|
||||||
|
satisfies: string[]
|
||||||
isRunning: boolean
|
isRunning: boolean
|
||||||
configSatisfied: boolean
|
configSatisfied: boolean
|
||||||
healthChecks: { [key: HealthCheckId]: HealthCheckResult }
|
healthChecks: { [key: HealthCheckId]: NamedHealthCheckResult }
|
||||||
version: string | null
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
import type { ImageId } from "./ImageId"
|
import type { ImageId } from "./ImageId"
|
||||||
|
|
||||||
export type CreateOverlayedImageParams = { imageId: ImageId }
|
export type CreateSubcontainerFsParams = { imageId: ImageId }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
import type { Guid } from "./Guid"
|
import type { Guid } from "./Guid"
|
||||||
|
|
||||||
export type DestroyOverlayedImageParams = { guid: Guid }
|
export type DestroySubcontainerFsParams = { guid: Guid }
|
||||||
@@ -8,7 +8,6 @@ export type ExportServiceInterfaceParams = {
|
|||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
hasPrimary: boolean
|
hasPrimary: boolean
|
||||||
disabled: boolean
|
|
||||||
masked: boolean
|
masked: boolean
|
||||||
addressInfo: AddressInfo
|
addressInfo: AddressInfo
|
||||||
type: ServiceInterfaceType
|
type: ServiceInterfaceType
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
export type HardwareRequirements = {
|
export type HardwareRequirements = {
|
||||||
device: { device?: string; processor?: string }
|
device: { display?: string; processor?: string }
|
||||||
ram: number | null
|
ram: number | null
|
||||||
arch: string[] | null
|
arch: string[] | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
import type { HealthCheckId } from "./HealthCheckId"
|
import type { HealthCheckId } from "./HealthCheckId"
|
||||||
import type { HealthCheckResult } from "./HealthCheckResult"
|
import type { NamedHealthCheckResult } from "./NamedHealthCheckResult"
|
||||||
|
import type { StartStop } from "./StartStop"
|
||||||
|
|
||||||
export type MainStatus =
|
export type MainStatus =
|
||||||
| { status: "stopped" }
|
| { status: "stopped" }
|
||||||
| { status: "restarting" }
|
| { status: "restarting" }
|
||||||
| { status: "restoring" }
|
| { status: "restoring" }
|
||||||
| { status: "stopping" }
|
| { status: "stopping" }
|
||||||
| { status: "starting" }
|
| {
|
||||||
|
status: "starting"
|
||||||
|
health: { [key: HealthCheckId]: NamedHealthCheckResult }
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
status: "running"
|
status: "running"
|
||||||
started: string
|
started: string
|
||||||
health: { [key: HealthCheckId]: HealthCheckResult }
|
health: { [key: HealthCheckId]: NamedHealthCheckResult }
|
||||||
}
|
|
||||||
| {
|
|
||||||
status: "backingUp"
|
|
||||||
started: string | null
|
|
||||||
health: { [key: HealthCheckId]: HealthCheckResult }
|
|
||||||
}
|
}
|
||||||
|
| { status: "backingUp"; onComplete: StartStop }
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export type Manifest = {
|
|||||||
version: Version
|
version: Version
|
||||||
satisfies: Array<Version>
|
satisfies: Array<Version>
|
||||||
releaseNotes: string
|
releaseNotes: string
|
||||||
|
canMigrateTo: string
|
||||||
|
canMigrateFrom: string
|
||||||
license: string
|
license: string
|
||||||
wrapperRepo: string
|
wrapperRepo: string
|
||||||
upstreamRepo: string
|
upstreamRepo: string
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
export type HealthCheckResult = { name: string } & (
|
export type NamedHealthCheckResult = { name: string } & (
|
||||||
| { result: "success"; message: string | null }
|
| { result: "success"; message: string | null }
|
||||||
| { result: "disabled"; message: string | null }
|
| { result: "disabled"; message: string | null }
|
||||||
| { result: "starting"; message: string | null }
|
| { result: "starting"; message: string | null }
|
||||||
@@ -8,9 +8,11 @@ import type { PackageState } from "./PackageState"
|
|||||||
import type { ServiceInterface } from "./ServiceInterface"
|
import type { ServiceInterface } from "./ServiceInterface"
|
||||||
import type { ServiceInterfaceId } from "./ServiceInterfaceId"
|
import type { ServiceInterfaceId } from "./ServiceInterfaceId"
|
||||||
import type { Status } from "./Status"
|
import type { Status } from "./Status"
|
||||||
|
import type { Version } from "./Version"
|
||||||
|
|
||||||
export type PackageDataEntry = {
|
export type PackageDataEntry = {
|
||||||
stateInfo: PackageState
|
stateInfo: PackageState
|
||||||
|
dataVersion: Version | null
|
||||||
status: Status
|
status: Status
|
||||||
registry: string | null
|
registry: string | null
|
||||||
developerKey: string
|
developerKey: string
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export type ServiceInterface = {
|
|||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
hasPrimary: boolean
|
hasPrimary: boolean
|
||||||
disabled: boolean
|
|
||||||
masked: boolean
|
masked: boolean
|
||||||
addressInfo: AddressInfo
|
addressInfo: AddressInfo
|
||||||
type: ServiceInterfaceType
|
type: ServiceInterfaceType
|
||||||
|
|||||||
3
sdk/lib/osBindings/SetDataVersionParams.ts
Normal file
3
sdk/lib/osBindings/SetDataVersionParams.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type SetDataVersionParams = { version: string }
|
||||||
3
sdk/lib/osBindings/StartStop.ts
Normal file
3
sdk/lib/osBindings/StartStop.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type StartStop = "start" | "stop"
|
||||||
@@ -31,7 +31,7 @@ export { CheckDependenciesParam } from "./CheckDependenciesParam"
|
|||||||
export { CheckDependenciesResult } from "./CheckDependenciesResult"
|
export { CheckDependenciesResult } from "./CheckDependenciesResult"
|
||||||
export { Cifs } from "./Cifs"
|
export { Cifs } from "./Cifs"
|
||||||
export { ContactInfo } from "./ContactInfo"
|
export { ContactInfo } from "./ContactInfo"
|
||||||
export { CreateOverlayedImageParams } from "./CreateOverlayedImageParams"
|
export { CreateSubcontainerFsParams } from "./CreateSubcontainerFsParams"
|
||||||
export { CurrentDependencies } from "./CurrentDependencies"
|
export { CurrentDependencies } from "./CurrentDependencies"
|
||||||
export { CurrentDependencyInfo } from "./CurrentDependencyInfo"
|
export { CurrentDependencyInfo } from "./CurrentDependencyInfo"
|
||||||
export { DataUrl } from "./DataUrl"
|
export { DataUrl } from "./DataUrl"
|
||||||
@@ -41,7 +41,7 @@ export { DependencyMetadata } from "./DependencyMetadata"
|
|||||||
export { DependencyRequirement } from "./DependencyRequirement"
|
export { DependencyRequirement } from "./DependencyRequirement"
|
||||||
export { DepInfo } from "./DepInfo"
|
export { DepInfo } from "./DepInfo"
|
||||||
export { Description } from "./Description"
|
export { Description } from "./Description"
|
||||||
export { DestroyOverlayedImageParams } from "./DestroyOverlayedImageParams"
|
export { DestroySubcontainerFsParams } from "./DestroySubcontainerFsParams"
|
||||||
export { Duration } from "./Duration"
|
export { Duration } from "./Duration"
|
||||||
export { EchoParams } from "./EchoParams"
|
export { EchoParams } from "./EchoParams"
|
||||||
export { EncryptedWire } from "./EncryptedWire"
|
export { EncryptedWire } from "./EncryptedWire"
|
||||||
@@ -69,7 +69,6 @@ export { Governor } from "./Governor"
|
|||||||
export { Guid } from "./Guid"
|
export { Guid } from "./Guid"
|
||||||
export { HardwareRequirements } from "./HardwareRequirements"
|
export { HardwareRequirements } from "./HardwareRequirements"
|
||||||
export { HealthCheckId } from "./HealthCheckId"
|
export { HealthCheckId } from "./HealthCheckId"
|
||||||
export { HealthCheckResult } from "./HealthCheckResult"
|
|
||||||
export { HostAddress } from "./HostAddress"
|
export { HostAddress } from "./HostAddress"
|
||||||
export { HostId } from "./HostId"
|
export { HostId } from "./HostId"
|
||||||
export { HostKind } from "./HostKind"
|
export { HostKind } from "./HostKind"
|
||||||
@@ -98,6 +97,7 @@ export { MaybeUtf8String } from "./MaybeUtf8String"
|
|||||||
export { MerkleArchiveCommitment } from "./MerkleArchiveCommitment"
|
export { MerkleArchiveCommitment } from "./MerkleArchiveCommitment"
|
||||||
export { MountParams } from "./MountParams"
|
export { MountParams } from "./MountParams"
|
||||||
export { MountTarget } from "./MountTarget"
|
export { MountTarget } from "./MountTarget"
|
||||||
|
export { NamedHealthCheckResult } from "./NamedHealthCheckResult"
|
||||||
export { NamedProgress } from "./NamedProgress"
|
export { NamedProgress } from "./NamedProgress"
|
||||||
export { OnionHostname } from "./OnionHostname"
|
export { OnionHostname } from "./OnionHostname"
|
||||||
export { OsIndex } from "./OsIndex"
|
export { OsIndex } from "./OsIndex"
|
||||||
@@ -132,6 +132,7 @@ export { SessionList } from "./SessionList"
|
|||||||
export { Sessions } from "./Sessions"
|
export { Sessions } from "./Sessions"
|
||||||
export { Session } from "./Session"
|
export { Session } from "./Session"
|
||||||
export { SetConfigured } from "./SetConfigured"
|
export { SetConfigured } from "./SetConfigured"
|
||||||
|
export { SetDataVersionParams } from "./SetDataVersionParams"
|
||||||
export { SetDependenciesParams } from "./SetDependenciesParams"
|
export { SetDependenciesParams } from "./SetDependenciesParams"
|
||||||
export { SetHealth } from "./SetHealth"
|
export { SetHealth } from "./SetHealth"
|
||||||
export { SetMainStatusStatus } from "./SetMainStatusStatus"
|
export { SetMainStatusStatus } from "./SetMainStatusStatus"
|
||||||
@@ -144,6 +145,7 @@ export { SetupStatusRes } from "./SetupStatusRes"
|
|||||||
export { SignAssetParams } from "./SignAssetParams"
|
export { SignAssetParams } from "./SignAssetParams"
|
||||||
export { SignerInfo } from "./SignerInfo"
|
export { SignerInfo } from "./SignerInfo"
|
||||||
export { SmtpValue } from "./SmtpValue"
|
export { SmtpValue } from "./SmtpValue"
|
||||||
|
export { StartStop } from "./StartStop"
|
||||||
export { Status } from "./Status"
|
export { Status } from "./Status"
|
||||||
export { UpdatingState } from "./UpdatingState"
|
export { UpdatingState } from "./UpdatingState"
|
||||||
export { VerifyCifsParams } from "./VerifyCifsParams"
|
export { VerifyCifsParams } from "./VerifyCifsParams"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { asError } from "../../util/asError"
|
||||||
|
|
||||||
const msb = 0x80
|
const msb = 0x80
|
||||||
const dropMsb = 0x7f
|
const dropMsb = 0x7f
|
||||||
const maxSize = Math.floor((8 * 8 + 7) / 7)
|
const maxSize = Math.floor((8 * 8 + 7) / 7)
|
||||||
@@ -38,7 +40,7 @@ export class VarIntProcessor {
|
|||||||
if (success) {
|
if (success) {
|
||||||
return result
|
return result
|
||||||
} else {
|
} else {
|
||||||
console.error(this.buf)
|
console.error(asError(this.buf))
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { Variants } from "../config/builder/variants"
|
|||||||
import { ValueSpec } from "../config/configTypes"
|
import { ValueSpec } from "../config/configTypes"
|
||||||
import { setupManifest } from "../manifest/setupManifest"
|
import { setupManifest } from "../manifest/setupManifest"
|
||||||
import { StartSdk } from "../StartSdk"
|
import { StartSdk } from "../StartSdk"
|
||||||
|
import { VersionGraph } from "../version/VersionGraph"
|
||||||
|
import { VersionInfo } from "../version/VersionInfo"
|
||||||
|
|
||||||
describe("builder tests", () => {
|
describe("builder tests", () => {
|
||||||
test("text", async () => {
|
test("text", async () => {
|
||||||
@@ -366,42 +368,48 @@ describe("values", () => {
|
|||||||
test("datetime", async () => {
|
test("datetime", async () => {
|
||||||
const sdk = StartSdk.of()
|
const sdk = StartSdk.of()
|
||||||
.withManifest(
|
.withManifest(
|
||||||
setupManifest({
|
setupManifest(
|
||||||
id: "testOutput",
|
VersionGraph.of(
|
||||||
title: "",
|
VersionInfo.of({
|
||||||
version: "1.0.0:0",
|
version: "1.0.0:0",
|
||||||
releaseNotes: "",
|
releaseNotes: "",
|
||||||
license: "",
|
migrations: {},
|
||||||
replaces: [],
|
}),
|
||||||
wrapperRepo: "",
|
),
|
||||||
upstreamRepo: "",
|
{
|
||||||
supportSite: "",
|
id: "testOutput",
|
||||||
marketingSite: "",
|
title: "",
|
||||||
donationUrl: null,
|
license: "",
|
||||||
description: {
|
wrapperRepo: "",
|
||||||
short: "",
|
upstreamRepo: "",
|
||||||
long: "",
|
supportSite: "",
|
||||||
},
|
marketingSite: "",
|
||||||
containers: {},
|
donationUrl: null,
|
||||||
images: {},
|
description: {
|
||||||
volumes: [],
|
short: "",
|
||||||
assets: [],
|
long: "",
|
||||||
alerts: {
|
},
|
||||||
install: null,
|
containers: {},
|
||||||
update: null,
|
images: {},
|
||||||
uninstall: null,
|
volumes: [],
|
||||||
restore: null,
|
assets: [],
|
||||||
start: null,
|
alerts: {
|
||||||
stop: null,
|
install: null,
|
||||||
},
|
update: null,
|
||||||
dependencies: {
|
uninstall: null,
|
||||||
"remote-test": {
|
restore: null,
|
||||||
description: "",
|
start: null,
|
||||||
optional: true,
|
stop: null,
|
||||||
s9pk: "https://example.com/remote-test.s9pk",
|
},
|
||||||
|
dependencies: {
|
||||||
|
"remote-test": {
|
||||||
|
description: "",
|
||||||
|
optional: true,
|
||||||
|
s9pk: "https://example.com/remote-test.s9pk",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
),
|
||||||
)
|
)
|
||||||
.withStore<{ test: "a" }>()
|
.withStore<{ test: "a" }>()
|
||||||
.build(true)
|
.build(true)
|
||||||
|
|||||||
148
sdk/lib/test/graph.test.ts
Normal file
148
sdk/lib/test/graph.test.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { Graph } from "../util/graph"
|
||||||
|
|
||||||
|
describe("graph", () => {
|
||||||
|
{
|
||||||
|
{
|
||||||
|
test("findVertex", () => {
|
||||||
|
const graph = new Graph<string, string>()
|
||||||
|
const foo = graph.addVertex("foo", [], [])
|
||||||
|
const bar = graph.addVertex(
|
||||||
|
"bar",
|
||||||
|
[{ from: foo, metadata: "foo-bar" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const baz = graph.addVertex(
|
||||||
|
"baz",
|
||||||
|
[{ from: bar, metadata: "bar-baz" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const qux = graph.addVertex(
|
||||||
|
"qux",
|
||||||
|
[{ from: baz, metadata: "baz-qux" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const match = Array.from(graph.findVertex((v) => v.metadata === "qux"))
|
||||||
|
expect(match).toHaveLength(1)
|
||||||
|
expect(match[0]).toBe(qux)
|
||||||
|
})
|
||||||
|
test("shortestPathA", () => {
|
||||||
|
const graph = new Graph<string, string>()
|
||||||
|
const foo = graph.addVertex("foo", [], [])
|
||||||
|
const bar = graph.addVertex(
|
||||||
|
"bar",
|
||||||
|
[{ from: foo, metadata: "foo-bar" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const baz = graph.addVertex(
|
||||||
|
"baz",
|
||||||
|
[{ from: bar, metadata: "bar-baz" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const qux = graph.addVertex(
|
||||||
|
"qux",
|
||||||
|
[{ from: baz, metadata: "baz-qux" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
graph.addEdge("foo-qux", foo, qux)
|
||||||
|
expect(graph.shortestPath(foo, qux) || []).toHaveLength(1)
|
||||||
|
})
|
||||||
|
test("shortestPathB", () => {
|
||||||
|
const graph = new Graph<string, string>()
|
||||||
|
const foo = graph.addVertex("foo", [], [])
|
||||||
|
const bar = graph.addVertex(
|
||||||
|
"bar",
|
||||||
|
[{ from: foo, metadata: "foo-bar" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const baz = graph.addVertex(
|
||||||
|
"baz",
|
||||||
|
[{ from: bar, metadata: "bar-baz" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const qux = graph.addVertex(
|
||||||
|
"qux",
|
||||||
|
[{ from: baz, metadata: "baz-qux" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
graph.addEdge("bar-qux", bar, qux)
|
||||||
|
expect(graph.shortestPath(foo, qux) || []).toHaveLength(2)
|
||||||
|
})
|
||||||
|
test("shortestPathC", () => {
|
||||||
|
const graph = new Graph<string, string>()
|
||||||
|
const foo = graph.addVertex("foo", [], [])
|
||||||
|
const bar = graph.addVertex(
|
||||||
|
"bar",
|
||||||
|
[{ from: foo, metadata: "foo-bar" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const baz = graph.addVertex(
|
||||||
|
"baz",
|
||||||
|
[{ from: bar, metadata: "bar-baz" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const qux = graph.addVertex(
|
||||||
|
"qux",
|
||||||
|
[{ from: baz, metadata: "baz-qux" }],
|
||||||
|
[{ to: foo, metadata: "qux-foo" }],
|
||||||
|
)
|
||||||
|
expect(graph.shortestPath(foo, qux) || []).toHaveLength(3)
|
||||||
|
})
|
||||||
|
test("bfs", () => {
|
||||||
|
const graph = new Graph<string, string>()
|
||||||
|
const foo = graph.addVertex("foo", [], [])
|
||||||
|
const bar = graph.addVertex(
|
||||||
|
"bar",
|
||||||
|
[{ from: foo, metadata: "foo-bar" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const baz = graph.addVertex(
|
||||||
|
"baz",
|
||||||
|
[{ from: bar, metadata: "bar-baz" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const qux = graph.addVertex(
|
||||||
|
"qux",
|
||||||
|
[
|
||||||
|
{ from: foo, metadata: "foo-qux" },
|
||||||
|
{ from: baz, metadata: "baz-qux" },
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const bfs = Array.from(graph.breadthFirstSearch(foo))
|
||||||
|
expect(bfs).toHaveLength(4)
|
||||||
|
expect(bfs[0]).toBe(foo)
|
||||||
|
expect(bfs[1]).toBe(bar)
|
||||||
|
expect(bfs[2]).toBe(qux)
|
||||||
|
expect(bfs[3]).toBe(baz)
|
||||||
|
})
|
||||||
|
test("reverseBfs", () => {
|
||||||
|
const graph = new Graph<string, string>()
|
||||||
|
const foo = graph.addVertex("foo", [], [])
|
||||||
|
const bar = graph.addVertex(
|
||||||
|
"bar",
|
||||||
|
[{ from: foo, metadata: "foo-bar" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const baz = graph.addVertex(
|
||||||
|
"baz",
|
||||||
|
[{ from: bar, metadata: "bar-baz" }],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const qux = graph.addVertex(
|
||||||
|
"qux",
|
||||||
|
[
|
||||||
|
{ from: foo, metadata: "foo-qux" },
|
||||||
|
{ from: baz, metadata: "baz-qux" },
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const bfs = Array.from(graph.reverseBreadthFirstSearch(qux))
|
||||||
|
expect(bfs).toHaveLength(4)
|
||||||
|
expect(bfs[0]).toBe(qux)
|
||||||
|
expect(bfs[1]).toBe(foo)
|
||||||
|
expect(bfs[2]).toBe(baz)
|
||||||
|
expect(bfs[3]).toBe(bar)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -16,7 +16,6 @@ describe("host", () => {
|
|||||||
id: "foo",
|
id: "foo",
|
||||||
description: "A Foo",
|
description: "A Foo",
|
||||||
hasPrimary: false,
|
hasPrimary: false,
|
||||||
disabled: false,
|
|
||||||
type: "ui",
|
type: "ui",
|
||||||
username: "bar",
|
username: "bar",
|
||||||
path: "/baz",
|
path: "/baz",
|
||||||
|
|||||||
@@ -1,45 +1,56 @@
|
|||||||
import { StartSdk } from "../StartSdk"
|
import { StartSdk } from "../StartSdk"
|
||||||
import { setupManifest } from "../manifest/setupManifest"
|
import { setupManifest } from "../manifest/setupManifest"
|
||||||
|
import { VersionInfo } from "../version/VersionInfo"
|
||||||
|
import { VersionGraph } from "../version/VersionGraph"
|
||||||
|
|
||||||
export type Manifest = any
|
export type Manifest = any
|
||||||
export const sdk = StartSdk.of()
|
export const sdk = StartSdk.of()
|
||||||
.withManifest(
|
.withManifest(
|
||||||
setupManifest({
|
setupManifest(
|
||||||
id: "testOutput",
|
VersionGraph.of(
|
||||||
title: "",
|
VersionInfo.of({
|
||||||
version: "1.0:0",
|
version: "1.0.0:0",
|
||||||
releaseNotes: "",
|
releaseNotes: "",
|
||||||
license: "",
|
migrations: {},
|
||||||
replaces: [],
|
})
|
||||||
wrapperRepo: "",
|
.satisfies("#other:1.0.0:0")
|
||||||
upstreamRepo: "",
|
.satisfies("#other:2.0.0:0"),
|
||||||
supportSite: "",
|
),
|
||||||
marketingSite: "",
|
{
|
||||||
donationUrl: null,
|
id: "testOutput",
|
||||||
description: {
|
title: "",
|
||||||
short: "",
|
license: "",
|
||||||
long: "",
|
replaces: [],
|
||||||
},
|
wrapperRepo: "",
|
||||||
containers: {},
|
upstreamRepo: "",
|
||||||
images: {},
|
supportSite: "",
|
||||||
volumes: [],
|
marketingSite: "",
|
||||||
assets: [],
|
donationUrl: null,
|
||||||
alerts: {
|
description: {
|
||||||
install: null,
|
short: "",
|
||||||
update: null,
|
long: "",
|
||||||
uninstall: null,
|
},
|
||||||
restore: null,
|
containers: {},
|
||||||
start: null,
|
images: {},
|
||||||
stop: null,
|
volumes: [],
|
||||||
},
|
assets: [],
|
||||||
dependencies: {
|
alerts: {
|
||||||
"remote-test": {
|
install: null,
|
||||||
description: "",
|
update: null,
|
||||||
optional: false,
|
uninstall: null,
|
||||||
s9pk: "https://example.com/remote-test.s9pk",
|
restore: null,
|
||||||
|
start: null,
|
||||||
|
stop: null,
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
"remote-test": {
|
||||||
|
description: "",
|
||||||
|
optional: false,
|
||||||
|
s9pk: "https://example.com/remote-test.s9pk",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
),
|
||||||
)
|
)
|
||||||
.withStore<{ storeRoot: { storeLeaf: "value" } }>()
|
.withStore<{ storeRoot: { storeLeaf: "value" } }>()
|
||||||
.build(true)
|
.build(true)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user