mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
Refactor/actions (#2733)
* store, properties, manifest * interfaces * init and backups * fix init and backups * file models * more versions * dependencies * config except dynamic types * clean up config * remove disabled from non-dynamic vaues * actions * standardize example code block formats * wip: actions refactor Co-authored-by: Jade <Blu-J@users.noreply.github.com> * commit types * fix types * update types * update action request type * update apis * add description to actionrequest * clean up imports * revert package json * chore: Remove the recursive to the index * chore: Remove the other thing I was testing * flatten action requests * update container runtime with new config paradigm * new actions strategy * seems to be working * misc backend fixes * fix fe bugs * only show breakages if breakages * only show success modal if result * don't panic on failed removal * hide config from actions page * polyfill autoconfig * use metadata strategy for actions instead of prev * misc fixes * chore: split the sdk into 2 libs (#2736) * follow sideload progress (#2718) * follow sideload progress * small bugfix * shareReplay with no refcount false * don't wrap sideload progress in RPCResult * dont present toast --------- Co-authored-by: Aiden McClelland <me@drbonez.dev> * chore: Add the initial of the creation of the two sdk * chore: Add in the baseDist * chore: Add in the baseDist * chore: Get the web and the runtime-container running * chore: Remove the empty file * chore: Fix it so the container-runtime works --------- Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com> Co-authored-by: Aiden McClelland <me@drbonez.dev> * misc fixes * update todos * minor clean up * fix link script * update node version in CI test * fix node version syntax in ci build * wip: fixing callbacks * fix sdk makefile dependencies * add support for const outside of main * update apis * don't panic! * Chore: Capture weird case on rpc, and log that * fix procedure id issue * pass input value for dep auto config * handle disabled and warning for actions * chore: Fix for link not having node_modules * sdk fixes * fix build * fix build * fix build --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> Co-authored-by: Jade <Blu-J@users.noreply.github.com> Co-authored-by: J H <dragondef@gmail.com> Co-authored-by: Jade <2364004+Blu-J@users.noreply.github.com> Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
This commit is contained in:
3
.github/workflows/startos-iso.yaml
vendored
3
.github/workflows/startos-iso.yaml
vendored
@@ -187,11 +187,14 @@ jobs:
|
||||
run: |
|
||||
mkdir -p web/node_modules
|
||||
mkdir -p web/dist/raw
|
||||
mkdir -p core/startos/bindings
|
||||
mkdir -p sdk/base/lib/osBindings
|
||||
mkdir -p container-runtime/node_modules
|
||||
mkdir -p container-runtime/dist
|
||||
mkdir -p container-runtime/dist/node_modules
|
||||
mkdir -p core/startos/bindings
|
||||
mkdir -p sdk/dist
|
||||
mkdir -p sdk/baseDist
|
||||
mkdir -p patch-db/client/node_modules
|
||||
mkdir -p patch-db/client/dist
|
||||
mkdir -p web/.angular
|
||||
|
||||
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
- next/*
|
||||
|
||||
env:
|
||||
NODEJS_VERSION: "18.15.0"
|
||||
NODEJS_VERSION: "20.16.0"
|
||||
ENVIRONMENT: dev-unstable
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -27,6 +27,7 @@ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
|
||||
source ~/.bashrc
|
||||
nvm install 20
|
||||
nvm use 20
|
||||
nvm alias default 20 # this prevents your machine from reverting back to another version
|
||||
```
|
||||
|
||||
## Cloning the repository
|
||||
|
||||
29
Makefile
29
Makefile
@@ -17,7 +17,7 @@ COMPAT_SRC := $(shell git ls-files system-images/compat/)
|
||||
UTILS_SRC := $(shell git ls-files system-images/utils/)
|
||||
BINFMT_SRC := $(shell git ls-files system-images/binfmt/)
|
||||
CORE_SRC := $(shell git ls-files core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE)
|
||||
WEB_SHARED_SRC := $(shell git ls-files web/projects/shared) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules/.package-lock.json web/config.json patch-db/client/dist/index.js web/patchdb-ui-seed.json sdk/dist/package.json
|
||||
WEB_SHARED_SRC := $(shell git ls-files web/projects/shared) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules/.package-lock.json web/config.json patch-db/client/dist/index.js sdk/baseDist/package.json web/patchdb-ui-seed.json sdk/dist/package.json
|
||||
WEB_UI_SRC := $(shell git ls-files web/projects/ui)
|
||||
WEB_SETUP_WIZARD_SRC := $(shell git ls-files web/projects/setup-wizard)
|
||||
WEB_INSTALL_WIZARD_SRC := $(shell git ls-files web/projects/install-wizard)
|
||||
@@ -48,7 +48,7 @@ endif
|
||||
|
||||
.DELETE_ON_ERROR:
|
||||
|
||||
.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs sudo wormhole wormhole-deb test test-core test-sdk test-container-runtime
|
||||
.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs sudo wormhole wormhole-deb test test-core test-sdk test-container-runtime registry
|
||||
|
||||
all: $(ALL_TARGETS)
|
||||
|
||||
@@ -95,7 +95,7 @@ test: | test-core test-sdk test-container-runtime
|
||||
test-core: $(CORE_SRC) $(ENVIRONMENT_FILE)
|
||||
./core/run-tests.sh
|
||||
|
||||
test-sdk: $(shell git ls-files sdk) sdk/lib/osBindings/index.ts
|
||||
test-sdk: $(shell git ls-files sdk) sdk/base/lib/osBindings/index.ts
|
||||
cd sdk && make test
|
||||
|
||||
test-container-runtime: container-runtime/node_modules/.package-lock.json $(shell git ls-files container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json
|
||||
@@ -104,6 +104,9 @@ test-container-runtime: container-runtime/node_modules/.package-lock.json $(shel
|
||||
cli:
|
||||
cd core && ./install-cli.sh
|
||||
|
||||
registry:
|
||||
cd core && ./build-registrybox.sh
|
||||
|
||||
deb: results/$(BASENAME).deb
|
||||
|
||||
debian/control: build/lib/depends build/lib/conflicts
|
||||
@@ -223,20 +226,22 @@ container-runtime/node_modules/.package-lock.json: container-runtime/package.jso
|
||||
npm --prefix container-runtime ci
|
||||
touch container-runtime/node_modules/.package-lock.json
|
||||
|
||||
sdk/lib/osBindings/index.ts: core/startos/bindings/index.ts
|
||||
rsync -ac --delete core/startos/bindings/ sdk/lib/osBindings/
|
||||
touch sdk/lib/osBindings/index.ts
|
||||
sdk/base/lib/osBindings/index.ts: core/startos/bindings/index.ts
|
||||
mkdir -p sdk/base/lib/osBindings
|
||||
rsync -ac --delete core/startos/bindings/ sdk/base/lib/osBindings/
|
||||
touch sdk/base/lib/osBindings/index.ts
|
||||
|
||||
core/startos/bindings/index.ts: $(shell git ls-files core) $(ENVIRONMENT_FILE)
|
||||
rm -rf core/startos/bindings
|
||||
./core/build-ts.sh
|
||||
ls core/startos/bindings/*.ts | sed 's/core\/startos\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' > core/startos/bindings/index.ts
|
||||
npm --prefix sdk exec -- prettier --config ./sdk/package.json -w ./core/startos/bindings/*.ts
|
||||
ls core/startos/bindings/*.ts | sed 's/core\/startos\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' | grep -v '"./index"' | tee core/startos/bindings/index.ts
|
||||
npm --prefix sdk exec -- prettier --config ./sdk/base/package.json -w ./core/startos/bindings/*.ts
|
||||
touch core/startos/bindings/index.ts
|
||||
|
||||
sdk/dist/package.json: $(shell git ls-files sdk) sdk/lib/osBindings/index.ts
|
||||
sdk/dist/package.json sdk/baseDist/package.json: $(shell git ls-files sdk) sdk/base/lib/osBindings/index.ts
|
||||
(cd sdk && make bundle)
|
||||
touch sdk/dist/package.json
|
||||
touch sdk/baseDist/package.json
|
||||
|
||||
# TODO: make container-runtime its own makefile?
|
||||
container-runtime/dist/index.js: container-runtime/node_modules/.package-lock.json $(shell git ls-files container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json
|
||||
@@ -272,11 +277,11 @@ core/target/$(ARCH)-unknown-linux-musl/release/containerbox: $(CORE_SRC) $(ENVIR
|
||||
ARCH=$(ARCH) ./core/build-containerbox.sh
|
||||
touch core/target/$(ARCH)-unknown-linux-musl/release/containerbox
|
||||
|
||||
web/node_modules/.package-lock.json: web/package.json sdk/dist/package.json
|
||||
web/node_modules/.package-lock.json: web/package.json sdk/baseDist/package.json
|
||||
npm --prefix web ci
|
||||
touch web/node_modules/.package-lock.json
|
||||
|
||||
web/.angular/.updated: patch-db/client/dist/index.js sdk/dist/package.json web/node_modules/.package-lock.json
|
||||
web/.angular/.updated: patch-db/client/dist/index.js sdk/baseDist/package.json web/node_modules/.package-lock.json
|
||||
rm -rf web/.angular
|
||||
mkdir -p web/.angular
|
||||
touch web/.angular/.updated
|
||||
@@ -329,4 +334,4 @@ cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console:
|
||||
ARCH=$(ARCH) PREINSTALL="apk add musl-dev pkgconfig" ./build-cargo-dep.sh tokio-console
|
||||
|
||||
cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs:
|
||||
ARCH=$(ARCH) PREINSTALL="apk add fuse3 fuse3-dev fuse3-static musl-dev pkgconfig" ./build-cargo-dep.sh --git https://github.com/Start9Labs/start-fs.git startos-backup-fs
|
||||
ARCH=$(ARCH) PREINSTALL="apk add fuse3 fuse3-dev fuse3-static musl-dev pkgconfig" ./build-cargo-dep.sh --git https://github.com/Start9Labs/start-fs.git startos-backup-fs
|
||||
|
||||
46
container-runtime/package-lock.json
generated
46
container-runtime/package-lock.json
generated
@@ -20,7 +20,6 @@
|
||||
"node-fetch": "^3.1.0",
|
||||
"ts-matches": "^5.5.1",
|
||||
"tslib": "^2.5.3",
|
||||
"tslog": "^4.9.3",
|
||||
"typescript": "^5.1.3",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
@@ -36,9 +35,36 @@
|
||||
"typescript": ">5.2"
|
||||
}
|
||||
},
|
||||
"../sdk/baseDist": {
|
||||
"name": "@start9labs/start-sdk-base",
|
||||
"extraneous": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@noble/curves": "^1.4.0",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"mime": "^4.0.3",
|
||||
"ts-matches": "^5.5.1",
|
||||
"yaml": "^2.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/lodash.merge": "^4.6.2",
|
||||
"jest": "^29.4.3",
|
||||
"peggy": "^3.0.2",
|
||||
"prettier": "^3.2.5",
|
||||
"ts-jest": "^29.0.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-pegjs": "^4.2.1",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.0.4"
|
||||
}
|
||||
},
|
||||
"../sdk/dist": {
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.3.6-alpha6",
|
||||
"version": "0.3.6-alpha8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
@@ -5628,17 +5654,6 @@
|
||||
"version": "2.6.3",
|
||||
"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": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
||||
@@ -9758,11 +9773,6 @@
|
||||
"tslib": {
|
||||
"version": "2.6.3"
|
||||
},
|
||||
"tslog": {
|
||||
"version": "4.9.3",
|
||||
"resolved": "https://registry.npmjs.org/tslog/-/tslog-4.9.3.tgz",
|
||||
"integrity": "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw=="
|
||||
},
|
||||
"type-check": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
## Testing
|
||||
|
||||
So, we are going to
|
||||
|
||||
1. create a fake server
|
||||
2. pretend socket server os (while the fake server is running)
|
||||
3. Run a fake effects system (while 1/2 are running)
|
||||
|
||||
In order to simulate that we created a server like the start-os and
|
||||
a fake server (in this case I am using syncthing-wrapper)
|
||||
|
||||
### TODO
|
||||
|
||||
Undo the packing that I have done earlier, and hijack the embassy.js to use the bundle service + code
|
||||
|
||||
Converting embassy.js -> service.js
|
||||
|
||||
```sequence {theme="hand"}
|
||||
startOs ->> startInit.js: Rpc Call
|
||||
startInit.js ->> service.js: Rpc Converted into js code
|
||||
```
|
||||
|
||||
### Create a fake server
|
||||
|
||||
```bash
|
||||
run_test () {
|
||||
(
|
||||
set -e
|
||||
libs=/home/jh/Projects/start-os/libs/start_init
|
||||
sockets=/tmp/start9
|
||||
service=/home/jh/Projects/syncthing-wrapper
|
||||
|
||||
docker run \
|
||||
-v $libs:/libs \
|
||||
-v $service:/service \
|
||||
-w /libs \
|
||||
--rm node:18-alpine \
|
||||
sh -c "
|
||||
npm i &&
|
||||
npm run bundle:esbuild &&
|
||||
npm run bundle:service
|
||||
"
|
||||
|
||||
|
||||
|
||||
docker run \
|
||||
-v ./libs/start_init/:/libs \
|
||||
-w /libs \
|
||||
--rm node:18-alpine \
|
||||
sh -c "
|
||||
npm i &&
|
||||
npm run bundle:esbuild
|
||||
"
|
||||
|
||||
|
||||
|
||||
rm -rf $sockets || true
|
||||
mkdir -p $sockets/sockets
|
||||
cd $service
|
||||
docker run \
|
||||
-v $libs:/start-init \
|
||||
-v $sockets:/start9 \
|
||||
--rm -it $(docker build -q .) sh -c "
|
||||
apk add nodejs &&
|
||||
node /start-init/bundleEs.js
|
||||
"
|
||||
)
|
||||
}
|
||||
run_test
|
||||
```
|
||||
|
||||
### Pretend Socket Server OS
|
||||
|
||||
First we are going to create our fake server client with the bash then send it the json possible data
|
||||
|
||||
```bash
|
||||
sudo socat - unix-client:/tmp/start9/sockets/rpc.sock
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```json
|
||||
{"id":"a","method":"run","params":{"methodName":"/dependencyMounts","methodArgs":[]}}
|
||||
{"id":"a","method":"run","params":{"methodName":"/actions/test","methodArgs":{"input":{"id": 1}}}}
|
||||
{"id":"b","method":"run","params":{"methodName":"/actions/test","methodArgs":{"id": 1}}}
|
||||
|
||||
```
|
||||
@@ -4,7 +4,7 @@ import { object, string, number, literals, some, unknown } from "ts-matches"
|
||||
import { Effects } from "../Models/Effects"
|
||||
|
||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||
import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk"
|
||||
import { asError } from "@start9labs/start-sdk/base/lib/util"
|
||||
const matchRpcError = object({
|
||||
error: object(
|
||||
{
|
||||
@@ -35,7 +35,8 @@ let hostSystemId = 0
|
||||
|
||||
export type EffectContext = {
|
||||
procedureId: string | null
|
||||
callbacks: CallbackHolder | null
|
||||
callbacks?: CallbackHolder
|
||||
constRetry: () => void
|
||||
}
|
||||
|
||||
const rpcRoundFor =
|
||||
@@ -50,7 +51,7 @@ const rpcRoundFor =
|
||||
JSON.stringify({
|
||||
id,
|
||||
method,
|
||||
params: { ...params, procedureId },
|
||||
params: { ...params, procedureId: procedureId || undefined },
|
||||
}) + "\n",
|
||||
)
|
||||
})
|
||||
@@ -67,7 +68,7 @@ const rpcRoundFor =
|
||||
let message = res.error.message
|
||||
console.error(
|
||||
"Error in host RPC:",
|
||||
utils.asError({ method, params }),
|
||||
utils.asError({ method, params, error: res.error }),
|
||||
)
|
||||
if (string.test(res.error.data)) {
|
||||
message += ": " + res.error.data
|
||||
@@ -100,9 +101,49 @@ const rpcRoundFor =
|
||||
})
|
||||
}
|
||||
|
||||
function makeEffects(context: EffectContext): Effects {
|
||||
export function makeEffects(context: EffectContext): Effects {
|
||||
const rpcRound = rpcRoundFor(context.procedureId)
|
||||
const self: Effects = {
|
||||
constRetry: context.constRetry,
|
||||
clearCallbacks(...[options]: Parameters<T.Effects["clearCallbacks"]>) {
|
||||
return rpcRound("clear-callbacks", {
|
||||
...options,
|
||||
}) as ReturnType<T.Effects["clearCallbacks"]>
|
||||
},
|
||||
action: {
|
||||
clear(...[options]: Parameters<T.Effects["action"]["clear"]>) {
|
||||
return rpcRound("action.clear", {
|
||||
...options,
|
||||
}) as ReturnType<T.Effects["action"]["clear"]>
|
||||
},
|
||||
export(...[options]: Parameters<T.Effects["action"]["export"]>) {
|
||||
return rpcRound("action.export", {
|
||||
...options,
|
||||
}) as ReturnType<T.Effects["action"]["export"]>
|
||||
},
|
||||
getInput(...[options]: Parameters<T.Effects["action"]["getInput"]>) {
|
||||
return rpcRound("action.get-input", {
|
||||
...options,
|
||||
}) as ReturnType<T.Effects["action"]["getInput"]>
|
||||
},
|
||||
request(...[options]: Parameters<T.Effects["action"]["request"]>) {
|
||||
return rpcRound("action.request", {
|
||||
...options,
|
||||
}) as ReturnType<T.Effects["action"]["request"]>
|
||||
},
|
||||
run(...[options]: Parameters<T.Effects["action"]["run"]>) {
|
||||
return rpcRound("action.run", {
|
||||
...options,
|
||||
}) as ReturnType<T.Effects["action"]["run"]>
|
||||
},
|
||||
clearRequests(
|
||||
...[options]: Parameters<T.Effects["action"]["clearRequests"]>
|
||||
) {
|
||||
return rpcRound("action.clear-requests", {
|
||||
...options,
|
||||
}) as ReturnType<T.Effects["action"]["clearRequests"]>
|
||||
},
|
||||
},
|
||||
bind(...[options]: Parameters<T.Effects["bind"]>) {
|
||||
return rpcRound("bind", {
|
||||
...options,
|
||||
@@ -138,16 +179,6 @@ function makeEffects(context: EffectContext): Effects {
|
||||
>
|
||||
},
|
||||
},
|
||||
executeAction(...[options]: Parameters<T.Effects["executeAction"]>) {
|
||||
return rpcRound("execute-action", options) as ReturnType<
|
||||
T.Effects["executeAction"]
|
||||
>
|
||||
},
|
||||
exportAction(...[options]: Parameters<T.Effects["exportAction"]>) {
|
||||
return rpcRound("export-action", options) as ReturnType<
|
||||
T.Effects["exportAction"]
|
||||
>
|
||||
},
|
||||
exportServiceInterface: ((
|
||||
...[options]: Parameters<Effects["exportServiceInterface"]>
|
||||
) => {
|
||||
@@ -162,11 +193,6 @@ function makeEffects(context: EffectContext): Effects {
|
||||
T.Effects["exposeForDependents"]
|
||||
>
|
||||
},
|
||||
getConfigured(...[]: Parameters<T.Effects["getConfigured"]>) {
|
||||
return rpcRound("get-configured", {}) as ReturnType<
|
||||
T.Effects["getConfigured"]
|
||||
>
|
||||
},
|
||||
getContainerIp(...[]: Parameters<T.Effects["getContainerIp"]>) {
|
||||
return rpcRound("get-container-ip", {}) as ReturnType<
|
||||
T.Effects["getContainerIp"]
|
||||
@@ -230,19 +256,9 @@ function makeEffects(context: EffectContext): Effects {
|
||||
mount(...[options]: Parameters<T.Effects["mount"]>) {
|
||||
return rpcRound("mount", options) as ReturnType<T.Effects["mount"]>
|
||||
},
|
||||
clearActions(...[]: Parameters<T.Effects["clearActions"]>) {
|
||||
return rpcRound("clear-actions", {}) as ReturnType<
|
||||
T.Effects["clearActions"]
|
||||
>
|
||||
},
|
||||
restart(...[]: Parameters<T.Effects["restart"]>) {
|
||||
return rpcRound("restart", {}) as ReturnType<T.Effects["restart"]>
|
||||
},
|
||||
setConfigured(...[configured]: Parameters<T.Effects["setConfigured"]>) {
|
||||
return rpcRound("set-configured", { configured }) as ReturnType<
|
||||
T.Effects["setConfigured"]
|
||||
>
|
||||
},
|
||||
setDependencies(
|
||||
dependencies: Parameters<T.Effects["setDependencies"]>[0],
|
||||
): ReturnType<T.Effects["setDependencies"]> {
|
||||
@@ -299,18 +315,3 @@ function makeEffects(context: EffectContext): Effects {
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
export function makeProcedureEffects(procedureId: string): Effects {
|
||||
return makeEffects({ procedureId, callbacks: null })
|
||||
}
|
||||
|
||||
export function makeMainEffects(): MainEffects {
|
||||
const rpcRound = rpcRoundFor(null)
|
||||
return {
|
||||
_type: "main",
|
||||
clearCallbacks: () => {
|
||||
return rpcRound("clearCallbacks", {}) as Promise<void>
|
||||
},
|
||||
...makeEffects({ procedureId: null, callbacks: new CallbackHolder() }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,17 +14,14 @@ import {
|
||||
anyOf,
|
||||
} from "ts-matches"
|
||||
|
||||
import { types as T } from "@start9labs/start-sdk"
|
||||
import { types as T, utils } from "@start9labs/start-sdk"
|
||||
import * as fs from "fs"
|
||||
|
||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||
import { AllGetDependencies } from "../Interfaces/AllGetDependencies"
|
||||
import { jsonPath, unNestPath } from "../Models/JsonPath"
|
||||
import { RunningMain, System } from "../Interfaces/System"
|
||||
import {
|
||||
MakeMainEffects,
|
||||
MakeProcedureEffects,
|
||||
} from "../Interfaces/MakeEffects"
|
||||
import { System } from "../Interfaces/System"
|
||||
import { makeEffects } from "./EffectCreator"
|
||||
type MaybePromise<T> = T | Promise<T>
|
||||
export const matchRpcResult = anyOf(
|
||||
object({ result: any }),
|
||||
@@ -55,33 +52,39 @@ const jsonrpc = "2.0" as const
|
||||
const isResult = object({ result: any }).test
|
||||
|
||||
const idType = some(string, number, literal(null))
|
||||
type IdType = null | string | number
|
||||
const runType = object({
|
||||
id: idType,
|
||||
method: literal("execute"),
|
||||
params: object(
|
||||
{
|
||||
id: string,
|
||||
procedure: string,
|
||||
input: any,
|
||||
timeout: number,
|
||||
},
|
||||
["timeout"],
|
||||
),
|
||||
})
|
||||
const sandboxRunType = object({
|
||||
id: idType,
|
||||
method: literal("sandbox"),
|
||||
params: object(
|
||||
{
|
||||
id: string,
|
||||
procedure: string,
|
||||
input: any,
|
||||
timeout: number,
|
||||
},
|
||||
["timeout"],
|
||||
),
|
||||
})
|
||||
type IdType = null | string | number | undefined
|
||||
const runType = object(
|
||||
{
|
||||
id: idType,
|
||||
method: literal("execute"),
|
||||
params: object(
|
||||
{
|
||||
id: string,
|
||||
procedure: string,
|
||||
input: any,
|
||||
timeout: number,
|
||||
},
|
||||
["timeout"],
|
||||
),
|
||||
},
|
||||
["id"],
|
||||
)
|
||||
const sandboxRunType = object(
|
||||
{
|
||||
id: idType,
|
||||
method: literal("sandbox"),
|
||||
params: object(
|
||||
{
|
||||
id: string,
|
||||
procedure: string,
|
||||
input: any,
|
||||
timeout: number,
|
||||
},
|
||||
["timeout"],
|
||||
),
|
||||
},
|
||||
["id"],
|
||||
)
|
||||
const callbackType = object({
|
||||
method: literal("callback"),
|
||||
params: object({
|
||||
@@ -89,29 +92,44 @@ const callbackType = object({
|
||||
args: array,
|
||||
}),
|
||||
})
|
||||
const initType = object({
|
||||
id: idType,
|
||||
method: literal("init"),
|
||||
})
|
||||
const startType = object({
|
||||
id: idType,
|
||||
method: literal("start"),
|
||||
})
|
||||
const stopType = object({
|
||||
id: idType,
|
||||
method: literal("stop"),
|
||||
})
|
||||
const exitType = object({
|
||||
id: idType,
|
||||
method: literal("exit"),
|
||||
})
|
||||
const evalType = object({
|
||||
id: idType,
|
||||
method: literal("eval"),
|
||||
params: object({
|
||||
script: string,
|
||||
}),
|
||||
})
|
||||
const initType = object(
|
||||
{
|
||||
id: idType,
|
||||
method: literal("init"),
|
||||
},
|
||||
["id"],
|
||||
)
|
||||
const startType = object(
|
||||
{
|
||||
id: idType,
|
||||
method: literal("start"),
|
||||
},
|
||||
["id"],
|
||||
)
|
||||
const stopType = object(
|
||||
{
|
||||
id: idType,
|
||||
method: literal("stop"),
|
||||
},
|
||||
["id"],
|
||||
)
|
||||
const exitType = object(
|
||||
{
|
||||
id: idType,
|
||||
method: literal("exit"),
|
||||
},
|
||||
["id"],
|
||||
)
|
||||
const evalType = object(
|
||||
{
|
||||
id: idType,
|
||||
method: literal("eval"),
|
||||
params: object({
|
||||
script: string,
|
||||
}),
|
||||
},
|
||||
["id"],
|
||||
)
|
||||
|
||||
const jsonParse = (x: string) => JSON.parse(x)
|
||||
|
||||
@@ -144,8 +162,7 @@ const hasId = object({ id: idType }).test
|
||||
export class RpcListener {
|
||||
unixSocketServer = net.createServer(async (server) => {})
|
||||
private _system: System | undefined
|
||||
private _makeProcedureEffects: MakeProcedureEffects | undefined
|
||||
private _makeMainEffects: MakeMainEffects | undefined
|
||||
private callbacks: CallbackHolder | undefined
|
||||
|
||||
constructor(readonly getDependencies: AllGetDependencies) {
|
||||
if (!fs.existsSync(SOCKET_PARENT)) {
|
||||
@@ -212,18 +229,33 @@ export class RpcListener {
|
||||
return this._system
|
||||
}
|
||||
|
||||
private get makeProcedureEffects() {
|
||||
if (!this._makeProcedureEffects) {
|
||||
this._makeProcedureEffects = this.getDependencies.makeProcedureEffects()
|
||||
private callbackHolders: Map<string, CallbackHolder> = new Map()
|
||||
private removeCallbackHolderFor(procedure: string) {
|
||||
const prev = this.callbackHolders.get(procedure)
|
||||
if (prev) {
|
||||
this.callbackHolders.delete(procedure)
|
||||
this.callbacks?.removeChild(prev)
|
||||
}
|
||||
return this._makeProcedureEffects
|
||||
}
|
||||
private callbackHolderFor(procedure: string): CallbackHolder {
|
||||
this.removeCallbackHolderFor(procedure)
|
||||
const callbackHolder = this.callbacks!.child()
|
||||
this.callbackHolders.set(procedure, callbackHolder)
|
||||
return callbackHolder
|
||||
}
|
||||
|
||||
private get makeMainEffects() {
|
||||
if (!this._makeMainEffects) {
|
||||
this._makeMainEffects = this.getDependencies.makeMainEffects()
|
||||
callCallback(callback: number, args: any[]): void {
|
||||
if (this.callbacks) {
|
||||
this.callbacks
|
||||
.callCallback(callback, args)
|
||||
.catch((error) =>
|
||||
console.error(`callback ${callback} failed`, utils.asError(error)),
|
||||
)
|
||||
} else {
|
||||
console.warn(
|
||||
`callback ${callback} ignored because system is not initialized`,
|
||||
)
|
||||
}
|
||||
return this._makeMainEffects
|
||||
}
|
||||
|
||||
private dealWithInput(input: unknown): MaybePromise<SocketResponse> {
|
||||
@@ -231,40 +263,49 @@ export class RpcListener {
|
||||
.when(runType, async ({ id, params }) => {
|
||||
const system = this.system
|
||||
const procedure = jsonPath.unsafeCast(params.procedure)
|
||||
const effects = this.getDependencies.makeProcedureEffects()(params.id)
|
||||
const input = params.input
|
||||
const timeout = params.timeout
|
||||
const result = getResult(procedure, system, effects, timeout, input)
|
||||
const { input, timeout, id: procedureId } = params
|
||||
const result = this.getResult(
|
||||
procedure,
|
||||
system,
|
||||
procedureId,
|
||||
timeout,
|
||||
input,
|
||||
)
|
||||
|
||||
return handleRpc(id, result)
|
||||
})
|
||||
.when(sandboxRunType, async ({ id, params }) => {
|
||||
const system = this.system
|
||||
const procedure = jsonPath.unsafeCast(params.procedure)
|
||||
const effects = this.makeProcedureEffects(params.id)
|
||||
const result = getResult(
|
||||
const { input, timeout, id: procedureId } = params
|
||||
const result = this.getResult(
|
||||
procedure,
|
||||
system,
|
||||
effects,
|
||||
params.input,
|
||||
params.input,
|
||||
procedureId,
|
||||
timeout,
|
||||
input,
|
||||
)
|
||||
|
||||
return handleRpc(id, result)
|
||||
})
|
||||
.when(callbackType, async ({ params: { callback, args } }) => {
|
||||
this.system.callCallback(callback, args)
|
||||
this.callCallback(callback, args)
|
||||
return null
|
||||
})
|
||||
.when(startType, async ({ id }) => {
|
||||
const callbacks = this.callbackHolderFor("main")
|
||||
const effects = makeEffects({
|
||||
procedureId: null,
|
||||
callbacks,
|
||||
constRetry: () => {},
|
||||
})
|
||||
return handleRpc(
|
||||
id,
|
||||
this.system
|
||||
.start(this.makeMainEffects())
|
||||
.then((result) => ({ result })),
|
||||
this.system.start(effects).then((result) => ({ result })),
|
||||
)
|
||||
})
|
||||
.when(stopType, async ({ id }) => {
|
||||
this.removeCallbackHolderFor("main")
|
||||
return handleRpc(
|
||||
id,
|
||||
this.system.stop().then((result) => ({ result })),
|
||||
@@ -284,7 +325,20 @@ export class RpcListener {
|
||||
(async () => {
|
||||
if (!this._system) {
|
||||
const system = await this.getDependencies.system()
|
||||
await system.containerInit()
|
||||
this.callbacks = new CallbackHolder(
|
||||
makeEffects({
|
||||
procedureId: null,
|
||||
constRetry: () => {},
|
||||
}),
|
||||
)
|
||||
const callbacks = this.callbackHolderFor("containerInit")
|
||||
await system.containerInit(
|
||||
makeEffects({
|
||||
procedureId: null,
|
||||
callbacks,
|
||||
constRetry: () => {},
|
||||
}),
|
||||
)
|
||||
this._system = system
|
||||
}
|
||||
})().then((result) => ({ result })),
|
||||
@@ -316,17 +370,20 @@ export class RpcListener {
|
||||
})(),
|
||||
)
|
||||
})
|
||||
.when(shape({ id: idType, method: string }), ({ id, method }) => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
error: {
|
||||
code: -32601,
|
||||
message: `Method not found`,
|
||||
data: {
|
||||
details: method,
|
||||
.when(
|
||||
shape({ id: idType, method: string }, ["id"]),
|
||||
({ id, method }) => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
error: {
|
||||
code: -32601,
|
||||
message: `Method not found`,
|
||||
data: {
|
||||
details: method,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
}),
|
||||
)
|
||||
|
||||
.defaultToLazy(() => {
|
||||
console.warn(
|
||||
@@ -345,98 +402,84 @@ 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,
|
||||
)
|
||||
}
|
||||
private getResult(
|
||||
procedure: typeof jsonPath._TYPE,
|
||||
system: System,
|
||||
procedureId: string,
|
||||
timeout: number | undefined,
|
||||
input: any,
|
||||
) {
|
||||
const ensureResultTypeShape = (
|
||||
result: void | T.ActionInput | T.PropertiesReturn | T.ActionResult | null,
|
||||
): { result: any } => {
|
||||
if (isResult(result)) return result
|
||||
return { result }
|
||||
}
|
||||
})().then(ensureResultTypeShape, (error) =>
|
||||
matches(error)
|
||||
.when(
|
||||
object(
|
||||
{
|
||||
error: string,
|
||||
code: number,
|
||||
},
|
||||
["code"],
|
||||
{ code: 0 },
|
||||
),
|
||||
(error) => ({
|
||||
const callbacks = this.callbackHolderFor(procedure)
|
||||
const effects = makeEffects({
|
||||
procedureId,
|
||||
callbacks,
|
||||
constRetry: () => {},
|
||||
})
|
||||
|
||||
return (async () => {
|
||||
switch (procedure) {
|
||||
case "/backup/create":
|
||||
return system.createBackup(effects, timeout || null)
|
||||
case "/backup/restore":
|
||||
return system.restoreBackup(effects, timeout || null)
|
||||
case "/properties":
|
||||
return system.properties(effects, timeout || null)
|
||||
case "/packageInit":
|
||||
return system.packageInit(effects, timeout || null)
|
||||
case "/packageUninit":
|
||||
return system.packageUninit(
|
||||
effects,
|
||||
string.optional().unsafeCast(input),
|
||||
timeout || null,
|
||||
)
|
||||
default:
|
||||
const procedures = unNestPath(procedure)
|
||||
switch (true) {
|
||||
case procedures[1] === "actions" && procedures[3] === "getInput":
|
||||
return system.getActionInput(
|
||||
effects,
|
||||
procedures[2],
|
||||
timeout || null,
|
||||
)
|
||||
case procedures[1] === "actions" && procedures[3] === "run":
|
||||
return system.runAction(
|
||||
effects,
|
||||
procedures[2],
|
||||
input.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: error.code,
|
||||
message: error.error,
|
||||
code: 0,
|
||||
message: String(error),
|
||||
},
|
||||
}),
|
||||
)
|
||||
.defaultToLazy(() => ({
|
||||
error: {
|
||||
code: 0,
|
||||
message: String(error),
|
||||
},
|
||||
})),
|
||||
)
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
CommandOptions,
|
||||
ExecOptions,
|
||||
ExecSpawnable,
|
||||
} from "@start9labs/start-sdk/cjs/lib/util/SubContainer"
|
||||
} from "@start9labs/start-sdk/package/lib/util/SubContainer"
|
||||
export const exec = promisify(cp.exec)
|
||||
export const execFile = promisify(cp.execFile)
|
||||
|
||||
|
||||
@@ -2,11 +2,10 @@ import { polyfillEffects } from "./polyfillEffects"
|
||||
import { DockerProcedureContainer } from "./DockerProcedureContainer"
|
||||
import { SystemForEmbassy } from "."
|
||||
import { T, utils } from "@start9labs/start-sdk"
|
||||
import { Daemon } from "@start9labs/start-sdk/cjs/lib/mainFn/Daemon"
|
||||
import { Daemon } from "@start9labs/start-sdk/package/lib/mainFn/Daemon"
|
||||
import { Effects } from "../../../Models/Effects"
|
||||
import { off } from "node:process"
|
||||
import { CommandController } from "@start9labs/start-sdk/cjs/lib/mainFn/CommandController"
|
||||
import { asError } from "@start9labs/start-sdk/cjs/lib/util"
|
||||
import { CommandController } from "@start9labs/start-sdk/package/lib/mainFn/CommandController"
|
||||
|
||||
const EMBASSY_HEALTH_INTERVAL = 15 * 1000
|
||||
const EMBASSY_PROPERTIES_LOOP = 30 * 1000
|
||||
@@ -137,7 +136,7 @@ export class MainLoop {
|
||||
delete this.healthLoops
|
||||
await main?.daemon
|
||||
.stop()
|
||||
.catch((e) => console.error(`Main loop error`, utils.asError(e)))
|
||||
.catch((e: unknown) => console.error(`Main loop error`, utils.asError(e)))
|
||||
this.effects.setMainStatus({ status: "stopped" })
|
||||
if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval))
|
||||
}
|
||||
@@ -155,7 +154,7 @@ export class MainLoop {
|
||||
result: "starting",
|
||||
message: null,
|
||||
})
|
||||
.catch((e) => console.error(asError(e)))
|
||||
.catch((e) => console.error(utils.asError(e)))
|
||||
const interval = setInterval(async () => {
|
||||
const actionProcedure = value
|
||||
const timeChanged = Date.now() - start
|
||||
|
||||
@@ -2,8 +2,8 @@ import { ExtendedVersion, types as T, utils } from "@start9labs/start-sdk"
|
||||
import * as fs from "fs/promises"
|
||||
|
||||
import { polyfillEffects } from "./polyfillEffects"
|
||||
import { Duration, duration, fromDuration } from "../../../Models/Duration"
|
||||
import { System, Procedure } from "../../../Interfaces/System"
|
||||
import { fromDuration } from "../../../Models/Duration"
|
||||
import { System } from "../../../Interfaces/System"
|
||||
import { matchManifest, Manifest } from "./matchManifest"
|
||||
import * as childProcess from "node:child_process"
|
||||
import { DockerProcedureContainer } from "./DockerProcedureContainer"
|
||||
@@ -27,19 +27,12 @@ import {
|
||||
Parser,
|
||||
array,
|
||||
} from "ts-matches"
|
||||
import { JsonPath, unNestPath } from "../../../Models/JsonPath"
|
||||
import { RpcResult, matchRpcResult } from "../../RpcListener"
|
||||
import { CT } from "@start9labs/start-sdk"
|
||||
import {
|
||||
AddSslOptions,
|
||||
BindOptions,
|
||||
} from "@start9labs/start-sdk/cjs/lib/osBindings"
|
||||
import { AddSslOptions } from "@start9labs/start-sdk/base/lib/osBindings"
|
||||
import {
|
||||
BindOptionsByProtocol,
|
||||
Host,
|
||||
MultiHost,
|
||||
} from "@start9labs/start-sdk/cjs/lib/interfaces/Host"
|
||||
import { ServiceInterfaceBuilder } from "@start9labs/start-sdk/cjs/lib/interfaces/ServiceInterfaceBuilder"
|
||||
} from "@start9labs/start-sdk/base/lib/interfaces/Host"
|
||||
import { ServiceInterfaceBuilder } from "@start9labs/start-sdk/base/lib/interfaces/ServiceInterfaceBuilder"
|
||||
import { Effects } from "../../../Models/Effects"
|
||||
import {
|
||||
OldConfigSpec,
|
||||
@@ -48,18 +41,16 @@ import {
|
||||
transformNewConfigToOld,
|
||||
transformOldConfigToNew,
|
||||
} from "./transformConfigSpec"
|
||||
import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk"
|
||||
import { StorePath } from "@start9labs/start-sdk/cjs/lib/store/PathBuilder"
|
||||
import { partialDiff } from "@start9labs/start-sdk/base/lib/util"
|
||||
|
||||
type Optional<A> = A | undefined | null
|
||||
function todo(): never {
|
||||
throw new Error("Not implemented")
|
||||
}
|
||||
const execFile = promisify(childProcess.execFile)
|
||||
|
||||
const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json"
|
||||
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 utils.StorePath
|
||||
|
||||
const matchResult = object({
|
||||
result: any,
|
||||
@@ -248,50 +239,21 @@ export class SystemForEmbassy implements System {
|
||||
readonly moduleCode: Partial<U.ExpectedExports>,
|
||||
) {}
|
||||
|
||||
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(effects: Effects): Promise<void> {
|
||||
for (let depId in this.manifest.dependencies) {
|
||||
if (this.manifest.dependencies[depId].config) {
|
||||
await this.dependenciesAutoconfig(effects, depId, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async containerInit(): Promise<void> {}
|
||||
|
||||
async exit(): Promise<void> {
|
||||
if (this.currentRunning) await this.currentRunning.clean()
|
||||
delete this.currentRunning
|
||||
}
|
||||
|
||||
async start(effects: MainEffects): Promise<void> {
|
||||
async start(effects: T.Effects): Promise<void> {
|
||||
effects.constRetry = utils.once(() => effects.restart())
|
||||
if (!!this.currentRunning) return
|
||||
|
||||
this.currentRunning = await MainLoop.of(this, effects)
|
||||
@@ -308,13 +270,18 @@ export class SystemForEmbassy implements System {
|
||||
}
|
||||
}
|
||||
|
||||
async packageInit(
|
||||
effects: Effects,
|
||||
previousVersion: Optional<string>,
|
||||
timeoutMs: number | null,
|
||||
): Promise<void> {
|
||||
if (previousVersion)
|
||||
await this.migration(effects, previousVersion, timeoutMs)
|
||||
async packageInit(effects: Effects, timeoutMs: number | null): Promise<void> {
|
||||
const previousVersion = await effects.getDataVersion()
|
||||
if (previousVersion) {
|
||||
if (
|
||||
(await this.migration(effects, previousVersion, timeoutMs)).configured
|
||||
) {
|
||||
await effects.action.clearRequests({ only: ["needs-config"] })
|
||||
}
|
||||
await effects.setDataVersion({
|
||||
version: ExtendedVersion.parseEmver(this.manifest.version).toString(),
|
||||
})
|
||||
}
|
||||
await effects.setMainStatus({ status: "stopped" })
|
||||
await this.exportActions(effects)
|
||||
await this.exportNetwork(effects)
|
||||
@@ -400,10 +367,57 @@ export class SystemForEmbassy implements System {
|
||||
)
|
||||
}
|
||||
}
|
||||
async getActionInput(
|
||||
effects: Effects,
|
||||
actionId: string,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.ActionInput | null> {
|
||||
if (actionId === "config") {
|
||||
const config = await this.getConfig(effects, timeoutMs)
|
||||
return { spec: config.spec, value: config.config }
|
||||
} else {
|
||||
const oldSpec = this.manifest.actions?.[actionId]?.["input-spec"]
|
||||
if (!oldSpec) return null
|
||||
return {
|
||||
spec: transformConfigSpec(oldSpec as OldConfigSpec),
|
||||
value: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
async runAction(
|
||||
effects: Effects,
|
||||
actionId: string,
|
||||
input: unknown,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.ActionResult | null> {
|
||||
if (actionId === "config") {
|
||||
await this.setConfig(effects, input, timeoutMs)
|
||||
return null
|
||||
} else {
|
||||
return this.action(effects, actionId, input, timeoutMs)
|
||||
}
|
||||
}
|
||||
async exportActions(effects: Effects) {
|
||||
const manifest = this.manifest
|
||||
if (!manifest.actions) return
|
||||
for (const [actionId, action] of Object.entries(manifest.actions)) {
|
||||
const actions = {
|
||||
...manifest.actions,
|
||||
}
|
||||
if (manifest.config) {
|
||||
actions.config = {
|
||||
name: "Configure",
|
||||
description: "Edit the configuration of this service",
|
||||
"allowed-statuses": ["running", "stopped"],
|
||||
"input-spec": {},
|
||||
implementation: { type: "script", args: [] },
|
||||
}
|
||||
await effects.action.request({
|
||||
packageId: this.manifest.id,
|
||||
actionId: "config",
|
||||
replayId: "needs-config",
|
||||
description: "This service must be configured before it can be run",
|
||||
})
|
||||
}
|
||||
for (const [actionId, action] of Object.entries(actions)) {
|
||||
const hasRunning = !!action["allowed-statuses"].find(
|
||||
(x) => x === "running",
|
||||
)
|
||||
@@ -412,21 +426,22 @@ export class SystemForEmbassy implements System {
|
||||
)
|
||||
// prettier-ignore
|
||||
const allowedStatuses = hasRunning && hasStopped ? "any":
|
||||
hasRunning ? "onlyRunning" :
|
||||
"onlyStopped"
|
||||
await effects.exportAction({
|
||||
hasRunning ? "only-running" :
|
||||
"only-stopped"
|
||||
await effects.action.export({
|
||||
id: actionId,
|
||||
metadata: {
|
||||
name: action.name,
|
||||
description: action.description,
|
||||
warning: action.warning || null,
|
||||
input: action["input-spec"] as CT.InputSpec,
|
||||
disabled: false,
|
||||
visibility: "enabled",
|
||||
allowedStatuses,
|
||||
hasInput: !!action["input-spec"],
|
||||
group: null,
|
||||
},
|
||||
})
|
||||
}
|
||||
await effects.action.clear({ except: Object.keys(actions) })
|
||||
}
|
||||
async packageUninit(
|
||||
effects: Effects,
|
||||
@@ -483,10 +498,7 @@ export class SystemForEmbassy implements System {
|
||||
await moduleCode.restoreBackup?.(polyfillEffects(effects, this.manifest))
|
||||
}
|
||||
}
|
||||
async getConfig(
|
||||
effects: Effects,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.ConfigRes> {
|
||||
async getConfig(effects: Effects, timeoutMs: number | null) {
|
||||
return this.getConfigUncleaned(effects, timeoutMs).then(convertToNewConfig)
|
||||
}
|
||||
private async getConfigUncleaned(
|
||||
@@ -614,7 +626,7 @@ export class SystemForEmbassy implements System {
|
||||
effects: Effects,
|
||||
fromVersion: string,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.MigrationRes> {
|
||||
): Promise<{ configured: boolean }> {
|
||||
const fromEmver = ExtendedVersion.parseEmver(fromVersion)
|
||||
const currentEmver = ExtendedVersion.parseEmver(this.manifest.version)
|
||||
if (!this.manifest.migrations) return { configured: true }
|
||||
@@ -828,24 +840,44 @@ export class SystemForEmbassy implements System {
|
||||
async dependenciesAutoconfig(
|
||||
effects: Effects,
|
||||
id: string,
|
||||
input: unknown,
|
||||
timeoutMs: number | null,
|
||||
): Promise<void> {
|
||||
const oldConfig = object({ remoteConfig: any }).unsafeCast(
|
||||
input,
|
||||
).remoteConfig
|
||||
// TODO: docker
|
||||
const oldConfig = (await effects.store.get({
|
||||
packageId: id,
|
||||
path: EMBASSY_POINTER_PATH_PREFIX,
|
||||
callback: () => {
|
||||
this.dependenciesAutoconfig(effects, id, timeoutMs)
|
||||
},
|
||||
})) as U.Config
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.dependencies?.[id]?.autoConfigure
|
||||
if (!method) return
|
||||
return (await method(
|
||||
const newConfig = (await method(
|
||||
polyfillEffects(effects, this.manifest),
|
||||
oldConfig,
|
||||
JSON.parse(JSON.stringify(oldConfig)),
|
||||
).then((x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
})) as any
|
||||
const diff = partialDiff(oldConfig, newConfig)
|
||||
if (diff) {
|
||||
await effects.action.request({
|
||||
actionId: "config",
|
||||
packageId: id,
|
||||
replayId: `${id}/config`,
|
||||
description: `Configure this dependency for the needs of ${this.manifest.title}`,
|
||||
input: {
|
||||
kind: "partial",
|
||||
value: diff.diff,
|
||||
},
|
||||
when: {
|
||||
condition: "input-not-matches",
|
||||
once: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1020,9 +1052,7 @@ function extractServiceInterfaceId(manifest: Manifest, specInterface: string) {
|
||||
const serviceInterfaceId = `${specInterface}-${internalPort}`
|
||||
return serviceInterfaceId
|
||||
}
|
||||
async function convertToNewConfig(
|
||||
value: OldGetConfigRes,
|
||||
): Promise<T.ConfigRes> {
|
||||
async function convertToNewConfig(value: OldGetConfigRes) {
|
||||
const valueSpec: OldConfigSpec = matchOldConfigSpec.unsafeCast(value.spec)
|
||||
const spec = transformConfigSpec(valueSpec)
|
||||
if (!value.config) return { spec, config: null }
|
||||
|
||||
@@ -42,6 +42,7 @@ const matchAction = object(
|
||||
export const matchManifest = object(
|
||||
{
|
||||
id: string,
|
||||
title: string,
|
||||
version: string,
|
||||
main: matchDockerProcedure,
|
||||
assets: object(
|
||||
|
||||
@@ -105,12 +105,14 @@ export const polyfillEffects = (
|
||||
args?: string[] | undefined
|
||||
timeoutMillis?: number | undefined
|
||||
}): Promise<oet.ResultType<string>> {
|
||||
const commands: [string, ...string[]] = [command, ...(args || [])]
|
||||
return startSdk
|
||||
.runCommand(
|
||||
effects,
|
||||
{ id: manifest.main.image },
|
||||
[command, ...(args || [])],
|
||||
commands,
|
||||
{},
|
||||
commands.join(" "),
|
||||
)
|
||||
.then((x: any) => ({
|
||||
stderr: x.stderr.toString(),
|
||||
@@ -154,11 +156,17 @@ export const polyfillEffects = (
|
||||
path: string
|
||||
uid: string
|
||||
}): Promise<null> {
|
||||
const commands: [string, ...string[]] = [
|
||||
"chown",
|
||||
"--recursive",
|
||||
input.uid,
|
||||
`/drive/${input.path}`,
|
||||
]
|
||||
await startSdk
|
||||
.runCommand(
|
||||
effects,
|
||||
{ id: manifest.main.image },
|
||||
["chown", "--recursive", input.uid, `/drive/${input.path}`],
|
||||
commands,
|
||||
{
|
||||
mounts: [
|
||||
{
|
||||
@@ -172,6 +180,7 @@ export const polyfillEffects = (
|
||||
},
|
||||
],
|
||||
},
|
||||
commands.join(" "),
|
||||
)
|
||||
.then((x: any) => ({
|
||||
stderr: x.stderr.toString(),
|
||||
@@ -189,11 +198,17 @@ export const polyfillEffects = (
|
||||
path: string
|
||||
mode: string
|
||||
}): Promise<null> {
|
||||
const commands: [string, ...string[]] = [
|
||||
"chmod",
|
||||
"--recursive",
|
||||
input.mode,
|
||||
`/drive/${input.path}`,
|
||||
]
|
||||
await startSdk
|
||||
.runCommand(
|
||||
effects,
|
||||
{ id: manifest.main.image },
|
||||
["chmod", "--recursive", input.mode, `/drive/${input.path}`],
|
||||
commands,
|
||||
{
|
||||
mounts: [
|
||||
{
|
||||
@@ -207,6 +222,7 @@ export const polyfillEffects = (
|
||||
},
|
||||
],
|
||||
},
|
||||
commands.join(" "),
|
||||
)
|
||||
.then((x: any) => ({
|
||||
stderr: x.stderr.toString(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CT } from "@start9labs/start-sdk"
|
||||
import { IST } from "@start9labs/start-sdk"
|
||||
import {
|
||||
dictionary,
|
||||
object,
|
||||
@@ -15,9 +15,9 @@ import {
|
||||
literal,
|
||||
} from "ts-matches"
|
||||
|
||||
export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec {
|
||||
export function transformConfigSpec(oldSpec: OldConfigSpec): IST.InputSpec {
|
||||
return Object.entries(oldSpec).reduce((inputSpec, [key, oldVal]) => {
|
||||
let newVal: CT.ValueSpec
|
||||
let newVal: IST.ValueSpec
|
||||
|
||||
if (oldVal.type === "boolean") {
|
||||
newVal = {
|
||||
@@ -124,7 +124,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec {
|
||||
spec: transformConfigSpec(matchOldConfigSpec.unsafeCast(spec)),
|
||||
},
|
||||
}),
|
||||
{} as Record<string, { name: string; spec: CT.InputSpec }>,
|
||||
{} as Record<string, { name: string; spec: IST.InputSpec }>,
|
||||
),
|
||||
disabled: false,
|
||||
required: true,
|
||||
@@ -141,7 +141,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec {
|
||||
...inputSpec,
|
||||
[key]: newVal,
|
||||
}
|
||||
}, {} as CT.InputSpec)
|
||||
}, {} as IST.InputSpec)
|
||||
}
|
||||
|
||||
export function transformOldConfigToNew(
|
||||
@@ -233,10 +233,10 @@ export function transformNewConfigToOld(
|
||||
|
||||
function getListSpec(
|
||||
oldVal: OldValueSpecList,
|
||||
): CT.ValueSpecMultiselect | CT.ValueSpecList {
|
||||
): IST.ValueSpecMultiselect | IST.ValueSpecList {
|
||||
const range = Range.from(oldVal.range)
|
||||
|
||||
let partial: Omit<CT.ValueSpecList, "type" | "spec" | "default"> = {
|
||||
let partial: Omit<IST.ValueSpecList, "type" | "spec" | "default"> = {
|
||||
name: oldVal.name,
|
||||
description: oldVal.description || null,
|
||||
warning: oldVal.warning || null,
|
||||
|
||||
@@ -6,16 +6,13 @@ import { RpcResult, matchRpcResult } from "../RpcListener"
|
||||
import { duration } from "../../Models/Duration"
|
||||
import { T, utils } from "@start9labs/start-sdk"
|
||||
import { Volume } from "../../Models/Volume"
|
||||
import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk"
|
||||
import { CallbackHolder } from "../../Models/CallbackHolder"
|
||||
import { Optional } from "ts-matches/lib/parsers/interfaces"
|
||||
|
||||
export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js"
|
||||
|
||||
type RunningMain = {
|
||||
effects: MainEffects
|
||||
stop: () => Promise<void>
|
||||
callbacks: CallbackHolder
|
||||
}
|
||||
|
||||
export class SystemForStartOs implements System {
|
||||
@@ -25,23 +22,24 @@ export class SystemForStartOs implements System {
|
||||
return new SystemForStartOs(require(STARTOS_JS_LOCATION))
|
||||
}
|
||||
|
||||
constructor(readonly abi: T.ABI) {}
|
||||
containerInit(): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
constructor(readonly abi: T.ABI) {
|
||||
this
|
||||
}
|
||||
async containerInit(effects: Effects): Promise<void> {
|
||||
return void (await this.abi.containerInit({ effects }))
|
||||
}
|
||||
async packageInit(
|
||||
effects: Effects,
|
||||
previousVersion: Optional<string> = null,
|
||||
timeoutMs: number | null = null,
|
||||
): Promise<void> {
|
||||
return void (await this.abi.init({ effects }))
|
||||
return void (await this.abi.packageInit({ effects }))
|
||||
}
|
||||
async packageUninit(
|
||||
effects: Effects,
|
||||
nextVersion: Optional<string> = null,
|
||||
timeoutMs: number | null = null,
|
||||
): Promise<void> {
|
||||
return void (await this.abi.uninit({ effects, nextVersion }))
|
||||
return void (await this.abi.packageUninit({ effects, nextVersion }))
|
||||
}
|
||||
async createBackup(
|
||||
effects: T.Effects,
|
||||
@@ -49,8 +47,6 @@ export class SystemForStartOs implements System {
|
||||
): Promise<void> {
|
||||
return void (await this.abi.createBackup({
|
||||
effects,
|
||||
pathMaker: ((options) =>
|
||||
new Volume(options.volume, options.path).path) as T.PathMaker,
|
||||
}))
|
||||
}
|
||||
async restoreBackup(
|
||||
@@ -59,80 +55,38 @@ export class SystemForStartOs implements System {
|
||||
): 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(
|
||||
getActionInput(
|
||||
effects: Effects,
|
||||
id: string,
|
||||
formData: unknown,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.ActionResult> {
|
||||
const action = (await this.abi.actions({ effects }))[id]
|
||||
): Promise<T.ActionInput | null> {
|
||||
const action = this.abi.actions.get(id)
|
||||
if (!action) throw new Error(`Action ${id} not found`)
|
||||
return action.run({ effects })
|
||||
return action.getInput({ effects })
|
||||
}
|
||||
dependenciesCheck(
|
||||
runAction(
|
||||
effects: Effects,
|
||||
id: string,
|
||||
oldConfig: unknown,
|
||||
input: 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 })
|
||||
): Promise<T.ActionResult | null> {
|
||||
const action = this.abi.actions.get(id)
|
||||
if (!action) throw new Error(`Action ${id} not found`)
|
||||
return action.run({ effects, input })
|
||||
}
|
||||
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 exit(): Promise<void> {}
|
||||
|
||||
async start(effects: MainEffects): Promise<void> {
|
||||
async start(effects: Effects): Promise<void> {
|
||||
effects.constRetry = utils.once(() => effects.restart())
|
||||
if (this.runningMain) await this.stop()
|
||||
let mainOnTerm: () => Promise<void> | undefined
|
||||
const started = async (onTerm: () => Promise<void>) => {
|
||||
@@ -141,36 +95,21 @@ export class SystemForStartOs implements System {
|
||||
}
|
||||
const daemons = await (
|
||||
await this.abi.main({
|
||||
effects: effects as MainEffects,
|
||||
effects,
|
||||
started,
|
||||
})
|
||||
).build()
|
||||
this.runningMain = {
|
||||
effects,
|
||||
stop: async () => {
|
||||
if (mainOnTerm) await mainOnTerm()
|
||||
await daemons.term()
|
||||
},
|
||||
callbacks: new CallbackHolder(),
|
||||
}
|
||||
}
|
||||
|
||||
callCallback(callback: number, args: any[]): void {
|
||||
if (this.runningMain) {
|
||||
this.runningMain.callbacks
|
||||
.callCallback(callback, args)
|
||||
.catch((error) =>
|
||||
console.error(`callback ${callback} failed`, utils.asError(error)),
|
||||
)
|
||||
} else {
|
||||
console.warn(`callback ${callback} ignored because system is not running`)
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.runningMain) {
|
||||
await this.runningMain.stop()
|
||||
await this.runningMain.effects.clearCallbacks()
|
||||
this.runningMain = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { GetDependency } from "./GetDependency"
|
||||
import { System } from "./System"
|
||||
import { MakeMainEffects, MakeProcedureEffects } from "./MakeEffects"
|
||||
|
||||
export type AllGetDependencies = GetDependency<"system", Promise<System>> &
|
||||
GetDependency<"makeProcedureEffects", MakeProcedureEffects> &
|
||||
GetDependency<"makeMainEffects", MakeMainEffects>
|
||||
export type AllGetDependencies = GetDependency<"system", Promise<System>>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { Effects } from "../Models/Effects"
|
||||
import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk"
|
||||
export type MakeProcedureEffects = (procedureId: string) => Effects
|
||||
export type MakeMainEffects = () => MainEffects
|
||||
@@ -1,39 +1,27 @@
|
||||
import { types as T } from "@start9labs/start-sdk"
|
||||
import { RpcResult } from "../Adapters/RpcListener"
|
||||
import { Effects } from "../Models/Effects"
|
||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||
import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk"
|
||||
import { Optional } from "ts-matches/lib/parsers/interfaces"
|
||||
|
||||
export type Procedure =
|
||||
| "/init"
|
||||
| "/uninit"
|
||||
| "/config/set"
|
||||
| "/config/get"
|
||||
| "/packageInit"
|
||||
| "/packageUninit"
|
||||
| "/backup/create"
|
||||
| "/backup/restore"
|
||||
| "/actions/metadata"
|
||||
| "/properties"
|
||||
| `/actions/${string}/get`
|
||||
| `/actions/${string}/getInput`
|
||||
| `/actions/${string}/run`
|
||||
| `/dependencies/${string}/query`
|
||||
| `/dependencies/${string}/update`
|
||||
|
||||
export type ExecuteResult =
|
||||
| { ok: unknown }
|
||||
| { err: { code: number; message: string } }
|
||||
export type System = {
|
||||
containerInit(): Promise<void>
|
||||
containerInit(effects: T.Effects): Promise<void>
|
||||
|
||||
start(effects: MainEffects): Promise<void>
|
||||
callCallback(callback: number, args: any[]): void
|
||||
start(effects: T.Effects): Promise<void>
|
||||
stop(): Promise<void>
|
||||
|
||||
packageInit(
|
||||
effects: Effects,
|
||||
previousVersion: Optional<string>,
|
||||
timeoutMs: number | null,
|
||||
): Promise<void>
|
||||
packageInit(effects: Effects, timeoutMs: number | null): Promise<void>
|
||||
packageUninit(
|
||||
effects: Effects,
|
||||
nextVersion: Optional<string>,
|
||||
@@ -42,41 +30,21 @@ export type System = {
|
||||
|
||||
createBackup(effects: T.Effects, timeoutMs: number | null): Promise<void>
|
||||
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(
|
||||
runAction(
|
||||
effects: Effects,
|
||||
actionId: string,
|
||||
formData: unknown,
|
||||
input: unknown,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.ActionResult>
|
||||
|
||||
dependenciesCheck(
|
||||
): Promise<T.ActionResult | null>
|
||||
getActionInput(
|
||||
effects: Effects,
|
||||
id: string,
|
||||
oldConfig: unknown,
|
||||
actionId: string,
|
||||
timeoutMs: number | null,
|
||||
): Promise<any>
|
||||
dependenciesAutoconfig(
|
||||
effects: Effects,
|
||||
id: string,
|
||||
oldConfig: unknown,
|
||||
timeoutMs: number | null,
|
||||
): Promise<void>
|
||||
actionsMetadata(effects: T.Effects): Promise<T.ActionMetadata[]>
|
||||
): Promise<T.ActionInput | null>
|
||||
|
||||
exit(): Promise<void>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import { T } from "@start9labs/start-sdk"
|
||||
|
||||
const CallbackIdCell = { inc: 0 }
|
||||
|
||||
const callbackRegistry = new FinalizationRegistry(
|
||||
async (options: { cbs: Map<number, Function>; effects: T.Effects }) => {
|
||||
await options.effects.clearCallbacks({
|
||||
only: Array.from(options.cbs.keys()),
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
export class CallbackHolder {
|
||||
constructor() {}
|
||||
private inc = 0
|
||||
constructor(private effects?: T.Effects) {}
|
||||
|
||||
private callbacks = new Map<number, Function>()
|
||||
private children: WeakRef<CallbackHolder>[] = []
|
||||
private newId() {
|
||||
return this.inc++
|
||||
return CallbackIdCell.inc++
|
||||
}
|
||||
addCallback(callback?: Function) {
|
||||
if (!callback) {
|
||||
@@ -11,12 +24,38 @@ export class CallbackHolder {
|
||||
}
|
||||
const id = this.newId()
|
||||
this.callbacks.set(id, callback)
|
||||
if (this.effects)
|
||||
callbackRegistry.register(this, {
|
||||
cbs: this.callbacks,
|
||||
effects: this.effects,
|
||||
})
|
||||
return id
|
||||
}
|
||||
child(): CallbackHolder {
|
||||
const child = new CallbackHolder()
|
||||
this.children.push(new WeakRef(child))
|
||||
return child
|
||||
}
|
||||
removeChild(child: CallbackHolder) {
|
||||
this.children = this.children.filter((c) => {
|
||||
const ref = c.deref()
|
||||
return ref && ref !== child
|
||||
})
|
||||
}
|
||||
private getCallback(index: number): Function | undefined {
|
||||
let callback = this.callbacks.get(index)
|
||||
if (callback) this.callbacks.delete(index)
|
||||
else {
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
callback = this.children[i].deref()?.getCallback(index)
|
||||
if (callback) return callback
|
||||
}
|
||||
}
|
||||
return callback
|
||||
}
|
||||
callCallback(index: number, args: any[]): Promise<unknown> {
|
||||
const callback = this.callbacks.get(index)
|
||||
if (!callback) throw new Error(`Callback ${index} does not exist`)
|
||||
this.callbacks.delete(index)
|
||||
const callback = this.getCallback(index)
|
||||
if (!callback) return Promise.resolve()
|
||||
return Promise.resolve().then(() => callback(...args))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { literals, some, string } from "ts-matches"
|
||||
|
||||
type NestedPath<A extends string, B extends string> = `/${A}/${string}/${B}`
|
||||
type NestedPaths =
|
||||
| NestedPath<"actions", "run" | "get">
|
||||
| NestedPath<"dependencies", "query" | "update">
|
||||
type NestedPaths = NestedPath<"actions", "run" | "getInput">
|
||||
// prettier-ignore
|
||||
type UnNestPaths<A> =
|
||||
A extends `${infer A}/${infer B}` ? [...UnNestPaths<A>, ... UnNestPaths<B>] :
|
||||
@@ -15,21 +13,14 @@ export function unNestPath<A extends string>(a: A): UnNestPaths<A> {
|
||||
function isNestedPath(path: string): path is NestedPaths {
|
||||
const paths = path.split("/")
|
||||
if (paths.length !== 4) return false
|
||||
if (paths[1] === "actions" && (paths[3] === "run" || paths[3] === "get"))
|
||||
return true
|
||||
if (
|
||||
paths[1] === "dependencies" &&
|
||||
(paths[3] === "query" || paths[3] === "update")
|
||||
)
|
||||
if (paths[1] === "actions" && (paths[3] === "run" || paths[3] === "getInput"))
|
||||
return true
|
||||
return false
|
||||
}
|
||||
export const jsonPath = some(
|
||||
literals(
|
||||
"/init",
|
||||
"/uninit",
|
||||
"/config/set",
|
||||
"/config/get",
|
||||
"/packageInit",
|
||||
"/packageUninit",
|
||||
"/backup/create",
|
||||
"/backup/restore",
|
||||
"/actions/metadata",
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { RpcListener } from "./Adapters/RpcListener"
|
||||
import { SystemForEmbassy } from "./Adapters/Systems/SystemForEmbassy"
|
||||
import { makeMainEffects, makeProcedureEffects } from "./Adapters/EffectCreator"
|
||||
import { AllGetDependencies } from "./Interfaces/AllGetDependencies"
|
||||
import { getSystem } from "./Adapters/Systems"
|
||||
|
||||
const getDependencies: AllGetDependencies = {
|
||||
system: getSystem,
|
||||
makeProcedureEffects: () => makeProcedureEffects,
|
||||
makeMainEffects: () => makeMainEffects,
|
||||
}
|
||||
|
||||
new RpcListener(getDependencies)
|
||||
|
||||
33
core/Cargo.lock
generated
33
core/Cargo.lock
generated
@@ -667,12 +667,24 @@ version = "3.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.7.1"
|
||||
@@ -2387,6 +2399,17 @@ dependencies = [
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imbl"
|
||||
version = "2.0.3"
|
||||
@@ -3813,6 +3836,15 @@ dependencies = [
|
||||
"psl-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qrcode"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
|
||||
dependencies = [
|
||||
"image",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
@@ -5090,6 +5122,7 @@ dependencies = [
|
||||
"procfs",
|
||||
"proptest",
|
||||
"proptest-derive",
|
||||
"qrcode",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest",
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::time::Duration;
|
||||
use color_eyre::eyre::{eyre, Context, Error};
|
||||
use futures::future::BoxFuture;
|
||||
use futures::FutureExt;
|
||||
use models::ResultExt;
|
||||
use tokio::fs::File;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::task::{JoinError, JoinHandle, LocalSet};
|
||||
@@ -176,7 +177,7 @@ impl Drop for AtomicFile {
|
||||
if let Some(file) = self.file.take() {
|
||||
drop(file);
|
||||
let path = std::mem::take(&mut self.tmp_path);
|
||||
tokio::spawn(async move { tokio::fs::remove_file(path).await.unwrap() });
|
||||
tokio::spawn(async move { tokio::fs::remove_file(path).await.log_err() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::builder::TypedValueParser;
|
||||
|
||||
use crate::prelude::*;
|
||||
use rpc_toolkit::clap;
|
||||
use rpc_toolkit::clap::builder::TypedValueParser;
|
||||
|
||||
pub struct FromStrParser<T>(PhantomData<T>);
|
||||
impl<T> FromStrParser<T> {
|
||||
@@ -11,6 +11,7 @@ mod host;
|
||||
mod image;
|
||||
mod invalid_id;
|
||||
mod package;
|
||||
mod replay;
|
||||
mod service_interface;
|
||||
mod volume;
|
||||
|
||||
@@ -20,6 +21,7 @@ pub use host::HostId;
|
||||
pub use image::ImageId;
|
||||
pub use invalid_id::InvalidId;
|
||||
pub use package::{PackageId, SYSTEM_PACKAGE_ID};
|
||||
pub use replay::ReplayId;
|
||||
pub use service_interface::ServiceInterfaceId;
|
||||
pub use volume::VolumeId;
|
||||
|
||||
|
||||
45
core/models/src/id/replay.rs
Normal file
45
core/models/src/id/replay.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use std::convert::Infallible;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
use yasi::InternedString;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, TS)]
|
||||
#[ts(type = "string")]
|
||||
pub struct ReplayId(InternedString);
|
||||
impl FromStr for ReplayId {
|
||||
type Err = Infallible;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(ReplayId(InternedString::intern(s)))
|
||||
}
|
||||
}
|
||||
impl AsRef<ReplayId> for ReplayId {
|
||||
fn as_ref(&self) -> &ReplayId {
|
||||
self
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for ReplayId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", &self.0)
|
||||
}
|
||||
}
|
||||
impl AsRef<str> for ReplayId {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl AsRef<Path> for ReplayId {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for ReplayId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
Ok(ReplayId(serde::Deserialize::deserialize(deserializer)?))
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use rpc_toolkit::clap::builder::ValueParserFactory;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::Id;
|
||||
use crate::{FromStrParser, Id};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, TS)]
|
||||
#[ts(export, type = "string")]
|
||||
@@ -59,3 +61,15 @@ impl sqlx::Type<sqlx::Postgres> for ServiceInterfaceId {
|
||||
<&str as sqlx::Type<sqlx::Postgres>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
impl FromStr for ServiceInterfaceId {
|
||||
type Err = <Id as FromStr>::Err;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Id::from_str(s).map(Self)
|
||||
}
|
||||
}
|
||||
impl ValueParserFactory for ServiceInterfaceId {
|
||||
type Parser = FromStrParser<Self>;
|
||||
fn value_parser() -> Self::Parser {
|
||||
FromStrParser::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod clap;
|
||||
mod data_url;
|
||||
mod errors;
|
||||
mod id;
|
||||
@@ -5,6 +6,7 @@ mod mime;
|
||||
mod procedure_name;
|
||||
mod version;
|
||||
|
||||
pub use clap::*;
|
||||
pub use data_url::*;
|
||||
pub use errors::*;
|
||||
pub use id::*;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{ActionId, PackageId};
|
||||
use crate::ActionId;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ProcedureName {
|
||||
@@ -9,30 +9,24 @@ pub enum ProcedureName {
|
||||
CreateBackup,
|
||||
Properties,
|
||||
RestoreBackup,
|
||||
ActionMetadata,
|
||||
GetActionInput(ActionId),
|
||||
RunAction(ActionId),
|
||||
GetAction(ActionId),
|
||||
QueryDependency(PackageId),
|
||||
UpdateDependency(PackageId),
|
||||
Init,
|
||||
Uninit,
|
||||
PackageInit,
|
||||
PackageUninit,
|
||||
}
|
||||
|
||||
impl ProcedureName {
|
||||
pub fn js_function_name(&self) -> String {
|
||||
match self {
|
||||
ProcedureName::Init => "/init".to_string(),
|
||||
ProcedureName::Uninit => "/uninit".to_string(),
|
||||
ProcedureName::PackageInit => "/packageInit".to_string(),
|
||||
ProcedureName::PackageUninit => "/packageUninit".to_string(),
|
||||
ProcedureName::SetConfig => "/config/set".to_string(),
|
||||
ProcedureName::GetConfig => "/config/get".to_string(),
|
||||
ProcedureName::CreateBackup => "/backup/create".to_string(),
|
||||
ProcedureName::Properties => "/properties".to_string(),
|
||||
ProcedureName::RestoreBackup => "/backup/restore".to_string(),
|
||||
ProcedureName::ActionMetadata => "/actions/metadata".to_string(),
|
||||
ProcedureName::RunAction(id) => format!("/actions/{}/run", id),
|
||||
ProcedureName::GetAction(id) => format!("/actions/{}/get", id),
|
||||
ProcedureName::QueryDependency(id) => format!("/dependencies/{}/query", id),
|
||||
ProcedureName::UpdateDependency(id) => format!("/dependencies/{}/update", id),
|
||||
ProcedureName::GetActionInput(id) => format!("/actions/{}/getInput", id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +156,7 @@ prettytable-rs = "0.10.0"
|
||||
procfs = { version = "0.16.0", optional = true }
|
||||
proptest = "1.3.1"
|
||||
proptest-derive = "0.5.0"
|
||||
qrcode = "0.14.1"
|
||||
rand = { version = "0.8.5", features = ["std"] }
|
||||
regex = "1.10.2"
|
||||
reqwest = { version = "0.12.4", features = ["stream", "json", "socks"] }
|
||||
|
||||
@@ -1,15 +1,76 @@
|
||||
use clap::Parser;
|
||||
use std::fmt;
|
||||
|
||||
use clap::{CommandFactory, FromArgMatches, Parser};
|
||||
pub use models::ActionId;
|
||||
use models::PackageId;
|
||||
use qrcode::QrCode;
|
||||
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::RpcContext;
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::util::serde::{display_serializable, StdinDeserializable, WithIoFormat};
|
||||
use crate::util::serde::{
|
||||
display_serializable, HandlerExtSerde, StdinDeserializable, WithIoFormat,
|
||||
};
|
||||
|
||||
pub fn action_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"get-input",
|
||||
from_fn_async(get_action_input)
|
||||
.with_display_serializable()
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"run",
|
||||
from_fn_async(run_action)
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|_, res| {
|
||||
if let Some(res) = res {
|
||||
println!("{res}")
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ActionInput {
|
||||
#[ts(type = "Record<string, unknown>")]
|
||||
pub spec: Value,
|
||||
#[ts(type = "Record<string, unknown> | null")]
|
||||
pub value: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, TS, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetActionInputParams {
|
||||
pub package_id: PackageId,
|
||||
pub action_id: ActionId,
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_action_input(
|
||||
ctx: RpcContext,
|
||||
GetActionInputParams {
|
||||
package_id,
|
||||
action_id,
|
||||
}: GetActionInputParams,
|
||||
) -> Result<Option<ActionInput>, Error> {
|
||||
ctx.services
|
||||
.get(&package_id)
|
||||
.await
|
||||
.as_ref()
|
||||
.or_not_found(lazy_format!("Manager for {}", package_id))?
|
||||
.get_action_input(Guid::new(), action_id)
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "version")]
|
||||
@@ -17,6 +78,13 @@ pub enum ActionResult {
|
||||
#[serde(rename = "0")]
|
||||
V0(ActionResultV0),
|
||||
}
|
||||
impl fmt::Display for ActionResult {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::V0(res) => res.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ActionResultV0 {
|
||||
@@ -25,63 +93,111 @@ pub struct ActionResultV0 {
|
||||
pub copyable: bool,
|
||||
pub qr: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum DockerStatus {
|
||||
Running,
|
||||
Stopped,
|
||||
impl fmt::Display for ActionResultV0 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.message)?;
|
||||
if let Some(value) = &self.value {
|
||||
write!(f, ":\n{value}")?;
|
||||
if self.qr {
|
||||
use qrcode::render::unicode;
|
||||
write!(
|
||||
f,
|
||||
"\n{}",
|
||||
QrCode::new(value.as_bytes())
|
||||
.unwrap()
|
||||
.render::<unicode::Dense1x2>()
|
||||
.build()
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_action_result(params: WithIoFormat<ActionParams>, result: ActionResult) {
|
||||
pub fn display_action_result<T: Serialize>(params: WithIoFormat<T>, result: Option<ActionResult>) {
|
||||
let Some(result) = result else {
|
||||
return;
|
||||
};
|
||||
if let Some(format) = params.format {
|
||||
return display_serializable(format, result);
|
||||
}
|
||||
match result {
|
||||
ActionResult::V0(ar) => {
|
||||
println!(
|
||||
"{}: {}",
|
||||
ar.message,
|
||||
serde_json::to_string(&ar.value).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
println!("{result}")
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||
#[derive(Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub struct ActionParams {
|
||||
#[arg(id = "id")]
|
||||
#[serde(rename = "id")]
|
||||
pub struct RunActionParams {
|
||||
pub package_id: PackageId,
|
||||
pub action_id: ActionId,
|
||||
#[ts(optional, type = "any")]
|
||||
pub input: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct CliRunActionParams {
|
||||
pub package_id: PackageId,
|
||||
pub action_id: ActionId,
|
||||
#[command(flatten)]
|
||||
#[ts(type = "{ [key: string]: any } | null")]
|
||||
#[serde(default)]
|
||||
pub input: StdinDeserializable<Option<Config>>,
|
||||
pub input: StdinDeserializable<Option<Value>>,
|
||||
}
|
||||
impl From<CliRunActionParams> for RunActionParams {
|
||||
fn from(
|
||||
CliRunActionParams {
|
||||
package_id,
|
||||
action_id,
|
||||
input,
|
||||
}: CliRunActionParams,
|
||||
) -> Self {
|
||||
Self {
|
||||
package_id,
|
||||
action_id,
|
||||
input: input.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl CommandFactory for RunActionParams {
|
||||
fn command() -> clap::Command {
|
||||
CliRunActionParams::command()
|
||||
}
|
||||
fn command_for_update() -> clap::Command {
|
||||
CliRunActionParams::command_for_update()
|
||||
}
|
||||
}
|
||||
impl FromArgMatches for RunActionParams {
|
||||
fn from_arg_matches(matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
|
||||
CliRunActionParams::from_arg_matches(matches).map(Self::from)
|
||||
}
|
||||
fn from_arg_matches_mut(matches: &mut clap::ArgMatches) -> Result<Self, clap::Error> {
|
||||
CliRunActionParams::from_arg_matches_mut(matches).map(Self::from)
|
||||
}
|
||||
fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> {
|
||||
*self = CliRunActionParams::from_arg_matches(matches).map(Self::from)?;
|
||||
Ok(())
|
||||
}
|
||||
fn update_from_arg_matches_mut(
|
||||
&mut self,
|
||||
matches: &mut clap::ArgMatches,
|
||||
) -> Result<(), clap::Error> {
|
||||
*self = CliRunActionParams::from_arg_matches_mut(matches).map(Self::from)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// impl C
|
||||
|
||||
// #[command(about = "Executes an action", display(display_action_result))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn action(
|
||||
pub async fn run_action(
|
||||
ctx: RpcContext,
|
||||
ActionParams {
|
||||
RunActionParams {
|
||||
package_id,
|
||||
action_id,
|
||||
input: StdinDeserializable(input),
|
||||
}: ActionParams,
|
||||
) -> Result<ActionResult, Error> {
|
||||
input,
|
||||
}: RunActionParams,
|
||||
) -> Result<Option<ActionResult>, Error> {
|
||||
ctx.services
|
||||
.get(&package_id)
|
||||
.await
|
||||
.as_ref()
|
||||
.or_not_found(lazy_format!("Manager for {}", package_id))?
|
||||
.action(
|
||||
Guid::new(),
|
||||
action_id,
|
||||
input.map(|c| to_value(&c)).transpose()?.unwrap_or_default(),
|
||||
)
|
||||
.run_action(Guid::new(), action_id, input.unwrap_or_default())
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ impl Drop for BackupStatusGuard {
|
||||
.ser(&None)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.log_err()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use digest::generic_array::GenericArray;
|
||||
use digest::OutputSizeUser;
|
||||
use exver::Version;
|
||||
use imbl_value::InternedString;
|
||||
use models::PackageId;
|
||||
use models::{FromStrParser, PackageId};
|
||||
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
@@ -27,7 +27,6 @@ use crate::disk::mount::filesystem::{FileSystem, MountType, ReadWrite};
|
||||
use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
|
||||
use crate::disk::util::PartitionInfo;
|
||||
use crate::prelude::*;
|
||||
use crate::util::clap::FromStrParser;
|
||||
use crate::util::serde::{
|
||||
deserialize_from_str, display_serializable, serialize_display, HandlerExtSerde, WithIoFormat,
|
||||
};
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use models::PackageId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Config, ConfigSpec};
|
||||
#[allow(unused_imports)]
|
||||
use crate::prelude::*;
|
||||
use crate::status::health_check::HealthCheckId;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ConfigRes {
|
||||
pub config: Option<Config>,
|
||||
pub spec: ConfigSpec,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetResult {
|
||||
pub depends_on: BTreeMap<PackageId, BTreeSet<HealthCheckId>>,
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::eyre;
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use itertools::Itertools;
|
||||
use models::{ErrorKind, OptionExt, PackageId};
|
||||
use patch_db::value::InternedString;
|
||||
use patch_db::Value;
|
||||
use regex::Regex;
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::util::serde::{HandlerExtSerde, StdinDeserializable};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct ConfigSpec(pub IndexMap<InternedString, Value>);
|
||||
|
||||
pub mod action;
|
||||
pub mod util;
|
||||
|
||||
use util::NumRange;
|
||||
|
||||
use self::action::ConfigRes;
|
||||
|
||||
pub type Config = patch_db::value::InOMap<InternedString, Value>;
|
||||
pub trait TypeOf {
|
||||
fn type_of(&self) -> &'static str;
|
||||
}
|
||||
impl TypeOf for Value {
|
||||
fn type_of(&self) -> &'static str {
|
||||
match self {
|
||||
Value::Array(_) => "list",
|
||||
Value::Bool(_) => "boolean",
|
||||
Value::Null => "null",
|
||||
Value::Number(_) => "number",
|
||||
Value::Object(_) => "object",
|
||||
Value::String(_) => "string",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConfigurationError {
|
||||
#[error("Timeout Error")]
|
||||
TimeoutError(#[from] TimeoutError),
|
||||
#[error("No Match: {0}")]
|
||||
NoMatch(#[from] NoMatchWithPath),
|
||||
#[error("System Error: {0}")]
|
||||
SystemError(Error),
|
||||
}
|
||||
impl From<ConfigurationError> for Error {
|
||||
fn from(err: ConfigurationError) -> Self {
|
||||
let kind = match &err {
|
||||
ConfigurationError::SystemError(e) => e.kind,
|
||||
_ => crate::ErrorKind::ConfigGen,
|
||||
};
|
||||
crate::Error::new(err, kind)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, thiserror::Error)]
|
||||
#[error("Timeout Error")]
|
||||
pub struct TimeoutError;
|
||||
|
||||
#[derive(Clone, Debug, thiserror::Error)]
|
||||
pub struct NoMatchWithPath {
|
||||
pub path: Vec<InternedString>,
|
||||
pub error: MatchError,
|
||||
}
|
||||
impl NoMatchWithPath {
|
||||
pub fn new(error: MatchError) -> Self {
|
||||
NoMatchWithPath {
|
||||
path: Vec::new(),
|
||||
error,
|
||||
}
|
||||
}
|
||||
pub fn prepend(mut self, seg: InternedString) -> Self {
|
||||
self.path.push(seg);
|
||||
self
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for NoMatchWithPath {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}: {}", self.path.iter().rev().join("."), self.error)
|
||||
}
|
||||
}
|
||||
impl From<NoMatchWithPath> for Error {
|
||||
fn from(e: NoMatchWithPath) -> Self {
|
||||
ConfigurationError::from(e).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, thiserror::Error)]
|
||||
pub enum MatchError {
|
||||
#[error("String {0:?} Does Not Match Pattern {1}")]
|
||||
Pattern(Arc<String>, Regex),
|
||||
#[error("String {0:?} Is Not In Enum {1:?}")]
|
||||
Enum(Arc<String>, IndexSet<String>),
|
||||
#[error("Field Is Not Nullable")]
|
||||
NotNullable,
|
||||
#[error("Length Mismatch: expected {0}, actual: {1}")]
|
||||
LengthMismatch(NumRange<usize>, usize),
|
||||
#[error("Invalid Type: expected {0}, actual: {1}")]
|
||||
InvalidType(&'static str, &'static str),
|
||||
#[error("Number Out Of Range: expected {0}, actual: {1}")]
|
||||
OutOfRange(NumRange<f64>, f64),
|
||||
#[error("Number Is Not Integral: {0}")]
|
||||
NonIntegral(f64),
|
||||
#[error("Variant {0:?} Is Not In Union {1:?}")]
|
||||
Union(Arc<String>, IndexSet<String>),
|
||||
#[error("Variant Is Missing Tag {0:?}")]
|
||||
MissingTag(InternedString),
|
||||
#[error("Property {0:?} Of Variant {1:?} Conflicts With Union Tag")]
|
||||
PropertyMatchesUnionTag(InternedString, String),
|
||||
#[error("Name of Property {0:?} Conflicts With Map Tag Name")]
|
||||
PropertyNameMatchesMapTag(String),
|
||||
#[error("Object Key Is Invalid: {0}")]
|
||||
InvalidKey(String),
|
||||
#[error("Value In List Is Not Unique")]
|
||||
ListUniquenessViolation,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub struct ConfigParams {
|
||||
pub id: PackageId,
|
||||
}
|
||||
|
||||
// #[command(subcommands(get, set))]
|
||||
pub fn config<C: Context>() -> ParentHandler<C, ConfigParams> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"get",
|
||||
from_fn_async(get)
|
||||
.with_inherited(|ConfigParams { id }, _| id)
|
||||
.with_display_serializable()
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"set",
|
||||
set::<C>().with_inherited(|ConfigParams { id }, _| id),
|
||||
)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get(ctx: RpcContext, _: Empty, id: PackageId) -> Result<ConfigRes, Error> {
|
||||
ctx.services
|
||||
.get(&id)
|
||||
.await
|
||||
.as_ref()
|
||||
.or_not_found(lazy_format!("Manager for {id}"))?
|
||||
.get_config(Guid::new())
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetParams {
|
||||
#[arg(long = "timeout")]
|
||||
pub timeout: Option<crate::util::serde::Duration>,
|
||||
#[command(flatten)]
|
||||
#[ts(type = "{ [key: string]: any } | null")]
|
||||
pub config: StdinDeserializable<Option<Config>>,
|
||||
}
|
||||
|
||||
// #[command(
|
||||
// subcommands(self(set_impl(async, context(RpcContext))), set_dry),
|
||||
// display(display_none),
|
||||
// metadata(sync_db = true)
|
||||
// )]
|
||||
#[instrument(skip_all)]
|
||||
pub fn set<C: Context>() -> ParentHandler<C, SetParams, PackageId> {
|
||||
ParentHandler::new()
|
||||
.root_handler(
|
||||
from_fn_async(set_impl)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.with_inherited(|set_params, id| (id, set_params))
|
||||
.no_display()
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"dry",
|
||||
from_fn_async(set_dry)
|
||||
.with_inherited(|set_params, id| (id, set_params))
|
||||
.no_display()
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn set_dry(
|
||||
ctx: RpcContext,
|
||||
_: Empty,
|
||||
(
|
||||
id,
|
||||
SetParams {
|
||||
timeout,
|
||||
config: StdinDeserializable(config),
|
||||
},
|
||||
): (PackageId, SetParams),
|
||||
) -> Result<BTreeSet<PackageId>, Error> {
|
||||
let mut breakages = BTreeSet::new();
|
||||
|
||||
let procedure_id = Guid::new();
|
||||
|
||||
let db = ctx.db.peek().await;
|
||||
for dep in db
|
||||
.as_public()
|
||||
.as_package_data()
|
||||
.as_entries()?
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|(k, v)| match v.as_current_dependencies().contains_key(&id) {
|
||||
Ok(true) => Some(Ok(k)),
|
||||
Ok(false) => None,
|
||||
Err(e) => Some(Err(e)),
|
||||
},
|
||||
)
|
||||
{
|
||||
let dep_id = dep?;
|
||||
|
||||
let Some(dependent) = &*ctx.services.get(&dep_id).await else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if dependent
|
||||
.dependency_config(procedure_id.clone(), id.clone(), config.clone())
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
breakages.insert(dep_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(breakages)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ConfigureContext {
|
||||
pub timeout: Option<Duration>,
|
||||
pub config: Option<Config>,
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn set_impl(
|
||||
ctx: RpcContext,
|
||||
_: Empty,
|
||||
(
|
||||
id,
|
||||
SetParams {
|
||||
timeout,
|
||||
config: StdinDeserializable(config),
|
||||
},
|
||||
): (PackageId, SetParams),
|
||||
) -> Result<(), Error> {
|
||||
let configure_context = ConfigureContext {
|
||||
timeout: timeout.map(|t| *t),
|
||||
config,
|
||||
};
|
||||
ctx.services
|
||||
.get(&id)
|
||||
.await
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("There is no manager running for {id}"),
|
||||
ErrorKind::Unknown,
|
||||
)
|
||||
})?
|
||||
.configure(Guid::new(), configure_context)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,406 +0,0 @@
|
||||
use std::borrow::Cow;
|
||||
use std::ops::{Bound, RangeBounds, RangeInclusive};
|
||||
|
||||
use patch_db::Value;
|
||||
use rand::distributions::Distribution;
|
||||
use rand::Rng;
|
||||
|
||||
use super::Config;
|
||||
|
||||
pub const STATIC_NULL: Value = Value::Null;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CharSet(pub Vec<(RangeInclusive<char>, usize)>, usize);
|
||||
impl CharSet {
|
||||
pub fn contains(&self, c: &char) -> bool {
|
||||
self.0.iter().any(|r| r.0.contains(c))
|
||||
}
|
||||
pub fn gen<R: Rng>(&self, rng: &mut R) -> char {
|
||||
let mut idx = rng.gen_range(0..self.1);
|
||||
for r in &self.0 {
|
||||
if idx < r.1 {
|
||||
return std::convert::TryFrom::try_from(
|
||||
rand::distributions::Uniform::new_inclusive(
|
||||
u32::from(*r.0.start()),
|
||||
u32::from(*r.0.end()),
|
||||
)
|
||||
.sample(rng),
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
idx -= r.1;
|
||||
}
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
impl Default for CharSet {
|
||||
fn default() -> Self {
|
||||
CharSet(vec![('!'..='~', 94)], 94)
|
||||
}
|
||||
}
|
||||
impl<'de> serde::de::Deserialize<'de> for CharSet {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
let mut res = Vec::new();
|
||||
let mut len = 0;
|
||||
let mut a: Option<char> = None;
|
||||
let mut b: Option<char> = None;
|
||||
let mut in_range = false;
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
',' => match (a, b, in_range) {
|
||||
(Some(start), Some(end), _) => {
|
||||
if !end.is_ascii() {
|
||||
return Err(serde::de::Error::custom("Invalid Character"));
|
||||
}
|
||||
if start >= end {
|
||||
return Err(serde::de::Error::custom("Invalid Bounds"));
|
||||
}
|
||||
let l = u32::from(end) - u32::from(start) + 1;
|
||||
res.push((start..=end, l as usize));
|
||||
len += l as usize;
|
||||
a = None;
|
||||
b = None;
|
||||
in_range = false;
|
||||
}
|
||||
(Some(start), None, false) => {
|
||||
len += 1;
|
||||
res.push((start..=start, 1));
|
||||
a = None;
|
||||
}
|
||||
(Some(_), None, true) => {
|
||||
b = Some(',');
|
||||
}
|
||||
(None, None, false) => {
|
||||
a = Some(',');
|
||||
}
|
||||
_ => {
|
||||
return Err(serde::de::Error::custom("Syntax Error"));
|
||||
}
|
||||
},
|
||||
'-' => {
|
||||
if a.is_none() {
|
||||
a = Some('-');
|
||||
} else if !in_range {
|
||||
in_range = true;
|
||||
} else if b.is_none() {
|
||||
b = Some('-')
|
||||
} else {
|
||||
return Err(serde::de::Error::custom("Syntax Error"));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if a.is_none() {
|
||||
a = Some(c);
|
||||
} else if in_range && b.is_none() {
|
||||
b = Some(c);
|
||||
} else {
|
||||
return Err(serde::de::Error::custom("Syntax Error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match (a, b) {
|
||||
(Some(start), Some(end)) => {
|
||||
if !end.is_ascii() {
|
||||
return Err(serde::de::Error::custom("Invalid Character"));
|
||||
}
|
||||
if start >= end {
|
||||
return Err(serde::de::Error::custom("Invalid Bounds"));
|
||||
}
|
||||
let l = u32::from(end) - u32::from(start) + 1;
|
||||
res.push((start..=end, l as usize));
|
||||
len += l as usize;
|
||||
}
|
||||
(Some(c), None) => {
|
||||
len += 1;
|
||||
res.push((c..=c, 1));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(CharSet(res, len))
|
||||
}
|
||||
}
|
||||
impl serde::ser::Serialize for CharSet {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::ser::Serializer,
|
||||
{
|
||||
<&str>::serialize(
|
||||
&self
|
||||
.0
|
||||
.iter()
|
||||
.map(|r| match r.1 {
|
||||
1 => format!("{}", r.0.start()),
|
||||
_ => format!("{}-{}", r.0.start(), r.0.end()),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
.as_str(),
|
||||
serializer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MergeWith {
|
||||
fn merge_with(&mut self, other: &serde_json::Value);
|
||||
}
|
||||
|
||||
impl MergeWith for serde_json::Value {
|
||||
fn merge_with(&mut self, other: &serde_json::Value) {
|
||||
use serde_json::Value::Object;
|
||||
if let (Object(orig), Object(ref other)) = (self, other) {
|
||||
for (key, val) in other.into_iter() {
|
||||
match (orig.get_mut(key), val) {
|
||||
(Some(new_orig @ Object(_)), other @ Object(_)) => {
|
||||
new_orig.merge_with(other);
|
||||
}
|
||||
(None, _) => {
|
||||
orig.insert(key.clone(), val.clone());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_with_tests() {
|
||||
use serde_json::json;
|
||||
|
||||
let mut a = json!(
|
||||
{"a": 1, "c": {"d": "123"}, "i": [1,2,3], "j": {}, "k":[1,2,3], "l": "test"}
|
||||
);
|
||||
a.merge_with(
|
||||
&json!({"a":"a", "b": "b", "c":{"d":"d", "e":"e"}, "f":{"g":"g"}, "h": [1,2,3], "i":"i", "j":[1,2,3], "k":{}}),
|
||||
);
|
||||
assert_eq!(
|
||||
a,
|
||||
json!({"a": 1, "c": {"d": "123", "e":"e"}, "b":"b", "f": {"g":"g"}, "h":[1,2,3], "i":[1,2,3], "j": {}, "k":[1,2,3], "l": "test"})
|
||||
)
|
||||
}
|
||||
pub mod serde_regex {
|
||||
use regex::Regex;
|
||||
use serde::*;
|
||||
|
||||
pub fn serialize<S>(regex: &Regex, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
<&str>::serialize(®ex.as_str(), serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Regex, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Regex::new(&s).map_err(|e| de::Error::custom(e))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NumRange<T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd>(
|
||||
pub (Bound<T>, Bound<T>),
|
||||
);
|
||||
impl<T> std::ops::Deref for NumRange<T>
|
||||
where
|
||||
T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd,
|
||||
{
|
||||
type Target = (Bound<T>, Bound<T>);
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl<'de, T> serde::de::Deserialize<'de> for NumRange<T>
|
||||
where
|
||||
T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd,
|
||||
<T as std::str::FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
let mut split = s.split(",");
|
||||
let start = split
|
||||
.next()
|
||||
.map(|s| match s.get(..1) {
|
||||
Some("(") => match s.get(1..2) {
|
||||
Some("*") => Ok(Bound::Unbounded),
|
||||
_ => s[1..]
|
||||
.trim()
|
||||
.parse()
|
||||
.map(Bound::Excluded)
|
||||
.map_err(|e| serde::de::Error::custom(e)),
|
||||
},
|
||||
Some("[") => s[1..]
|
||||
.trim()
|
||||
.parse()
|
||||
.map(Bound::Included)
|
||||
.map_err(|e| serde::de::Error::custom(e)),
|
||||
_ => Err(serde::de::Error::custom(format!(
|
||||
"Could not parse left bound: {}",
|
||||
s
|
||||
))),
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap();
|
||||
let end = split
|
||||
.next()
|
||||
.map(|s| match s.get(s.len() - 1..) {
|
||||
Some(")") => match s.get(s.len() - 2..s.len() - 1) {
|
||||
Some("*") => Ok(Bound::Unbounded),
|
||||
_ => s[..s.len() - 1]
|
||||
.trim()
|
||||
.parse()
|
||||
.map(Bound::Excluded)
|
||||
.map_err(|e| serde::de::Error::custom(e)),
|
||||
},
|
||||
Some("]") => s[..s.len() - 1]
|
||||
.trim()
|
||||
.parse()
|
||||
.map(Bound::Included)
|
||||
.map_err(|e| serde::de::Error::custom(e)),
|
||||
_ => Err(serde::de::Error::custom(format!(
|
||||
"Could not parse right bound: {}",
|
||||
s
|
||||
))),
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or(Bound::Unbounded);
|
||||
|
||||
Ok(NumRange((start, end)))
|
||||
}
|
||||
}
|
||||
impl<T> std::fmt::Display for NumRange<T>
|
||||
where
|
||||
T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.start_bound() {
|
||||
Bound::Excluded(n) => write!(f, "({},", n)?,
|
||||
Bound::Included(n) => write!(f, "[{},", n)?,
|
||||
Bound::Unbounded => write!(f, "(*,")?,
|
||||
};
|
||||
match self.end_bound() {
|
||||
Bound::Excluded(n) => write!(f, "{})", n),
|
||||
Bound::Included(n) => write!(f, "{}]", n),
|
||||
Bound::Unbounded => write!(f, "*)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> serde::ser::Serialize for NumRange<T>
|
||||
where
|
||||
T: std::str::FromStr + std::fmt::Display + std::cmp::PartialOrd,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::ser::Serializer,
|
||||
{
|
||||
<&str>::serialize(&format!("{}", self).as_str(), serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum UniqueBy {
|
||||
Any(Vec<UniqueBy>),
|
||||
All(Vec<UniqueBy>),
|
||||
Exactly(String),
|
||||
NotUnique,
|
||||
}
|
||||
impl UniqueBy {
|
||||
pub fn eq(&self, lhs: &Config, rhs: &Config) -> bool {
|
||||
match self {
|
||||
UniqueBy::Any(any) => any.iter().any(|u| u.eq(lhs, rhs)),
|
||||
UniqueBy::All(all) => all.iter().all(|u| u.eq(lhs, rhs)),
|
||||
UniqueBy::Exactly(key) => lhs.get(&**key) == rhs.get(&**key),
|
||||
UniqueBy::NotUnique => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Default for UniqueBy {
|
||||
fn default() -> Self {
|
||||
UniqueBy::NotUnique
|
||||
}
|
||||
}
|
||||
impl<'de> serde::de::Deserialize<'de> for UniqueBy {
|
||||
fn deserialize<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
struct Visitor;
|
||||
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||
type Value = UniqueBy;
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(formatter, "a key, an \"any\" object, or an \"all\" object")
|
||||
}
|
||||
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
|
||||
Ok(UniqueBy::Exactly(v.to_owned()))
|
||||
}
|
||||
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Self::Value, E> {
|
||||
Ok(UniqueBy::Exactly(v))
|
||||
}
|
||||
fn visit_map<A: serde::de::MapAccess<'de>>(
|
||||
self,
|
||||
mut map: A,
|
||||
) -> Result<Self::Value, A::Error> {
|
||||
let mut variant = None;
|
||||
while let Some(key) = map.next_key::<Cow<str>>()? {
|
||||
match key.as_ref() {
|
||||
"any" => {
|
||||
return Ok(UniqueBy::Any(map.next_value()?));
|
||||
}
|
||||
"all" => {
|
||||
return Ok(UniqueBy::All(map.next_value()?));
|
||||
}
|
||||
_ => {
|
||||
variant = Some(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(serde::de::Error::unknown_variant(
|
||||
variant.unwrap_or_default().as_ref(),
|
||||
&["any", "all"],
|
||||
))
|
||||
}
|
||||
fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
|
||||
Ok(UniqueBy::NotUnique)
|
||||
}
|
||||
fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
|
||||
Ok(UniqueBy::NotUnique)
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_any(Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::Serialize for UniqueBy {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::ser::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeMap;
|
||||
|
||||
match self {
|
||||
UniqueBy::Any(any) => {
|
||||
let mut map = serializer.serialize_map(Some(1))?;
|
||||
map.serialize_key("any")?;
|
||||
map.serialize_value(any)?;
|
||||
map.end()
|
||||
}
|
||||
UniqueBy::All(all) => {
|
||||
let mut map = serializer.serialize_map(Some(1))?;
|
||||
map.serialize_key("all")?;
|
||||
map.serialize_value(all)?;
|
||||
map.end()
|
||||
}
|
||||
UniqueBy::Exactly(key) => serializer.serialize_str(key),
|
||||
UniqueBy::NotUnique => serializer.serialize_unit(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::future::Future;
|
||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::ops::Deref;
|
||||
@@ -9,8 +9,11 @@ use std::time::Duration;
|
||||
|
||||
use chrono::{TimeDelta, Utc};
|
||||
use helpers::NonDetachingJoinHandle;
|
||||
use imbl::OrdMap;
|
||||
use imbl_value::InternedString;
|
||||
use itertools::Itertools;
|
||||
use josekit::jwk::Jwk;
|
||||
use models::{ActionId, PackageId};
|
||||
use reqwest::{Client, Proxy};
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use rpc_toolkit::{CallRemote, Context, Empty};
|
||||
@@ -23,7 +26,6 @@ use crate::account::AccountInfo;
|
||||
use crate::auth::Sessions;
|
||||
use crate::context::config::ServerConfig;
|
||||
use crate::db::model::Database;
|
||||
use crate::dependencies::compute_dependency_config_errs;
|
||||
use crate::disk::OsPartitionInfo;
|
||||
use crate::init::check_time_is_synchronized;
|
||||
use crate::lxc::{ContainerId, LxcContainer, LxcManager};
|
||||
@@ -33,6 +35,7 @@ use crate::net::wifi::WpaCli;
|
||||
use crate::prelude::*;
|
||||
use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle};
|
||||
use crate::rpc_continuations::{Guid, OpenAuthedContinuations, RpcContinuations};
|
||||
use crate::service::action::update_requested_actions;
|
||||
use crate::service::effects::callbacks::ServiceCallbacks;
|
||||
use crate::service::ServiceMap;
|
||||
use crate::shutdown::Shutdown;
|
||||
@@ -100,14 +103,14 @@ impl InitRpcContextPhases {
|
||||
pub struct CleanupInitPhases {
|
||||
cleanup_sessions: PhaseProgressTrackerHandle,
|
||||
init_services: PhaseProgressTrackerHandle,
|
||||
check_dependencies: PhaseProgressTrackerHandle,
|
||||
check_requested_actions: PhaseProgressTrackerHandle,
|
||||
}
|
||||
impl CleanupInitPhases {
|
||||
pub fn new(handle: &FullProgressTracker) -> Self {
|
||||
Self {
|
||||
cleanup_sessions: handle.add_phase("Cleaning up sessions".into(), Some(1)),
|
||||
init_services: handle.add_phase("Initializing services".into(), Some(10)),
|
||||
check_dependencies: handle.add_phase("Checking dependencies".into(), Some(1)),
|
||||
check_requested_actions: handle.add_phase("Checking action requests".into(), Some(1)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,7 +312,7 @@ impl RpcContext {
|
||||
CleanupInitPhases {
|
||||
mut cleanup_sessions,
|
||||
init_services,
|
||||
mut check_dependencies,
|
||||
mut check_requested_actions,
|
||||
}: CleanupInitPhases,
|
||||
) -> Result<(), Error> {
|
||||
cleanup_sessions.start();
|
||||
@@ -366,35 +369,68 @@ impl RpcContext {
|
||||
cleanup_sessions.complete();
|
||||
|
||||
self.services.init(&self, init_services).await?;
|
||||
tracing::info!("Initialized Package Managers");
|
||||
tracing::info!("Initialized Services");
|
||||
|
||||
check_dependencies.start();
|
||||
let mut updated_current_dependents = BTreeMap::new();
|
||||
// TODO
|
||||
check_requested_actions.start();
|
||||
let peek = self.db.peek().await;
|
||||
for (package_id, package) in peek.as_public().as_package_data().as_entries()?.into_iter() {
|
||||
let package = package.clone();
|
||||
let mut current_dependencies = package.as_current_dependencies().de()?;
|
||||
compute_dependency_config_errs(self, &package_id, &mut current_dependencies)
|
||||
.await
|
||||
.log_err();
|
||||
updated_current_dependents.insert(package_id.clone(), current_dependencies);
|
||||
let mut action_input: OrdMap<PackageId, BTreeMap<ActionId, Value>> = OrdMap::new();
|
||||
let requested_actions: BTreeSet<_> = peek
|
||||
.as_public()
|
||||
.as_package_data()
|
||||
.as_entries()?
|
||||
.into_iter()
|
||||
.map(|(_, pde)| {
|
||||
Ok(pde
|
||||
.as_requested_actions()
|
||||
.as_entries()?
|
||||
.into_iter()
|
||||
.map(|(_, r)| {
|
||||
Ok::<_, Error>((
|
||||
r.as_request().as_package_id().de()?,
|
||||
r.as_request().as_action_id().de()?,
|
||||
))
|
||||
}))
|
||||
})
|
||||
.flatten_ok()
|
||||
.map(|a| a.and_then(|a| a))
|
||||
.try_collect()?;
|
||||
let procedure_id = Guid::new();
|
||||
for (package_id, action_id) in requested_actions {
|
||||
if let Some(service) = self.services.get(&package_id).await.as_ref() {
|
||||
if let Some(input) = service
|
||||
.get_action_input(procedure_id.clone(), action_id.clone())
|
||||
.await?
|
||||
.and_then(|i| i.value)
|
||||
{
|
||||
action_input
|
||||
.entry(package_id)
|
||||
.or_default()
|
||||
.insert(action_id, input);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.db
|
||||
.mutate(|v| {
|
||||
for (package_id, deps) in updated_current_dependents {
|
||||
if let Some(model) = v
|
||||
.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(&package_id)
|
||||
.map(|i| i.as_current_dependencies_mut())
|
||||
{
|
||||
model.ser(&deps)?;
|
||||
.mutate(|db| {
|
||||
for (package_id, action_input) in &action_input {
|
||||
for (action_id, input) in action_input {
|
||||
for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
|
||||
pde.as_requested_actions_mut().mutate(|requested_actions| {
|
||||
Ok(update_requested_actions(
|
||||
requested_actions,
|
||||
package_id,
|
||||
action_id,
|
||||
input,
|
||||
false,
|
||||
))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
check_dependencies.complete();
|
||||
check_requested_actions.complete();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ use chrono::{DateTime, Utc};
|
||||
use exver::VersionRange;
|
||||
use imbl_value::InternedString;
|
||||
use models::{
|
||||
ActionId, DataUrl, HealthCheckId, HostId, PackageId, ServiceInterfaceId, VersionString,
|
||||
ActionId, DataUrl, HealthCheckId, HostId, PackageId, ReplayId, ServiceInterfaceId,
|
||||
VersionString,
|
||||
};
|
||||
use patch_db::json_ptr::JsonPointer;
|
||||
use patch_db::HasModel;
|
||||
@@ -17,8 +18,8 @@ use crate::net::service_interface::ServiceInterface;
|
||||
use crate::prelude::*;
|
||||
use crate::progress::FullProgress;
|
||||
use crate::s9pk::manifest::Manifest;
|
||||
use crate::status::Status;
|
||||
use crate::util::serde::Pem;
|
||||
use crate::status::MainStatus;
|
||||
use crate::util::serde::{is_partial_of, Pem};
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, TS)]
|
||||
#[ts(export)]
|
||||
@@ -310,9 +311,9 @@ pub struct InstallingInfo {
|
||||
}
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum AllowedStatuses {
|
||||
OnlyRunning, // onlyRunning
|
||||
OnlyRunning,
|
||||
OnlyStopped,
|
||||
Any,
|
||||
}
|
||||
@@ -324,13 +325,28 @@ pub struct ActionMetadata {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub warning: Option<String>,
|
||||
#[ts(type = "any")]
|
||||
pub input: Value,
|
||||
pub disabled: bool,
|
||||
#[serde(default)]
|
||||
pub visibility: ActionVisibility,
|
||||
pub allowed_statuses: AllowedStatuses,
|
||||
pub has_input: bool,
|
||||
pub group: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[serde(rename_all_fields = "camelCase")]
|
||||
pub enum ActionVisibility {
|
||||
Hidden,
|
||||
Disabled { reason: String },
|
||||
Enabled,
|
||||
}
|
||||
impl Default for ActionVisibility {
|
||||
fn default() -> Self {
|
||||
Self::Enabled
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
@@ -338,7 +354,7 @@ pub struct ActionMetadata {
|
||||
pub struct PackageDataEntry {
|
||||
pub state_info: PackageState,
|
||||
pub data_version: Option<VersionString>,
|
||||
pub status: Status,
|
||||
pub status: MainStatus,
|
||||
#[ts(type = "string | null")]
|
||||
pub registry: Option<Url>,
|
||||
#[ts(type = "string")]
|
||||
@@ -348,6 +364,8 @@ pub struct PackageDataEntry {
|
||||
pub last_backup: Option<DateTime<Utc>>,
|
||||
pub current_dependencies: CurrentDependencies,
|
||||
pub actions: BTreeMap<ActionId, ActionMetadata>,
|
||||
#[ts(as = "BTreeMap::<String, ActionRequestEntry>")]
|
||||
pub requested_actions: BTreeMap<ReplayId, ActionRequestEntry>,
|
||||
pub service_interfaces: BTreeMap<ServiceInterfaceId, ServiceInterface>,
|
||||
pub hosts: Hosts,
|
||||
#[ts(type = "string[]")]
|
||||
@@ -384,8 +402,9 @@ impl Map for CurrentDependencies {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS, HasModel)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct CurrentDependencyInfo {
|
||||
#[ts(type = "string | null")]
|
||||
pub title: Option<InternedString>,
|
||||
@@ -394,11 +413,10 @@ pub struct CurrentDependencyInfo {
|
||||
pub kind: CurrentDependencyKind,
|
||||
#[ts(type = "string")]
|
||||
pub version_range: VersionRange,
|
||||
pub config_satisfied: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum CurrentDependencyKind {
|
||||
Exists,
|
||||
@@ -410,6 +428,66 @@ pub enum CurrentDependencyKind {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS, HasModel)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct ActionRequestEntry {
|
||||
pub request: ActionRequest,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS, HasModel)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct ActionRequest {
|
||||
pub package_id: PackageId,
|
||||
pub action_id: ActionId,
|
||||
#[ts(optional)]
|
||||
pub description: Option<String>,
|
||||
#[ts(optional)]
|
||||
pub when: Option<ActionRequestTrigger>,
|
||||
#[ts(optional)]
|
||||
pub input: Option<ActionRequestInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct ActionRequestTrigger {
|
||||
#[serde(default)]
|
||||
pub once: bool,
|
||||
pub condition: ActionRequestCondition,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[ts(export)]
|
||||
pub enum ActionRequestCondition {
|
||||
InputNotMatches,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum ActionRequestInput {
|
||||
Partial {
|
||||
#[ts(type = "Record<string, unknown>")]
|
||||
value: Value,
|
||||
},
|
||||
}
|
||||
impl ActionRequestInput {
|
||||
pub fn matches(&self, input: Option<&Value>) -> bool {
|
||||
match self {
|
||||
Self::Partial { value } => match input {
|
||||
None => false,
|
||||
Some(full) => is_partial_of(value, full),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
pub struct InterfaceAddressMap(pub BTreeMap<HostId, InterfaceAddresses>);
|
||||
impl Map for InterfaceAddressMap {
|
||||
|
||||
@@ -1,28 +1,14 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use imbl_value::InternedString;
|
||||
use models::PackageId;
|
||||
use patch_db::json_patch::merge;
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::config::{Config, ConfigSpec, ConfigureContext};
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::db::model::package::CurrentDependencies;
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::util::serde::HandlerExtSerde;
|
||||
use crate::util::PathOrUrl;
|
||||
use crate::Error;
|
||||
|
||||
pub fn dependency<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new().subcommand("configure", configure::<C>())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
@@ -56,129 +42,3 @@ pub struct DependencyMetadata {
|
||||
#[ts(type = "string")]
|
||||
pub title: InternedString,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub struct ConfigureParams {
|
||||
dependent_id: PackageId,
|
||||
dependency_id: PackageId,
|
||||
}
|
||||
pub fn configure<C: Context>() -> ParentHandler<C, ConfigureParams> {
|
||||
ParentHandler::new()
|
||||
.root_handler(
|
||||
from_fn_async(configure_impl)
|
||||
.with_inherited(|params, _| params)
|
||||
.no_display()
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"dry",
|
||||
from_fn_async(configure_dry)
|
||||
.with_inherited(|params, _| params)
|
||||
.with_display_serializable()
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn configure_impl(
|
||||
ctx: RpcContext,
|
||||
_: Empty,
|
||||
ConfigureParams {
|
||||
dependent_id,
|
||||
dependency_id,
|
||||
}: ConfigureParams,
|
||||
) -> Result<(), Error> {
|
||||
let ConfigDryRes {
|
||||
old_config: _,
|
||||
new_config,
|
||||
spec: _,
|
||||
} = configure_logic(ctx.clone(), (dependent_id, dependency_id.clone())).await?;
|
||||
|
||||
let configure_context = ConfigureContext {
|
||||
timeout: Some(Duration::from_secs(3).into()),
|
||||
config: Some(new_config),
|
||||
};
|
||||
ctx.services
|
||||
.get(&dependency_id)
|
||||
.await
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("There is no manager running for {dependency_id}"),
|
||||
ErrorKind::Unknown,
|
||||
)
|
||||
})?
|
||||
.configure(Guid::new(), configure_context)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn configure_dry(
|
||||
ctx: RpcContext,
|
||||
_: Empty,
|
||||
ConfigureParams {
|
||||
dependent_id,
|
||||
dependency_id,
|
||||
}: ConfigureParams,
|
||||
) -> Result<ConfigDryRes, Error> {
|
||||
configure_logic(ctx.clone(), (dependent_id, dependency_id.clone())).await
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ConfigDryRes {
|
||||
pub old_config: Config,
|
||||
pub new_config: Config,
|
||||
pub spec: ConfigSpec,
|
||||
}
|
||||
|
||||
pub async fn configure_logic(
|
||||
ctx: RpcContext,
|
||||
(dependent_id, dependency_id): (PackageId, PackageId),
|
||||
) -> Result<ConfigDryRes, Error> {
|
||||
let procedure_id = Guid::new();
|
||||
let dependency_guard = ctx.services.get(&dependency_id).await;
|
||||
let dependency = dependency_guard.as_ref().or_not_found(&dependency_id)?;
|
||||
let dependent_guard = ctx.services.get(&dependent_id).await;
|
||||
let dependent = dependent_guard.as_ref().or_not_found(&dependent_id)?;
|
||||
let config_res = dependency.get_config(procedure_id.clone()).await?;
|
||||
let diff = Value::Object(
|
||||
dependent
|
||||
.dependency_config(procedure_id, dependency_id, config_res.config.clone())
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
let mut new_config = Value::Object(config_res.config.clone().unwrap_or_default());
|
||||
merge(&mut new_config, &diff);
|
||||
Ok(ConfigDryRes {
|
||||
old_config: config_res.config.unwrap_or_default(),
|
||||
new_config: new_config.as_object().cloned().unwrap_or_default(),
|
||||
spec: config_res.spec,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn compute_dependency_config_errs(
|
||||
ctx: &RpcContext,
|
||||
id: &PackageId,
|
||||
current_dependencies: &mut CurrentDependencies,
|
||||
) -> Result<(), Error> {
|
||||
let procedure_id = Guid::new();
|
||||
let service_guard = ctx.services.get(id).await;
|
||||
let service = service_guard.as_ref().or_not_found(id)?;
|
||||
for (dep_id, dep_info) in current_dependencies.0.iter_mut() {
|
||||
// check if config passes dependency check
|
||||
let Some(dependency) = &*ctx.services.get(dep_id).await else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let dep_config = dependency.get_config(procedure_id.clone()).await?.config;
|
||||
|
||||
dep_info.config_satisfied = service
|
||||
.dependency_config(procedure_id.clone(), dep_id.clone(), dep_config)
|
||||
.await?
|
||||
.is_none();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -219,10 +219,10 @@ impl<G: GenericMountGuard> Drop for BackupMountGuard<G> {
|
||||
let second = self.backup_disk_mount_guard.take();
|
||||
tokio::spawn(async move {
|
||||
if let Some(guard) = first {
|
||||
guard.unmount().await.unwrap();
|
||||
guard.unmount().await.log_err();
|
||||
}
|
||||
if let Some(guard) = second {
|
||||
guard.unmount().await.unwrap();
|
||||
guard.unmount().await.log_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -151,12 +151,12 @@ impl<G: GenericMountGuard> Drop for OverlayGuard<G> {
|
||||
let guard = self.inner_guard.take();
|
||||
if lower.is_some() || upper.is_some() || guard.mounted {
|
||||
tokio::spawn(async move {
|
||||
guard.unmount(false).await.unwrap();
|
||||
guard.unmount(false).await.log_err();
|
||||
if let Some(lower) = lower {
|
||||
lower.unmount().await.unwrap();
|
||||
lower.unmount().await.log_err();
|
||||
}
|
||||
if let Some(upper) = upper {
|
||||
upper.delete().await.unwrap();
|
||||
upper.delete().await.log_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ impl Drop for MountGuard {
|
||||
fn drop(&mut self) {
|
||||
if self.mounted {
|
||||
let mountpoint = std::mem::take(&mut self.mountpoint);
|
||||
tokio::spawn(async move { unmount(mountpoint, true).await.unwrap() });
|
||||
tokio::spawn(async move { unmount(mountpoint, true).await.log_err() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use exver::VersionRange;
|
||||
use futures::{AsyncWriteExt, StreamExt};
|
||||
use imbl_value::{json, InternedString};
|
||||
use itertools::Itertools;
|
||||
use models::VersionString;
|
||||
use models::{FromStrParser, VersionString};
|
||||
use reqwest::header::{HeaderMap, CONTENT_LENGTH};
|
||||
use reqwest::Url;
|
||||
use rpc_toolkit::yajrc::{GenericRpcMethod, RpcError};
|
||||
@@ -30,7 +30,6 @@ use crate::registry::package::get::GetPackageResponse;
|
||||
use crate::rpc_continuations::{Guid, RpcContinuation};
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::upload::upload;
|
||||
use crate::util::clap::FromStrParser;
|
||||
use crate::util::io::open_file;
|
||||
use crate::util::net::WebSocketExt;
|
||||
use crate::util::Never;
|
||||
|
||||
@@ -29,7 +29,6 @@ pub mod action;
|
||||
pub mod auth;
|
||||
pub mod backup;
|
||||
pub mod bins;
|
||||
pub mod config;
|
||||
pub mod context;
|
||||
pub mod control;
|
||||
pub mod db;
|
||||
@@ -70,7 +69,6 @@ pub mod volume;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use clap::Parser;
|
||||
pub use config::Config;
|
||||
pub use error::{Error, ErrorKind, ResultExt};
|
||||
use imbl_value::Value;
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
@@ -240,15 +238,7 @@ pub fn server<C: Context>() -> ParentHandler<C> {
|
||||
|
||||
pub fn package<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"action",
|
||||
from_fn_async(action::action)
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|handle, result| {
|
||||
Ok(action::display_action_result(handle.params, result))
|
||||
})
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand("action", action::action_api::<C>())
|
||||
.subcommand(
|
||||
"install",
|
||||
from_fn_async(install::install)
|
||||
@@ -281,7 +271,6 @@ pub fn package<C: Context>() -> ParentHandler<C> {
|
||||
.with_display_serializable()
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand("config", config::config::<C>())
|
||||
.subcommand(
|
||||
"start",
|
||||
from_fn_async(control::start)
|
||||
@@ -316,7 +305,6 @@ pub fn package<C: Context>() -> ParentHandler<C> {
|
||||
})
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand("dependency", dependencies::dependency::<C>())
|
||||
.subcommand("backup", backup::package_backup::<C>())
|
||||
.subcommand("connect", from_fn_async(service::connect_rpc).no_cli())
|
||||
.subcommand(
|
||||
|
||||
@@ -12,7 +12,7 @@ use color_eyre::eyre::eyre;
|
||||
use futures::stream::BoxStream;
|
||||
use futures::{Future, FutureExt, Stream, StreamExt, TryStreamExt};
|
||||
use itertools::Itertools;
|
||||
use models::PackageId;
|
||||
use models::{FromStrParser, PackageId};
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use rpc_toolkit::{
|
||||
from_fn_async, CallRemote, Context, Empty, HandlerArgs, HandlerExt, HandlerFor, ParentHandler,
|
||||
@@ -30,7 +30,6 @@ use crate::error::ResultExt;
|
||||
use crate::lxc::ContainerId;
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations};
|
||||
use crate::util::clap::FromStrParser;
|
||||
use crate::util::serde::Reversible;
|
||||
use crate::util::Invoke;
|
||||
|
||||
@@ -487,14 +486,18 @@ fn logs_follow<
|
||||
context,
|
||||
inherited_params:
|
||||
LogsParams {
|
||||
extra, limit, boot, ..
|
||||
extra,
|
||||
cursor,
|
||||
limit,
|
||||
boot,
|
||||
..
|
||||
},
|
||||
..
|
||||
}: HandlerArgs<C, Empty, LogsParams<Extra>>| {
|
||||
let f = f.clone();
|
||||
async move {
|
||||
let src = f.call(&context, extra).await?;
|
||||
follow_logs(context, src, limit, boot.map(String::from)).await
|
||||
follow_logs(context, src, cursor, limit, boot.map(String::from)).await
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -525,7 +528,7 @@ pub fn package_logs() -> ParentHandler<RpcContext, LogsParams<PackageIdParams>>
|
||||
|
||||
pub async fn journalctl(
|
||||
id: LogSource,
|
||||
limit: usize,
|
||||
limit: Option<usize>,
|
||||
cursor: Option<&str>,
|
||||
boot: Option<&str>,
|
||||
before: bool,
|
||||
@@ -533,11 +536,12 @@ pub async fn journalctl(
|
||||
) -> Result<LogStream, Error> {
|
||||
let mut cmd = gen_journalctl_command(&id);
|
||||
|
||||
cmd.arg(format!("--lines={}", limit));
|
||||
if let Some(limit) = limit {
|
||||
cmd.arg(format!("--lines={}", limit));
|
||||
}
|
||||
|
||||
let cursor_formatted = format!("--after-cursor={}", cursor.unwrap_or(""));
|
||||
if cursor.is_some() {
|
||||
cmd.arg(&cursor_formatted);
|
||||
if let Some(cursor) = cursor {
|
||||
cmd.arg(&format!("--after-cursor={}", cursor));
|
||||
if before {
|
||||
cmd.arg("--reverse");
|
||||
}
|
||||
@@ -638,8 +642,15 @@ pub async fn fetch_logs(
|
||||
before: bool,
|
||||
) -> Result<LogResponse, Error> {
|
||||
let limit = limit.unwrap_or(50);
|
||||
let mut stream =
|
||||
journalctl(id, limit, cursor.as_deref(), boot.as_deref(), before, false).await?;
|
||||
let mut stream = journalctl(
|
||||
id,
|
||||
Some(limit),
|
||||
cursor.as_deref(),
|
||||
boot.as_deref(),
|
||||
before,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut entries = Vec::with_capacity(limit);
|
||||
let mut start_cursor = None;
|
||||
@@ -682,11 +693,16 @@ pub async fn fetch_logs(
|
||||
pub async fn follow_logs<Context: AsRef<RpcContinuations>>(
|
||||
ctx: Context,
|
||||
id: LogSource,
|
||||
cursor: Option<String>,
|
||||
limit: Option<usize>,
|
||||
boot: Option<String>,
|
||||
) -> Result<LogFollowResponse, Error> {
|
||||
let limit = limit.unwrap_or(50);
|
||||
let mut stream = journalctl(id, limit, None, boot.as_deref(), false, true).await?;
|
||||
let limit = if cursor.is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(limit.unwrap_or(50))
|
||||
};
|
||||
let mut stream = journalctl(id, limit, cursor.as_deref(), boot.as_deref(), false, true).await?;
|
||||
|
||||
let mut start_cursor = None;
|
||||
let mut first_entry = None;
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::time::Duration;
|
||||
use clap::builder::ValueParserFactory;
|
||||
use futures::{AsyncWriteExt, StreamExt};
|
||||
use imbl_value::{InOMap, InternedString};
|
||||
use models::InvalidId;
|
||||
use models::{FromStrParser, InvalidId};
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use rpc_toolkit::{GenericRpcMethod, RpcRequest, RpcResponse};
|
||||
use rustyline_async::{ReadlineEvent, SharedWriter};
|
||||
@@ -28,7 +28,6 @@ use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard};
|
||||
use crate::disk::mount::util::unmount;
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::{Guid, RpcContinuation};
|
||||
use crate::util::clap::FromStrParser;
|
||||
use crate::util::io::open_file;
|
||||
use crate::util::rpc_client::UnixRpcClient;
|
||||
use crate::util::{new_guid, Invoke};
|
||||
@@ -365,7 +364,7 @@ impl Drop for LxcContainer {
|
||||
tracing::error!("Error reading logs from crashed container: {e}");
|
||||
tracing::debug!("{e:?}")
|
||||
}
|
||||
rootfs.unmount(true).await.unwrap();
|
||||
rootfs.unmount(true).await.log_err();
|
||||
drop(guid);
|
||||
if let Err(e) = manager.gc().await {
|
||||
tracing::error!("Error cleaning up dangling LXC containers: {e}");
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::builder::ValueParserFactory;
|
||||
use models::{FromStrParser, HostId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
@@ -5,10 +9,37 @@ use crate::net::forward::AvailablePorts;
|
||||
use crate::net::vhost::AlpnInfo;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BindId {
|
||||
pub id: HostId,
|
||||
pub internal_port: u16,
|
||||
}
|
||||
impl ValueParserFactory for BindId {
|
||||
type Parser = FromStrParser<Self>;
|
||||
fn value_parser() -> Self::Parser {
|
||||
FromStrParser::new()
|
||||
}
|
||||
}
|
||||
impl FromStr for BindId {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let (id, port) = s
|
||||
.split_once(":")
|
||||
.ok_or_else(|| Error::new(eyre!("expected <id>:<port>"), ErrorKind::ParseUrl))?;
|
||||
Ok(Self {
|
||||
id: id.parse()?,
|
||||
internal_port: port.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct BindInfo {
|
||||
pub enabled: bool,
|
||||
pub options: BindOptions,
|
||||
pub lan: LanInfo,
|
||||
}
|
||||
@@ -30,6 +61,7 @@ impl BindInfo {
|
||||
assigned_ssl_port = Some(available_ports.alloc()?);
|
||||
}
|
||||
Ok(Self {
|
||||
enabled: true,
|
||||
options,
|
||||
lan: LanInfo {
|
||||
assigned_port,
|
||||
@@ -69,7 +101,14 @@ impl BindInfo {
|
||||
available_ports.free([port]);
|
||||
}
|
||||
}
|
||||
Ok(Self { options, lan })
|
||||
Ok(Self {
|
||||
enabled: true,
|
||||
options,
|
||||
lan,
|
||||
})
|
||||
}
|
||||
pub fn disable(&mut self) {
|
||||
self.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ use crate::hostname::Hostname;
|
||||
use crate::net::dns::DnsController;
|
||||
use crate::net::forward::LanPortForwardController;
|
||||
use crate::net::host::address::HostAddress;
|
||||
use crate::net::host::binding::{AddSslOptions, BindOptions, LanInfo};
|
||||
use crate::net::host::{host_for, Host, HostKind};
|
||||
use crate::net::host::binding::{AddSslOptions, BindId, BindOptions, LanInfo};
|
||||
use crate::net::host::{host_for, Host, HostKind, Hosts};
|
||||
use crate::net::service_interface::{HostnameInfo, IpHostname, OnionHostname};
|
||||
use crate::net::tor::TorController;
|
||||
use crate::net::vhost::{AlpnInfo, VHostController};
|
||||
@@ -154,14 +154,16 @@ impl NetController {
|
||||
) -> Result<NetService, Error> {
|
||||
let dns = self.dns.add(Some(package.clone()), ip).await?;
|
||||
|
||||
Ok(NetService {
|
||||
let mut res = NetService {
|
||||
shutdown: false,
|
||||
id: package,
|
||||
ip,
|
||||
dns,
|
||||
controller: Arc::downgrade(self),
|
||||
binds: BTreeMap::new(),
|
||||
})
|
||||
};
|
||||
res.clear_bindings(Default::default()).await?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,31 +223,41 @@ impl NetService {
|
||||
self.update(id, host).await
|
||||
}
|
||||
|
||||
pub async fn clear_bindings(&mut self) -> Result<(), Error> {
|
||||
let ctrl = self.net_controller()?;
|
||||
pub async fn clear_bindings(&mut self, except: BTreeSet<BindId>) -> Result<(), Error> {
|
||||
let pkg_id = &self.id;
|
||||
let hosts = self
|
||||
.net_controller()?
|
||||
.db
|
||||
.mutate(|db| {
|
||||
let mut res = Hosts::default();
|
||||
for (host_id, host) in db
|
||||
.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(pkg_id)
|
||||
.or_not_found(pkg_id)?
|
||||
.as_hosts_mut()
|
||||
.as_entries_mut()?
|
||||
{
|
||||
host.as_bindings_mut().mutate(|b| {
|
||||
for (internal_port, info) in b {
|
||||
if !except.contains(&BindId {
|
||||
id: host_id.clone(),
|
||||
internal_port: *internal_port,
|
||||
}) {
|
||||
info.disable();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
res.0.insert(host_id, host.de()?);
|
||||
}
|
||||
Ok(res)
|
||||
})
|
||||
.await?;
|
||||
let mut errors = ErrorCollection::new();
|
||||
for (_, binds) in std::mem::take(&mut self.binds) {
|
||||
for (_, (lan, _, hostnames, rc)) in binds.lan {
|
||||
drop(rc);
|
||||
if let Some(external) = lan.assigned_ssl_port {
|
||||
for hostname in ctrl.server_hostnames.iter().cloned() {
|
||||
ctrl.vhost.gc(hostname, external).await?;
|
||||
}
|
||||
for hostname in hostnames {
|
||||
ctrl.vhost.gc(Some(hostname), external).await?;
|
||||
}
|
||||
}
|
||||
if let Some(external) = lan.assigned_port {
|
||||
ctrl.forward.gc(external).await?;
|
||||
}
|
||||
}
|
||||
for (addr, (_, rcs)) in binds.tor {
|
||||
drop(rcs);
|
||||
errors.handle(ctrl.tor.gc(Some(addr), None).await);
|
||||
}
|
||||
for (id, host) in hosts.0 {
|
||||
errors.handle(self.update(id, host).await);
|
||||
}
|
||||
std::mem::take(&mut self.dns);
|
||||
errors.handle(ctrl.dns.gc(Some(self.id.clone()), self.ip).await);
|
||||
errors.into_result()
|
||||
}
|
||||
|
||||
@@ -261,6 +273,9 @@ impl NetService {
|
||||
let ip_info = server_info.as_ip_info().de()?;
|
||||
let hostname = server_info.as_hostname().de()?;
|
||||
for (port, bind) in &host.bindings {
|
||||
if !bind.enabled {
|
||||
continue;
|
||||
}
|
||||
let old_lan_bind = binds.lan.remove(port);
|
||||
let lan_bind = old_lan_bind
|
||||
.as_ref()
|
||||
@@ -395,7 +410,7 @@ impl NetService {
|
||||
}
|
||||
let mut removed = BTreeSet::new();
|
||||
binds.lan.retain(|internal, (external, _, hostnames, _)| {
|
||||
if host.bindings.contains_key(internal) {
|
||||
if host.bindings.get(internal).map_or(false, |b| b.enabled) {
|
||||
true
|
||||
} else {
|
||||
removed.insert((*external, std::mem::take(hostnames)));
|
||||
@@ -424,6 +439,9 @@ impl NetService {
|
||||
let mut tor_hostname_ports = BTreeMap::<u16, TorHostnamePorts>::new();
|
||||
let mut tor_binds = OrdMap::<u16, SocketAddr>::new();
|
||||
for (internal, info) in &host.bindings {
|
||||
if !info.enabled {
|
||||
continue;
|
||||
}
|
||||
tor_binds.insert(
|
||||
info.options.preferred_external_port,
|
||||
SocketAddr::from((self.ip, *internal)),
|
||||
@@ -511,7 +529,7 @@ impl NetService {
|
||||
pub async fn remove_all(mut self) -> Result<(), Error> {
|
||||
self.shutdown = true;
|
||||
if let Some(ctrl) = Weak::upgrade(&self.controller) {
|
||||
self.clear_bindings().await?;
|
||||
self.clear_bindings(Default::default()).await?;
|
||||
drop(ctrl);
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -566,7 +584,7 @@ impl Drop for NetService {
|
||||
binds: BTreeMap::new(),
|
||||
},
|
||||
);
|
||||
tokio::spawn(async move { svc.remove_all().await.unwrap() });
|
||||
tokio::spawn(async move { svc.remove_all().await.log_err() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,7 +307,7 @@ async fn torctl(
|
||||
|
||||
let logs = journalctl(
|
||||
LogSource::Unit(SYSTEMD_UNIT),
|
||||
0,
|
||||
Some(0),
|
||||
None,
|
||||
Some("0"),
|
||||
false,
|
||||
|
||||
@@ -7,7 +7,7 @@ use clap::builder::ValueParserFactory;
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::eyre;
|
||||
use imbl_value::InternedString;
|
||||
use models::PackageId;
|
||||
use models::{FromStrParser, PackageId};
|
||||
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
@@ -17,7 +17,6 @@ use crate::backup::BackupReport;
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::db::model::DatabaseModel;
|
||||
use crate::prelude::*;
|
||||
use crate::util::clap::FromStrParser;
|
||||
use crate::util::serde::HandlerExtSerde;
|
||||
|
||||
// #[command(subcommands(list, delete, delete_before, create))]
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::str::FromStr;
|
||||
|
||||
use clap::builder::ValueParserFactory;
|
||||
use itertools::Itertools;
|
||||
use models::FromStrParser;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
use url::Url;
|
||||
@@ -10,7 +11,6 @@ use url::Url;
|
||||
use crate::prelude::*;
|
||||
use crate::registry::signer::commitment::Digestable;
|
||||
use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme};
|
||||
use crate::util::clap::FromStrParser;
|
||||
|
||||
pub mod commitment;
|
||||
pub mod sign;
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::str::FromStr;
|
||||
use ::ed25519::pkcs8::BitStringRef;
|
||||
use clap::builder::ValueParserFactory;
|
||||
use der::referenced::OwnedToRef;
|
||||
use models::FromStrParser;
|
||||
use pkcs8::der::AnyRef;
|
||||
use pkcs8::{PrivateKeyInfo, SubjectPublicKeyInfo};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -13,7 +14,6 @@ use ts_rs::TS;
|
||||
use crate::prelude::*;
|
||||
use crate::registry::signer::commitment::Digestable;
|
||||
use crate::registry::signer::sign::ed25519::Ed25519;
|
||||
use crate::util::clap::FromStrParser;
|
||||
use crate::util::serde::{deserialize_from_str, serialize_display};
|
||||
|
||||
pub mod ed25519;
|
||||
|
||||
@@ -13,12 +13,12 @@ use futures::future::BoxFuture;
|
||||
use futures::{Future, FutureExt};
|
||||
use helpers::TimedResource;
|
||||
use imbl_value::InternedString;
|
||||
use models::FromStrParser;
|
||||
use tokio::sync::{broadcast, Mutex as AsyncMutex};
|
||||
use ts_rs::TS;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::prelude::*;
|
||||
use crate::util::clap::FromStrParser;
|
||||
use crate::util::new_guid;
|
||||
|
||||
#[derive(
|
||||
|
||||
@@ -249,7 +249,6 @@ impl TryFrom<ManifestV1> for Manifest {
|
||||
hardware_requirements: value.hardware_requirements,
|
||||
git_hash: value.git_hash,
|
||||
os_version: value.eos_version,
|
||||
has_config: value.config.is_some(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +68,6 @@ pub struct Manifest {
|
||||
#[serde(default = "current_version")]
|
||||
#[ts(type = "string")]
|
||||
pub os_version: Version,
|
||||
#[serde(default = "const_true")]
|
||||
pub has_config: bool,
|
||||
}
|
||||
impl Manifest {
|
||||
pub fn validate_for<'a, T: Clone>(
|
||||
|
||||
@@ -1,42 +1,38 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use models::{ActionId, ProcedureName};
|
||||
use imbl_value::json;
|
||||
use models::{ActionId, PackageId, ProcedureName, ReplayId};
|
||||
|
||||
use crate::action::ActionResult;
|
||||
use crate::action::{ActionInput, ActionResult};
|
||||
use crate::db::model::package::{ActionRequestCondition, ActionRequestEntry, ActionRequestInput};
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::service::config::GetConfig;
|
||||
use crate::service::dependencies::DependencyConfig;
|
||||
use crate::service::{Service, ServiceActor};
|
||||
use crate::util::actor::background::BackgroundJobQueue;
|
||||
use crate::util::actor::{ConflictBuilder, Handler};
|
||||
use crate::util::serde::is_partial_of;
|
||||
|
||||
pub(super) struct Action {
|
||||
pub(super) struct GetActionInput {
|
||||
id: ActionId,
|
||||
input: Value,
|
||||
}
|
||||
impl Handler<Action> for ServiceActor {
|
||||
type Response = Result<ActionResult, Error>;
|
||||
fn conflicts_with(_: &Action) -> ConflictBuilder<Self> {
|
||||
ConflictBuilder::everything()
|
||||
.except::<GetConfig>()
|
||||
.except::<DependencyConfig>()
|
||||
impl Handler<GetActionInput> for ServiceActor {
|
||||
type Response = Result<Option<ActionInput>, Error>;
|
||||
fn conflicts_with(_: &GetActionInput) -> ConflictBuilder<Self> {
|
||||
ConflictBuilder::nothing()
|
||||
}
|
||||
async fn handle(
|
||||
&mut self,
|
||||
id: Guid,
|
||||
Action {
|
||||
id: action_id,
|
||||
input,
|
||||
}: Action,
|
||||
GetActionInput { id: action_id }: GetActionInput,
|
||||
_: &BackgroundJobQueue,
|
||||
) -> Self::Response {
|
||||
let container = &self.0.persistent_container;
|
||||
container
|
||||
.execute::<ActionResult>(
|
||||
.execute::<Option<ActionInput>>(
|
||||
id,
|
||||
ProcedureName::RunAction(action_id),
|
||||
input,
|
||||
ProcedureName::GetActionInput(action_id),
|
||||
Value::Null,
|
||||
Some(Duration::from_secs(30)),
|
||||
)
|
||||
.await
|
||||
@@ -45,16 +41,139 @@ impl Handler<Action> for ServiceActor {
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub async fn action(
|
||||
pub async fn get_action_input(
|
||||
&self,
|
||||
id: Guid,
|
||||
action_id: ActionId,
|
||||
) -> Result<Option<ActionInput>, Error> {
|
||||
if !self
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.peek()
|
||||
.await
|
||||
.as_public()
|
||||
.as_package_data()
|
||||
.as_idx(&self.seed.id)
|
||||
.or_not_found(&self.seed.id)?
|
||||
.as_actions()
|
||||
.as_idx(&action_id)
|
||||
.or_not_found(&action_id)?
|
||||
.as_has_input()
|
||||
.de()?
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
self.actor
|
||||
.send(id, GetActionInput { id: action_id })
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_requested_actions(
|
||||
requested_actions: &mut BTreeMap<ReplayId, ActionRequestEntry>,
|
||||
package_id: &PackageId,
|
||||
action_id: &ActionId,
|
||||
input: &Value,
|
||||
was_run: bool,
|
||||
) {
|
||||
requested_actions.retain(|_, v| {
|
||||
if &v.request.package_id != package_id || &v.request.action_id != action_id {
|
||||
return true;
|
||||
}
|
||||
if let Some(when) = &v.request.when {
|
||||
match &when.condition {
|
||||
ActionRequestCondition::InputNotMatches => match &v.request.input {
|
||||
Some(ActionRequestInput::Partial { value }) => {
|
||||
if is_partial_of(value, input) {
|
||||
if when.once {
|
||||
return !was_run;
|
||||
} else {
|
||||
v.active = false;
|
||||
}
|
||||
} else {
|
||||
v.active = true;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
tracing::error!(
|
||||
"action request exists in an invalid state {:?}",
|
||||
v.request
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
true
|
||||
} else {
|
||||
!was_run
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) struct RunAction {
|
||||
id: ActionId,
|
||||
input: Value,
|
||||
}
|
||||
impl Handler<RunAction> for ServiceActor {
|
||||
type Response = Result<Option<ActionResult>, Error>;
|
||||
fn conflicts_with(_: &RunAction) -> ConflictBuilder<Self> {
|
||||
ConflictBuilder::everything().except::<GetActionInput>()
|
||||
}
|
||||
async fn handle(
|
||||
&mut self,
|
||||
id: Guid,
|
||||
RunAction {
|
||||
id: action_id,
|
||||
input,
|
||||
}: RunAction,
|
||||
_: &BackgroundJobQueue,
|
||||
) -> Self::Response {
|
||||
let container = &self.0.persistent_container;
|
||||
let result = container
|
||||
.execute::<Option<ActionResult>>(
|
||||
id,
|
||||
ProcedureName::RunAction(action_id.clone()),
|
||||
json!({
|
||||
"input": input,
|
||||
}),
|
||||
Some(Duration::from_secs(30)),
|
||||
)
|
||||
.await
|
||||
.with_kind(ErrorKind::Action)?;
|
||||
let package_id = &self.0.id;
|
||||
self.0
|
||||
.ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
|
||||
pde.as_requested_actions_mut().mutate(|requested_actions| {
|
||||
Ok(update_requested_actions(
|
||||
requested_actions,
|
||||
package_id,
|
||||
&action_id,
|
||||
&input,
|
||||
true,
|
||||
))
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub async fn run_action(
|
||||
&self,
|
||||
id: Guid,
|
||||
action_id: ActionId,
|
||||
input: Value,
|
||||
) -> Result<ActionResult, Error> {
|
||||
) -> Result<Option<ActionResult>, Error> {
|
||||
self.actor
|
||||
.send(
|
||||
id,
|
||||
Action {
|
||||
RunAction {
|
||||
id: action_id,
|
||||
input,
|
||||
},
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use models::ProcedureName;
|
||||
|
||||
use crate::config::action::ConfigRes;
|
||||
use crate::config::ConfigureContext;
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::service::dependencies::DependencyConfig;
|
||||
use crate::service::{Service, ServiceActor};
|
||||
use crate::util::actor::background::BackgroundJobQueue;
|
||||
use crate::util::actor::{ConflictBuilder, Handler};
|
||||
use crate::util::serde::NoOutput;
|
||||
|
||||
pub(super) struct Configure(ConfigureContext);
|
||||
impl Handler<Configure> for ServiceActor {
|
||||
type Response = Result<(), Error>;
|
||||
fn conflicts_with(_: &Configure) -> ConflictBuilder<Self> {
|
||||
ConflictBuilder::everything().except::<DependencyConfig>()
|
||||
}
|
||||
async fn handle(
|
||||
&mut self,
|
||||
id: Guid,
|
||||
Configure(ConfigureContext { timeout, config }): Configure,
|
||||
_: &BackgroundJobQueue,
|
||||
) -> Self::Response {
|
||||
let container = &self.0.persistent_container;
|
||||
let package_id = &self.0.id;
|
||||
|
||||
container
|
||||
.execute::<NoOutput>(id, ProcedureName::SetConfig, to_value(&config)?, timeout)
|
||||
.await
|
||||
.with_kind(ErrorKind::ConfigRulesViolation)?;
|
||||
self.0
|
||||
.ctx
|
||||
.db
|
||||
.mutate(move |db| {
|
||||
db.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(package_id)
|
||||
.or_not_found(package_id)?
|
||||
.as_status_mut()
|
||||
.as_configured_mut()
|
||||
.ser(&true)
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct GetConfig;
|
||||
impl Handler<GetConfig> for ServiceActor {
|
||||
type Response = Result<ConfigRes, Error>;
|
||||
fn conflicts_with(_: &GetConfig) -> ConflictBuilder<Self> {
|
||||
ConflictBuilder::nothing().except::<Configure>()
|
||||
}
|
||||
async fn handle(&mut self, id: Guid, _: GetConfig, _: &BackgroundJobQueue) -> Self::Response {
|
||||
let container = &self.0.persistent_container;
|
||||
container
|
||||
.execute::<ConfigRes>(
|
||||
id,
|
||||
ProcedureName::GetConfig,
|
||||
Value::Null,
|
||||
Some(Duration::from_secs(30)), // TODO timeout
|
||||
)
|
||||
.await
|
||||
.with_kind(ErrorKind::ConfigRulesViolation)
|
||||
}
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub async fn configure(&self, id: Guid, ctx: ConfigureContext) -> Result<(), Error> {
|
||||
self.actor.send(id, Configure(ctx)).await?
|
||||
}
|
||||
pub async fn get_config(&self, id: Guid) -> Result<ConfigRes, Error> {
|
||||
self.actor.send(id, GetConfig).await?
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::service::config::GetConfig;
|
||||
use crate::service::dependencies::DependencyConfig;
|
||||
use crate::service::action::RunAction;
|
||||
use crate::service::start_stop::StartStop;
|
||||
use crate::service::transition::TransitionKind;
|
||||
use crate::service::{Service, ServiceActor};
|
||||
@@ -12,9 +11,7 @@ pub(super) struct Start;
|
||||
impl Handler<Start> for ServiceActor {
|
||||
type Response = ();
|
||||
fn conflicts_with(_: &Start) -> ConflictBuilder<Self> {
|
||||
ConflictBuilder::everything()
|
||||
.except::<GetConfig>()
|
||||
.except::<DependencyConfig>()
|
||||
ConflictBuilder::everything().except::<RunAction>()
|
||||
}
|
||||
async fn handle(&mut self, _: Guid, _: Start, _: &BackgroundJobQueue) -> Self::Response {
|
||||
self.0.persistent_container.state.send_modify(|x| {
|
||||
@@ -33,9 +30,7 @@ struct Stop;
|
||||
impl Handler<Stop> for ServiceActor {
|
||||
type Response = ();
|
||||
fn conflicts_with(_: &Stop) -> ConflictBuilder<Self> {
|
||||
ConflictBuilder::everything()
|
||||
.except::<GetConfig>()
|
||||
.except::<DependencyConfig>()
|
||||
ConflictBuilder::everything().except::<RunAction>()
|
||||
}
|
||||
async fn handle(&mut self, _: Guid, _: Stop, _: &BackgroundJobQueue) -> Self::Response {
|
||||
let mut transition_state = None;
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use imbl_value::json;
|
||||
use models::{PackageId, ProcedureName};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::service::{Service, ServiceActor, ServiceActorSeed};
|
||||
use crate::util::actor::background::BackgroundJobQueue;
|
||||
use crate::util::actor::{ConflictBuilder, Handler};
|
||||
use crate::Config;
|
||||
|
||||
impl ServiceActorSeed {
|
||||
async fn dependency_config(
|
||||
&self,
|
||||
id: Guid,
|
||||
dependency_id: PackageId,
|
||||
remote_config: Option<Config>,
|
||||
) -> Result<Option<Config>, Error> {
|
||||
let container = &self.persistent_container;
|
||||
container
|
||||
.sanboxed::<Option<Config>>(
|
||||
id.clone(),
|
||||
ProcedureName::UpdateDependency(dependency_id.clone()),
|
||||
json!({
|
||||
"queryResults": container
|
||||
.execute::<Value>(
|
||||
id,
|
||||
ProcedureName::QueryDependency(dependency_id),
|
||||
Value::Null,
|
||||
Some(Duration::from_secs(30)),
|
||||
)
|
||||
.await
|
||||
.with_kind(ErrorKind::Dependency)?,
|
||||
"remoteConfig": remote_config,
|
||||
}),
|
||||
Some(Duration::from_secs(30)),
|
||||
)
|
||||
.await
|
||||
.with_kind(ErrorKind::Dependency)
|
||||
.map(|res| res.filter(|c| !c.is_empty() && Some(c) != remote_config.as_ref()))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct DependencyConfig {
|
||||
dependency_id: PackageId,
|
||||
remote_config: Option<Config>,
|
||||
}
|
||||
impl Handler<DependencyConfig> for ServiceActor {
|
||||
type Response = Result<Option<Config>, Error>;
|
||||
fn conflicts_with(_: &DependencyConfig) -> ConflictBuilder<Self> {
|
||||
ConflictBuilder::nothing()
|
||||
}
|
||||
async fn handle(
|
||||
&mut self,
|
||||
id: Guid,
|
||||
DependencyConfig {
|
||||
dependency_id,
|
||||
remote_config,
|
||||
}: DependencyConfig,
|
||||
_: &BackgroundJobQueue,
|
||||
) -> Self::Response {
|
||||
self.0
|
||||
.dependency_config(id, dependency_id, remote_config)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub async fn dependency_config(
|
||||
&self,
|
||||
id: Guid,
|
||||
dependency_id: PackageId,
|
||||
remote_config: Option<Config>,
|
||||
) -> Result<Option<Config>, Error> {
|
||||
self.actor
|
||||
.send(
|
||||
id,
|
||||
DependencyConfig {
|
||||
dependency_id,
|
||||
remote_config,
|
||||
},
|
||||
)
|
||||
.await?
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,59 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use models::{ActionId, PackageId};
|
||||
use models::{ActionId, PackageId, ReplayId};
|
||||
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
|
||||
|
||||
use crate::action::ActionResult;
|
||||
use crate::db::model::package::ActionMetadata;
|
||||
use crate::action::{display_action_result, ActionInput, ActionResult};
|
||||
use crate::db::model::package::{
|
||||
ActionMetadata, ActionRequest, ActionRequestCondition, ActionRequestEntry, ActionRequestTrigger,
|
||||
};
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::service::cli::ContainerCliContext;
|
||||
use crate::service::effects::prelude::*;
|
||||
use crate::util::serde::HandlerExtSerde;
|
||||
|
||||
pub fn action_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand("export", from_fn_async(export_action).no_cli())
|
||||
.subcommand(
|
||||
"clear",
|
||||
from_fn_async(clear_actions)
|
||||
.no_display()
|
||||
.with_call_remote::<ContainerCliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"get-input",
|
||||
from_fn_async(get_action_input)
|
||||
.with_display_serializable()
|
||||
.with_call_remote::<ContainerCliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"run",
|
||||
from_fn_async(run_action)
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|args, res| Ok(display_action_result(args.params, res)))
|
||||
.with_call_remote::<ContainerCliContext>(),
|
||||
)
|
||||
.subcommand("request", from_fn_async(request_action).no_cli())
|
||||
.subcommand(
|
||||
"clear-requests",
|
||||
from_fn_async(clear_action_requests)
|
||||
.no_display()
|
||||
.with_call_remote::<ContainerCliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExportActionParams {
|
||||
#[ts(optional)]
|
||||
package_id: Option<PackageId>,
|
||||
id: ActionId,
|
||||
metadata: ActionMetadata,
|
||||
}
|
||||
pub async fn export_action(context: EffectContext, data: ExportActionParams) -> Result<(), Error> {
|
||||
pub async fn export_action(
|
||||
context: EffectContext,
|
||||
ExportActionParams { id, metadata }: ExportActionParams,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
let package_id = context.seed.id.clone();
|
||||
context
|
||||
@@ -31,17 +68,26 @@ pub async fn export_action(context: EffectContext, data: ExportActionParams) ->
|
||||
.or_not_found(&package_id)?
|
||||
.as_actions_mut();
|
||||
let mut value = model.de()?;
|
||||
value
|
||||
.insert(data.id, data.metadata)
|
||||
.map(|_| ())
|
||||
.unwrap_or_default();
|
||||
value.insert(id, metadata);
|
||||
model.ser(&value)
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn clear_actions(context: EffectContext) -> Result<(), Error> {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClearActionsParams {
|
||||
#[arg(long)]
|
||||
pub except: Vec<ActionId>,
|
||||
}
|
||||
|
||||
async fn clear_actions(
|
||||
context: EffectContext,
|
||||
ClearActionsParams { except }: ClearActionsParams,
|
||||
) -> Result<(), Error> {
|
||||
let except: BTreeSet<_> = except.into_iter().collect();
|
||||
let context = context.deref()?;
|
||||
let package_id = context.seed.id.clone();
|
||||
context
|
||||
@@ -54,34 +100,32 @@ pub async fn clear_actions(context: EffectContext) -> Result<(), Error> {
|
||||
.as_idx_mut(&package_id)
|
||||
.or_not_found(&package_id)?
|
||||
.as_actions_mut()
|
||||
.ser(&BTreeMap::new())
|
||||
.mutate(|a| Ok(a.retain(|e, _| except.contains(e))))
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct ExecuteAction {
|
||||
pub struct GetActionInputParams {
|
||||
#[serde(default)]
|
||||
#[ts(skip)]
|
||||
#[arg(skip)]
|
||||
procedure_id: Guid,
|
||||
#[ts(optional)]
|
||||
package_id: Option<PackageId>,
|
||||
action_id: ActionId,
|
||||
#[ts(type = "any")]
|
||||
input: Value,
|
||||
}
|
||||
pub async fn execute_action(
|
||||
async fn get_action_input(
|
||||
context: EffectContext,
|
||||
ExecuteAction {
|
||||
GetActionInputParams {
|
||||
procedure_id,
|
||||
package_id,
|
||||
action_id,
|
||||
input,
|
||||
}: ExecuteAction,
|
||||
) -> Result<ActionResult, Error> {
|
||||
}: GetActionInputParams,
|
||||
) -> Result<Option<ActionInput>, Error> {
|
||||
let context = context.deref()?;
|
||||
|
||||
if let Some(package_id) = package_id {
|
||||
@@ -93,9 +137,179 @@ pub async fn execute_action(
|
||||
.await
|
||||
.as_ref()
|
||||
.or_not_found(&package_id)?
|
||||
.action(procedure_id, action_id, input)
|
||||
.get_action_input(procedure_id, action_id)
|
||||
.await
|
||||
} else {
|
||||
context.action(procedure_id, action_id, input).await
|
||||
context.get_action_input(procedure_id, action_id).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct RunActionParams {
|
||||
#[serde(default)]
|
||||
#[ts(skip)]
|
||||
#[arg(skip)]
|
||||
procedure_id: Guid,
|
||||
#[ts(optional)]
|
||||
package_id: Option<PackageId>,
|
||||
action_id: ActionId,
|
||||
#[ts(type = "any")]
|
||||
input: Value,
|
||||
}
|
||||
async fn run_action(
|
||||
context: EffectContext,
|
||||
RunActionParams {
|
||||
procedure_id,
|
||||
package_id,
|
||||
action_id,
|
||||
input,
|
||||
}: RunActionParams,
|
||||
) -> Result<Option<ActionResult>, Error> {
|
||||
let context = context.deref()?;
|
||||
|
||||
let package_id = package_id.as_ref().unwrap_or(&context.seed.id);
|
||||
|
||||
if package_id != &context.seed.id {
|
||||
return Err(Error::new(
|
||||
eyre!("calling actions on other packages is unsupported at this time"),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
context
|
||||
.seed
|
||||
.ctx
|
||||
.services
|
||||
.get(&package_id)
|
||||
.await
|
||||
.as_ref()
|
||||
.or_not_found(&package_id)?
|
||||
.run_action(procedure_id, action_id, input)
|
||||
.await
|
||||
} else {
|
||||
context.run_action(procedure_id, action_id, input).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct RequestActionParams {
|
||||
#[serde(default)]
|
||||
#[ts(skip)]
|
||||
procedure_id: Guid,
|
||||
replay_id: ReplayId,
|
||||
#[serde(flatten)]
|
||||
request: ActionRequest,
|
||||
}
|
||||
async fn request_action(
|
||||
context: EffectContext,
|
||||
RequestActionParams {
|
||||
procedure_id,
|
||||
replay_id,
|
||||
request,
|
||||
}: RequestActionParams,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
|
||||
let src_id = &context.seed.id;
|
||||
let active = match &request.when {
|
||||
Some(ActionRequestTrigger { once, condition }) => match condition {
|
||||
ActionRequestCondition::InputNotMatches => {
|
||||
let Some(input) = request.input.as_ref() else {
|
||||
return Err(Error::new(
|
||||
eyre!("input-not-matches trigger requires input to be specified"),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
};
|
||||
if let Some(service) = context
|
||||
.seed
|
||||
.ctx
|
||||
.services
|
||||
.get(&request.package_id)
|
||||
.await
|
||||
.as_ref()
|
||||
{
|
||||
let Some(prev) = service
|
||||
.get_action_input(procedure_id, request.action_id.clone())
|
||||
.await?
|
||||
else {
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"action {} of {} has no input",
|
||||
request.action_id,
|
||||
request.package_id
|
||||
),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
};
|
||||
if input.matches(prev.value.as_ref()) {
|
||||
if *once {
|
||||
return Ok(());
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
true // update when service is installed
|
||||
}
|
||||
}
|
||||
},
|
||||
None => true,
|
||||
};
|
||||
context
|
||||
.seed
|
||||
.ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
db.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(src_id)
|
||||
.or_not_found(src_id)?
|
||||
.as_requested_actions_mut()
|
||||
.insert(&replay_id, &ActionRequestEntry { active, request })
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)]
|
||||
#[ts(type = "{ only: string[] } | { except: string[] }")]
|
||||
#[ts(export)]
|
||||
pub struct ClearActionRequestsParams {
|
||||
#[arg(long, conflicts_with = "except")]
|
||||
pub only: Option<Vec<ReplayId>>,
|
||||
#[arg(long, conflicts_with = "only")]
|
||||
pub except: Option<Vec<ReplayId>>,
|
||||
}
|
||||
|
||||
async fn clear_action_requests(
|
||||
context: EffectContext,
|
||||
ClearActionRequestsParams { only, except }: ClearActionRequestsParams,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
let package_id = context.seed.id.clone();
|
||||
let only = only.map(|only| only.into_iter().collect::<BTreeSet<_>>());
|
||||
let except = except.map(|except| except.into_iter().collect::<BTreeSet<_>>());
|
||||
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_requested_actions_mut()
|
||||
.mutate(|a| {
|
||||
Ok(a.retain(|e, _| {
|
||||
only.as_ref().map_or(true, |only| !only.contains(e))
|
||||
&& except.as_ref().map_or(true, |except| except.contains(e))
|
||||
}))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,19 +3,22 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::sync::{Arc, Mutex, Weak};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use clap::Parser;
|
||||
use futures::future::join_all;
|
||||
use helpers::NonDetachingJoinHandle;
|
||||
use imbl::{vector, Vector};
|
||||
use imbl_value::InternedString;
|
||||
use models::{HostId, PackageId, ServiceInterfaceId};
|
||||
use patch_db::json_ptr::JsonPointer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::warn;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::net::ssl::FullchainCertData;
|
||||
use crate::prelude::*;
|
||||
use crate::service::effects::context::EffectContext;
|
||||
use crate::service::effects::net::ssl::Algorithm;
|
||||
use crate::service::rpc::CallbackHandle;
|
||||
use crate::service::rpc::{CallbackHandle, CallbackId};
|
||||
use crate::service::{Service, ServiceActorSeed};
|
||||
use crate::util::collections::EqMap;
|
||||
|
||||
@@ -272,6 +275,7 @@ impl CallbackHandler {
|
||||
}
|
||||
}
|
||||
pub async fn call(mut self, args: Vector<Value>) -> Result<(), Error> {
|
||||
dbg!(eyre!("callback fired: {}", self.handle.is_active()));
|
||||
if let Some(seed) = self.seed.upgrade() {
|
||||
seed.persistent_container
|
||||
.callback(self.handle.take(), args)
|
||||
@@ -299,13 +303,29 @@ impl CallbackHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn clear_callbacks(context: EffectContext) -> Result<(), Error> {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)]
|
||||
#[ts(type = "{ only: number[] } | { except: number[] }")]
|
||||
#[ts(export)]
|
||||
pub struct ClearCallbacksParams {
|
||||
#[arg(long, conflicts_with = "except")]
|
||||
pub only: Option<Vec<CallbackId>>,
|
||||
#[arg(long, conflicts_with = "only")]
|
||||
pub except: Option<Vec<CallbackId>>,
|
||||
}
|
||||
|
||||
pub(super) fn clear_callbacks(
|
||||
context: EffectContext,
|
||||
ClearCallbacksParams { only, except }: ClearCallbacksParams,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
context
|
||||
.seed
|
||||
.persistent_container
|
||||
.state
|
||||
.send_if_modified(|s| !std::mem::take(&mut s.callbacks).is_empty());
|
||||
let only = only.map(|only| only.into_iter().collect::<BTreeSet<_>>());
|
||||
let except = except.map(|except| except.into_iter().collect::<BTreeSet<_>>());
|
||||
context.seed.persistent_container.state.send_modify(|s| {
|
||||
s.callbacks.retain(|cb| {
|
||||
only.as_ref().map_or(true, |only| !only.contains(cb))
|
||||
&& except.as_ref().map_or(true, |except| except.contains(cb))
|
||||
})
|
||||
});
|
||||
context.seed.ctx.callbacks.gc();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
use models::PackageId;
|
||||
|
||||
use crate::service::effects::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct GetConfiguredParams {
|
||||
#[ts(optional)]
|
||||
package_id: Option<PackageId>,
|
||||
}
|
||||
pub async fn get_configured(context: EffectContext) -> Result<bool, Error> {
|
||||
let context = context.deref()?;
|
||||
let peeked = context.seed.ctx.db.peek().await;
|
||||
let package_id = &context.seed.id;
|
||||
peeked
|
||||
.as_public()
|
||||
.as_package_data()
|
||||
.as_idx(package_id)
|
||||
.or_not_found(package_id)?
|
||||
.as_status()
|
||||
.as_configured()
|
||||
.de()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct SetConfigured {
|
||||
configured: bool,
|
||||
}
|
||||
pub async fn set_configured(
|
||||
context: EffectContext,
|
||||
SetConfigured { configured }: SetConfigured,
|
||||
) -> 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_status_mut()
|
||||
.as_configured_mut()
|
||||
.ser(&configured)
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::builder::ValueParserFactory;
|
||||
use models::FromStrParser;
|
||||
|
||||
use crate::service::effects::prelude::*;
|
||||
use crate::util::clap::FromStrParser;
|
||||
|
||||
pub async fn restart(
|
||||
context: EffectContext,
|
||||
|
||||
@@ -6,13 +6,13 @@ use clap::builder::ValueParserFactory;
|
||||
use exver::VersionRange;
|
||||
use imbl::OrdMap;
|
||||
use imbl_value::InternedString;
|
||||
use itertools::Itertools;
|
||||
use models::{HealthCheckId, PackageId, VersionString, VolumeId};
|
||||
use models::{FromStrParser, HealthCheckId, PackageId, ReplayId, VersionString, VolumeId};
|
||||
use patch_db::json_ptr::JsonPointer;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::db::model::package::{
|
||||
CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, ManifestPreference,
|
||||
ActionRequestEntry, CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind,
|
||||
ManifestPreference,
|
||||
};
|
||||
use crate::disk::mount::filesystem::bind::Bind;
|
||||
use crate::disk::mount::filesystem::idmapped::IdMapped;
|
||||
@@ -20,7 +20,6 @@ use crate::disk::mount::filesystem::{FileSystem, MountType};
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::service::effects::prelude::*;
|
||||
use crate::status::health_check::NamedHealthCheckResult;
|
||||
use crate::util::clap::FromStrParser;
|
||||
use crate::util::Invoke;
|
||||
use crate::volume::data_dir;
|
||||
|
||||
@@ -113,6 +112,7 @@ pub async fn expose_for_dependents(
|
||||
context: EffectContext,
|
||||
ExposeForDependentsParams { paths }: ExposeForDependentsParams,
|
||||
) -> Result<(), Error> {
|
||||
// TODO
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -192,16 +192,11 @@ impl ValueParserFactory for DependencyRequirement {
|
||||
#[command(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct SetDependenciesParams {
|
||||
#[serde(default)]
|
||||
procedure_id: Guid,
|
||||
dependencies: Vec<DependencyRequirement>,
|
||||
}
|
||||
pub async fn set_dependencies(
|
||||
context: EffectContext,
|
||||
SetDependenciesParams {
|
||||
procedure_id,
|
||||
dependencies,
|
||||
}: SetDependenciesParams,
|
||||
SetDependenciesParams { dependencies }: SetDependenciesParams,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
let id = &context.seed.id;
|
||||
@@ -222,19 +217,6 @@ pub async fn set_dependencies(
|
||||
version_range,
|
||||
),
|
||||
};
|
||||
let config_satisfied =
|
||||
if let Some(dep_service) = &*context.seed.ctx.services.get(&dep_id).await {
|
||||
context
|
||||
.dependency_config(
|
||||
procedure_id.clone(),
|
||||
dep_id.clone(),
|
||||
dep_service.get_config(procedure_id.clone()).await?.config,
|
||||
)
|
||||
.await?
|
||||
.is_none()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
let info = CurrentDependencyInfo {
|
||||
title: context
|
||||
.seed
|
||||
@@ -251,7 +233,6 @@ pub async fn set_dependencies(
|
||||
.await?,
|
||||
kind,
|
||||
version_range,
|
||||
config_satisfied,
|
||||
};
|
||||
deps.insert(dep_id, info);
|
||||
}
|
||||
@@ -282,7 +263,8 @@ pub async fn get_dependencies(context: EffectContext) -> Result<Vec<DependencyRe
|
||||
.as_current_dependencies()
|
||||
.de()?;
|
||||
|
||||
data.0
|
||||
Ok(data
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|(id, current_dependency_info)| {
|
||||
let CurrentDependencyInfo {
|
||||
@@ -290,7 +272,7 @@ pub async fn get_dependencies(context: EffectContext) -> Result<Vec<DependencyRe
|
||||
kind,
|
||||
..
|
||||
} = current_dependency_info;
|
||||
Ok::<_, Error>(match kind {
|
||||
match kind {
|
||||
CurrentDependencyKind::Exists => {
|
||||
DependencyRequirement::Exists { id, version_range }
|
||||
}
|
||||
@@ -301,9 +283,9 @@ pub async fn get_dependencies(context: EffectContext) -> Result<Vec<DependencyRe
|
||||
version_range,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.try_collect()
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)]
|
||||
@@ -320,12 +302,10 @@ pub struct CheckDependenciesResult {
|
||||
package_id: PackageId,
|
||||
#[ts(type = "string | null")]
|
||||
title: Option<InternedString>,
|
||||
#[ts(type = "string | null")]
|
||||
installed_version: Option<exver::ExtendedVersion>,
|
||||
#[ts(type = "string[]")]
|
||||
installed_version: Option<VersionString>,
|
||||
satisfies: BTreeSet<VersionString>,
|
||||
is_running: bool,
|
||||
config_satisfied: bool,
|
||||
requested_actions: BTreeMap<ReplayId, ActionRequestEntry>,
|
||||
#[ts(as = "BTreeMap::<HealthCheckId, NamedHealthCheckResult>")]
|
||||
health_checks: OrdMap<HealthCheckId, NamedHealthCheckResult>,
|
||||
}
|
||||
@@ -335,14 +315,14 @@ pub async fn check_dependencies(
|
||||
) -> Result<Vec<CheckDependenciesResult>, Error> {
|
||||
let context = context.deref()?;
|
||||
let db = context.seed.ctx.db.peek().await;
|
||||
let current_dependencies = db
|
||||
let pde = db
|
||||
.as_public()
|
||||
.as_package_data()
|
||||
.as_idx(&context.seed.id)
|
||||
.or_not_found(&context.seed.id)?
|
||||
.as_current_dependencies()
|
||||
.de()?;
|
||||
let package_ids: Vec<_> = package_ids
|
||||
.or_not_found(&context.seed.id)?;
|
||||
let current_dependencies = pde.as_current_dependencies().de()?;
|
||||
let requested_actions = pde.as_requested_actions().de()?;
|
||||
let package_dependency_info: Vec<_> = package_ids
|
||||
.unwrap_or_else(|| current_dependencies.0.keys().cloned().collect())
|
||||
.into_iter()
|
||||
.filter_map(|x| {
|
||||
@@ -350,18 +330,23 @@ pub async fn check_dependencies(
|
||||
Some((x, info))
|
||||
})
|
||||
.collect();
|
||||
let mut results = Vec::with_capacity(package_ids.len());
|
||||
let mut results = Vec::with_capacity(package_dependency_info.len());
|
||||
|
||||
for (package_id, dependency_info) in package_ids {
|
||||
for (package_id, dependency_info) in package_dependency_info {
|
||||
let title = dependency_info.title.clone();
|
||||
let Some(package) = db.as_public().as_package_data().as_idx(&package_id) else {
|
||||
let requested_actions = requested_actions
|
||||
.iter()
|
||||
.filter(|(_, v)| v.request.package_id == package_id)
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
results.push(CheckDependenciesResult {
|
||||
package_id,
|
||||
title,
|
||||
installed_version: None,
|
||||
satisfies: BTreeSet::new(),
|
||||
is_running: false,
|
||||
config_satisfied: false,
|
||||
requested_actions,
|
||||
health_checks: Default::default(),
|
||||
});
|
||||
continue;
|
||||
@@ -369,22 +354,27 @@ pub async fn check_dependencies(
|
||||
let manifest = package.as_state_info().as_manifest(ManifestPreference::New);
|
||||
let installed_version = manifest.as_version().de()?.into_version();
|
||||
let satisfies = manifest.as_satisfies().de()?;
|
||||
let installed_version = Some(installed_version.clone());
|
||||
let installed_version = Some(installed_version.clone().into());
|
||||
let is_installed = true;
|
||||
let status = package.as_status().as_main().de()?;
|
||||
let status = package.as_status().de()?;
|
||||
let is_running = if is_installed {
|
||||
status.running()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let health_checks = status.health().cloned().unwrap_or_default();
|
||||
let requested_actions = requested_actions
|
||||
.iter()
|
||||
.filter(|(_, v)| v.request.package_id == package_id)
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
results.push(CheckDependenciesResult {
|
||||
package_id,
|
||||
title,
|
||||
installed_version,
|
||||
satisfies,
|
||||
is_running,
|
||||
config_satisfied: dependency_info.config_satisfied,
|
||||
requested_actions,
|
||||
health_checks,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ pub async fn set_health(
|
||||
.as_idx_mut(package_id)
|
||||
.or_not_found(package_id)?
|
||||
.as_status_mut()
|
||||
.as_main_mut()
|
||||
.mutate(|main| {
|
||||
match main {
|
||||
MainStatus::Running { ref mut health, .. }
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::service::effects::context::EffectContext;
|
||||
|
||||
mod action;
|
||||
pub mod callbacks;
|
||||
mod config;
|
||||
pub mod context;
|
||||
mod control;
|
||||
mod dependency;
|
||||
@@ -26,34 +25,12 @@ pub fn handler<C: Context>() -> ParentHandler<C> {
|
||||
from_fn(echo::<EffectContext>).with_call_remote::<ContainerCliContext>(),
|
||||
)
|
||||
// action
|
||||
.subcommand(
|
||||
"execute-action",
|
||||
from_fn_async(action::execute_action).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"export-action",
|
||||
from_fn_async(action::export_action).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"clear-actions",
|
||||
from_fn_async(action::clear_actions).no_cli(),
|
||||
)
|
||||
.subcommand("action", action::action_api::<C>())
|
||||
// callbacks
|
||||
.subcommand(
|
||||
"clear-callbacks",
|
||||
from_fn(callbacks::clear_callbacks).no_cli(),
|
||||
)
|
||||
// config
|
||||
.subcommand(
|
||||
"get-configured",
|
||||
from_fn_async(config::get_configured).no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"set-configured",
|
||||
from_fn_async(config::set_configured)
|
||||
.no_display()
|
||||
.with_call_remote::<ContainerCliContext>(),
|
||||
)
|
||||
// control
|
||||
.subcommand(
|
||||
"restart",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use models::{HostId, PackageId};
|
||||
|
||||
use crate::net::host::binding::{BindOptions, LanInfo};
|
||||
use crate::net::host::binding::{BindId, BindOptions, LanInfo};
|
||||
use crate::net::host::HostKind;
|
||||
use crate::service::effects::prelude::*;
|
||||
|
||||
@@ -28,10 +28,20 @@ pub async fn bind(
|
||||
svc.bind(kind, id, internal_port, options).await
|
||||
}
|
||||
|
||||
pub async fn clear_bindings(context: EffectContext) -> Result<(), Error> {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClearBindingsParams {
|
||||
pub except: Vec<BindId>,
|
||||
}
|
||||
|
||||
pub async fn clear_bindings(
|
||||
context: EffectContext,
|
||||
ClearBindingsParams { except }: ClearBindingsParams,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
let mut svc = context.seed.persistent_container.net_service.lock().await;
|
||||
svc.clear_bindings().await?;
|
||||
svc.clear_bindings(except.into_iter().collect()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -165,7 +165,17 @@ pub async fn list_service_interfaces(
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn clear_service_interfaces(context: EffectContext) -> Result<(), Error> {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClearServiceInterfacesParams {
|
||||
pub except: Vec<ServiceInterfaceId>,
|
||||
}
|
||||
|
||||
pub async fn clear_service_interfaces(
|
||||
context: EffectContext,
|
||||
ClearServiceInterfacesParams { except }: ClearServiceInterfacesParams,
|
||||
) -> Result<(), Error> {
|
||||
let context = context.deref()?;
|
||||
let package_id = context.seed.id.clone();
|
||||
|
||||
@@ -179,7 +189,7 @@ pub async fn clear_service_interfaces(context: EffectContext) -> Result<(), Erro
|
||||
.as_idx_mut(&package_id)
|
||||
.or_not_found(&package_id)?
|
||||
.as_service_interfaces_mut()
|
||||
.ser(&Default::default())
|
||||
.mutate(|s| Ok(s.retain(|id, _| except.contains(id))))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ pub async fn get_store(
|
||||
callback,
|
||||
}: GetStoreParams,
|
||||
) -> Result<Value, Error> {
|
||||
dbg!(&callback);
|
||||
let context = context.deref()?;
|
||||
let peeked = context.seed.ctx.db.peek().await;
|
||||
let package_id = package_id.unwrap_or(context.seed.id.clone());
|
||||
@@ -33,8 +34,9 @@ pub async fn get_store(
|
||||
.as_private()
|
||||
.as_package_stores()
|
||||
.as_idx(&package_id)
|
||||
.or_not_found(&package_id)?
|
||||
.de()?;
|
||||
.map(|s| s.de())
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(callback) = callback {
|
||||
let callback = callback.register(&context.seed.persistent_container);
|
||||
@@ -45,10 +47,7 @@ pub async fn get_store(
|
||||
);
|
||||
}
|
||||
|
||||
Ok(path
|
||||
.get(&value)
|
||||
.ok_or_else(|| Error::new(eyre!("Did not find value at path"), ErrorKind::NotFound))?
|
||||
.clone())
|
||||
Ok(path.get(&value).cloned().unwrap_or_default())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::ffi::OsString;
|
||||
use std::io::IsTerminal;
|
||||
use std::ops::Deref;
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::time::Duration;
|
||||
use std::{ffi::OsString, path::PathBuf};
|
||||
|
||||
use axum::extract::ws::WebSocket;
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -15,7 +16,7 @@ use futures::stream::FusedStream;
|
||||
use futures::{SinkExt, StreamExt, TryStreamExt};
|
||||
use imbl_value::{json, InternedString};
|
||||
use itertools::Itertools;
|
||||
use models::{ImageId, PackageId, ProcedureName};
|
||||
use models::{ActionId, ImageId, PackageId, ProcedureName};
|
||||
use nix::sys::signal::Signal;
|
||||
use persistent_container::{PersistentContainer, Subcontainer};
|
||||
use rpc_toolkit::{from_fn_async, CallRemoteHandler, Empty, HandlerArgs, HandlerFor};
|
||||
@@ -39,6 +40,7 @@ use crate::prelude::*;
|
||||
use crate::progress::{NamedProgress, Progress};
|
||||
use crate::rpc_continuations::{Guid, RpcContinuation};
|
||||
use crate::s9pk::S9pk;
|
||||
use crate::service::action::update_requested_actions;
|
||||
use crate::service::service_map::InstallProgressHandles;
|
||||
use crate::util::actor::concurrent::ConcurrentActor;
|
||||
use crate::util::io::{create_file, AsyncReadStream};
|
||||
@@ -48,11 +50,9 @@ use crate::util::Never;
|
||||
use crate::volume::data_dir;
|
||||
use crate::CAP_1_KiB;
|
||||
|
||||
mod action;
|
||||
pub mod action;
|
||||
pub mod cli;
|
||||
mod config;
|
||||
mod control;
|
||||
mod dependencies;
|
||||
pub mod effects;
|
||||
pub mod persistent_container;
|
||||
mod properties;
|
||||
@@ -97,7 +97,7 @@ impl ServiceRef {
|
||||
.persistent_container
|
||||
.execute::<NoOutput>(
|
||||
Guid::new(),
|
||||
ProcedureName::Uninit,
|
||||
ProcedureName::PackageUninit,
|
||||
to_value(&target_version)?,
|
||||
None,
|
||||
) // TODO timeout
|
||||
@@ -257,7 +257,7 @@ impl Service {
|
||||
tokio::fs::create_dir_all(&path).await?;
|
||||
}
|
||||
}
|
||||
let start_stop = if i.as_status().as_main().de()?.running() {
|
||||
let start_stop = if i.as_status().de()?.running() {
|
||||
StartStop::Start
|
||||
} else {
|
||||
StartStop::Stop
|
||||
@@ -429,12 +429,13 @@ impl Service {
|
||||
.clone(),
|
||||
);
|
||||
}
|
||||
let procedure_id = Guid::new();
|
||||
service
|
||||
.seed
|
||||
.persistent_container
|
||||
.execute::<NoOutput>(
|
||||
Guid::new(),
|
||||
ProcedureName::Init,
|
||||
procedure_id.clone(),
|
||||
ProcedureName::PackageInit,
|
||||
to_value(&src_version)?,
|
||||
None,
|
||||
) // TODO timeout
|
||||
@@ -445,16 +446,60 @@ impl Service {
|
||||
progress.progress.complete();
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
|
||||
let peek = ctx.db.peek().await;
|
||||
let mut action_input: BTreeMap<ActionId, Value> = BTreeMap::new();
|
||||
let requested_actions: BTreeSet<_> = peek
|
||||
.as_public()
|
||||
.as_package_data()
|
||||
.as_entries()?
|
||||
.into_iter()
|
||||
.map(|(_, pde)| {
|
||||
Ok(pde
|
||||
.as_requested_actions()
|
||||
.as_entries()?
|
||||
.into_iter()
|
||||
.map(|(_, r)| {
|
||||
Ok::<_, Error>(if r.as_request().as_package_id().de()? == manifest.id {
|
||||
Some(r.as_request().as_action_id().de()?)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
})
|
||||
.filter_map_ok(|a| a))
|
||||
})
|
||||
.flatten_ok()
|
||||
.map(|a| a.and_then(|a| a))
|
||||
.try_collect()?;
|
||||
for action_id in requested_actions {
|
||||
if let Some(input) = service
|
||||
.get_action_input(procedure_id.clone(), action_id.clone())
|
||||
.await?
|
||||
.and_then(|i| i.value)
|
||||
{
|
||||
action_input.insert(action_id, input);
|
||||
}
|
||||
}
|
||||
ctx.db
|
||||
.mutate(|d| {
|
||||
let entry = d
|
||||
.mutate(|db| {
|
||||
for (action_id, input) in &action_input {
|
||||
for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
|
||||
pde.as_requested_actions_mut().mutate(|requested_actions| {
|
||||
Ok(update_requested_actions(
|
||||
requested_actions,
|
||||
&manifest.id,
|
||||
action_id,
|
||||
input,
|
||||
false,
|
||||
))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
let entry = db
|
||||
.as_public_mut()
|
||||
.as_package_data_mut()
|
||||
.as_idx_mut(&manifest.id)
|
||||
.or_not_found(&manifest.id)?;
|
||||
if !manifest.has_config {
|
||||
entry.as_status_mut().as_configured_mut().ser(&true)?;
|
||||
}
|
||||
entry
|
||||
.as_state_info_mut()
|
||||
.ser(&PackageState::Installed(InstalledState { manifest }))?;
|
||||
|
||||
@@ -379,7 +379,11 @@ impl PersistentContainer {
|
||||
));
|
||||
}
|
||||
|
||||
self.rpc_client.request(rpc::Init, Empty {}).await?;
|
||||
self.rpc_client
|
||||
.request(rpc::Init, Empty {})
|
||||
.await
|
||||
.map_err(Error::from)
|
||||
.log_err();
|
||||
|
||||
self.state.send_modify(|s| s.rt_initialized = true);
|
||||
|
||||
@@ -548,7 +552,7 @@ impl PersistentContainer {
|
||||
impl Drop for PersistentContainer {
|
||||
fn drop(&mut self) {
|
||||
if let Some(destroy) = self.destroy() {
|
||||
tokio::spawn(async move { destroy.await.unwrap() });
|
||||
tokio::spawn(async move { destroy.await.log_err() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::builder::ValueParserFactory;
|
||||
use imbl::Vector;
|
||||
use imbl_value::Value;
|
||||
use models::ProcedureName;
|
||||
use models::{FromStrParser, ProcedureName};
|
||||
use rpc_toolkit::yajrc::RpcMethod;
|
||||
use rpc_toolkit::Empty;
|
||||
use ts_rs::TS;
|
||||
@@ -153,6 +155,11 @@ impl serde::Serialize for Sandbox {
|
||||
pub struct CallbackId(u64);
|
||||
impl CallbackId {
|
||||
pub fn register(self, container: &PersistentContainer) -> CallbackHandle {
|
||||
dbg!(eyre!(
|
||||
"callback {} registered for {}",
|
||||
self.0,
|
||||
container.s9pk.as_manifest().id
|
||||
));
|
||||
let this = Arc::new(self);
|
||||
let res = Arc::downgrade(&this);
|
||||
container
|
||||
@@ -161,6 +168,18 @@ impl CallbackId {
|
||||
CallbackHandle(res)
|
||||
}
|
||||
}
|
||||
impl FromStr for CallbackId {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
u64::from_str(s).map_err(Error::from).map(Self)
|
||||
}
|
||||
}
|
||||
impl ValueParserFactory for CallbackId {
|
||||
type Parser = FromStrParser<Self>;
|
||||
fn value_parser() -> Self::Parser {
|
||||
FromStrParser::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CallbackHandle(Weak<CallbackId>);
|
||||
impl CallbackHandle {
|
||||
|
||||
@@ -49,7 +49,7 @@ async fn service_actor_loop(
|
||||
.db
|
||||
.mutate(|d| {
|
||||
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().de()?;
|
||||
let main_status = match &kinds {
|
||||
ServiceStateKinds {
|
||||
transition_state: Some(TransitionKind::Restarting),
|
||||
@@ -89,7 +89,7 @@ async fn service_actor_loop(
|
||||
..
|
||||
} => MainStatus::Stopped,
|
||||
};
|
||||
i.as_status_mut().as_main_mut().ser(&main_status)?;
|
||||
i.as_status_mut().ser(&main_status)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ use crate::s9pk::manifest::PackageId;
|
||||
use crate::s9pk::merkle_archive::source::FileSource;
|
||||
use crate::s9pk::S9pk;
|
||||
use crate::service::{LoadDisposition, Service, ServiceRef};
|
||||
use crate::status::{MainStatus, Status};
|
||||
use crate::status::MainStatus;
|
||||
use crate::util::serde::Pem;
|
||||
|
||||
pub type DownloadInstallFuture = BoxFuture<'static, Result<InstallFuture, Error>>;
|
||||
@@ -174,16 +174,14 @@ impl ServiceMap {
|
||||
PackageState::Installing(installing)
|
||||
},
|
||||
data_version: None,
|
||||
status: Status {
|
||||
configured: false,
|
||||
main: MainStatus::Stopped,
|
||||
},
|
||||
status: MainStatus::Stopped,
|
||||
registry: None,
|
||||
developer_key: Pem::new(developer_key),
|
||||
icon,
|
||||
last_backup: None,
|
||||
current_dependencies: Default::default(),
|
||||
actions: Default::default(),
|
||||
requested_actions: Default::default(),
|
||||
service_interfaces: Default::default(),
|
||||
hosts: Default::default(),
|
||||
store_exposed_dependents: Default::default(),
|
||||
|
||||
@@ -9,8 +9,7 @@ use super::TempDesiredRestore;
|
||||
use crate::disk::mount::filesystem::ReadWrite;
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::service::config::GetConfig;
|
||||
use crate::service::dependencies::DependencyConfig;
|
||||
use crate::service::action::GetActionInput;
|
||||
use crate::service::transition::{TransitionKind, TransitionState};
|
||||
use crate::service::ServiceActor;
|
||||
use crate::util::actor::background::BackgroundJobQueue;
|
||||
@@ -23,9 +22,7 @@ pub(in crate::service) struct Backup {
|
||||
impl Handler<Backup> for ServiceActor {
|
||||
type Response = Result<BoxFuture<'static, Result<(), Error>>, Error>;
|
||||
fn conflicts_with(_: &Backup) -> ConflictBuilder<Self> {
|
||||
ConflictBuilder::everything()
|
||||
.except::<GetConfig>()
|
||||
.except::<DependencyConfig>()
|
||||
ConflictBuilder::everything().except::<GetActionInput>()
|
||||
}
|
||||
async fn handle(
|
||||
&mut self,
|
||||
|
||||
@@ -3,8 +3,7 @@ use futures::FutureExt;
|
||||
use super::TempDesiredRestore;
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::service::config::GetConfig;
|
||||
use crate::service::dependencies::DependencyConfig;
|
||||
use crate::service::action::GetActionInput;
|
||||
use crate::service::transition::{TransitionKind, TransitionState};
|
||||
use crate::service::{Service, ServiceActor};
|
||||
use crate::util::actor::background::BackgroundJobQueue;
|
||||
@@ -15,9 +14,7 @@ pub(super) struct Restart;
|
||||
impl Handler<Restart> for ServiceActor {
|
||||
type Response = ();
|
||||
fn conflicts_with(_: &Restart) -> ConflictBuilder<Self> {
|
||||
ConflictBuilder::everything()
|
||||
.except::<GetConfig>()
|
||||
.except::<DependencyConfig>()
|
||||
ConflictBuilder::everything().except::<GetActionInput>()
|
||||
}
|
||||
async fn handle(&mut self, _: Guid, _: Restart, jobs: &BackgroundJobQueue) -> Self::Response {
|
||||
// So Need a handle to just a single field in the state
|
||||
|
||||
@@ -5,6 +5,7 @@ use clap::builder::ValueParserFactory;
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::eyre;
|
||||
use imbl_value::InternedString;
|
||||
use models::FromStrParser;
|
||||
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
@@ -12,7 +13,6 @@ use ts_rs::TS;
|
||||
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::prelude::*;
|
||||
use crate::util::clap::FromStrParser;
|
||||
use crate::util::io::create_file;
|
||||
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::builder::ValueParserFactory;
|
||||
use models::FromStrParser;
|
||||
pub use models::HealthCheckId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::util::clap::FromStrParser;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NamedHealthCheckResult {
|
||||
|
||||
@@ -11,17 +11,9 @@ use crate::service::start_stop::StartStop;
|
||||
use crate::status::health_check::NamedHealthCheckResult;
|
||||
|
||||
pub mod health_check;
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct Status {
|
||||
pub configured: bool,
|
||||
pub main: MainStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, TS)]
|
||||
#[serde(tag = "status")]
|
||||
#[serde(tag = "main")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(rename_all_fields = "camelCase")]
|
||||
pub enum MainStatus {
|
||||
|
||||
@@ -442,6 +442,13 @@ impl<T> BackTrackingIO<T> {
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn read_buffer(&self) -> &[u8] {
|
||||
match &self.buffer {
|
||||
BTBuffer::NotBuffering => &[],
|
||||
BTBuffer::Buffering { read, .. } => read,
|
||||
BTBuffer::Rewound { read } => read.remaining_slice(),
|
||||
}
|
||||
}
|
||||
#[must_use]
|
||||
pub fn stop_buffering(&mut self) -> Vec<u8> {
|
||||
match std::mem::take(&mut self.buffer) {
|
||||
@@ -512,6 +519,28 @@ impl<T: AsyncRead> AsyncRead for BackTrackingIO<T> {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T: std::io::Read> std::io::Read for BackTrackingIO<T> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
match &mut self.buffer {
|
||||
BTBuffer::Buffering { read, .. } => {
|
||||
let n = self.io.read(buf)?;
|
||||
read.extend_from_slice(&buf[..n]);
|
||||
Ok(n)
|
||||
}
|
||||
BTBuffer::NotBuffering => self.io.read(buf),
|
||||
BTBuffer::Rewound { read } => {
|
||||
let mut ready = false;
|
||||
if (read.position() as usize) < read.get_ref().len() {
|
||||
let n = std::io::Read::read(read, buf)?;
|
||||
if n != 0 {
|
||||
return Ok(n);
|
||||
}
|
||||
}
|
||||
self.io.read(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsyncWrite> AsyncWrite for BackTrackingIO<T> {
|
||||
fn is_write_vectored(&self) -> bool {
|
||||
@@ -869,7 +898,7 @@ impl Drop for TmpDir {
|
||||
if self.path.exists() {
|
||||
let path = std::mem::take(&mut self.path);
|
||||
tokio::spawn(async move {
|
||||
tokio::fs::remove_dir_all(&path).await.unwrap();
|
||||
tokio::fs::remove_dir_all(&path).await.log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ use crate::util::serde::{deserialize_from_str, serialize_display};
|
||||
use crate::{Error, ErrorKind, ResultExt as _};
|
||||
|
||||
pub mod actor;
|
||||
pub mod clap;
|
||||
pub mod collections;
|
||||
pub mod cpupower;
|
||||
pub mod crypto;
|
||||
@@ -568,7 +567,7 @@ pub struct FileLock(#[allow(unused)] OwnedMutexGuard<()>, Option<FdLock<File>>);
|
||||
impl Drop for FileLock {
|
||||
fn drop(&mut self) {
|
||||
if let Some(fd_lock) = self.1.take() {
|
||||
tokio::task::spawn_blocking(|| fd_lock.unlock(true).map_err(|(_, e)| e).unwrap());
|
||||
tokio::task::spawn_blocking(|| fd_lock.unlock(true).map_err(|(_, e)| e).log_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::any::Any;
|
||||
use std::collections::VecDeque;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Deref;
|
||||
@@ -9,6 +8,7 @@ use clap::builder::ValueParserFactory;
|
||||
use clap::{ArgMatches, CommandFactory, FromArgMatches};
|
||||
use color_eyre::eyre::eyre;
|
||||
use imbl::OrdMap;
|
||||
use models::FromStrParser;
|
||||
use openssl::pkey::{PKey, Private};
|
||||
use openssl::x509::X509;
|
||||
use rpc_toolkit::{
|
||||
@@ -17,12 +17,10 @@ use rpc_toolkit::{
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::ser::{SerializeMap, SerializeSeq};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::Value;
|
||||
use ts_rs::TS;
|
||||
|
||||
use super::IntoDoubleEndedIterator;
|
||||
use crate::prelude::*;
|
||||
use crate::util::clap::FromStrParser;
|
||||
use crate::util::Apply;
|
||||
|
||||
pub fn deserialize_from_str<
|
||||
@@ -272,7 +270,7 @@ impl std::fmt::Display for IoFormat {
|
||||
impl std::str::FromStr for IoFormat {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
serde_json::from_value(Value::String(s.to_owned()))
|
||||
serde_json::from_value(serde_json::Value::String(s.to_owned()))
|
||||
.with_kind(crate::ErrorKind::Deserialization)
|
||||
}
|
||||
}
|
||||
@@ -566,7 +564,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, TS)]
|
||||
#[derive(Deserialize, Serialize, TS, Clone)]
|
||||
pub struct StdinDeserializable<T>(pub T);
|
||||
impl<T> Default for StdinDeserializable<T>
|
||||
where
|
||||
@@ -1358,3 +1356,19 @@ impl Serialize for MaybeUtf8String {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_partial_of(partial: &Value, full: &Value) -> bool {
|
||||
match (partial, full) {
|
||||
(Value::Object(partial), Value::Object(full)) => partial.iter().all(|(k, v)| {
|
||||
if let Some(v_full) = full.get(k) {
|
||||
is_partial_of(v, v_full)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}),
|
||||
(Value::Array(partial), Value::Array(full)) => partial
|
||||
.iter()
|
||||
.all(|v| full.iter().any(|v_full| is_partial_of(v, v_full))),
|
||||
(_, _) => partial == full,
|
||||
}
|
||||
}
|
||||
|
||||
9
sdk/.gitignore
vendored
9
sdk/.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
.vscode
|
||||
dist/
|
||||
node_modules/
|
||||
lib/coverage
|
||||
lib/test/output.ts
|
||||
baseDist/
|
||||
base/lib/coverage
|
||||
base/lib/node_modules
|
||||
package/lib/coverage
|
||||
package/lib/node_modules
|
||||
@@ -1 +1 @@
|
||||
/lib/exver/exver.ts
|
||||
/base/lib/exver/exver.ts
|
||||
71
sdk/Makefile
71
sdk/Makefile
@@ -1,48 +1,75 @@
|
||||
TS_FILES := $(shell git ls-files lib) lib/test/output.ts
|
||||
PACKAGE_TS_FILES := $(shell git ls-files package/lib) package/lib/test/output.ts
|
||||
BASE_TS_FILES := $(shell git ls-files base/lib) package/lib/test/output.ts
|
||||
version = $(shell git tag --sort=committerdate | tail -1)
|
||||
|
||||
.PHONY: test clean bundle fmt buildOutput check
|
||||
.PHONY: test base/test package/test clean bundle fmt buildOutput check
|
||||
|
||||
all: bundle
|
||||
|
||||
test: $(TS_FILES) lib/test/output.ts
|
||||
npm test
|
||||
package/test: $(PACKAGE_TS_FILES) package/lib/test/output.ts package/node_modules base/node_modules
|
||||
cd package && npm test
|
||||
|
||||
base/test: $(BASE_TS_FILES) base/node_modules
|
||||
cd base && npm test
|
||||
|
||||
test: base/test package/test
|
||||
|
||||
clean:
|
||||
rm -rf base/node_modules
|
||||
rm -rf dist
|
||||
rm -f lib/test/output.ts
|
||||
rm -rf node_modules
|
||||
rm -rf baseDist
|
||||
rm -f package/lib/test/output.ts
|
||||
rm -rf package/node_modules
|
||||
|
||||
lib/test/output.ts: node_modules lib/test/makeOutput.ts scripts/oldSpecToBuilder.ts
|
||||
npm run buildOutput
|
||||
package/lib/test/output.ts: package/node_modules package/lib/test/makeOutput.ts package/scripts/oldSpecToBuilder.ts
|
||||
cd package && npm run buildOutput
|
||||
|
||||
bundle: dist | test fmt
|
||||
bundle: dist baseDist | test fmt
|
||||
touch dist
|
||||
|
||||
lib/exver/exver.ts: node_modules lib/exver/exver.pegjs
|
||||
npx peggy --allowed-start-rules '*' --plugin ./node_modules/ts-pegjs/dist/tspegjs -o lib/exver/exver.ts lib/exver/exver.pegjs
|
||||
base/lib/exver/exver.ts: base/node_modules base/lib/exver/exver.pegjs
|
||||
cd base && npm run peggy
|
||||
|
||||
dist: $(TS_FILES) package.json node_modules README.md LICENSE
|
||||
npx tsc
|
||||
npx tsc --project tsconfig-cjs.json
|
||||
cp package.json dist/package.json
|
||||
cp README.md dist/README.md
|
||||
cp LICENSE dist/LICENSE
|
||||
baseDist: $(PACKAGE_TS_FILES) $(BASE_TS_FILES) base/package.json base/node_modules base/README.md base/LICENSE
|
||||
(cd base && npm run tsc)
|
||||
rsync -ac base/node_modules baseDist/
|
||||
cp base/package.json baseDist/package.json
|
||||
cp base/README.md baseDist/README.md
|
||||
cp base/LICENSE baseDist/LICENSE
|
||||
touch baseDist
|
||||
|
||||
dist: $(PACKAGE_TS_FILES) $(BASE_TS_FILES) package/package.json package/.npmignore package/node_modules package/README.md package/LICENSE
|
||||
(cd package && npm run tsc)
|
||||
rsync -ac package/node_modules dist/
|
||||
cp package/.npmignore dist/.npmignore
|
||||
cp package/package.json dist/package.json
|
||||
cp package/README.md dist/README.md
|
||||
cp package/LICENSE dist/LICENSE
|
||||
touch dist
|
||||
|
||||
full-bundle: bundle
|
||||
|
||||
check:
|
||||
cd package
|
||||
npm run check
|
||||
cd ../base
|
||||
npm run check
|
||||
|
||||
fmt: node_modules
|
||||
fmt: package/node_modules base/node_modules
|
||||
npx prettier . "**/*.ts" --write
|
||||
|
||||
node_modules: package.json
|
||||
npm ci
|
||||
|
||||
publish: bundle package.json README.md LICENSE
|
||||
cd dist && npm publish --access=public
|
||||
package/node_modules: package/package.json
|
||||
cd package && npm ci
|
||||
|
||||
base/node_modules: base/package.json
|
||||
cd base && npm ci
|
||||
|
||||
node_modules: package/node_modules base/node_modules
|
||||
|
||||
publish: bundle package/package.json README.md LICENSE
|
||||
cd dist
|
||||
npm publish --access=public
|
||||
|
||||
link: bundle
|
||||
cd dist && npm link
|
||||
|
||||
5
sdk/base/.gitignore
vendored
Normal file
5
sdk/base/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.vscode
|
||||
dist/
|
||||
node_modules/
|
||||
lib/coverage
|
||||
lib/test/output.ts
|
||||
1
sdk/base/README.md
Normal file
1
sdk/base/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# See ../package/README.md
|
||||
191
sdk/base/lib/Effects.ts
Normal file
191
sdk/base/lib/Effects.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import {
|
||||
ActionId,
|
||||
ActionInput,
|
||||
ActionMetadata,
|
||||
SetMainStatus,
|
||||
DependencyRequirement,
|
||||
CheckDependenciesResult,
|
||||
SetHealth,
|
||||
BindParams,
|
||||
HostId,
|
||||
LanInfo,
|
||||
Host,
|
||||
ExportServiceInterfaceParams,
|
||||
ServiceInterface,
|
||||
ActionRequest,
|
||||
RequestActionParams,
|
||||
} from "./osBindings"
|
||||
import { StorePath } from "./util/PathBuilder"
|
||||
import {
|
||||
PackageId,
|
||||
Dependencies,
|
||||
ServiceInterfaceId,
|
||||
SmtpValue,
|
||||
ActionResult,
|
||||
} from "./types"
|
||||
import { UrlString } from "./util/getServiceInterface"
|
||||
|
||||
/** Used to reach out from the pure js runtime */
|
||||
|
||||
export type Effects = {
|
||||
constRetry: () => void
|
||||
clearCallbacks: (
|
||||
options: { only: number[] } | { except: number[] },
|
||||
) => Promise<void>
|
||||
|
||||
// action
|
||||
action: {
|
||||
/** Define an action that can be invoked by a user or service */
|
||||
export(options: { id: ActionId; metadata: ActionMetadata }): Promise<void>
|
||||
/** Remove all exported actions */
|
||||
clear(options: { except: ActionId[] }): Promise<void>
|
||||
getInput(options: {
|
||||
packageId?: PackageId
|
||||
actionId: ActionId
|
||||
}): Promise<ActionInput | null>
|
||||
run<Input extends Record<string, unknown>>(options: {
|
||||
packageId?: PackageId
|
||||
actionId: ActionId
|
||||
input?: Input
|
||||
}): Promise<ActionResult | null>
|
||||
request<Input extends Record<string, unknown>>(
|
||||
options: RequestActionParams,
|
||||
): Promise<void>
|
||||
clearRequests(
|
||||
options: { only: ActionId[] } | { except: ActionId[] },
|
||||
): Promise<void>
|
||||
}
|
||||
|
||||
// control
|
||||
/** restart this service's main function */
|
||||
restart(): Promise<void>
|
||||
/** stop this service's main function */
|
||||
shutdown(): Promise<void>
|
||||
/** indicate to the host os what runstate the service is in */
|
||||
setMainStatus(options: SetMainStatus): Promise<void>
|
||||
|
||||
// dependency
|
||||
/** Set the dependencies of what the service needs, usually run during the inputSpec action as a best practice */
|
||||
setDependencies(options: { dependencies: Dependencies }): Promise<void>
|
||||
/** Get the list of the dependencies, both the dynamic set by the effect of setDependencies and the end result any required in the manifest */
|
||||
getDependencies(): Promise<DependencyRequirement[]>
|
||||
/** Test whether current dependency requirements are satisfied */
|
||||
checkDependencies(options: {
|
||||
packageIds?: PackageId[]
|
||||
}): Promise<CheckDependenciesResult[]>
|
||||
/** mount a volume of a dependency */
|
||||
mount(options: {
|
||||
location: string
|
||||
target: {
|
||||
packageId: string
|
||||
volumeId: string
|
||||
subpath: string | null
|
||||
readonly: boolean
|
||||
}
|
||||
}): Promise<string>
|
||||
/** Returns a list of the ids of all installed packages */
|
||||
getInstalledPackages(): Promise<string[]>
|
||||
/** grants access to certain paths in the store to dependents */
|
||||
exposeForDependents(options: { paths: string[] }): Promise<void>
|
||||
|
||||
// health
|
||||
/** sets the result of a health check */
|
||||
setHealth(o: SetHealth): Promise<void>
|
||||
|
||||
// subcontainer
|
||||
subcontainer: {
|
||||
/** A low level api used by SubContainer */
|
||||
createFs(options: {
|
||||
imageId: string
|
||||
name: string | null
|
||||
}): Promise<[string, string]>
|
||||
/** A low level api used by SubContainer */
|
||||
destroyFs(options: { guid: string }): Promise<void>
|
||||
}
|
||||
|
||||
// net
|
||||
// bind
|
||||
/** Creates a host connected to the specified port with the provided options */
|
||||
bind(options: BindParams): Promise<void>
|
||||
/** Get the port address for a service */
|
||||
getServicePortForward(options: {
|
||||
packageId?: PackageId
|
||||
hostId: HostId
|
||||
internalPort: number
|
||||
}): Promise<LanInfo>
|
||||
/** Removes all network bindings, called in the setupInputSpec */
|
||||
clearBindings(options: {
|
||||
except: { id: HostId; internalPort: number }[]
|
||||
}): Promise<void>
|
||||
// host
|
||||
/** Returns information about the specified host, if it exists */
|
||||
getHostInfo(options: {
|
||||
packageId?: PackageId
|
||||
hostId: HostId
|
||||
callback?: () => void
|
||||
}): Promise<Host | null>
|
||||
/** Returns the primary url that a user has selected for a host, if it exists */
|
||||
getPrimaryUrl(options: {
|
||||
packageId?: PackageId
|
||||
hostId: HostId
|
||||
callback?: () => void
|
||||
}): Promise<UrlString | null>
|
||||
/** Returns the IP address of the container */
|
||||
getContainerIp(): Promise<string>
|
||||
// interface
|
||||
/** Creates an interface bound to a specific host and port to show to the user */
|
||||
exportServiceInterface(options: ExportServiceInterfaceParams): Promise<void>
|
||||
/** Returns an exported service interface */
|
||||
getServiceInterface(options: {
|
||||
packageId?: PackageId
|
||||
serviceInterfaceId: ServiceInterfaceId
|
||||
callback?: () => void
|
||||
}): Promise<ServiceInterface | null>
|
||||
/** Returns all exported service interfaces for a package */
|
||||
listServiceInterfaces(options: {
|
||||
packageId?: PackageId
|
||||
callback?: () => void
|
||||
}): Promise<Record<ServiceInterfaceId, ServiceInterface>>
|
||||
/** Removes all service interfaces */
|
||||
clearServiceInterfaces(options: {
|
||||
except: ServiceInterfaceId[]
|
||||
}): Promise<void>
|
||||
// ssl
|
||||
/** Returns a PEM encoded fullchain for the hostnames specified */
|
||||
getSslCertificate: (options: {
|
||||
hostnames: string[]
|
||||
algorithm?: "ecdsa" | "ed25519"
|
||||
callback?: () => void
|
||||
}) => Promise<[string, string, string]>
|
||||
/** Returns a PEM encoded private key corresponding to the certificate for the hostnames specified */
|
||||
getSslKey: (options: {
|
||||
hostnames: string[]
|
||||
algorithm?: "ecdsa" | "ed25519"
|
||||
}) => Promise<string>
|
||||
|
||||
// store
|
||||
store: {
|
||||
/** Get a value in a json like data, can be observed and subscribed */
|
||||
get<Store = never, ExtractStore = unknown>(options: {
|
||||
/** If there is no packageId it is assumed the current package */
|
||||
packageId?: string
|
||||
/** The path defaults to root level, using the [JsonPath](https://jsonpath.com/) */
|
||||
path: StorePath
|
||||
callback?: () => void
|
||||
}): Promise<ExtractStore>
|
||||
/** Used to store values that can be accessed and subscribed to */
|
||||
set<Store = never, ExtractStore = unknown>(options: {
|
||||
/** Sets the value for the wrapper at the path, it will override, using the [JsonPath](https://jsonpath.com/) */
|
||||
path: StorePath
|
||||
value: ExtractStore
|
||||
}): Promise<void>
|
||||
}
|
||||
/** sets the version that this service's data has been migrated to */
|
||||
setDataVersion(options: { version: string }): Promise<void>
|
||||
/** returns the version that this service's data has been migrated to */
|
||||
getDataVersion(): Promise<string | null>
|
||||
|
||||
// system
|
||||
/** Returns globally configured SMTP settings, if they exist */
|
||||
getSystemSmtp(options: { callback?: () => void }): Promise<SmtpValue | null>
|
||||
}
|
||||
65
sdk/base/lib/actions/index.ts
Normal file
65
sdk/base/lib/actions/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import * as T from "../types"
|
||||
import * as IST from "../actions/input/inputSpecTypes"
|
||||
|
||||
export type RunActionInput<Input> =
|
||||
| Input
|
||||
| ((prev?: { spec: IST.InputSpec; value: Input | null }) => Input)
|
||||
|
||||
export const runAction = async <
|
||||
Input extends Record<string, unknown>,
|
||||
>(options: {
|
||||
effects: T.Effects
|
||||
// packageId?: T.PackageId
|
||||
actionId: T.ActionId
|
||||
input?: RunActionInput<Input>
|
||||
}) => {
|
||||
if (options.input) {
|
||||
if (options.input instanceof Function) {
|
||||
const prev = await options.effects.action.getInput({
|
||||
// packageId: options.packageId,
|
||||
actionId: options.actionId,
|
||||
})
|
||||
const input = options.input(
|
||||
prev
|
||||
? { spec: prev.spec as IST.InputSpec, value: prev.value as Input }
|
||||
: undefined,
|
||||
)
|
||||
return options.effects.action.run({
|
||||
// packageId: options.packageId,
|
||||
actionId: options.actionId,
|
||||
input,
|
||||
})
|
||||
} else {
|
||||
return options.effects.action.run({
|
||||
// packageId: options.packageId,
|
||||
actionId: options.actionId,
|
||||
input: options.input,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return options.effects.action.run({
|
||||
// packageId: options.packageId,
|
||||
actionId: options.actionId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
export type ActionRequest<T extends Omit<T.ActionRequest, "packageId">> =
|
||||
T extends { when: { condition: "input-not-matches" } }
|
||||
? (T extends { input: T.ActionRequestInput } ? T : "input is required for condition 'input-not-matches'")
|
||||
: T
|
||||
|
||||
export const requestAction = <
|
||||
T extends Omit<T.ActionRequest, "packageId">,
|
||||
>(options: {
|
||||
effects: T.Effects
|
||||
request: ActionRequest<T> & { replayId?: string; packageId: T.PackageId }
|
||||
}) => {
|
||||
const request = options.request
|
||||
const req = {
|
||||
...request,
|
||||
replayId: request.replayId || `${request.packageId}:${request.actionId}`,
|
||||
}
|
||||
return options.effects.action.request(req)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Config } from "./config"
|
||||
import { InputSpec } from "./inputSpec"
|
||||
import { List } from "./list"
|
||||
import { Value } from "./value"
|
||||
import { Variants } from "./variants"
|
||||
|
||||
export { Config, List, Value, Variants }
|
||||
export { InputSpec as InputSpec, List, Value, Variants }
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ValueSpec } from "../configTypes"
|
||||
import { ValueSpec } from "../inputSpecTypes"
|
||||
import { Value } from "./value"
|
||||
import { _ } from "../../util"
|
||||
import { Effects } from "../../types"
|
||||
import { _ } from "../../../util"
|
||||
import { Effects } from "../../../Effects"
|
||||
import { Parser, object } from "ts-matches"
|
||||
|
||||
export type LazyBuildOptions<Store> = {
|
||||
@@ -12,20 +12,20 @@ export type LazyBuild<Store, ExpectedOut> = (
|
||||
) => Promise<ExpectedOut> | ExpectedOut
|
||||
|
||||
// prettier-ignore
|
||||
export type ExtractConfigType<A extends Record<string, any> | Config<Record<string, any>, any> | Config<Record<string, any>, never>> =
|
||||
A extends Config<infer B, any> | Config<infer B, never> ? B :
|
||||
export type ExtractInputSpecType<A extends Record<string, any> | InputSpec<Record<string, any>, any> | InputSpec<Record<string, any>, never>> =
|
||||
A extends InputSpec<infer B, any> | InputSpec<infer B, never> ? B :
|
||||
A
|
||||
|
||||
export type ConfigSpecOf<A extends Record<string, any>, Store = never> = {
|
||||
export type InputSpecOf<A extends Record<string, any>, Store = never> = {
|
||||
[K in keyof A]: Value<A[K], Store>
|
||||
}
|
||||
|
||||
export type MaybeLazyValues<A> = LazyBuild<any, A> | A
|
||||
/**
|
||||
* Configs are the specs that are used by the os configuration form for this service.
|
||||
* Here is an example of a simple configuration
|
||||
* InputSpecs are the specs that are used by the os input specification form for this service.
|
||||
* Here is an example of a simple input specification
|
||||
```ts
|
||||
const smallConfig = Config.of({
|
||||
const smallInputSpec = InputSpec.of({
|
||||
test: Value.boolean({
|
||||
name: "Test",
|
||||
description: "This is the description for the test",
|
||||
@@ -35,17 +35,17 @@ export type MaybeLazyValues<A> = LazyBuild<any, A> | A
|
||||
});
|
||||
```
|
||||
|
||||
The idea of a config is that now the form is going to ask for
|
||||
The idea of an inputSpec is that now the form is going to ask for
|
||||
Test: [ ] and the value is going to be checked as a boolean.
|
||||
There are more complex values like selects, lists, and objects. See {@link Value}
|
||||
|
||||
Also, there is the ability to get a validator/parser from this config spec.
|
||||
Also, there is the ability to get a validator/parser from this inputSpec spec.
|
||||
```ts
|
||||
const matchSmallConfig = smallConfig.validator();
|
||||
type SmallConfig = typeof matchSmallConfig._TYPE;
|
||||
const matchSmallInputSpec = smallInputSpec.validator();
|
||||
type SmallInputSpec = typeof matchSmallInputSpec._TYPE;
|
||||
```
|
||||
|
||||
Here is an example of a more complex configuration which came from a configuration for a service
|
||||
Here is an example of a more complex input specification which came from an input specification for a service
|
||||
that works with bitcoin, like c-lightning.
|
||||
```ts
|
||||
|
||||
@@ -73,11 +73,11 @@ export const port = Value.number({
|
||||
units: null,
|
||||
placeholder: null,
|
||||
});
|
||||
export const addNodesSpec = Config.of({ hostname: hostname, port: port });
|
||||
export const addNodesSpec = InputSpec.of({ hostname: hostname, port: port });
|
||||
|
||||
```
|
||||
*/
|
||||
export class Config<Type extends Record<string, any>, Store = never> {
|
||||
export class InputSpec<Type extends Record<string, any>, Store = never> {
|
||||
private constructor(
|
||||
private readonly spec: {
|
||||
[K in keyof Type]: Value<Type[K], Store> | Value<Type[K], never>
|
||||
@@ -105,7 +105,7 @@ export class Config<Type extends Record<string, any>, Store = never> {
|
||||
validatorObj[key] = spec[key].validator
|
||||
}
|
||||
const validator = object(validatorObj)
|
||||
return new Config<
|
||||
return new InputSpec<
|
||||
{
|
||||
[K in keyof Spec]: Spec[K] extends
|
||||
| Value<infer T, Store>
|
||||
@@ -119,19 +119,19 @@ export class Config<Type extends Record<string, any>, Store = never> {
|
||||
|
||||
/**
|
||||
* Use this during the times that the input needs a more specific type.
|
||||
* Used in types that the value/ variant/ list/ config is constructed somewhere else.
|
||||
* Used in types that the value/ variant/ list/ inputSpec is constructed somewhere else.
|
||||
```ts
|
||||
const a = Config.text({
|
||||
const a = InputSpec.text({
|
||||
name: "a",
|
||||
required: false,
|
||||
})
|
||||
|
||||
return Config.of<Store>()({
|
||||
return InputSpec.of<Store>()({
|
||||
myValue: a.withStore(),
|
||||
})
|
||||
```
|
||||
*/
|
||||
withStore<NewStore extends Store extends never ? any : Store>() {
|
||||
return this as any as Config<Type, NewStore>
|
||||
return this as any as InputSpec<Type, NewStore>
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Config, LazyBuild } from "./config"
|
||||
import { InputSpec, LazyBuild } from "./inputSpec"
|
||||
import {
|
||||
ListValueSpecText,
|
||||
Pattern,
|
||||
@@ -6,45 +6,55 @@ import {
|
||||
UniqueBy,
|
||||
ValueSpecList,
|
||||
ValueSpecListOf,
|
||||
} from "../configTypes"
|
||||
import { Parser, arrayOf, number, string } from "ts-matches"
|
||||
/**
|
||||
* Used as a subtype of Value.list
|
||||
```ts
|
||||
export const authorizationList = List.string({
|
||||
"name": "Authorization",
|
||||
"range": "[0,*)",
|
||||
"default": [],
|
||||
"description": "Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.",
|
||||
"warning": null
|
||||
}, {"masked":false,"placeholder":null,"pattern":"^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$","patternDescription":"Each item must be of the form \"<USERNAME>:<SALT>$<HASH>\"."});
|
||||
export const auth = Value.list(authorizationList);
|
||||
```
|
||||
*/
|
||||
} from "../inputSpecTypes"
|
||||
import { Parser, arrayOf, string } from "ts-matches"
|
||||
|
||||
export class List<Type, Store> {
|
||||
private constructor(
|
||||
public build: LazyBuild<Store, ValueSpecList>,
|
||||
public validator: Parser<unknown, Type>,
|
||||
) {}
|
||||
|
||||
static text(
|
||||
a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
/** Default = [] */
|
||||
default?: string[]
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
},
|
||||
aSpec: {
|
||||
/** Default = false */
|
||||
/**
|
||||
* @description Mask (aka camouflage) text input with dots: ● ● ●
|
||||
* @default false
|
||||
*/
|
||||
masked?: boolean
|
||||
placeholder?: string | null
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
patterns: Pattern[]
|
||||
/** Default = "text" */
|
||||
/**
|
||||
* @description A list of regular expressions to which the text must conform to pass validation. A human readable description is provided in case the validation fails.
|
||||
* @default []
|
||||
* @example
|
||||
* ```
|
||||
[
|
||||
{
|
||||
regex: "[a-z]",
|
||||
description: "May only contain lower case letters from the English alphabet."
|
||||
}
|
||||
]
|
||||
* ```
|
||||
*/
|
||||
patterns?: Pattern[]
|
||||
/**
|
||||
* @description Informs the browser how to behave and which keyboard to display on mobile
|
||||
* @default "text"
|
||||
*/
|
||||
inputmode?: ListValueSpecText["inputmode"]
|
||||
/**
|
||||
* @description Displays a button that will generate a random string according to the provided charset and len attributes.
|
||||
*/
|
||||
generate?: null | RandomString
|
||||
},
|
||||
) {
|
||||
@@ -57,6 +67,7 @@ export class List<Type, Store> {
|
||||
masked: false,
|
||||
inputmode: "text" as const,
|
||||
generate: null,
|
||||
patterns: aSpec.patterns || [],
|
||||
...aSpec,
|
||||
}
|
||||
const built: ValueSpecListOf<"text"> = {
|
||||
@@ -73,6 +84,7 @@ export class List<Type, Store> {
|
||||
return built
|
||||
}, arrayOf(string))
|
||||
}
|
||||
|
||||
static dynamicText<Store = never>(
|
||||
getA: LazyBuild<
|
||||
Store,
|
||||
@@ -80,20 +92,17 @@ export class List<Type, Store> {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
/** Default = [] */
|
||||
default?: string[]
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
disabled?: false | string
|
||||
generate?: null | RandomString
|
||||
spec: {
|
||||
/** Default = false */
|
||||
masked?: boolean
|
||||
placeholder?: string | null
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
patterns: Pattern[]
|
||||
/** Default = "text" */
|
||||
patterns?: Pattern[]
|
||||
inputmode?: ListValueSpecText["inputmode"]
|
||||
}
|
||||
}
|
||||
@@ -109,6 +118,7 @@ export class List<Type, Store> {
|
||||
masked: false,
|
||||
inputmode: "text" as const,
|
||||
generate: null,
|
||||
patterns: aSpec.patterns || [],
|
||||
...aSpec,
|
||||
}
|
||||
const built: ValueSpecListOf<"text"> = {
|
||||
@@ -125,18 +135,18 @@ export class List<Type, Store> {
|
||||
return built
|
||||
}, arrayOf(string))
|
||||
}
|
||||
|
||||
static obj<Type extends Record<string, any>, Store>(
|
||||
a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
/** Default [] */
|
||||
default?: []
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
},
|
||||
aSpec: {
|
||||
spec: Config<Type, Store>
|
||||
spec: InputSpec<Type, Store>
|
||||
displayAs?: null | string
|
||||
uniqueBy?: null | UniqueBy
|
||||
},
|
||||
@@ -170,14 +180,14 @@ export class List<Type, Store> {
|
||||
|
||||
/**
|
||||
* Use this during the times that the input needs a more specific type.
|
||||
* Used in types that the value/ variant/ list/ config is constructed somewhere else.
|
||||
* Used in types that the value/ variant/ list/ inputSpec is constructed somewhere else.
|
||||
```ts
|
||||
const a = Config.text({
|
||||
const a = InputSpec.text({
|
||||
name: "a",
|
||||
required: false,
|
||||
})
|
||||
|
||||
return Config.of<Store>()({
|
||||
return InputSpec.of<Store>()({
|
||||
myValue: a.withStore(),
|
||||
})
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Config, LazyBuild, LazyBuildOptions } from "./config"
|
||||
import { InputSpec, LazyBuild } from "./inputSpec"
|
||||
import { List } from "./list"
|
||||
import { Variants } from "./variants"
|
||||
import {
|
||||
@@ -7,13 +7,15 @@ import {
|
||||
RandomString,
|
||||
ValueSpec,
|
||||
ValueSpecDatetime,
|
||||
ValueSpecHidden,
|
||||
ValueSpecText,
|
||||
ValueSpecTextarea,
|
||||
} from "../configTypes"
|
||||
import { DefaultString } from "../configTypes"
|
||||
import { _ } from "../../util"
|
||||
} from "../inputSpecTypes"
|
||||
import { DefaultString } from "../inputSpecTypes"
|
||||
import { _, once } from "../../../util"
|
||||
import {
|
||||
Parser,
|
||||
any,
|
||||
anyOf,
|
||||
arrayOf,
|
||||
boolean,
|
||||
@@ -24,7 +26,6 @@ import {
|
||||
string,
|
||||
unknown,
|
||||
} from "ts-matches"
|
||||
import { once } from "../../util/once"
|
||||
|
||||
export type RequiredDefault<A> =
|
||||
| false
|
||||
@@ -54,11 +55,6 @@ type AsRequired<Type, MaybeRequiredType> = MaybeRequiredType extends
|
||||
? Type
|
||||
: Type | null | undefined
|
||||
|
||||
type InputAsRequired<A, Type> = A extends
|
||||
| { required: { default: any } | never }
|
||||
| never
|
||||
? Type
|
||||
: Type | null | undefined
|
||||
const testForAsRequiredParser = once(
|
||||
() => object({ required: object({ default: unknown }) }).test,
|
||||
)
|
||||
@@ -73,28 +69,6 @@ function asRequiredParser<
|
||||
return parser.optional() as any
|
||||
}
|
||||
|
||||
/**
|
||||
* A value is going to be part of the form in the FE of the OS.
|
||||
* Something like a boolean, a string, a number, etc.
|
||||
* in the fe it will ask for the name of value, and use the rest of the value to determine how to render it.
|
||||
* While writing with a value, you will start with `Value.` then let the IDE suggest the rest.
|
||||
* for things like string, the options are going to be in {}.
|
||||
* Keep an eye out for another config builder types as params.
|
||||
* Note, usually this is going to be used in a `Config` {@link Config} builder.
|
||||
```ts
|
||||
const username = Value.string({
|
||||
name: "Username",
|
||||
default: "bitcoin",
|
||||
description: "The username for connecting to Bitcoin over RPC.",
|
||||
warning: null,
|
||||
required: true,
|
||||
masked: true,
|
||||
placeholder: null,
|
||||
pattern: "^[a-zA-Z0-9_]+$",
|
||||
patternDescription: "Must be alphanumeric (can contain underscore).",
|
||||
});
|
||||
```
|
||||
*/
|
||||
export class Value<Type, Store> {
|
||||
protected constructor(
|
||||
public build: LazyBuild<Store, ValueSpec>,
|
||||
@@ -103,10 +77,13 @@ export class Value<Type, Store> {
|
||||
static toggle(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
default: boolean
|
||||
/** Immutable means it can only be configed at the first config then never again
|
||||
Default is false */
|
||||
/**
|
||||
* @description Once set, the value can never be changed.
|
||||
* @default false
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
return new Value<boolean, never>(
|
||||
@@ -148,22 +125,53 @@ export class Value<Type, Store> {
|
||||
static text<Required extends RequiredDefault<DefaultString>>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
/**
|
||||
* @description Determines if the field is required. If so, optionally provide a default value.
|
||||
* @type { false | { default: string | RandomString | null } }
|
||||
* @example required: false
|
||||
* @example required: { default: null }
|
||||
* @example required: { default: 'World' }
|
||||
* @example required: { default: { charset: 'abcdefg', len: 16 } }
|
||||
*/
|
||||
required: Required
|
||||
|
||||
/** Default = false */
|
||||
/**
|
||||
* @description Mask (aka camouflage) text input with dots: ● ● ●
|
||||
* @default false
|
||||
*/
|
||||
masked?: boolean
|
||||
placeholder?: string | null
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
/**
|
||||
* @description A list of regular expressions to which the text must conform to pass validation. A human readable description is provided in case the validation fails.
|
||||
* @default []
|
||||
* @example
|
||||
* ```
|
||||
[
|
||||
{
|
||||
regex: "[a-z]",
|
||||
description: "May only contain lower case letters from the English alphabet."
|
||||
}
|
||||
]
|
||||
* ```
|
||||
*/
|
||||
patterns?: Pattern[]
|
||||
/** Default = 'text' */
|
||||
/**
|
||||
* @description Informs the browser how to behave and which keyboard to display on mobile
|
||||
* @default "text"
|
||||
*/
|
||||
inputmode?: ValueSpecText["inputmode"]
|
||||
/** Immutable means it can only be configured at the first config then never again
|
||||
* Default is false
|
||||
/**
|
||||
* @description Once set, the value can never be changed.
|
||||
* @default false
|
||||
*/
|
||||
immutable?: boolean
|
||||
generate?: null | RandomString
|
||||
/**
|
||||
* @description Displays a button that will generate a random string according to the provided charset and len attributes.
|
||||
*/
|
||||
generate?: RandomString | null
|
||||
}) {
|
||||
return new Value<AsRequired<string, Required>, never>(
|
||||
async () => ({
|
||||
@@ -193,19 +201,13 @@ export class Value<Type, Store> {
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
required: RequiredDefault<DefaultString>
|
||||
|
||||
/** Default = false */
|
||||
masked?: boolean
|
||||
placeholder?: string | null
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
patterns?: Pattern[]
|
||||
/** Default = 'text' */
|
||||
inputmode?: ValueSpecText["inputmode"]
|
||||
disabled?: string | false
|
||||
/** Immutable means it can only be configured at the first config then never again
|
||||
* Default is false
|
||||
*/
|
||||
generate?: null | RandomString
|
||||
}
|
||||
>,
|
||||
@@ -233,13 +235,19 @@ export class Value<Type, Store> {
|
||||
static textarea(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
/**
|
||||
* @description Unlike other "required" fields, for textarea this is a simple boolean.
|
||||
*/
|
||||
required: boolean
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
placeholder?: string | null
|
||||
/** Immutable means it can only be configed at the first config then never again
|
||||
Default is false */
|
||||
/**
|
||||
* @description Once set, the value can never be changed.
|
||||
* @default false
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
return new Value<string, never>(async () => {
|
||||
@@ -290,17 +298,36 @@ export class Value<Type, Store> {
|
||||
static number<Required extends RequiredDefault<number>>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
/**
|
||||
* @description Determines if the field is required. If so, optionally provide a default value.
|
||||
* @type { false | { default: number | null } }
|
||||
* @example required: false
|
||||
* @example required: { default: null }
|
||||
* @example required: { default: 7 }
|
||||
*/
|
||||
required: Required
|
||||
min?: number | null
|
||||
max?: number | null
|
||||
/** Default = '1' */
|
||||
/**
|
||||
* @description How much does the number increase/decrease when using the arrows provided by the browser.
|
||||
* @default 1
|
||||
*/
|
||||
step?: number | null
|
||||
/**
|
||||
* @description Requires the number to be an integer.
|
||||
*/
|
||||
integer: boolean
|
||||
/**
|
||||
* @description Optionally display units to the right of the input box.
|
||||
*/
|
||||
units?: string | null
|
||||
placeholder?: string | null
|
||||
/** Immutable means it can only be configed at the first config then never again
|
||||
Default is false */
|
||||
/**
|
||||
* @description Once set, the value can never be changed.
|
||||
* @default false
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
return new Value<AsRequired<number, Required>, never>(
|
||||
@@ -331,7 +358,6 @@ export class Value<Type, Store> {
|
||||
required: RequiredDefault<number>
|
||||
min?: number | null
|
||||
max?: number | null
|
||||
/** Default = '1' */
|
||||
step?: number | null
|
||||
integer: boolean
|
||||
units?: string | null
|
||||
@@ -361,10 +387,20 @@ export class Value<Type, Store> {
|
||||
static color<Required extends RequiredDefault<string>>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
/**
|
||||
* @description Determines if the field is required. If so, optionally provide a default value.
|
||||
* @type { false | { default: string | null } }
|
||||
* @example required: false
|
||||
* @example required: { default: null }
|
||||
* @example required: { default: 'ffffff' }
|
||||
*/
|
||||
required: Required
|
||||
/** Immutable means it can only be configed at the first config then never again
|
||||
Default is false */
|
||||
/**
|
||||
* @description Once set, the value can never be changed.
|
||||
* @default false
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
return new Value<AsRequired<string, Required>, never>(
|
||||
@@ -410,14 +446,27 @@ export class Value<Type, Store> {
|
||||
static datetime<Required extends RequiredDefault<string>>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
/**
|
||||
* @description Determines if the field is required. If so, optionally provide a default value.
|
||||
* @type { false | { default: string | null } }
|
||||
* @example required: false
|
||||
* @example required: { default: null }
|
||||
* @example required: { default: '1985-12-16 18:00:00.000' }
|
||||
*/
|
||||
required: Required
|
||||
/** Default = 'datetime-local' */
|
||||
/**
|
||||
* @description Informs the browser how to behave and which date/time component to display.
|
||||
* @default "datetime-local"
|
||||
*/
|
||||
inputmode?: ValueSpecDatetime["inputmode"]
|
||||
min?: string | null
|
||||
max?: string | null
|
||||
/** Immutable means it can only be configed at the first config then never again
|
||||
Default is false */
|
||||
/**
|
||||
* @description Once set, the value can never be changed.
|
||||
* @default false
|
||||
*/
|
||||
immutable?: boolean
|
||||
}) {
|
||||
return new Value<AsRequired<string, Required>, never>(
|
||||
@@ -445,7 +494,6 @@ export class Value<Type, Store> {
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
required: RequiredDefault<string>
|
||||
/** Default = 'datetime-local' */
|
||||
inputmode?: ValueSpecDatetime["inputmode"]
|
||||
min?: string | null
|
||||
max?: string | null
|
||||
@@ -471,24 +519,39 @@ export class Value<Type, Store> {
|
||||
}
|
||||
static select<
|
||||
Required extends RequiredDefault<string>,
|
||||
B extends Record<string, string>,
|
||||
Values extends Record<string, string>,
|
||||
>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
required: Required
|
||||
values: B
|
||||
/**
|
||||
* Disabled: false means that there is nothing disabled, good to modify
|
||||
* string means that this is the message displayed and the whole thing is disabled
|
||||
* string[] means that the options are disabled
|
||||
* @description Determines if the field is required. If so, optionally provide a default value from the list of values.
|
||||
* @type { false | { default: string | null } }
|
||||
* @example required: false
|
||||
* @example required: { default: null }
|
||||
* @example required: { default: 'radio1' }
|
||||
*/
|
||||
required: Required
|
||||
/**
|
||||
* @description A mapping of unique radio options to their human readable display format.
|
||||
* @example
|
||||
* ```
|
||||
{
|
||||
radio1: "Radio 1"
|
||||
radio2: "Radio 2"
|
||||
radio3: "Radio 3"
|
||||
}
|
||||
* ```
|
||||
*/
|
||||
values: Values
|
||||
/**
|
||||
* @description Once set, the value can never be changed.
|
||||
* @default false
|
||||
*/
|
||||
disabled?: false | string | (string & keyof B)[]
|
||||
/** Immutable means it can only be configed at the first config then never again
|
||||
Default is false */
|
||||
immutable?: boolean
|
||||
}) {
|
||||
return new Value<AsRequired<keyof B, Required>, never>(
|
||||
return new Value<AsRequired<keyof Values, Required>, never>(
|
||||
() => ({
|
||||
description: null,
|
||||
warning: null,
|
||||
@@ -500,7 +563,9 @@ export class Value<Type, Store> {
|
||||
}),
|
||||
asRequiredParser(
|
||||
anyOf(
|
||||
...Object.keys(a.values).map((x: keyof B & string) => literal(x)),
|
||||
...Object.keys(a.values).map((x: keyof Values & string) =>
|
||||
literal(x),
|
||||
),
|
||||
),
|
||||
a,
|
||||
) as any,
|
||||
@@ -515,11 +580,6 @@ export class Value<Type, Store> {
|
||||
warning?: string | null
|
||||
required: RequiredDefault<string>
|
||||
values: Record<string, string>
|
||||
/**
|
||||
* Disabled: false means that there is nothing disabled, good to modify
|
||||
* string means that this is the message displayed and the whole thing is disabled
|
||||
* string[] means that the options are disabled
|
||||
*/
|
||||
disabled?: false | string | string[]
|
||||
}
|
||||
>,
|
||||
@@ -540,20 +600,31 @@ export class Value<Type, Store> {
|
||||
static multiselect<Values extends Record<string, string>>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
/**
|
||||
* @description A simple list of which options should be checked by default.
|
||||
*/
|
||||
default: string[]
|
||||
/**
|
||||
* @description A mapping of checkbox options to their human readable display format.
|
||||
* @example
|
||||
* ```
|
||||
{
|
||||
option1: "Option 1"
|
||||
option2: "Option 2"
|
||||
option3: "Option 3"
|
||||
}
|
||||
* ```
|
||||
*/
|
||||
values: Values
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
/** Immutable means it can only be configed at the first config then never again
|
||||
Default is false */
|
||||
immutable?: boolean
|
||||
/**
|
||||
* Disabled: false means that there is nothing disabled, good to modify
|
||||
* string means that this is the message displayed and the whole thing is disabled
|
||||
* string[] means that the options are disabled
|
||||
* @description Once set, the value can never be changed.
|
||||
* @default false
|
||||
*/
|
||||
disabled?: false | string | (string & keyof Values)[]
|
||||
immutable?: boolean
|
||||
}) {
|
||||
return new Value<(keyof Values)[], never>(
|
||||
() => ({
|
||||
@@ -582,11 +653,6 @@ export class Value<Type, Store> {
|
||||
values: Record<string, string>
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
/**
|
||||
* Disabled: false means that there is nothing disabled, good to modify
|
||||
* string means that this is the message displayed and the whole thing is disabled
|
||||
* string[] means that the options are disabled
|
||||
*/
|
||||
disabled?: false | string | string[]
|
||||
}
|
||||
>,
|
||||
@@ -609,9 +675,8 @@ export class Value<Type, Store> {
|
||||
a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
},
|
||||
spec: Config<Type, Store>,
|
||||
spec: InputSpec<Type, Store>,
|
||||
) {
|
||||
return new Value<Type, Store>(async (options) => {
|
||||
const built = await spec.build(options as any)
|
||||
@@ -624,12 +689,11 @@ export class Value<Type, Store> {
|
||||
}
|
||||
}, spec.validator)
|
||||
}
|
||||
static file<Required extends RequiredDefault<string>, Store>(a: {
|
||||
static file<Store>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
extensions: string[]
|
||||
required: Required
|
||||
required: boolean
|
||||
}) {
|
||||
const buildValue = {
|
||||
type: "file" as const,
|
||||
@@ -637,11 +701,9 @@ export class Value<Type, Store> {
|
||||
warning: null,
|
||||
...a,
|
||||
}
|
||||
return new Value<AsRequired<FilePath, Required>, Store>(
|
||||
return new Value<FilePath, Store>(
|
||||
() => ({
|
||||
...buildValue,
|
||||
|
||||
...requiredLikeToAbove(a.required),
|
||||
}),
|
||||
asRequiredParser(object({ filePath: string }), a),
|
||||
)
|
||||
@@ -672,17 +734,21 @@ export class Value<Type, Store> {
|
||||
a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
/** Presents a warning prompt before permitting the value to change. */
|
||||
warning?: string | null
|
||||
required: Required
|
||||
/** Immutable means it can only be configed at the first config then never again
|
||||
Default is false */
|
||||
immutable?: boolean
|
||||
/**
|
||||
* Disabled: false means that there is nothing disabled, good to modify
|
||||
* string means that this is the message displayed and the whole thing is disabled
|
||||
* string[] means that the options are disabled
|
||||
* @description Determines if the field is required. If so, optionally provide a default value from the list of variants.
|
||||
* @type { false | { default: string | null } }
|
||||
* @example required: false
|
||||
* @example required: { default: null }
|
||||
* @example required: { default: 'variant1' }
|
||||
*/
|
||||
disabled?: false | string | string[]
|
||||
required: Required
|
||||
/**
|
||||
* @description Once set, the value can never be changed.
|
||||
* @default false
|
||||
*/
|
||||
immutable?: boolean
|
||||
},
|
||||
aVariants: Variants<Type, Store>,
|
||||
) {
|
||||
@@ -736,11 +802,11 @@ export class Value<Type, Store> {
|
||||
getA: LazyBuild<
|
||||
Store,
|
||||
{
|
||||
disabled: string[] | false | string
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
required: Required
|
||||
disabled: string[] | false | string
|
||||
}
|
||||
>,
|
||||
aVariants: Variants<Type, Store> | Variants<Type, never>,
|
||||
@@ -763,16 +829,25 @@ export class Value<Type, Store> {
|
||||
return new Value<Type, Store>((options) => a.build(options), a.validator)
|
||||
}
|
||||
|
||||
static hidden<T>(parser: Parser<unknown, T> = any) {
|
||||
return new Value<T, never>(async () => {
|
||||
const built: ValueSpecHidden = {
|
||||
type: "hidden" as const,
|
||||
}
|
||||
return built
|
||||
}, parser)
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this during the times that the input needs a more specific type.
|
||||
* Used in types that the value/ variant/ list/ config is constructed somewhere else.
|
||||
* Used in types that the value/ variant/ list/ inputSpec is constructed somewhere else.
|
||||
```ts
|
||||
const a = Config.text({
|
||||
const a = InputSpec.text({
|
||||
name: "a",
|
||||
required: false,
|
||||
})
|
||||
|
||||
return Config.of<Store>()({
|
||||
return InputSpec.of<Store>()({
|
||||
myValue: a.withStore(),
|
||||
})
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
import { InputSpec, ValueSpecUnion } from "../configTypes"
|
||||
import { LazyBuild, Config } from "./config"
|
||||
import { ValueSpec, ValueSpecUnion } from "../inputSpecTypes"
|
||||
import { LazyBuild, InputSpec } from "./inputSpec"
|
||||
import { Parser, anyOf, literals, object } from "ts-matches"
|
||||
|
||||
/**
|
||||
@@ -8,7 +8,7 @@ import { Parser, anyOf, literals, object } from "ts-matches"
|
||||
* key to the tag.id in the Value.select
|
||||
```ts
|
||||
|
||||
export const disabled = Config.of({});
|
||||
export const disabled = InputSpec.of({});
|
||||
export const size = Value.number({
|
||||
name: "Max Chain Size",
|
||||
default: 550,
|
||||
@@ -20,7 +20,7 @@ export const size = Value.number({
|
||||
units: "MiB",
|
||||
placeholder: null,
|
||||
});
|
||||
export const automatic = Config.of({ size: size });
|
||||
export const automatic = InputSpec.of({ size: size });
|
||||
export const size1 = Value.number({
|
||||
name: "Failsafe Chain Size",
|
||||
default: 65536,
|
||||
@@ -32,7 +32,7 @@ export const size1 = Value.number({
|
||||
units: "MiB",
|
||||
placeholder: null,
|
||||
});
|
||||
export const manual = Config.of({ size: size1 });
|
||||
export const manual = InputSpec.of({ size: size1 });
|
||||
export const pruningSettingsVariants = Variants.of({
|
||||
disabled: { name: "Disabled", spec: disabled },
|
||||
automatic: { name: "Automatic", spec: automatic },
|
||||
@@ -61,7 +61,7 @@ export class Variants<Type, Store> {
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
name: string
|
||||
spec: Config<any, Store> | Config<any, never>
|
||||
spec: InputSpec<any, Store> | InputSpec<any, never>
|
||||
}
|
||||
},
|
||||
Store = never,
|
||||
@@ -81,14 +81,17 @@ export class Variants<Type, Store> {
|
||||
selection: K
|
||||
// prettier-ignore
|
||||
value:
|
||||
VariantValues[K]["spec"] extends (Config<infer B, Store> | Config<infer B, never>) ? B :
|
||||
VariantValues[K]["spec"] extends (InputSpec<infer B, Store> | InputSpec<infer B, never>) ? B :
|
||||
never
|
||||
}
|
||||
}[keyof VariantValues],
|
||||
Store
|
||||
>(async (options) => {
|
||||
const variants = {} as {
|
||||
[K in keyof VariantValues]: { name: string; spec: InputSpec }
|
||||
[K in keyof VariantValues]: {
|
||||
name: string
|
||||
spec: Record<string, ValueSpec>
|
||||
}
|
||||
}
|
||||
for (const key in a) {
|
||||
const value = a[key]
|
||||
@@ -102,14 +105,14 @@ export class Variants<Type, Store> {
|
||||
}
|
||||
/**
|
||||
* Use this during the times that the input needs a more specific type.
|
||||
* Used in types that the value/ variant/ list/ config is constructed somewhere else.
|
||||
* Used in types that the value/ variant/ list/ inputSpec is constructed somewhere else.
|
||||
```ts
|
||||
const a = Config.text({
|
||||
const a = InputSpec.text({
|
||||
name: "a",
|
||||
required: false,
|
||||
})
|
||||
|
||||
return Config.of<Store>()({
|
||||
return InputSpec.of<Store>()({
|
||||
myValue: a.withStore(),
|
||||
})
|
||||
```
|
||||
3
sdk/base/lib/actions/input/index.ts
Normal file
3
sdk/base/lib/actions/input/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * as constants from "./inputSpecConstants"
|
||||
export * as types from "./inputSpecTypes"
|
||||
export * as builder from "./builder"
|
||||
@@ -1,14 +1,13 @@
|
||||
import { SmtpValue } from "../types"
|
||||
import { GetSystemSmtp } from "../util/GetSystemSmtp"
|
||||
import { email } from "../util/patterns"
|
||||
import { Config, ConfigSpecOf } from "./builder/config"
|
||||
import { SmtpValue } from "../../types"
|
||||
import { GetSystemSmtp, Patterns } from "../../util"
|
||||
import { InputSpec, InputSpecOf } from "./builder/inputSpec"
|
||||
import { Value } from "./builder/value"
|
||||
import { Variants } from "./builder/variants"
|
||||
|
||||
/**
|
||||
* Base SMTP settings, to be used by StartOS for system wide SMTP
|
||||
*/
|
||||
export const customSmtp = Config.of<ConfigSpecOf<SmtpValue>, never>({
|
||||
export const customSmtp = InputSpec.of<InputSpecOf<SmtpValue>, never>({
|
||||
server: Value.text({
|
||||
name: "SMTP Server",
|
||||
required: {
|
||||
@@ -29,7 +28,7 @@ export const customSmtp = Config.of<ConfigSpecOf<SmtpValue>, never>({
|
||||
},
|
||||
placeholder: "<name>test@example.com",
|
||||
inputmode: "email",
|
||||
patterns: [email],
|
||||
patterns: [Patterns.email],
|
||||
}),
|
||||
login: Value.text({
|
||||
name: "Login",
|
||||
@@ -45,9 +44,9 @@ export const customSmtp = Config.of<ConfigSpecOf<SmtpValue>, never>({
|
||||
})
|
||||
|
||||
/**
|
||||
* For service config. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings
|
||||
* For service inputSpec. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings
|
||||
*/
|
||||
export const smtpConfig = Value.filteredUnion(
|
||||
export const smtpInputSpec = Value.filteredUnion(
|
||||
async ({ effects }) => {
|
||||
const smtp = await new GetSystemSmtp(effects).once()
|
||||
return smtp ? [] : ["system"]
|
||||
@@ -58,10 +57,10 @@ export const smtpConfig = Value.filteredUnion(
|
||||
required: { default: "disabled" },
|
||||
},
|
||||
Variants.of({
|
||||
disabled: { name: "Disabled", spec: Config.of({}) },
|
||||
disabled: { name: "Disabled", spec: InputSpec.of({}) },
|
||||
system: {
|
||||
name: "System Credentials",
|
||||
spec: Config.of({
|
||||
spec: InputSpec.of({
|
||||
customFrom: Value.text({
|
||||
name: "Custom From Address",
|
||||
description:
|
||||
@@ -69,7 +68,7 @@ export const smtpConfig = Value.filteredUnion(
|
||||
required: false,
|
||||
placeholder: "<name>test@example.com",
|
||||
inputmode: "email",
|
||||
patterns: [email],
|
||||
patterns: [Patterns.email],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
@@ -12,6 +12,7 @@ export type ValueType =
|
||||
| "object"
|
||||
| "file"
|
||||
| "union"
|
||||
| "hidden"
|
||||
export type ValueSpec = ValueSpecOf<ValueType>
|
||||
/** core spec types. These types provide the metadata for performing validations */
|
||||
// prettier-ignore
|
||||
@@ -28,6 +29,7 @@ export type ValueSpecOf<T extends ValueType> =
|
||||
T extends "object" ? ValueSpecObject :
|
||||
T extends "file" ? ValueSpecFile :
|
||||
T extends "union" ? ValueSpecUnion :
|
||||
T extends "hidden" ? ValueSpecHidden :
|
||||
never
|
||||
|
||||
export type ValueSpecText = {
|
||||
@@ -48,7 +50,6 @@ export type ValueSpecText = {
|
||||
default: DefaultString | null
|
||||
disabled: false | string
|
||||
generate: null | RandomString
|
||||
/** Immutable means it can only be configured at the first config then never again */
|
||||
immutable: boolean
|
||||
}
|
||||
export type ValueSpecTextarea = {
|
||||
@@ -62,7 +63,6 @@ export type ValueSpecTextarea = {
|
||||
maxLength: number | null
|
||||
required: boolean
|
||||
disabled: false | string
|
||||
/** Immutable means it can only be configured at the first config then never again */
|
||||
immutable: boolean
|
||||
}
|
||||
|
||||
@@ -83,7 +83,6 @@ export type ValueSpecNumber = {
|
||||
required: boolean
|
||||
default: number | null
|
||||
disabled: false | string
|
||||
/** Immutable means it can only be configured at the first config then never again */
|
||||
immutable: boolean
|
||||
}
|
||||
export type ValueSpecColor = {
|
||||
@@ -95,7 +94,6 @@ export type ValueSpecColor = {
|
||||
required: boolean
|
||||
default: string | null
|
||||
disabled: false | string
|
||||
/** Immutable means it can only be configured at the first config then never again */
|
||||
immutable: boolean
|
||||
}
|
||||
export type ValueSpecDatetime = {
|
||||
@@ -109,7 +107,6 @@ export type ValueSpecDatetime = {
|
||||
max: string | null
|
||||
default: string | null
|
||||
disabled: false | string
|
||||
/** Immutable means it can only be configured at the first config then never again */
|
||||
immutable: boolean
|
||||
}
|
||||
export type ValueSpecSelect = {
|
||||
@@ -120,13 +117,7 @@ export type ValueSpecSelect = {
|
||||
type: "select"
|
||||
required: boolean
|
||||
default: string | null
|
||||
/**
|
||||
* Disabled: false means that there is nothing disabled, good to modify
|
||||
* string means that this is the message displayed and the whole thing is disabled
|
||||
* string[] means that the options are disabled
|
||||
*/
|
||||
disabled: false | string | string[]
|
||||
/** Immutable means it can only be configured at the first config then never again */
|
||||
immutable: boolean
|
||||
}
|
||||
export type ValueSpecMultiselect = {
|
||||
@@ -139,14 +130,8 @@ export type ValueSpecMultiselect = {
|
||||
type: "multiselect"
|
||||
minLength: number | null
|
||||
maxLength: number | null
|
||||
/**
|
||||
* Disabled: false means that there is nothing disabled, good to modify
|
||||
* string means that this is the message displayed and the whole thing is disabled
|
||||
* string[] means that the options are disabled
|
||||
*/
|
||||
disabled: false | string | string[]
|
||||
default: string[]
|
||||
/** Immutable means it can only be configured at the first config then never again */
|
||||
immutable: boolean
|
||||
}
|
||||
export type ValueSpecToggle = {
|
||||
@@ -157,7 +142,6 @@ export type ValueSpecToggle = {
|
||||
type: "toggle"
|
||||
default: boolean | null
|
||||
disabled: false | string
|
||||
/** Immutable means it can only be configured at the first config then never again */
|
||||
immutable: boolean
|
||||
}
|
||||
export type ValueSpecUnion = {
|
||||
@@ -173,15 +157,9 @@ export type ValueSpecUnion = {
|
||||
spec: InputSpec
|
||||
}
|
||||
>
|
||||
/**
|
||||
* Disabled: false means that there is nothing disabled, good to modify
|
||||
* string means that this is the message displayed and the whole thing is disabled
|
||||
* string[] means that the options are disabled
|
||||
*/
|
||||
disabled: false | string | string[]
|
||||
required: boolean
|
||||
default: string | null
|
||||
/** Immutable means it can only be configured at the first config then never again */
|
||||
immutable: boolean
|
||||
}
|
||||
export type ValueSpecFile = {
|
||||
@@ -199,14 +177,15 @@ export type ValueSpecObject = {
|
||||
type: "object"
|
||||
spec: InputSpec
|
||||
}
|
||||
export type ValueSpecHidden = {
|
||||
type: "hidden"
|
||||
}
|
||||
export type ListValueSpecType = "text" | "object"
|
||||
/** represents a spec for the values of a list */
|
||||
// prettier-ignore
|
||||
export type ListValueSpecOf<T extends ListValueSpecType> =
|
||||
T extends "text" ? ListValueSpecText :
|
||||
T extends "object" ? ListValueSpecObject :
|
||||
never
|
||||
/** represents a spec for a list */
|
||||
export type ValueSpecList = ValueSpecListOf<ListValueSpecType>
|
||||
export type ValueSpecListOf<T extends ListValueSpecType> = {
|
||||
name: string
|
||||
@@ -242,13 +221,13 @@ export type ListValueSpecText = {
|
||||
}
|
||||
export type ListValueSpecObject = {
|
||||
type: "object"
|
||||
/** this is a mapped type of the config object at this level, replacing the object's values with specs on those values */
|
||||
spec: InputSpec
|
||||
/** indicates whether duplicates can be permitted in the list */
|
||||
uniqueBy: UniqueBy
|
||||
/** this should be a handlebars template which can make use of the entire config which corresponds to 'spec' */
|
||||
displayAs: string | null
|
||||
}
|
||||
// TODO Aiden do we really want this expressivity? Why not the below. Also what's with the "readonly" portion?
|
||||
// export type UniqueBy = null | string | { any: string[] } | { all: string[] }
|
||||
|
||||
export type UniqueBy =
|
||||
| null
|
||||
| string
|
||||
152
sdk/base/lib/actions/setupActions.ts
Normal file
152
sdk/base/lib/actions/setupActions.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { InputSpec } from "./input/builder"
|
||||
import { ExtractInputSpecType } from "./input/builder/inputSpec"
|
||||
import * as T from "../types"
|
||||
|
||||
export type Run<
|
||||
A extends
|
||||
| Record<string, any>
|
||||
| InputSpec<Record<string, any>, any>
|
||||
| InputSpec<Record<string, never>, never>,
|
||||
> = (options: {
|
||||
effects: T.Effects
|
||||
input: ExtractInputSpecType<A> & Record<string, any>
|
||||
}) => Promise<T.ActionResult | null>
|
||||
export type GetInput<
|
||||
A extends
|
||||
| Record<string, any>
|
||||
| InputSpec<Record<string, any>, any>
|
||||
| InputSpec<Record<string, any>, never>,
|
||||
> = (options: {
|
||||
effects: T.Effects
|
||||
}) => Promise<null | (ExtractInputSpecType<A> & Record<string, any>)>
|
||||
|
||||
export type MaybeFn<T> = T | ((options: { effects: T.Effects }) => Promise<T>)
|
||||
function callMaybeFn<T>(
|
||||
maybeFn: MaybeFn<T>,
|
||||
options: { effects: T.Effects },
|
||||
): Promise<T> {
|
||||
if (maybeFn instanceof Function) {
|
||||
return maybeFn(options)
|
||||
} else {
|
||||
return Promise.resolve(maybeFn)
|
||||
}
|
||||
}
|
||||
function mapMaybeFn<T, U>(
|
||||
maybeFn: MaybeFn<T>,
|
||||
map: (value: T) => U,
|
||||
): MaybeFn<U> {
|
||||
if (maybeFn instanceof Function) {
|
||||
return async (...args) => map(await maybeFn(...args))
|
||||
} else {
|
||||
return map(maybeFn)
|
||||
}
|
||||
}
|
||||
|
||||
export class Action<
|
||||
Id extends T.ActionId,
|
||||
Store,
|
||||
InputSpecType extends
|
||||
| Record<string, any>
|
||||
| InputSpec<any, Store>
|
||||
| InputSpec<any, never>,
|
||||
Type extends
|
||||
ExtractInputSpecType<InputSpecType> = ExtractInputSpecType<InputSpecType>,
|
||||
> {
|
||||
private constructor(
|
||||
readonly id: Id,
|
||||
private readonly metadataFn: MaybeFn<T.ActionMetadata>,
|
||||
private readonly inputSpec: InputSpecType,
|
||||
private readonly getInputFn: GetInput<Type>,
|
||||
private readonly runFn: Run<Type>,
|
||||
) {}
|
||||
static withInput<
|
||||
Id extends T.ActionId,
|
||||
Store,
|
||||
InputSpecType extends
|
||||
| Record<string, any>
|
||||
| InputSpec<any, Store>
|
||||
| InputSpec<any, never>,
|
||||
Type extends
|
||||
ExtractInputSpecType<InputSpecType> = ExtractInputSpecType<InputSpecType>,
|
||||
>(
|
||||
id: Id,
|
||||
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
|
||||
inputSpec: InputSpecType,
|
||||
getInput: GetInput<Type>,
|
||||
run: Run<Type>,
|
||||
): Action<Id, Store, InputSpecType, Type> {
|
||||
return new Action(
|
||||
id,
|
||||
mapMaybeFn(metadata, (m) => ({ ...m, hasInput: true })),
|
||||
inputSpec,
|
||||
getInput,
|
||||
run,
|
||||
)
|
||||
}
|
||||
static withoutInput<Id extends T.ActionId, Store>(
|
||||
id: Id,
|
||||
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
|
||||
run: Run<{}>,
|
||||
): Action<Id, Store, {}, {}> {
|
||||
return new Action(
|
||||
id,
|
||||
mapMaybeFn(metadata, (m) => ({ ...m, hasInput: true })),
|
||||
{},
|
||||
async () => null,
|
||||
run,
|
||||
)
|
||||
}
|
||||
async exportMetadata(options: {
|
||||
effects: T.Effects
|
||||
}): Promise<T.ActionMetadata> {
|
||||
const metadata = await callMaybeFn(this.metadataFn, options)
|
||||
await options.effects.action.export({ id: this.id, metadata })
|
||||
return metadata
|
||||
}
|
||||
async getInput(options: { effects: T.Effects }): Promise<T.ActionInput> {
|
||||
return {
|
||||
spec: await this.inputSpec.build(options),
|
||||
value: (await this.getInputFn(options)) || null,
|
||||
}
|
||||
}
|
||||
async run(options: {
|
||||
effects: T.Effects
|
||||
input: Type
|
||||
}): Promise<T.ActionResult | null> {
|
||||
return this.runFn(options)
|
||||
}
|
||||
}
|
||||
|
||||
export class Actions<
|
||||
Store,
|
||||
AllActions extends Record<T.ActionId, Action<T.ActionId, Store, any, any>>,
|
||||
> {
|
||||
private constructor(private readonly actions: AllActions) {}
|
||||
static of<Store>(): Actions<Store, {}> {
|
||||
return new Actions({})
|
||||
}
|
||||
addAction<A extends Action<T.ActionId, Store, any, any>>(
|
||||
action: A,
|
||||
): Actions<Store, AllActions & { [id in A["id"]]: A }> {
|
||||
return new Actions({ ...this.actions, [action.id]: action })
|
||||
}
|
||||
update(options: { effects: T.Effects }): Promise<void> {
|
||||
const updater = async (options: { effects: T.Effects }) => {
|
||||
for (let action of Object.values(this.actions)) {
|
||||
await action.exportMetadata(options)
|
||||
}
|
||||
await options.effects.action.clear({ except: Object.keys(this.actions) })
|
||||
}
|
||||
const updaterCtx = { options }
|
||||
updaterCtx.options = {
|
||||
effects: {
|
||||
...options.effects,
|
||||
constRetry: () => updater(updaterCtx.options),
|
||||
},
|
||||
}
|
||||
return updater(updaterCtx.options)
|
||||
}
|
||||
get<Id extends T.ActionId>(actionId: Id): AllActions[Id] {
|
||||
return this.actions[actionId]
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user