mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
Merge pull request #2512 from Start9Labs/integration/new-container-runtime
[feature]: new container runtime
This commit is contained in:
10
.github/workflows/startos-iso.yaml
vendored
10
.github/workflows/startos-iso.yaml
vendored
@@ -12,9 +12,6 @@ on:
|
||||
- dev
|
||||
- unstable
|
||||
- dev-unstable
|
||||
- docker
|
||||
- dev-docker
|
||||
- dev-unstable-docker
|
||||
runner:
|
||||
type: choice
|
||||
description: Runner
|
||||
@@ -82,9 +79,12 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
|
||||
- name: Set up QEMU
|
||||
- name: Set up docker QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up system QEMU
|
||||
run: sudo apt-get update && sudo apt-get install -y qemu-user-static
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
@@ -172,6 +172,8 @@ jobs:
|
||||
- name: Prevent rebuild of compiled artifacts
|
||||
run: |
|
||||
mkdir -p web/dist/raw
|
||||
touch core/startos/bindings
|
||||
mkdir -p container-runtime/dist
|
||||
PLATFORM=${{ matrix.platform }} make -t compiled-${{ env.ARCH }}.tar
|
||||
|
||||
- name: Run iso build
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -28,4 +28,5 @@ secrets.db
|
||||
/dpkg-workdir
|
||||
/compiled.tar
|
||||
/compiled-*.tar
|
||||
/firmware
|
||||
/firmware
|
||||
/tmp
|
||||
90
Makefile
90
Makefile
@@ -6,17 +6,17 @@ BASENAME := $(shell ./basename.sh)
|
||||
PLATFORM := $(shell if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi)
|
||||
ARCH := $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo aarch64; else echo $(PLATFORM) | sed 's/-nonfree$$//g'; fi)
|
||||
IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo iso; fi)
|
||||
BINS := core/target/$(ARCH)-unknown-linux-gnu/release/startbox core/target/aarch64-unknown-linux-musl/release/container-init core/target/x86_64-unknown-linux-musl/release/container-init
|
||||
BINS := core/target/$(ARCH)-unknown-linux-musl/release/startbox core/target/$(ARCH)-unknown-linux-musl/release/containerbox
|
||||
WEB_UIS := web/dist/raw/ui web/dist/raw/setup-wizard web/dist/raw/diagnostic-ui web/dist/raw/install-wizard
|
||||
FIRMWARE_ROMS := ./firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json)
|
||||
BUILD_SRC := $(shell git ls-files build) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS)
|
||||
BUILD_SRC := $(shell git ls-files build) build/lib/depends build/lib/conflicts container-runtime/rootfs.$(ARCH).squashfs $(FIRMWARE_ROMS)
|
||||
DEBIAN_SRC := $(shell git ls-files debian/)
|
||||
IMAGE_RECIPE_SRC := $(shell git ls-files image-recipe/)
|
||||
STARTD_SRC := core/startos/startd.service $(BUILD_SRC)
|
||||
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) web/dist/static web/patchdb-ui-seed.json $(GIT_HASH_FILE)
|
||||
CORE_SRC := $(shell git ls-files -- core ':!:core/startos/bindings/*') $(shell git ls-files --recurse-submodules patch-db) web/dist/static web/patchdb-ui-seed.json $(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 web/config.json patch-db/client/dist web/patchdb-ui-seed.json
|
||||
WEB_UI_SRC := $(shell git ls-files web/projects/ui)
|
||||
WEB_SETUP_WIZARD_SRC := $(shell git ls-files web/projects/setup-wizard)
|
||||
@@ -25,8 +25,8 @@ WEB_INSTALL_WIZARD_SRC := $(shell git ls-files web/projects/install-wizard)
|
||||
PATCH_DB_CLIENT_SRC := $(shell git ls-files --recurse-submodules patch-db/client)
|
||||
GZIP_BIN := $(shell which pigz || which gzip)
|
||||
TAR_BIN := $(shell which gtar || which tar)
|
||||
COMPILED_TARGETS := $(BINS) system-images/compat/docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar system-images/binfmt/docker-images/$(ARCH).tar
|
||||
ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-gnu/release/pi-beep; fi) $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then echo cargo-deps/$(ARCH)-unknown-linux-gnu/release/tokio-console; fi') $(PLATFORM_FILE)
|
||||
COMPILED_TARGETS := $(BINS) system-images/compat/docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar system-images/binfmt/docker-images/$(ARCH).tar container-runtime/rootfs.$(ARCH).squashfs
|
||||
ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; fi) $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; fi') $(PLATFORM_FILE) sdk/lib/test
|
||||
|
||||
ifeq ($(REMOTE),)
|
||||
mkdir = mkdir -p $1
|
||||
@@ -49,10 +49,13 @@ endif
|
||||
|
||||
.DELETE_ON_ERROR:
|
||||
|
||||
.PHONY: all metadata install clean format sdk snapshots uis ui reflash deb $(IMAGE_TYPE) squashfs sudo wormhole test
|
||||
.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs sudo wormhole test
|
||||
|
||||
all: $(ALL_TARGETS)
|
||||
|
||||
touch:
|
||||
touch $(ALL_TARGETS)
|
||||
|
||||
metadata: $(VERSION_FILE) $(PLATFORM_FILE) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE)
|
||||
|
||||
sudo:
|
||||
@@ -74,6 +77,11 @@ clean:
|
||||
rm -rf image-recipe/deb
|
||||
rm -rf results
|
||||
rm -rf build/lib/firmware
|
||||
rm -rf container-runtime/dist
|
||||
rm -rf container-runtime/node_modules
|
||||
rm -f container-runtime/*.squashfs
|
||||
rm -rf sdk/dist
|
||||
rm -rf sdk/node_modules
|
||||
rm -f ENVIRONMENT.txt
|
||||
rm -f PLATFORM.txt
|
||||
rm -f GIT_HASH.txt
|
||||
@@ -82,11 +90,13 @@ clean:
|
||||
format:
|
||||
cd core && cargo +nightly fmt
|
||||
|
||||
test: $(CORE_SRC) $(ENVIRONMENT_FILE)
|
||||
cd core && cargo build && cargo test
|
||||
test: $(CORE_SRC) $(ENVIRONMENT_FILE)
|
||||
(cd core && cargo build && cargo test)
|
||||
npm --prefix sdk exec -- prettier -w ./core/startos/bindings/*.ts
|
||||
(cd sdk && make test)
|
||||
|
||||
sdk:
|
||||
cd core && ./install-sdk.sh
|
||||
cli:
|
||||
cd core && ./install-cli.sh
|
||||
|
||||
deb: results/$(BASENAME).deb
|
||||
|
||||
@@ -104,17 +114,15 @@ results/$(BASENAME).$(IMAGE_TYPE) results/$(BASENAME).squashfs: $(IMAGE_RECIPE_S
|
||||
./image-recipe/run-local-build.sh "results/$(BASENAME).deb"
|
||||
|
||||
# For creating os images. DO NOT USE
|
||||
install: $(ALL_TARGETS)
|
||||
install: $(ALL_TARGETS)
|
||||
$(call mkdir,$(DESTDIR)/usr/bin)
|
||||
$(call cp,core/target/$(ARCH)-unknown-linux-gnu/release/startbox,$(DESTDIR)/usr/bin/startbox)
|
||||
$(call cp,core/target/$(ARCH)-unknown-linux-musl/release/startbox,$(DESTDIR)/usr/bin/startbox)
|
||||
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/startd)
|
||||
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-cli)
|
||||
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-sdk)
|
||||
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-deno)
|
||||
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/avahi-alias)
|
||||
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/embassy-cli)
|
||||
if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,cargo-deps/aarch64-unknown-linux-gnu/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi
|
||||
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then $(call cp,cargo-deps/$(ARCH)-unknown-linux-gnu/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); fi
|
||||
if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,cargo-deps/aarch64-unknown-linux-musl/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi
|
||||
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then $(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); fi
|
||||
|
||||
$(call mkdir,$(DESTDIR)/lib/systemd/system)
|
||||
$(call cp,core/startos/startd.service,$(DESTDIR)/lib/systemd/system/startd.service)
|
||||
@@ -122,16 +130,14 @@ install: $(ALL_TARGETS)
|
||||
$(call mkdir,$(DESTDIR)/usr/lib)
|
||||
$(call rm,$(DESTDIR)/usr/lib/startos)
|
||||
$(call cp,build/lib,$(DESTDIR)/usr/lib/startos)
|
||||
$(call mkdir,$(DESTDIR)/usr/lib/startos/container-runtime)
|
||||
$(call cp,container-runtime/rootfs.$(ARCH).squashfs,$(DESTDIR)/usr/lib/startos/container-runtime/rootfs.squashfs)
|
||||
|
||||
$(call cp,PLATFORM.txt,$(DESTDIR)/usr/lib/startos/PLATFORM.txt)
|
||||
$(call cp,ENVIRONMENT.txt,$(DESTDIR)/usr/lib/startos/ENVIRONMENT.txt)
|
||||
$(call cp,GIT_HASH.txt,$(DESTDIR)/usr/lib/startos/GIT_HASH.txt)
|
||||
$(call cp,VERSION.txt,$(DESTDIR)/usr/lib/startos/VERSION.txt)
|
||||
|
||||
$(call mkdir,$(DESTDIR)/usr/lib/startos/container)
|
||||
$(call cp,core/target/aarch64-unknown-linux-musl/release/container-init,$(DESTDIR)/usr/lib/startos/container/container-init.arm64)
|
||||
$(call cp,core/target/x86_64-unknown-linux-musl/release/container-init,$(DESTDIR)/usr/lib/startos/container/container-init.amd64)
|
||||
|
||||
$(call mkdir,$(DESTDIR)/usr/lib/startos/system-images)
|
||||
$(call cp,system-images/compat/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/compat.tar)
|
||||
$(call cp,system-images/utils/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/utils.tar)
|
||||
@@ -148,8 +154,9 @@ update-overlay: $(ALL_TARGETS)
|
||||
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) PLATFORM=$(PLATFORM)
|
||||
$(call ssh,"sudo systemctl start startd")
|
||||
|
||||
wormhole: core/target/$(ARCH)-unknown-linux-gnu/release/startbox
|
||||
@wormhole send core/target/$(ARCH)-unknown-linux-gnu/release/startbox 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade \"cd /usr/bin && rm startbox && wormhole receive --accept-file %s && chmod +x startbox\"\n", $$3 }'
|
||||
wormhole: core/target/$(ARCH)-unknown-linux-musl/release/startbox
|
||||
@echo "Paste the following command into the shell of your start-os server:"
|
||||
@wormhole send core/target/$(ARCH)-unknown-linux-musl/release/startbox 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade \"cd /usr/bin && rm startbox && wormhole receive --accept-file %s && chmod +x startbox\"\n", $$3 }'
|
||||
|
||||
update: $(ALL_TARGETS)
|
||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||
@@ -166,13 +173,39 @@ emulate-reflash: $(ALL_TARGETS)
|
||||
upload-ota: results/$(BASENAME).squashfs
|
||||
TARGET=$(TARGET) KEY=$(KEY) ./upload-ota.sh
|
||||
|
||||
container-runtime/alpine.$(ARCH).squashfs:
|
||||
ARCH=$(ARCH) ./container-runtime/download-base-image.sh
|
||||
|
||||
container-runtime/node_modules: container-runtime/package.json container-runtime/package-lock.json sdk/dist
|
||||
npm --prefix container-runtime ci
|
||||
touch container-runtime/node_modules
|
||||
|
||||
core/startos/bindings: $(shell git ls-files -- core ':!:core/startos/bindings/*') $(ENVIRONMENT_FILE)
|
||||
rm -rf core/startos/bindings
|
||||
(cd core/ && cargo test --features=test)
|
||||
npm --prefix sdk exec -- prettier -w ./core/startos/bindings/*.ts
|
||||
|
||||
sdk/dist: $(shell git ls-files sdk) core/startos/bindings
|
||||
(cd sdk && make bundle)
|
||||
|
||||
# TODO: make container-runtime its own makefile?
|
||||
container-runtime/dist/index.js: container-runtime/node_modules $(shell git ls-files container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json
|
||||
npm --prefix container-runtime run build
|
||||
|
||||
container-runtime/dist/node_modules container-runtime/dist/package.json container-runtime/dist/package-lock.json: container-runtime/package.json container-runtime/package-lock.json sdk/dist container-runtime/install-dist-deps.sh
|
||||
./container-runtime/install-dist-deps.sh
|
||||
touch container-runtime/dist/node_modules
|
||||
|
||||
container-runtime/rootfs.$(ARCH).squashfs: container-runtime/alpine.$(ARCH).squashfs container-runtime/containerRuntime.rc container-runtime/update-image.sh container-runtime/dist/index.js container-runtime/dist/node_modules core/target/$(ARCH)-unknown-linux-musl/release/containerbox | sudo
|
||||
ARCH=$(ARCH) ./container-runtime/update-image.sh
|
||||
|
||||
build/lib/depends build/lib/conflicts: build/dpkg-deps/*
|
||||
build/dpkg-deps/generate.sh
|
||||
|
||||
$(FIRMWARE_ROMS): build/lib/firmware.json download-firmware.sh $(PLATFORM_FILE)
|
||||
./download-firmware.sh $(PLATFORM)
|
||||
|
||||
system-images/compat/docker-images/$(ARCH).tar: $(COMPAT_SRC) core/Cargo.lock
|
||||
system-images/compat/docker-images/$(ARCH).tar: $(COMPAT_SRC)
|
||||
cd system-images/compat && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar
|
||||
|
||||
system-images/utils/docker-images/$(ARCH).tar: $(UTILS_SRC)
|
||||
@@ -181,15 +214,12 @@ system-images/utils/docker-images/$(ARCH).tar: $(UTILS_SRC)
|
||||
system-images/binfmt/docker-images/$(ARCH).tar: $(BINFMT_SRC)
|
||||
cd system-images/binfmt && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar
|
||||
|
||||
snapshots: core/snapshot-creator/Cargo.toml
|
||||
cd core/ && ARCH=aarch64 ./build-v8-snapshot.sh
|
||||
cd core/ && ARCH=x86_64 ./build-v8-snapshot.sh
|
||||
|
||||
$(BINS): $(CORE_SRC) $(ENVIRONMENT_FILE)
|
||||
cd core && ARCH=$(ARCH) ./build-prod.sh
|
||||
touch $(BINS)
|
||||
|
||||
web/node_modules: web/package.json
|
||||
web/node_modules: web/package.json sdk/dist
|
||||
(cd sdk && make bundle)
|
||||
npm --prefix web ci
|
||||
|
||||
web/dist/raw/ui: $(WEB_UI_SRC) $(WEB_SHARED_SRC)
|
||||
@@ -231,8 +261,8 @@ uis: $(WEB_UIS)
|
||||
# this is a convenience step to build the UI
|
||||
ui: web/dist/raw/ui
|
||||
|
||||
cargo-deps/aarch64-unknown-linux-gnu/release/pi-beep:
|
||||
cargo-deps/aarch64-unknown-linux-musl/release/pi-beep:
|
||||
ARCH=aarch64 ./build-cargo-dep.sh pi-beep
|
||||
|
||||
cargo-deps/$(ARCH)-unknown-linux-gnu/release/tokio-console:
|
||||
cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console:
|
||||
ARCH=$(ARCH) ./build-cargo-dep.sh tokio-console
|
||||
@@ -18,8 +18,8 @@ if [ -z "$ARCH" ]; then
|
||||
fi
|
||||
|
||||
mkdir -p cargo-deps
|
||||
alias 'rust-arm64-builder'='docker run $USE_TTY --rm -v "$HOME/.cargo/registry":/usr/local/cargo/registry -v "$(pwd)"/cargo-deps:/home/rust/src -P start9/rust-arm-cross:aarch64'
|
||||
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)"/cargo-deps:/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
||||
|
||||
rust-arm64-builder cargo install "$1" --target-dir /home/rust/src --target=$ARCH-unknown-linux-gnu
|
||||
rust-musl-builder cargo install "$1" --target-dir /home/rust/src --target=$ARCH-unknown-linux-musl
|
||||
sudo chown -R $USER cargo-deps
|
||||
sudo chown -R $USER ~/.cargo
|
||||
4
build/.gitignore
vendored
4
build/.gitignore
vendored
@@ -1,2 +1,2 @@
|
||||
lib/depends
|
||||
lib/conflicts
|
||||
/lib/depends
|
||||
/lib/conflicts
|
||||
@@ -20,12 +20,12 @@ httpdirfs
|
||||
iotop
|
||||
iw
|
||||
jq
|
||||
libavahi-client3
|
||||
libyajl2
|
||||
linux-cpupower
|
||||
lm-sensors
|
||||
lshw
|
||||
lvm2
|
||||
lxc
|
||||
magic-wormhole
|
||||
man-db
|
||||
ncdu
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
+ containerd.io
|
||||
+ docker-ce
|
||||
+ docker-ce-cli
|
||||
+ docker-compose-plugin
|
||||
- podman
|
||||
@@ -1,13 +1,13 @@
|
||||
[
|
||||
{
|
||||
"id": "pureboot-librem_mini_v2-basic_usb_autoboot_blob_jail-Release-28.3",
|
||||
"id": "pureboot-librem_mini_v2-basic_usb_autoboot_blob_jail-Release-29",
|
||||
"platform": ["x86_64"],
|
||||
"system-product-name": "librem_mini_v2",
|
||||
"bios-version": {
|
||||
"semver-prefix": "PureBoot-Release-",
|
||||
"semver-range": "<28.3"
|
||||
"semver-range": "<29"
|
||||
},
|
||||
"url": "https://source.puri.sm/firmware/releases/-/raw/master/librem_mini_v2/custom/pureboot-librem_mini_v2-basic_usb_autoboot_blob_jail-Release-28.3.rom.gz",
|
||||
"shasum": "5019bcf53f7493c7aa74f8ef680d18b5fc26ec156c705a841433aaa2fdef8f35"
|
||||
"url": "https://source.puri.sm/firmware/releases/-/raw/master/librem_mini_v2/custom/pureboot-librem_mini_v2-basic_usb_autoboot_blob_jail-Release-29.rom.gz",
|
||||
"shasum": "96ec04f21b1cfe8e28d9a2418f1ff533efe21f9bbbbf16e162f7c814761b068b"
|
||||
}
|
||||
]
|
||||
|
||||
8
container-runtime/.gitignore
vendored
Normal file
8
container-runtime/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
dist/
|
||||
bundle.js
|
||||
startInit.js
|
||||
service/
|
||||
service.js
|
||||
*.squashfs
|
||||
/tmp
|
||||
4
container-runtime/Dockerfile
Normal file
4
container-runtime/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
ADD ./startInit.js /usr/local/lib/startInit.js
|
||||
ADD ./entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
59
container-runtime/RPCSpec.md
Normal file
59
container-runtime/RPCSpec.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Container RPC SERVER Specification
|
||||
|
||||
## Methods
|
||||
|
||||
### init
|
||||
initialize runtime (mount `/proc`, `/sys`, `/dev`, and `/run` to each image in `/media/images`)
|
||||
|
||||
called after os has mounted js and images to the container
|
||||
#### args
|
||||
`[]`
|
||||
#### response
|
||||
`null`
|
||||
|
||||
### exit
|
||||
shutdown runtime
|
||||
#### args
|
||||
`[]`
|
||||
#### response
|
||||
`null`
|
||||
|
||||
### start
|
||||
run main method if not already running
|
||||
#### args
|
||||
`[]`
|
||||
#### response
|
||||
`null`
|
||||
|
||||
### stop
|
||||
stop main method by sending SIGTERM to child processes, and SIGKILL after timeout
|
||||
#### args
|
||||
`{ timeout: millis }`
|
||||
#### response
|
||||
`null`
|
||||
|
||||
### execute
|
||||
run a specific package procedure
|
||||
#### args
|
||||
```ts
|
||||
{
|
||||
procedure: JsonPath,
|
||||
input: any,
|
||||
timeout: millis,
|
||||
}
|
||||
```
|
||||
#### response
|
||||
`any`
|
||||
|
||||
### sandbox
|
||||
run a specific package procedure in sandbox mode
|
||||
#### args
|
||||
```ts
|
||||
{
|
||||
procedure: JsonPath,
|
||||
input: any,
|
||||
timeout: millis,
|
||||
}
|
||||
```
|
||||
#### response
|
||||
`any`
|
||||
10
container-runtime/containerRuntime.rc
Normal file
10
container-runtime/containerRuntime.rc
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
name=containerRuntime
|
||||
#cfgfile="/etc/containerRuntime/containerRuntime.conf"
|
||||
command="/usr/bin/node"
|
||||
command_args="--experimental-detect-module --unhandled-rejections=warn /usr/lib/startos/init/index.js"
|
||||
pidfile="/run/containerRuntime.pid"
|
||||
command_background="yes"
|
||||
output_log="/var/log/containerRuntime.log"
|
||||
error_log="/var/log/containerRuntime.err"
|
||||
19
container-runtime/download-base-image.sh
Executable file
19
container-runtime/download-base-image.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
set -e
|
||||
|
||||
DISTRO=alpine
|
||||
VERSION=3.19
|
||||
ARCH=${ARCH:-$(uname -m)}
|
||||
FLAVOR=default
|
||||
|
||||
_ARCH=$ARCH
|
||||
if [ "$_ARCH" = "x86_64" ]; then
|
||||
_ARCH=amd64
|
||||
elif [ "$_ARCH" = "aarch64" ]; then
|
||||
_ARCH=arm64
|
||||
fi
|
||||
|
||||
curl https://images.linuxcontainers.org/$(curl --silent https://images.linuxcontainers.org/meta/1.0/index-system | grep "^$DISTRO;$VERSION;$_ARCH;$FLAVOR;" | head -n1 | sed 's/^.*;//g')/rootfs.squashfs --output alpine.${ARCH}.squashfs
|
||||
10
container-runtime/install-dist-deps.sh
Executable file
10
container-runtime/install-dist-deps.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
set -e
|
||||
|
||||
cat ./package.json | sed 's/file:\.\([.\/]\)/file:..\/.\1/g' > ./dist/package.json
|
||||
cat ./package-lock.json | sed 's/"\.\([.\/]\)/"..\/.\1/g' > ./dist/package-lock.json
|
||||
|
||||
npm --prefix dist ci --omit=dev
|
||||
28
container-runtime/mkcontainer.sh
Normal file
28
container-runtime/mkcontainer.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
IMAGE=$1
|
||||
|
||||
if [ -z "$IMAGE" ]; then
|
||||
>&2 echo "usage: $0 <image id>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [ -d "/media/images/$IMAGE" ]; then
|
||||
>&2 echo "image does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
container=$(mktemp -d)
|
||||
mkdir -p $container/rootfs $container/upper $container/work
|
||||
mount -t overlay -olowerdir=/media/images/$IMAGE,upperdir=$container/upper,workdir=$container/work overlay $container/rootfs
|
||||
|
||||
rootfs=$container/rootfs
|
||||
|
||||
for special in dev sys proc run; do
|
||||
mkdir -p $rootfs/$special
|
||||
mount --bind /$special $rootfs/$special
|
||||
done
|
||||
|
||||
echo $rootfs
|
||||
4897
container-runtime/package-lock.json
generated
Normal file
4897
container-runtime/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
container-runtime/package.json
Normal file
37
container-runtime/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "start-init",
|
||||
"version": "0.0.0",
|
||||
"description": "We want to be the sdk intermitent for the system",
|
||||
"module": "./index.js",
|
||||
"scripts": {
|
||||
"check": "tsc --noEmit",
|
||||
"build": "prettier --write '**/*.ts' && rm -rf dist && tsc",
|
||||
"tsc": "rm -rf dist; tsc"
|
||||
},
|
||||
"author": "",
|
||||
"prettier": {
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": false
|
||||
},
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@start9labs/start-sdk": "file:../sdk/dist",
|
||||
"esbuild-plugin-resolve": "^2.0.0",
|
||||
"filebrowser": "^1.0.0",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"node-fetch": "^3.1.0",
|
||||
"ts-matches": "^5.4.1",
|
||||
"tslib": "^2.5.3",
|
||||
"typescript": "^5.1.3",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/cli": "^0.1.62",
|
||||
"@swc/core": "^1.3.65",
|
||||
"@types/node": "^20.11.13",
|
||||
"prettier": "^3.2.5",
|
||||
"typescript": ">5.2"
|
||||
}
|
||||
}
|
||||
86
container-runtime/readme.md
Normal file
86
container-runtime/readme.md
Normal file
@@ -0,0 +1,86 @@
|
||||
## 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}}}
|
||||
|
||||
```
|
||||
12
container-runtime/rmcontainer.sh
Normal file
12
container-runtime/rmcontainer.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
rootfs=$1
|
||||
if [ -z "$rootfs" ]; then
|
||||
>&2 echo "usage: $0 <container rootfs path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
umount --recursive $rootfs
|
||||
rm -rf $rootfs/..
|
||||
290
container-runtime/src/Adapters/HostSystemStartOs.ts
Normal file
290
container-runtime/src/Adapters/HostSystemStartOs.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import { types as T } from "@start9labs/start-sdk"
|
||||
import * as net from "net"
|
||||
import { object, string, number, literals, some, unknown } from "ts-matches"
|
||||
import { Effects } from "../Models/Effects"
|
||||
|
||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||
const matchRpcError = object({
|
||||
error: object(
|
||||
{
|
||||
code: number,
|
||||
message: string,
|
||||
data: some(
|
||||
string,
|
||||
object(
|
||||
{
|
||||
details: string,
|
||||
debug: string,
|
||||
},
|
||||
["debug"],
|
||||
),
|
||||
),
|
||||
},
|
||||
["data"],
|
||||
),
|
||||
})
|
||||
const testRpcError = matchRpcError.test
|
||||
const testRpcResult = object({
|
||||
result: unknown,
|
||||
}).test
|
||||
type RpcError = typeof matchRpcError._TYPE
|
||||
|
||||
const SOCKET_PATH = "/media/startos/rpc/host.sock"
|
||||
const MAIN = "/main" as const
|
||||
export class HostSystemStartOs implements Effects {
|
||||
static of(callbackHolder: CallbackHolder) {
|
||||
return new HostSystemStartOs(callbackHolder)
|
||||
}
|
||||
|
||||
constructor(readonly callbackHolder: CallbackHolder) {}
|
||||
id = 0
|
||||
rpcRound<K extends keyof Effects | "getStore" | "setStore">(
|
||||
method: K,
|
||||
params: unknown,
|
||||
) {
|
||||
const id = this.id++
|
||||
const client = net.createConnection({ path: SOCKET_PATH }, () => {
|
||||
client.write(
|
||||
JSON.stringify({
|
||||
id,
|
||||
method,
|
||||
params,
|
||||
}) + "\n",
|
||||
)
|
||||
})
|
||||
let bufs: Buffer[] = []
|
||||
return new Promise((resolve, reject) => {
|
||||
client.on("data", (data) => {
|
||||
try {
|
||||
bufs.push(data)
|
||||
if (data.reduce((acc, x) => acc || x == 10, false)) {
|
||||
const res: unknown = JSON.parse(
|
||||
Buffer.concat(bufs).toString().split("\n")[0],
|
||||
)
|
||||
if (testRpcError(res)) {
|
||||
let message = res.error.message
|
||||
console.error({ method, params, hostSystemStartOs: true })
|
||||
if (string.test(res.error.data)) {
|
||||
message += ": " + res.error.data
|
||||
console.error(res.error.data)
|
||||
} else {
|
||||
if (res.error.data?.details) {
|
||||
message += ": " + res.error.data.details
|
||||
console.error(res.error.data.details)
|
||||
}
|
||||
if (res.error.data?.debug) {
|
||||
message += "\n" + res.error.data.debug
|
||||
console.error("Debug: " + res.error.data.debug)
|
||||
}
|
||||
}
|
||||
reject(new Error(`${message}@${method}`))
|
||||
} else if (testRpcResult(res)) {
|
||||
resolve(res.result)
|
||||
} else {
|
||||
reject(new Error(`malformed response ${JSON.stringify(res)}`))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
client.end()
|
||||
})
|
||||
client.on("error", (error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
bind(...[options]: Parameters<T.Effects["bind"]>) {
|
||||
return this.rpcRound("bind", options) as ReturnType<T.Effects["bind"]>
|
||||
}
|
||||
clearBindings(...[]: Parameters<T.Effects["clearBindings"]>) {
|
||||
return this.rpcRound("clearBindings", null) as ReturnType<
|
||||
T.Effects["clearBindings"]
|
||||
>
|
||||
}
|
||||
clearServiceInterfaces(
|
||||
...[]: Parameters<T.Effects["clearServiceInterfaces"]>
|
||||
) {
|
||||
return this.rpcRound("clearServiceInterfaces", null) as ReturnType<
|
||||
T.Effects["clearServiceInterfaces"]
|
||||
>
|
||||
}
|
||||
createOverlayedImage(options: {
|
||||
imageId: string
|
||||
}): Promise<[string, string]> {
|
||||
return this.rpcRound("createOverlayedImage", options) as ReturnType<
|
||||
T.Effects["createOverlayedImage"]
|
||||
>
|
||||
}
|
||||
destroyOverlayedImage(options: { guid: string }): Promise<void> {
|
||||
return this.rpcRound("destroyOverlayedImage", options) as ReturnType<
|
||||
T.Effects["destroyOverlayedImage"]
|
||||
>
|
||||
}
|
||||
executeAction(...[options]: Parameters<T.Effects["executeAction"]>) {
|
||||
return this.rpcRound("executeAction", options) as ReturnType<
|
||||
T.Effects["executeAction"]
|
||||
>
|
||||
}
|
||||
exists(...[packageId]: Parameters<T.Effects["exists"]>) {
|
||||
return this.rpcRound("exists", packageId) as ReturnType<T.Effects["exists"]>
|
||||
}
|
||||
exportAction(...[options]: Parameters<T.Effects["exportAction"]>) {
|
||||
return this.rpcRound("exportAction", options) as ReturnType<
|
||||
T.Effects["exportAction"]
|
||||
>
|
||||
}
|
||||
exportServiceInterface: Effects["exportServiceInterface"] = (
|
||||
...[options]: Parameters<Effects["exportServiceInterface"]>
|
||||
) => {
|
||||
return this.rpcRound("exportServiceInterface", options) as ReturnType<
|
||||
T.Effects["exportServiceInterface"]
|
||||
>
|
||||
}
|
||||
exposeForDependents(...[options]: any) {
|
||||
return this.rpcRound("exposeForDependents", null) as ReturnType<
|
||||
T.Effects["exposeForDependents"]
|
||||
>
|
||||
}
|
||||
getConfigured(...[]: Parameters<T.Effects["getConfigured"]>) {
|
||||
return this.rpcRound("getConfigured", null) as ReturnType<
|
||||
T.Effects["getConfigured"]
|
||||
>
|
||||
}
|
||||
getContainerIp(...[]: Parameters<T.Effects["getContainerIp"]>) {
|
||||
return this.rpcRound("getContainerIp", null) as ReturnType<
|
||||
T.Effects["getContainerIp"]
|
||||
>
|
||||
}
|
||||
getHostInfo: Effects["getHostInfo"] = (...[allOptions]: any[]) => {
|
||||
const options = {
|
||||
...allOptions,
|
||||
callback: this.callbackHolder.addCallback(allOptions.callback),
|
||||
}
|
||||
return this.rpcRound("getHostInfo", options) as ReturnType<
|
||||
T.Effects["getHostInfo"]
|
||||
> as any
|
||||
}
|
||||
getServiceInterface(
|
||||
...[options]: Parameters<T.Effects["getServiceInterface"]>
|
||||
) {
|
||||
return this.rpcRound("getServiceInterface", {
|
||||
...options,
|
||||
callback: this.callbackHolder.addCallback(options.callback),
|
||||
}) as ReturnType<T.Effects["getServiceInterface"]>
|
||||
}
|
||||
|
||||
getPrimaryUrl(...[options]: Parameters<T.Effects["getPrimaryUrl"]>) {
|
||||
return this.rpcRound("getPrimaryUrl", {
|
||||
...options,
|
||||
callback: this.callbackHolder.addCallback(options.callback),
|
||||
}) as ReturnType<T.Effects["getPrimaryUrl"]>
|
||||
}
|
||||
getServicePortForward(
|
||||
...[options]: Parameters<T.Effects["getServicePortForward"]>
|
||||
) {
|
||||
return this.rpcRound("getServicePortForward", options) as ReturnType<
|
||||
T.Effects["getServicePortForward"]
|
||||
>
|
||||
}
|
||||
getSslCertificate(options: Parameters<T.Effects["getSslCertificate"]>[0]) {
|
||||
return this.rpcRound("getSslCertificate", options) as ReturnType<
|
||||
T.Effects["getSslCertificate"]
|
||||
>
|
||||
}
|
||||
getSslKey(options: Parameters<T.Effects["getSslKey"]>[0]) {
|
||||
return this.rpcRound("getSslKey", options) as ReturnType<
|
||||
T.Effects["getSslKey"]
|
||||
>
|
||||
}
|
||||
getSystemSmtp(...[options]: Parameters<T.Effects["getSystemSmtp"]>) {
|
||||
return this.rpcRound("getSystemSmtp", {
|
||||
...options,
|
||||
callback: this.callbackHolder.addCallback(options.callback),
|
||||
}) as ReturnType<T.Effects["getSystemSmtp"]>
|
||||
}
|
||||
listServiceInterfaces(
|
||||
...[options]: Parameters<T.Effects["listServiceInterfaces"]>
|
||||
) {
|
||||
return this.rpcRound("listServiceInterfaces", {
|
||||
...options,
|
||||
callback: this.callbackHolder.addCallback(options.callback),
|
||||
}) as ReturnType<T.Effects["listServiceInterfaces"]>
|
||||
}
|
||||
mount(...[options]: Parameters<T.Effects["mount"]>) {
|
||||
return this.rpcRound("mount", options) as ReturnType<T.Effects["mount"]>
|
||||
}
|
||||
removeAction(...[options]: Parameters<T.Effects["removeAction"]>) {
|
||||
return this.rpcRound("removeAction", options) as ReturnType<
|
||||
T.Effects["removeAction"]
|
||||
>
|
||||
}
|
||||
removeAddress(...[options]: Parameters<T.Effects["removeAddress"]>) {
|
||||
return this.rpcRound("removeAddress", options) as ReturnType<
|
||||
T.Effects["removeAddress"]
|
||||
>
|
||||
}
|
||||
restart(...[]: Parameters<T.Effects["restart"]>) {
|
||||
return this.rpcRound("restart", null)
|
||||
}
|
||||
reverseProxy(...[options]: Parameters<T.Effects["reverseProxy"]>) {
|
||||
return this.rpcRound("reverseProxy", options) as ReturnType<
|
||||
T.Effects["reverseProxy"]
|
||||
>
|
||||
}
|
||||
running(...[packageId]: Parameters<T.Effects["running"]>) {
|
||||
return this.rpcRound("running", { packageId }) as ReturnType<
|
||||
T.Effects["running"]
|
||||
>
|
||||
}
|
||||
// runRsync(...[options]: Parameters<T.Effects[""]>) {
|
||||
//
|
||||
// return this.rpcRound('executeAction', options) as ReturnType<T.Effects["executeAction"]>
|
||||
//
|
||||
// return this.rpcRound('executeAction', options) as ReturnType<T.Effects["executeAction"]>
|
||||
// }
|
||||
setConfigured(...[configured]: Parameters<T.Effects["setConfigured"]>) {
|
||||
return this.rpcRound("setConfigured", { configured }) as ReturnType<
|
||||
T.Effects["setConfigured"]
|
||||
>
|
||||
}
|
||||
setDependencies(
|
||||
...[dependencies]: Parameters<T.Effects["setDependencies"]>
|
||||
): ReturnType<T.Effects["setDependencies"]> {
|
||||
return this.rpcRound("setDependencies", { dependencies }) as ReturnType<
|
||||
T.Effects["setDependencies"]
|
||||
>
|
||||
}
|
||||
setHealth(...[options]: Parameters<T.Effects["setHealth"]>) {
|
||||
return this.rpcRound("setHealth", options) as ReturnType<
|
||||
T.Effects["setHealth"]
|
||||
>
|
||||
}
|
||||
|
||||
setMainStatus(o: { status: "running" | "stopped" }): Promise<void> {
|
||||
return this.rpcRound("setMainStatus", o) as ReturnType<
|
||||
T.Effects["setHealth"]
|
||||
>
|
||||
}
|
||||
|
||||
shutdown(...[]: Parameters<T.Effects["shutdown"]>) {
|
||||
return this.rpcRound("shutdown", null)
|
||||
}
|
||||
stopped(...[packageId]: Parameters<T.Effects["stopped"]>) {
|
||||
return this.rpcRound("stopped", { packageId }) as ReturnType<
|
||||
T.Effects["stopped"]
|
||||
>
|
||||
}
|
||||
store: T.Effects["store"] = {
|
||||
get: async (options: any) =>
|
||||
this.rpcRound("getStore", {
|
||||
...options,
|
||||
callback: this.callbackHolder.addCallback(options.callback),
|
||||
}) as any,
|
||||
set: async (options: any) =>
|
||||
this.rpcRound("setStore", options) as ReturnType<
|
||||
T.Effects["store"]["set"]
|
||||
>,
|
||||
}
|
||||
}
|
||||
312
container-runtime/src/Adapters/RpcListener.ts
Normal file
312
container-runtime/src/Adapters/RpcListener.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
// @ts-check
|
||||
|
||||
import * as net from "net"
|
||||
import {
|
||||
object,
|
||||
some,
|
||||
string,
|
||||
literal,
|
||||
array,
|
||||
number,
|
||||
matches,
|
||||
any,
|
||||
shape,
|
||||
anyOf,
|
||||
} from "ts-matches"
|
||||
|
||||
import { types as T } from "@start9labs/start-sdk"
|
||||
import * as CP from "child_process"
|
||||
import * as Mod from "module"
|
||||
import * as fs from "fs"
|
||||
|
||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||
import { AllGetDependencies } from "../Interfaces/AllGetDependencies"
|
||||
import { HostSystem } from "../Interfaces/HostSystem"
|
||||
import { jsonPath } from "../Models/JsonPath"
|
||||
import { System } from "../Interfaces/System"
|
||||
type MaybePromise<T> = T | Promise<T>
|
||||
export const matchRpcResult = anyOf(
|
||||
object({ result: any }),
|
||||
object({
|
||||
error: object(
|
||||
{
|
||||
code: number,
|
||||
message: string,
|
||||
data: object(
|
||||
{
|
||||
details: string,
|
||||
debug: any,
|
||||
},
|
||||
["details", "debug"],
|
||||
),
|
||||
},
|
||||
["data"],
|
||||
),
|
||||
}),
|
||||
)
|
||||
export type RpcResult = typeof matchRpcResult._TYPE
|
||||
type SocketResponse = { jsonrpc: "2.0"; id: IdType } & RpcResult
|
||||
|
||||
const SOCKET_PARENT = "/media/startos/rpc"
|
||||
const SOCKET_PATH = "/media/startos/rpc/service.sock"
|
||||
const jsonrpc = "2.0" as const
|
||||
|
||||
const idType = some(string, number, literal(null))
|
||||
type IdType = null | string | number
|
||||
const runType = object({
|
||||
id: idType,
|
||||
method: literal("execute"),
|
||||
params: object(
|
||||
{
|
||||
procedure: string,
|
||||
input: any,
|
||||
timeout: number,
|
||||
},
|
||||
["timeout"],
|
||||
),
|
||||
})
|
||||
const sandboxRunType = object({
|
||||
id: idType,
|
||||
method: literal("sandbox"),
|
||||
params: object(
|
||||
{
|
||||
procedure: string,
|
||||
input: any,
|
||||
timeout: number,
|
||||
},
|
||||
["timeout"],
|
||||
),
|
||||
})
|
||||
const callbackType = object({
|
||||
id: idType,
|
||||
method: literal("callback"),
|
||||
params: object({
|
||||
callback: string,
|
||||
args: array,
|
||||
}),
|
||||
})
|
||||
const initType = object({
|
||||
id: idType,
|
||||
method: literal("init"),
|
||||
})
|
||||
const exitType = object({
|
||||
id: idType,
|
||||
method: literal("exit"),
|
||||
})
|
||||
const evalType = object({
|
||||
id: idType,
|
||||
method: literal("eval"),
|
||||
params: object({
|
||||
script: string,
|
||||
}),
|
||||
})
|
||||
|
||||
const jsonParse = (x: Buffer) => JSON.parse(x.toString())
|
||||
function reduceMethod(
|
||||
methodArgs: object,
|
||||
effects: HostSystem,
|
||||
): (previousValue: any, currentValue: string) => any {
|
||||
return (x: any, method: string) =>
|
||||
Promise.resolve(x)
|
||||
.then((x) => x[method])
|
||||
.then((x) =>
|
||||
typeof x !== "function"
|
||||
? x
|
||||
: x({
|
||||
...methodArgs,
|
||||
effects,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const hasId = object({ id: idType }).test
|
||||
export class RpcListener {
|
||||
unixSocketServer = net.createServer(async (server) => {})
|
||||
private _system: System | undefined
|
||||
private _effects: HostSystem | undefined
|
||||
|
||||
constructor(
|
||||
readonly getDependencies: AllGetDependencies,
|
||||
private callbacks = new CallbackHolder(),
|
||||
) {
|
||||
if (!fs.existsSync(SOCKET_PARENT)) {
|
||||
fs.mkdirSync(SOCKET_PARENT, { recursive: true })
|
||||
}
|
||||
this.unixSocketServer.listen(SOCKET_PATH)
|
||||
|
||||
this.unixSocketServer.on("connection", (s) => {
|
||||
let id: IdType = null
|
||||
const captureId = <X>(x: X) => {
|
||||
if (hasId(x)) id = x.id
|
||||
return x
|
||||
}
|
||||
const logData =
|
||||
(location: string) =>
|
||||
<X>(x: X) => {
|
||||
console.log({
|
||||
location,
|
||||
stringified: JSON.stringify(x),
|
||||
type: typeof x,
|
||||
id,
|
||||
})
|
||||
return x
|
||||
}
|
||||
const mapError = (error: any): SocketResponse => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
error: {
|
||||
message: typeof error,
|
||||
data: {
|
||||
details: error?.message ?? String(error),
|
||||
debug: error?.stack,
|
||||
},
|
||||
code: 0,
|
||||
},
|
||||
})
|
||||
const writeDataToSocket = (x: SocketResponse) =>
|
||||
new Promise((resolve) => s.write(JSON.stringify(x), resolve))
|
||||
s.on("data", (a) =>
|
||||
Promise.resolve(a)
|
||||
.then(logData("dataIn"))
|
||||
.then(jsonParse)
|
||||
.then(captureId)
|
||||
.then((x) => this.dealWithInput(x))
|
||||
.catch(mapError)
|
||||
.then(logData("response"))
|
||||
.then(writeDataToSocket)
|
||||
.finally(() => void s.end()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private get effects() {
|
||||
return this.getDependencies.hostSystem()(this.callbacks)
|
||||
}
|
||||
|
||||
private get system() {
|
||||
if (!this._system) throw new Error("System not initialized")
|
||||
return this._system
|
||||
}
|
||||
|
||||
private dealWithInput(input: unknown): MaybePromise<SocketResponse> {
|
||||
return matches(input)
|
||||
.when(some(runType, sandboxRunType), async ({ id, params }) => {
|
||||
const system = this.system
|
||||
const procedure = jsonPath.unsafeCast(params.procedure)
|
||||
return system
|
||||
.execute(this.effects, {
|
||||
procedure,
|
||||
input: params.input,
|
||||
timeout: params.timeout,
|
||||
})
|
||||
.then((result) => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
...result,
|
||||
}))
|
||||
.then((x) => {
|
||||
if (
|
||||
("result" in x && x.result === undefined) ||
|
||||
!("error" in x || "result" in x)
|
||||
)
|
||||
(x as any).result = null
|
||||
return x
|
||||
})
|
||||
.catch((error) => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
error: {
|
||||
code: 0,
|
||||
message: typeof error,
|
||||
data: { details: "" + error, debug: error?.stack },
|
||||
},
|
||||
}))
|
||||
})
|
||||
.when(callbackType, async ({ id, params: { callback, args } }) =>
|
||||
Promise.resolve(this.callbacks.callCallback(callback, args))
|
||||
.then((result) => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
result,
|
||||
}))
|
||||
.catch((error) => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
|
||||
error: {
|
||||
code: 0,
|
||||
message: typeof error,
|
||||
data: {
|
||||
details: error?.message ?? String(error),
|
||||
debug: error?.stack,
|
||||
},
|
||||
},
|
||||
})),
|
||||
)
|
||||
.when(exitType, async ({ id }) => {
|
||||
if (this._system) this._system.exit(this.effects)
|
||||
delete this._system
|
||||
delete this._effects
|
||||
|
||||
return {
|
||||
jsonrpc,
|
||||
id,
|
||||
result: null,
|
||||
}
|
||||
})
|
||||
.when(initType, async ({ id }) => {
|
||||
this._system = await this.getDependencies.system()
|
||||
|
||||
return {
|
||||
jsonrpc,
|
||||
id,
|
||||
result: null,
|
||||
}
|
||||
})
|
||||
.when(evalType, async ({ id, params }) => {
|
||||
const result = await new Function(
|
||||
`return (async () => { return (${params.script}) }).call(this)`,
|
||||
).call({
|
||||
listener: this,
|
||||
require: require,
|
||||
})
|
||||
return {
|
||||
jsonrpc,
|
||||
id,
|
||||
result: !["string", "number", "boolean", "null", "object"].includes(
|
||||
typeof result,
|
||||
)
|
||||
? null
|
||||
: result,
|
||||
}
|
||||
})
|
||||
.when(shape({ id: idType, method: string }), ({ id, method }) => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
error: {
|
||||
code: -32601,
|
||||
message: `Method not found`,
|
||||
data: {
|
||||
details: method,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
.defaultToLazy(() => {
|
||||
console.warn(
|
||||
`Coudln't parse the following input ${JSON.stringify(input)}`,
|
||||
)
|
||||
return {
|
||||
jsonrpc,
|
||||
id: (input as any)?.id,
|
||||
error: {
|
||||
code: -32602,
|
||||
message: "invalid params",
|
||||
data: {
|
||||
details: JSON.stringify(input),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as cp from "child_process"
|
||||
import { Overlay, types as T } from "@start9labs/start-sdk"
|
||||
import { promisify } from "util"
|
||||
import { DockerProcedure, VolumeId } from "../../../Models/DockerProcedure"
|
||||
import { Volume } from "./matchVolume"
|
||||
export const exec = promisify(cp.exec)
|
||||
export const execFile = promisify(cp.execFile)
|
||||
|
||||
export class DockerProcedureContainer {
|
||||
private constructor(readonly overlay: Overlay) {}
|
||||
// static async readonlyOf(data: DockerProcedure) {
|
||||
// return DockerProcedureContainer.of(data, ["-o", "ro"])
|
||||
// }
|
||||
static async of(
|
||||
effects: T.Effects,
|
||||
data: DockerProcedure,
|
||||
volumes: { [id: VolumeId]: Volume },
|
||||
) {
|
||||
const overlay = await Overlay.of(effects, data.image)
|
||||
|
||||
if (data.mounts) {
|
||||
const mounts = data.mounts
|
||||
for (const mount in mounts) {
|
||||
const path = mounts[mount].startsWith("/")
|
||||
? `${overlay.rootfs}${mounts[mount]}`
|
||||
: `${overlay.rootfs}/${mounts[mount]}`
|
||||
await fs.mkdir(path, { recursive: true })
|
||||
const volumeMount = volumes[mount]
|
||||
if (volumeMount.type === "data") {
|
||||
await overlay.mount(
|
||||
{ type: "volume", id: mount, subpath: null, readonly: false },
|
||||
mounts[mount],
|
||||
)
|
||||
} else if (volumeMount.type === "assets") {
|
||||
await overlay.mount(
|
||||
{ type: "assets", id: mount, subpath: null },
|
||||
mounts[mount],
|
||||
)
|
||||
} else if (volumeMount.type === "certificate") {
|
||||
volumeMount
|
||||
const certChain = await effects.getSslCertificate({
|
||||
packageId: null,
|
||||
hostId: volumeMount["interface-id"],
|
||||
algorithm: null,
|
||||
})
|
||||
const key = await effects.getSslKey({
|
||||
packageId: null,
|
||||
hostId: volumeMount["interface-id"],
|
||||
algorithm: null,
|
||||
})
|
||||
await fs.writeFile(
|
||||
`${path}/${volumeMount["interface-id"]}.cert.pem`,
|
||||
certChain.join("\n"),
|
||||
)
|
||||
await fs.writeFile(
|
||||
`${path}/${volumeMount["interface-id"]}.key.pem`,
|
||||
key,
|
||||
)
|
||||
} else if (volumeMount.type === "pointer") {
|
||||
await effects.mount({
|
||||
location: path,
|
||||
target: {
|
||||
packageId: volumeMount["package-id"],
|
||||
subpath: volumeMount.path,
|
||||
readonly: volumeMount.readonly,
|
||||
volumeId: volumeMount["volume-id"],
|
||||
},
|
||||
})
|
||||
} else if (volumeMount.type === "backup") {
|
||||
throw new Error("TODO")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new DockerProcedureContainer(overlay)
|
||||
}
|
||||
|
||||
async exec(commands: string[]) {
|
||||
try {
|
||||
return await this.overlay.exec(commands)
|
||||
} finally {
|
||||
await this.overlay.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
async execSpawn(commands: string[]) {
|
||||
try {
|
||||
const spawned = await this.overlay.spawn(commands)
|
||||
return spawned
|
||||
} finally {
|
||||
await this.overlay.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
async spawn(commands: string[]): Promise<cp.ChildProcessWithoutNullStreams> {
|
||||
return await this.overlay.spawn(commands)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import { PolyfillEffects } from "./polyfillEffects"
|
||||
import { DockerProcedureContainer } from "./DockerProcedureContainer"
|
||||
import { SystemForEmbassy } from "."
|
||||
import { HostSystemStartOs } from "../../HostSystemStartOs"
|
||||
import { Daemons, T, daemons } from "@start9labs/start-sdk"
|
||||
|
||||
const EMBASSY_HEALTH_INTERVAL = 15 * 1000
|
||||
const EMBASSY_PROPERTIES_LOOP = 30 * 1000
|
||||
/**
|
||||
* We wanted something to represent what the main loop is doing, and
|
||||
* in this case it used to run the properties, health, and the docker/ js main.
|
||||
* Also, this has an ability to clean itself up too if need be.
|
||||
*/
|
||||
export class MainLoop {
|
||||
private healthLoops:
|
||||
| {
|
||||
name: string
|
||||
interval: NodeJS.Timeout
|
||||
}[]
|
||||
| undefined
|
||||
|
||||
private mainEvent:
|
||||
| Promise<{
|
||||
daemon: T.DaemonReturned
|
||||
wait: Promise<unknown>
|
||||
}>
|
||||
| undefined
|
||||
constructor(
|
||||
readonly system: SystemForEmbassy,
|
||||
readonly effects: HostSystemStartOs,
|
||||
) {
|
||||
this.healthLoops = this.constructHealthLoops()
|
||||
this.mainEvent = this.constructMainEvent()
|
||||
}
|
||||
|
||||
private async constructMainEvent() {
|
||||
const { system, effects } = this
|
||||
const currentCommand: [string, ...string[]] = [
|
||||
system.manifest.main.entrypoint,
|
||||
...system.manifest.main.args,
|
||||
]
|
||||
|
||||
await effects.setMainStatus({ status: "running" })
|
||||
const jsMain = (this.system.moduleCode as any)?.jsMain
|
||||
const dockerProcedureContainer = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
this.system.manifest.main,
|
||||
this.system.manifest.volumes,
|
||||
)
|
||||
if (jsMain) {
|
||||
const daemons = Daemons.of({
|
||||
effects,
|
||||
started: async (_) => {},
|
||||
healthReceipts: [],
|
||||
})
|
||||
throw new Error("todo")
|
||||
// return {
|
||||
// daemon,
|
||||
// wait: daemon.wait().finally(() => {
|
||||
// this.clean()
|
||||
// effects.setMainStatus({ status: "stopped" })
|
||||
// }),
|
||||
// }
|
||||
}
|
||||
const daemon = await daemons.runDaemon()(
|
||||
this.effects,
|
||||
this.system.manifest.main.image,
|
||||
currentCommand,
|
||||
{
|
||||
overlay: dockerProcedureContainer.overlay,
|
||||
},
|
||||
)
|
||||
return {
|
||||
daemon,
|
||||
wait: daemon.wait().finally(() => {
|
||||
this.clean()
|
||||
effects
|
||||
.setMainStatus({ status: "stopped" })
|
||||
.catch((e) => console.error("Could not set the status to stopped"))
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
public async clean(options?: { timeout?: number }) {
|
||||
const { mainEvent, healthLoops } = this
|
||||
const main = await mainEvent
|
||||
delete this.mainEvent
|
||||
delete this.healthLoops
|
||||
if (mainEvent) await main?.daemon.term()
|
||||
if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval))
|
||||
}
|
||||
|
||||
private constructHealthLoops() {
|
||||
const { manifest } = this.system
|
||||
const effects = this.effects
|
||||
const start = Date.now()
|
||||
return Object.entries(manifest["health-checks"]).map(
|
||||
([healthId, value]) => {
|
||||
const interval = setInterval(async () => {
|
||||
const actionProcedure = value
|
||||
const timeChanged = Date.now() - start
|
||||
if (actionProcedure.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
actionProcedure,
|
||||
manifest.volumes,
|
||||
)
|
||||
const executed = await container.execSpawn([
|
||||
actionProcedure.entrypoint,
|
||||
...actionProcedure.args,
|
||||
JSON.stringify(timeChanged),
|
||||
])
|
||||
if (executed.exitCode === 59) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "disabled",
|
||||
message:
|
||||
executed.stderr.toString() || executed.stdout.toString(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (executed.exitCode === 60) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "starting",
|
||||
message:
|
||||
executed.stderr.toString() || executed.stdout.toString(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (executed.exitCode === 61) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "loading",
|
||||
message:
|
||||
executed.stderr.toString() || executed.stdout.toString(),
|
||||
})
|
||||
return
|
||||
}
|
||||
const errorMessage = executed.stderr.toString()
|
||||
const message = executed.stdout.toString()
|
||||
if (!!errorMessage) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "failure",
|
||||
message: errorMessage,
|
||||
})
|
||||
return
|
||||
}
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "success",
|
||||
message,
|
||||
})
|
||||
return
|
||||
} else {
|
||||
actionProcedure
|
||||
const moduleCode = await this.system.moduleCode
|
||||
const method = moduleCode.health?.[healthId]
|
||||
if (!method) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "failure",
|
||||
message: `Expecting that the js health check ${healthId} exists`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const result = await method(
|
||||
new PolyfillEffects(effects, this.system.manifest),
|
||||
timeChanged,
|
||||
)
|
||||
|
||||
if ("result" in result) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "success",
|
||||
message: null,
|
||||
})
|
||||
return
|
||||
}
|
||||
if ("error" in result) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "failure",
|
||||
message: result.error,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!("error-code" in result)) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "failure",
|
||||
message: `Unknown error type ${JSON.stringify(result)}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
const [code, message] = result["error-code"]
|
||||
if (code === 59) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "disabled",
|
||||
message,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (code === 60) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "starting",
|
||||
message,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (code === 61) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "loading",
|
||||
message,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "failure",
|
||||
message: `${result["error-code"][0]}: ${result["error-code"][1]}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
}, EMBASSY_HEALTH_INTERVAL)
|
||||
|
||||
return { name: healthId, interval }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
806
container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts
Normal file
806
container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts
Normal file
@@ -0,0 +1,806 @@
|
||||
import { types as T, utils, EmVer } from "@start9labs/start-sdk"
|
||||
import * as fs from "fs/promises"
|
||||
|
||||
import { PolyfillEffects } from "./polyfillEffects"
|
||||
import { Duration, duration } from "../../../Models/Duration"
|
||||
import { System } from "../../../Interfaces/System"
|
||||
import { matchManifest, Manifest, Procedure } from "./matchManifest"
|
||||
import * as childProcess from "node:child_process"
|
||||
import { DockerProcedureContainer } from "./DockerProcedureContainer"
|
||||
import { promisify } from "node:util"
|
||||
import * as U from "./oldEmbassyTypes"
|
||||
import { MainLoop } from "./MainLoop"
|
||||
import {
|
||||
matches,
|
||||
boolean,
|
||||
dictionary,
|
||||
literal,
|
||||
literals,
|
||||
object,
|
||||
string,
|
||||
unknown,
|
||||
any,
|
||||
tuple,
|
||||
number,
|
||||
anyOf,
|
||||
deferred,
|
||||
Parser,
|
||||
} from "ts-matches"
|
||||
import { HostSystemStartOs } from "../../HostSystemStartOs"
|
||||
import { JsonPath, unNestPath } from "../../../Models/JsonPath"
|
||||
import { RpcResult, matchRpcResult } from "../../RpcListener"
|
||||
import { InputSpec } from "@start9labs/start-sdk/cjs/sdk/lib/config/configTypes"
|
||||
|
||||
type Optional<A> = A | undefined | null
|
||||
function todo(): never {
|
||||
throw new Error("Not implemented")
|
||||
}
|
||||
const execFile = promisify(childProcess.execFile)
|
||||
|
||||
const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json"
|
||||
const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js"
|
||||
const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig"
|
||||
|
||||
export type PackagePropertiesV2 = {
|
||||
[name: string]: PackagePropertyObject | PackagePropertyString
|
||||
}
|
||||
export type PackagePropertyString = {
|
||||
type: "string"
|
||||
description?: string
|
||||
value: string
|
||||
/** Let's the ui make this copyable button */
|
||||
copyable?: boolean
|
||||
/** Let the ui create a qr for this field */
|
||||
qr?: boolean
|
||||
/** Hiding the value unless toggled off for field */
|
||||
masked?: boolean
|
||||
}
|
||||
export type PackagePropertyObject = {
|
||||
value: PackagePropertiesV2
|
||||
type: "object"
|
||||
description: string
|
||||
}
|
||||
|
||||
const asProperty_ = (
|
||||
x: PackagePropertyString | PackagePropertyObject,
|
||||
): T.PropertiesValue => {
|
||||
if (x.type === "object") {
|
||||
return {
|
||||
...x,
|
||||
value: Object.fromEntries(
|
||||
Object.entries(x.value).map(([key, value]) => [
|
||||
key,
|
||||
asProperty_(value),
|
||||
]),
|
||||
),
|
||||
}
|
||||
}
|
||||
return {
|
||||
masked: false,
|
||||
description: null,
|
||||
qr: null,
|
||||
copyable: null,
|
||||
...x,
|
||||
}
|
||||
}
|
||||
const asProperty = (x: PackagePropertiesV2): T.PropertiesReturn =>
|
||||
Object.fromEntries(
|
||||
Object.entries(x).map(([key, value]) => [key, asProperty_(value)]),
|
||||
)
|
||||
const [matchPackageProperties, setMatchPackageProperties] =
|
||||
deferred<PackagePropertiesV2>()
|
||||
const matchPackagePropertyObject: Parser<unknown, PackagePropertyObject> =
|
||||
object({
|
||||
value: matchPackageProperties,
|
||||
type: literal("object"),
|
||||
description: string,
|
||||
})
|
||||
|
||||
const matchPackagePropertyString: Parser<unknown, PackagePropertyString> =
|
||||
object(
|
||||
{
|
||||
type: literal("string"),
|
||||
description: string,
|
||||
value: string,
|
||||
copyable: boolean,
|
||||
qr: boolean,
|
||||
masked: boolean,
|
||||
},
|
||||
["copyable", "description", "qr", "masked"],
|
||||
)
|
||||
setMatchPackageProperties(
|
||||
dictionary([
|
||||
string,
|
||||
anyOf(matchPackagePropertyObject, matchPackagePropertyString),
|
||||
]),
|
||||
)
|
||||
|
||||
const matchProperties = object({
|
||||
version: literal(2),
|
||||
data: matchPackageProperties,
|
||||
})
|
||||
|
||||
export class SystemForEmbassy implements System {
|
||||
currentRunning: MainLoop | undefined
|
||||
static async of(manifestLocation: string = MANIFEST_LOCATION) {
|
||||
const moduleCode = await import(EMBASSY_JS_LOCATION)
|
||||
.catch((_) => require(EMBASSY_JS_LOCATION))
|
||||
.catch(async (_) => {
|
||||
console.error("Could not load the js")
|
||||
console.error({
|
||||
exists: await fs.stat(EMBASSY_JS_LOCATION),
|
||||
})
|
||||
return {}
|
||||
})
|
||||
const manifestData = await fs.readFile(manifestLocation, "utf-8")
|
||||
return new SystemForEmbassy(
|
||||
matchManifest.unsafeCast(JSON.parse(manifestData)),
|
||||
moduleCode,
|
||||
)
|
||||
}
|
||||
constructor(
|
||||
readonly manifest: Manifest,
|
||||
readonly moduleCode: Partial<U.ExpectedExports>,
|
||||
) {}
|
||||
async execute(
|
||||
effects: HostSystemStartOs,
|
||||
options: {
|
||||
procedure: JsonPath
|
||||
input: unknown
|
||||
timeout?: number | undefined
|
||||
},
|
||||
): Promise<RpcResult> {
|
||||
return this._execute(effects, options)
|
||||
.then((x) =>
|
||||
matches(x)
|
||||
.when(
|
||||
object({
|
||||
result: any,
|
||||
}),
|
||||
(x) => x,
|
||||
)
|
||||
.when(
|
||||
object({
|
||||
error: string,
|
||||
}),
|
||||
(x) => ({
|
||||
error: {
|
||||
code: 0,
|
||||
message: x.error,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
object({
|
||||
"error-code": tuple(number, string),
|
||||
}),
|
||||
({ "error-code": [code, message] }) => ({
|
||||
error: {
|
||||
code,
|
||||
message,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.defaultTo({ result: x }),
|
||||
)
|
||||
.catch((error: unknown) => {
|
||||
if (error instanceof Error)
|
||||
return {
|
||||
error: {
|
||||
code: 0,
|
||||
message: error.name,
|
||||
data: {
|
||||
details: error.message,
|
||||
debug: `${error?.cause ?? "[noCause]"}:${error?.stack ?? "[noStack]"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
if (matchRpcResult.test(error)) return error
|
||||
return {
|
||||
error: {
|
||||
code: 0,
|
||||
message: String(error),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
async exit(effects: HostSystemStartOs): Promise<void> {
|
||||
if (this.currentRunning) await this.currentRunning.clean()
|
||||
delete this.currentRunning
|
||||
}
|
||||
async _execute(
|
||||
effects: HostSystemStartOs,
|
||||
options: {
|
||||
procedure: JsonPath
|
||||
input: unknown
|
||||
timeout?: number | undefined
|
||||
},
|
||||
): Promise<unknown> {
|
||||
const input = options.input
|
||||
switch (options.procedure) {
|
||||
case "/backup/create":
|
||||
return this.createBackup(effects)
|
||||
case "/backup/restore":
|
||||
return this.restoreBackup(effects)
|
||||
case "/config/get":
|
||||
return this.getConfig(effects)
|
||||
case "/config/set":
|
||||
return this.setConfig(effects, input)
|
||||
case "/properties":
|
||||
return this.properties(effects)
|
||||
case "/actions/metadata":
|
||||
return todo()
|
||||
case "/init":
|
||||
return this.init(effects, string.optional().unsafeCast(input))
|
||||
case "/uninit":
|
||||
return this.uninit(effects, string.optional().unsafeCast(input))
|
||||
case "/main/start":
|
||||
return this.mainStart(effects)
|
||||
case "/main/stop":
|
||||
return this.mainStop(effects)
|
||||
default:
|
||||
const procedures = unNestPath(options.procedure)
|
||||
switch (true) {
|
||||
case procedures[1] === "actions" && procedures[3] === "get":
|
||||
return this.action(effects, procedures[2], input)
|
||||
case procedures[1] === "actions" && procedures[3] === "run":
|
||||
return this.action(effects, procedures[2], input)
|
||||
case procedures[1] === "dependencies" && procedures[3] === "query":
|
||||
return this.dependenciesAutoconfig(effects, procedures[2], input)
|
||||
|
||||
case procedures[1] === "dependencies" && procedures[3] === "update":
|
||||
return this.dependenciesAutoconfig(effects, procedures[2], input)
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not find the path for ${options.procedure}`)
|
||||
}
|
||||
private async init(
|
||||
effects: HostSystemStartOs,
|
||||
previousVersion: Optional<string>,
|
||||
): Promise<void> {
|
||||
if (previousVersion) await this.migration(effects, previousVersion)
|
||||
await effects.setMainStatus({ status: "stopped" })
|
||||
await this.exportActions(effects)
|
||||
}
|
||||
async exportActions(effects: HostSystemStartOs) {
|
||||
const manifest = this.manifest
|
||||
if (!manifest.actions) return
|
||||
for (const [actionId, action] of Object.entries(manifest.actions)) {
|
||||
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"
|
||||
await effects.exportAction({
|
||||
id: actionId,
|
||||
metadata: {
|
||||
name: action.name,
|
||||
description: action.description,
|
||||
warning: action.warning || null,
|
||||
input: action["input-spec"] as InputSpec,
|
||||
disabled: false,
|
||||
allowedStatuses,
|
||||
group: null,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
private async uninit(
|
||||
effects: HostSystemStartOs,
|
||||
nextVersion: Optional<string>,
|
||||
): Promise<void> {
|
||||
// TODO Do a migration down if the version exists
|
||||
await effects.setMainStatus({ status: "stopped" })
|
||||
}
|
||||
private async mainStart(effects: HostSystemStartOs): Promise<void> {
|
||||
if (!!this.currentRunning) return
|
||||
|
||||
this.currentRunning = new MainLoop(this, effects)
|
||||
}
|
||||
private async mainStop(
|
||||
effects: HostSystemStartOs,
|
||||
options?: { timeout?: number },
|
||||
): Promise<Duration> {
|
||||
const { currentRunning } = this
|
||||
delete this.currentRunning
|
||||
if (currentRunning) {
|
||||
await currentRunning.clean({
|
||||
timeout: options?.timeout || this.manifest.main["sigterm-timeout"],
|
||||
})
|
||||
}
|
||||
return duration(this.manifest.main["sigterm-timeout"], "s")
|
||||
}
|
||||
private async createBackup(effects: HostSystemStartOs): Promise<void> {
|
||||
const backup = this.manifest.backup.create
|
||||
if (backup.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
backup,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
await container.exec([backup.entrypoint, ...backup.args])
|
||||
} else {
|
||||
const moduleCode = await this.moduleCode
|
||||
await moduleCode.createBackup?.(
|
||||
new PolyfillEffects(effects, this.manifest),
|
||||
)
|
||||
}
|
||||
}
|
||||
private async restoreBackup(effects: HostSystemStartOs): Promise<void> {
|
||||
const restoreBackup = this.manifest.backup.restore
|
||||
if (restoreBackup.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
restoreBackup,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
await container.exec([restoreBackup.entrypoint, ...restoreBackup.args])
|
||||
} else {
|
||||
const moduleCode = await this.moduleCode
|
||||
await moduleCode.restoreBackup?.(
|
||||
new PolyfillEffects(effects, this.manifest),
|
||||
)
|
||||
}
|
||||
}
|
||||
private async getConfig(effects: HostSystemStartOs): Promise<T.ConfigRes> {
|
||||
return this.getConfigUncleaned(effects).then(removePointers)
|
||||
}
|
||||
private async getConfigUncleaned(
|
||||
effects: HostSystemStartOs,
|
||||
): Promise<T.ConfigRes> {
|
||||
const config = this.manifest.config?.get
|
||||
if (!config) return { spec: {} }
|
||||
if (config.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
config,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
// TODO: yaml
|
||||
return JSON.parse(
|
||||
(
|
||||
await container.exec([config.entrypoint, ...config.args])
|
||||
).stdout.toString(),
|
||||
)
|
||||
} else {
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.getConfig
|
||||
if (!method) throw new Error("Expecting that the method getConfig exists")
|
||||
return (await method(new PolyfillEffects(effects, this.manifest)).then(
|
||||
(x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
},
|
||||
)) as any
|
||||
}
|
||||
}
|
||||
private async setConfig(
|
||||
effects: HostSystemStartOs,
|
||||
newConfigWithoutPointers: unknown,
|
||||
): Promise<T.SetResult> {
|
||||
const newConfig = structuredClone(newConfigWithoutPointers)
|
||||
await updateConfig(
|
||||
effects,
|
||||
await this.getConfigUncleaned(effects).then((x) => x.spec),
|
||||
newConfig,
|
||||
)
|
||||
const setConfigValue = this.manifest.config?.set
|
||||
if (!setConfigValue) return { signal: "SIGTERM", "depends-on": {} }
|
||||
if (setConfigValue.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
setConfigValue,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
return JSON.parse(
|
||||
(
|
||||
await container.exec([
|
||||
setConfigValue.entrypoint,
|
||||
...setConfigValue.args,
|
||||
JSON.stringify(newConfig),
|
||||
])
|
||||
).stdout.toString(),
|
||||
)
|
||||
} else if (setConfigValue.type === "script") {
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.setConfig
|
||||
if (!method) throw new Error("Expecting that the method setConfig exists")
|
||||
return await method(
|
||||
new PolyfillEffects(effects, this.manifest),
|
||||
newConfig as U.Config,
|
||||
).then((x): T.SetResult => {
|
||||
if ("result" in x)
|
||||
return {
|
||||
"depends-on": x.result["depends-on"],
|
||||
signal: x.result.signal === "SIGEMT" ? "SIGTERM" : x.result.signal,
|
||||
}
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
})
|
||||
} else {
|
||||
return {
|
||||
"depends-on": {},
|
||||
signal: "SIGTERM",
|
||||
}
|
||||
}
|
||||
}
|
||||
private async migration(
|
||||
effects: HostSystemStartOs,
|
||||
fromVersion: string,
|
||||
): Promise<T.MigrationRes> {
|
||||
const fromEmver = EmVer.from(fromVersion)
|
||||
const currentEmver = EmVer.from(this.manifest.version)
|
||||
if (!this.manifest.migrations) return { configured: true }
|
||||
const fromMigration = Object.entries(this.manifest.migrations.from)
|
||||
.map(([version, procedure]) => [EmVer.from(version), procedure] as const)
|
||||
.find(
|
||||
([versionEmver, procedure]) =>
|
||||
versionEmver.greaterThan(fromEmver) &&
|
||||
versionEmver.lessThanOrEqual(currentEmver),
|
||||
)
|
||||
const toMigration = Object.entries(this.manifest.migrations.to)
|
||||
.map(([version, procedure]) => [EmVer.from(version), procedure] as const)
|
||||
.find(
|
||||
([versionEmver, procedure]) =>
|
||||
versionEmver.greaterThan(fromEmver) &&
|
||||
versionEmver.lessThanOrEqual(currentEmver),
|
||||
)
|
||||
|
||||
// prettier-ignore
|
||||
const migration = (
|
||||
fromEmver.greaterThan(currentEmver) ? [toMigration, fromMigration] :
|
||||
[fromMigration, toMigration]).filter(Boolean)[0]
|
||||
|
||||
if (migration) {
|
||||
const [version, procedure] = migration
|
||||
if (procedure.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
procedure,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
return JSON.parse(
|
||||
(
|
||||
await container.exec([
|
||||
procedure.entrypoint,
|
||||
...procedure.args,
|
||||
JSON.stringify(fromVersion),
|
||||
])
|
||||
).stdout.toString(),
|
||||
)
|
||||
} else if (procedure.type === "script") {
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.migration
|
||||
if (!method)
|
||||
throw new Error("Expecting that the method migration exists")
|
||||
return (await method(
|
||||
new PolyfillEffects(effects, this.manifest),
|
||||
fromVersion as string,
|
||||
).then((x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
})) as any
|
||||
}
|
||||
}
|
||||
return { configured: true }
|
||||
}
|
||||
private async properties(
|
||||
effects: HostSystemStartOs,
|
||||
): Promise<ReturnType<T.ExpectedExports.Properties>> {
|
||||
// TODO BLU-J set the properties ever so often
|
||||
const setConfigValue = this.manifest.properties
|
||||
if (!setConfigValue) throw new Error("There is no properties")
|
||||
if (setConfigValue.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
setConfigValue,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
const properties = matchProperties.unsafeCast(
|
||||
JSON.parse(
|
||||
(
|
||||
await container.exec([
|
||||
setConfigValue.entrypoint,
|
||||
...setConfigValue.args,
|
||||
])
|
||||
).stdout.toString(),
|
||||
),
|
||||
)
|
||||
return asProperty(properties.data)
|
||||
} else if (setConfigValue.type === "script") {
|
||||
const moduleCode = this.moduleCode
|
||||
const method = moduleCode.properties
|
||||
if (!method)
|
||||
throw new Error("Expecting that the method properties exists")
|
||||
const properties = matchProperties.unsafeCast(
|
||||
await method(new PolyfillEffects(effects, this.manifest)).then((x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
}),
|
||||
)
|
||||
return asProperty(properties.data)
|
||||
}
|
||||
throw new Error(`Unknown type in the fetch properties: ${setConfigValue}`)
|
||||
}
|
||||
private async health(
|
||||
effects: HostSystemStartOs,
|
||||
healthId: string,
|
||||
timeSinceStarted: unknown,
|
||||
): Promise<void> {
|
||||
const healthProcedure = this.manifest["health-checks"][healthId]
|
||||
if (!healthProcedure) return
|
||||
if (healthProcedure.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
healthProcedure,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
return JSON.parse(
|
||||
(
|
||||
await container.exec([
|
||||
healthProcedure.entrypoint,
|
||||
...healthProcedure.args,
|
||||
JSON.stringify(timeSinceStarted),
|
||||
])
|
||||
).stdout.toString(),
|
||||
)
|
||||
} else if (healthProcedure.type === "script") {
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.health?.[healthId]
|
||||
if (!method) throw new Error("Expecting that the method health exists")
|
||||
await method(
|
||||
new PolyfillEffects(effects, this.manifest),
|
||||
Number(timeSinceStarted),
|
||||
).then((x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
})
|
||||
}
|
||||
}
|
||||
private async action(
|
||||
effects: HostSystemStartOs,
|
||||
actionId: string,
|
||||
formData: unknown,
|
||||
): Promise<T.ActionResult> {
|
||||
const actionProcedure = this.manifest.actions?.[actionId]?.implementation
|
||||
if (!actionProcedure) return { message: "Action not found", value: null }
|
||||
if (actionProcedure.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
actionProcedure,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
return JSON.parse(
|
||||
(
|
||||
await container.exec([
|
||||
actionProcedure.entrypoint,
|
||||
...actionProcedure.args,
|
||||
JSON.stringify(formData),
|
||||
])
|
||||
).stdout.toString(),
|
||||
)
|
||||
} else {
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.action?.[actionId]
|
||||
if (!method) throw new Error("Expecting that the method action exists")
|
||||
return (await method(
|
||||
new PolyfillEffects(effects, this.manifest),
|
||||
formData as any,
|
||||
).then((x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
})) as any
|
||||
}
|
||||
}
|
||||
private async dependenciesCheck(
|
||||
effects: HostSystemStartOs,
|
||||
id: string,
|
||||
oldConfig: unknown,
|
||||
): Promise<object> {
|
||||
const actionProcedure = this.manifest.dependencies?.[id]?.config?.check
|
||||
if (!actionProcedure) return { message: "Action not found", value: null }
|
||||
if (actionProcedure.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
actionProcedure,
|
||||
this.manifest.volumes,
|
||||
)
|
||||
return JSON.parse(
|
||||
(
|
||||
await container.exec([
|
||||
actionProcedure.entrypoint,
|
||||
...actionProcedure.args,
|
||||
JSON.stringify(oldConfig),
|
||||
])
|
||||
).stdout.toString(),
|
||||
)
|
||||
} else if (actionProcedure.type === "script") {
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.dependencies?.[id]?.check
|
||||
if (!method)
|
||||
throw new Error(
|
||||
`Expecting that the method dependency check ${id} exists`,
|
||||
)
|
||||
return (await method(
|
||||
new PolyfillEffects(effects, this.manifest),
|
||||
oldConfig as any,
|
||||
).then((x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
})) as any
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
private async dependenciesAutoconfig(
|
||||
effects: HostSystemStartOs,
|
||||
id: string,
|
||||
oldConfig: unknown,
|
||||
): Promise<void> {
|
||||
const moduleCode = await this.moduleCode
|
||||
const method = moduleCode.dependencies?.[id]?.autoConfigure
|
||||
if (!method)
|
||||
throw new Error(
|
||||
`Expecting that the method dependency autoConfigure ${id} exists`,
|
||||
)
|
||||
return (await method(
|
||||
new PolyfillEffects(effects, this.manifest),
|
||||
oldConfig as any,
|
||||
).then((x) => {
|
||||
if ("result" in x) return x.result
|
||||
if ("error" in x) throw new Error("Error getting config: " + x.error)
|
||||
throw new Error("Error getting config: " + x["error-code"][1])
|
||||
})) as any
|
||||
}
|
||||
}
|
||||
async function removePointers(value: T.ConfigRes): Promise<T.ConfigRes> {
|
||||
const startingSpec = structuredClone(value.spec)
|
||||
const config =
|
||||
value.config && cleanConfigFromPointers(value.config, startingSpec)
|
||||
const spec = cleanSpecOfPointers(startingSpec)
|
||||
|
||||
return { config, spec }
|
||||
}
|
||||
|
||||
const matchPointer = object({
|
||||
type: literal("pointer"),
|
||||
})
|
||||
|
||||
const matchPointerPackage = object({
|
||||
subtype: literal("package"),
|
||||
target: literals("tor-key", "tor-address", "lan-address"),
|
||||
"package-id": string,
|
||||
interface: string,
|
||||
})
|
||||
const matchPointerConfig = object({
|
||||
subtype: literal("package"),
|
||||
target: literals("config"),
|
||||
"package-id": string,
|
||||
selector: string,
|
||||
multi: boolean,
|
||||
})
|
||||
const matchSpec = object({
|
||||
spec: object,
|
||||
})
|
||||
const matchVariants = object({ variants: dictionary([string, unknown]) })
|
||||
function cleanSpecOfPointers<T>(mutSpec: T): T {
|
||||
if (!object.test(mutSpec)) return mutSpec
|
||||
for (const key in mutSpec) {
|
||||
const value = mutSpec[key]
|
||||
if (matchSpec.test(value)) value.spec = cleanSpecOfPointers(value.spec)
|
||||
if (matchVariants.test(value))
|
||||
value.variants = Object.fromEntries(
|
||||
Object.entries(value.variants).map(([key, value]) => [
|
||||
key,
|
||||
cleanSpecOfPointers(value),
|
||||
]),
|
||||
)
|
||||
if (!matchPointer.test(value)) continue
|
||||
delete mutSpec[key]
|
||||
// // if (value.target === )
|
||||
}
|
||||
|
||||
return mutSpec
|
||||
}
|
||||
function isKeyOf<O extends object>(
|
||||
key: string,
|
||||
ofObject: O,
|
||||
): key is keyof O & string {
|
||||
return key in ofObject
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
type CleanConfigFromPointers<C, S> =
|
||||
[C, S] extends [object, object] ? {
|
||||
[K in (keyof C & keyof S ) & string]: (
|
||||
S[K] extends {type: "pointer"} ? never :
|
||||
S[K] extends {spec: object & infer B} ? CleanConfigFromPointers<C[K], B> :
|
||||
C[K]
|
||||
)
|
||||
} :
|
||||
null
|
||||
|
||||
function cleanConfigFromPointers<C, S>(
|
||||
config: C,
|
||||
spec: S,
|
||||
): CleanConfigFromPointers<C, S> {
|
||||
const newConfig = {} as CleanConfigFromPointers<C, S>
|
||||
|
||||
if (!(object.test(config) && object.test(spec)) || newConfig == null)
|
||||
return null as CleanConfigFromPointers<C, S>
|
||||
|
||||
for (const key of Object.keys(spec)) {
|
||||
if (!isKeyOf(key, spec)) continue
|
||||
if (!isKeyOf(key, config)) continue
|
||||
const partSpec = spec[key]
|
||||
if (matchPointer.test(partSpec)) continue
|
||||
;(newConfig as any)[key] = matchSpec.test(partSpec)
|
||||
? cleanConfigFromPointers(config[key], partSpec.spec)
|
||||
: config[key]
|
||||
}
|
||||
return newConfig as CleanConfigFromPointers<C, S>
|
||||
}
|
||||
|
||||
async function updateConfig(
|
||||
effects: HostSystemStartOs,
|
||||
spec: unknown,
|
||||
mutConfigValue: unknown,
|
||||
) {
|
||||
if (!dictionary([string, unknown]).test(spec)) return
|
||||
if (!dictionary([string, unknown]).test(mutConfigValue)) return
|
||||
for (const key in spec) {
|
||||
const specValue = spec[key]
|
||||
|
||||
const newConfigValue = mutConfigValue[key]
|
||||
if (matchSpec.test(specValue)) {
|
||||
const updateObject = { spec: null }
|
||||
await updateConfig(effects, { spec: specValue.spec }, updateObject)
|
||||
mutConfigValue[key] = updateObject.spec
|
||||
}
|
||||
if (
|
||||
matchVariants.test(specValue) &&
|
||||
object({ tag: object({ id: string }) }).test(newConfigValue) &&
|
||||
newConfigValue.tag.id in specValue.variants
|
||||
) {
|
||||
// Not going to do anything on the variants...
|
||||
}
|
||||
if (!matchPointer.test(specValue)) continue
|
||||
if (matchPointerConfig.test(specValue)) {
|
||||
const configValue = (await effects.store.get({
|
||||
packageId: specValue["package-id"],
|
||||
callback() {},
|
||||
path: `${EMBASSY_POINTER_PATH_PREFIX}${specValue.selector}` as any,
|
||||
})) as any
|
||||
mutConfigValue[key] = configValue
|
||||
}
|
||||
if (matchPointerPackage.test(specValue)) {
|
||||
if (specValue.target === "tor-key")
|
||||
throw new Error("This service uses an unsupported target TorKey")
|
||||
const filled = await utils
|
||||
.getServiceInterface(effects, {
|
||||
packageId: specValue["package-id"],
|
||||
id: specValue.interface,
|
||||
})
|
||||
.once()
|
||||
.catch(() => null)
|
||||
|
||||
mutConfigValue[key] =
|
||||
filled === null
|
||||
? ""
|
||||
: specValue.target === "lan-address"
|
||||
? filled.addressInfo.localHostnames[0]
|
||||
: filled.addressInfo.onionHostnames[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
object,
|
||||
literal,
|
||||
string,
|
||||
array,
|
||||
boolean,
|
||||
dictionary,
|
||||
literals,
|
||||
number,
|
||||
unknown,
|
||||
some,
|
||||
every,
|
||||
} from "ts-matches"
|
||||
import { matchVolume } from "./matchVolume"
|
||||
import { matchDockerProcedure } from "../../../Models/DockerProcedure"
|
||||
|
||||
const matchJsProcedure = object(
|
||||
{
|
||||
type: literal("script"),
|
||||
args: array(unknown),
|
||||
},
|
||||
["args"],
|
||||
{
|
||||
args: [],
|
||||
},
|
||||
)
|
||||
|
||||
const matchProcedure = some(matchDockerProcedure, matchJsProcedure)
|
||||
export type Procedure = typeof matchProcedure._TYPE
|
||||
|
||||
const matchAction = object(
|
||||
{
|
||||
name: string,
|
||||
description: string,
|
||||
warning: string,
|
||||
implementation: matchProcedure,
|
||||
"allowed-statuses": array(literals("running", "stopped")),
|
||||
"input-spec": unknown,
|
||||
},
|
||||
["warning", "input-spec", "input-spec"],
|
||||
)
|
||||
export const matchManifest = object(
|
||||
{
|
||||
id: string,
|
||||
version: string,
|
||||
main: matchDockerProcedure,
|
||||
assets: object(
|
||||
{
|
||||
assets: string,
|
||||
scripts: string,
|
||||
},
|
||||
["assets", "scripts"],
|
||||
),
|
||||
"health-checks": dictionary([
|
||||
string,
|
||||
every(
|
||||
matchProcedure,
|
||||
object({
|
||||
name: string,
|
||||
}),
|
||||
),
|
||||
]),
|
||||
config: object({
|
||||
get: matchProcedure,
|
||||
set: matchProcedure,
|
||||
}),
|
||||
properties: matchProcedure,
|
||||
volumes: dictionary([string, matchVolume]),
|
||||
interfaces: dictionary([
|
||||
string,
|
||||
object({
|
||||
name: string,
|
||||
"tor-config": object({}),
|
||||
"lan-config": object({}),
|
||||
ui: boolean,
|
||||
protocols: array(string),
|
||||
}),
|
||||
]),
|
||||
backup: object({
|
||||
create: matchProcedure,
|
||||
restore: matchProcedure,
|
||||
}),
|
||||
migrations: object({
|
||||
to: dictionary([string, matchProcedure]),
|
||||
from: dictionary([string, matchProcedure]),
|
||||
}),
|
||||
dependencies: dictionary([
|
||||
string,
|
||||
object(
|
||||
{
|
||||
version: string,
|
||||
requirement: some(
|
||||
object({
|
||||
type: literal("opt-in"),
|
||||
how: string,
|
||||
}),
|
||||
object({
|
||||
type: literal("opt-out"),
|
||||
how: string,
|
||||
}),
|
||||
object({
|
||||
type: literal("required"),
|
||||
}),
|
||||
),
|
||||
description: string,
|
||||
config: object({
|
||||
check: matchProcedure,
|
||||
"auto-configure": matchProcedure,
|
||||
}),
|
||||
},
|
||||
["description", "config"],
|
||||
),
|
||||
]),
|
||||
|
||||
actions: dictionary([string, matchAction]),
|
||||
},
|
||||
["config", "actions", "properties", "migrations", "dependencies"],
|
||||
)
|
||||
export type Manifest = typeof matchManifest._TYPE
|
||||
@@ -0,0 +1,35 @@
|
||||
import { object, literal, string, boolean, some } from "ts-matches"
|
||||
|
||||
const matchDataVolume = object(
|
||||
{
|
||||
type: literal("data"),
|
||||
readonly: boolean,
|
||||
},
|
||||
["readonly"],
|
||||
)
|
||||
const matchAssetVolume = object({
|
||||
type: literal("assets"),
|
||||
})
|
||||
const matchPointerVolume = object({
|
||||
type: literal("pointer"),
|
||||
"package-id": string,
|
||||
"volume-id": string,
|
||||
path: string,
|
||||
readonly: boolean,
|
||||
})
|
||||
const matchCertificateVolume = object({
|
||||
type: literal("certificate"),
|
||||
"interface-id": string,
|
||||
})
|
||||
const matchBackupVolume = object({
|
||||
type: literal("backup"),
|
||||
readonly: boolean,
|
||||
})
|
||||
export const matchVolume = some(
|
||||
matchDataVolume,
|
||||
matchAssetVolume,
|
||||
matchPointerVolume,
|
||||
matchCertificateVolume,
|
||||
matchBackupVolume,
|
||||
)
|
||||
export type Volume = typeof matchVolume._TYPE
|
||||
@@ -0,0 +1,482 @@
|
||||
// deno-lint-ignore no-namespace
|
||||
export type ExpectedExports = {
|
||||
version: 2
|
||||
/** Set configuration is called after we have modified and saved the configuration in the embassy ui. Use this to make a file for the docker to read from for configuration. */
|
||||
setConfig: (effects: Effects, input: Config) => Promise<ResultType<SetResult>>
|
||||
/** Get configuration returns a shape that describes the format that the embassy ui will generate, and later send to the set config */
|
||||
getConfig: (effects: Effects) => Promise<ResultType<ConfigRes>>
|
||||
/** These are how we make sure the our dependency configurations are valid and if not how to fix them. */
|
||||
dependencies: Dependencies
|
||||
/** For backing up service data though the embassyOS UI */
|
||||
createBackup: (effects: Effects) => Promise<ResultType<unknown>>
|
||||
/** For restoring service data that was previously backed up using the embassyOS UI create backup flow. Backup restores are also triggered via the embassyOS UI, or doing a system restore flow during setup. */
|
||||
restoreBackup: (effects: Effects) => Promise<ResultType<unknown>>
|
||||
/** Properties are used to get values from the docker, like a username + password, what ports we are hosting from */
|
||||
properties: (effects: Effects) => Promise<ResultType<Properties>>
|
||||
health: {
|
||||
/** Should be the health check id */
|
||||
[id: string]: (
|
||||
effects: Effects,
|
||||
dateMs: number,
|
||||
) => Promise<ResultType<unknown>>
|
||||
}
|
||||
migration: (
|
||||
effects: Effects,
|
||||
version: string,
|
||||
...args: unknown[]
|
||||
) => Promise<ResultType<MigrationRes>>
|
||||
action: {
|
||||
[id: string]: (
|
||||
effects: Effects,
|
||||
config?: Config,
|
||||
) => Promise<ResultType<ActionResult>>
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the entrypoint for the main container. Used to start up something like the service that the
|
||||
* package represents, like running a bitcoind in a bitcoind-wrapper.
|
||||
*/
|
||||
main: (effects: Effects) => Promise<ResultType<unknown>>
|
||||
}
|
||||
|
||||
/** Used to reach out from the pure js runtime */
|
||||
export type Effects = {
|
||||
/** Usable when not sandboxed */
|
||||
writeFile(input: {
|
||||
path: string
|
||||
volumeId: string
|
||||
toWrite: string
|
||||
}): Promise<void>
|
||||
readFile(input: { volumeId: string; path: string }): Promise<string>
|
||||
metadata(input: { volumeId: string; path: string }): Promise<Metadata>
|
||||
/** Create a directory. Usable when not sandboxed */
|
||||
createDir(input: { volumeId: string; path: string }): Promise<string>
|
||||
|
||||
readDir(input: { volumeId: string; path: string }): Promise<string[]>
|
||||
/** Remove a directory. Usable when not sandboxed */
|
||||
removeDir(input: { volumeId: string; path: string }): Promise<string>
|
||||
removeFile(input: { volumeId: string; path: string }): Promise<void>
|
||||
|
||||
/** Write a json file into an object. Usable when not sandboxed */
|
||||
writeJsonFile(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
toWrite: Record<string, unknown>
|
||||
}): Promise<void>
|
||||
|
||||
/** Read a json file into an object */
|
||||
readJsonFile(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<Record<string, unknown>>
|
||||
|
||||
runCommand(input: {
|
||||
command: string
|
||||
args?: string[]
|
||||
timeoutMillis?: number
|
||||
}): Promise<ResultType<string>>
|
||||
runDaemon(input: { command: string; args?: string[] }): {
|
||||
wait(): Promise<ResultType<string>>
|
||||
term(): Promise<void>
|
||||
}
|
||||
|
||||
chown(input: { volumeId: string; path: string; uid: string }): Promise<null>
|
||||
chmod(input: { volumeId: string; path: string; mode: string }): Promise<null>
|
||||
|
||||
sleep(timeMs: number): Promise<null>
|
||||
|
||||
/** Log at the trace level */
|
||||
trace(whatToPrint: string): void
|
||||
/** Log at the warn level */
|
||||
warn(whatToPrint: string): void
|
||||
/** Log at the error level */
|
||||
error(whatToPrint: string): void
|
||||
/** Log at the debug level */
|
||||
debug(whatToPrint: string): void
|
||||
/** Log at the info level */
|
||||
info(whatToPrint: string): void
|
||||
|
||||
/** Sandbox mode lets us read but not write */
|
||||
is_sandboxed(): boolean
|
||||
|
||||
exists(input: { volumeId: string; path: string }): Promise<boolean>
|
||||
bindLocal(options: {
|
||||
internalPort: number
|
||||
name: string
|
||||
externalPort: number
|
||||
}): Promise<string>
|
||||
bindTor(options: {
|
||||
internalPort: number
|
||||
name: string
|
||||
externalPort: number
|
||||
}): Promise<string>
|
||||
|
||||
fetch(
|
||||
url: string,
|
||||
options?: {
|
||||
method?: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "PATCH"
|
||||
headers?: Record<string, string>
|
||||
body?: string
|
||||
},
|
||||
): Promise<{
|
||||
method: string
|
||||
ok: boolean
|
||||
status: number
|
||||
headers: Record<string, string>
|
||||
body?: string | null
|
||||
/// Returns the body as a string
|
||||
text(): Promise<string>
|
||||
/// Returns the body as a json
|
||||
json(): Promise<unknown>
|
||||
}>
|
||||
|
||||
runRsync(options: {
|
||||
srcVolume: string
|
||||
dstVolume: string
|
||||
srcPath: string
|
||||
dstPath: string
|
||||
// rsync options: https://linux.die.net/man/1/rsync
|
||||
options: BackupOptions
|
||||
}): {
|
||||
id: () => Promise<string>
|
||||
wait: () => Promise<null>
|
||||
progress: () => Promise<number>
|
||||
}
|
||||
}
|
||||
|
||||
// rsync options: https://linux.die.net/man/1/rsync
|
||||
export type BackupOptions = {
|
||||
delete: boolean
|
||||
force: boolean
|
||||
ignoreExisting: boolean
|
||||
exclude: string[]
|
||||
}
|
||||
export type Metadata = {
|
||||
fileType: string
|
||||
isDir: boolean
|
||||
isFile: boolean
|
||||
isSymlink: boolean
|
||||
len: number
|
||||
modified?: Date
|
||||
accessed?: Date
|
||||
created?: Date
|
||||
readonly: boolean
|
||||
uid: number
|
||||
gid: number
|
||||
mode: number
|
||||
}
|
||||
|
||||
export type MigrationRes = {
|
||||
configured: boolean
|
||||
}
|
||||
|
||||
export type ActionResult = {
|
||||
version: "0"
|
||||
message: string
|
||||
value?: string
|
||||
copyable: boolean
|
||||
qr: boolean
|
||||
}
|
||||
|
||||
export type ConfigRes = {
|
||||
/** This should be the previous config, that way during set config we start with the previous */
|
||||
config?: Config
|
||||
/** Shape that is describing the form in the ui */
|
||||
spec: ConfigSpec
|
||||
}
|
||||
export type Config = {
|
||||
[propertyName: string]: unknown
|
||||
}
|
||||
|
||||
export type ConfigSpec = {
|
||||
/** Given a config value, define what it should render with the following spec */
|
||||
[configValue: string]: ValueSpecAny
|
||||
}
|
||||
export type WithDefault<T, Default> = T & {
|
||||
default: Default
|
||||
}
|
||||
export type WithNullableDefault<T, Default> = T & {
|
||||
default?: Default
|
||||
}
|
||||
|
||||
export type WithDescription<T> = T & {
|
||||
description?: string
|
||||
name: string
|
||||
warning?: string
|
||||
}
|
||||
|
||||
export type WithOptionalDescription<T> = T & {
|
||||
/** @deprecated - optional only for backwards compatibility */
|
||||
description?: string
|
||||
/** @deprecated - optional only for backwards compatibility */
|
||||
name?: string
|
||||
warning?: string
|
||||
}
|
||||
|
||||
export type ListSpec<T> = {
|
||||
spec: T
|
||||
range: string
|
||||
}
|
||||
|
||||
export type Tag<T extends string, V> = V & {
|
||||
type: T
|
||||
}
|
||||
|
||||
export type Subtype<T extends string, V> = V & {
|
||||
subtype: T
|
||||
}
|
||||
|
||||
export type Target<T extends string, V> = V & {
|
||||
target: T
|
||||
}
|
||||
|
||||
export type UniqueBy =
|
||||
| {
|
||||
any: UniqueBy[]
|
||||
}
|
||||
| string
|
||||
| null
|
||||
|
||||
export type WithNullable<T> = T & {
|
||||
nullable: boolean
|
||||
}
|
||||
export type DefaultString =
|
||||
| string
|
||||
| {
|
||||
/** The chars available for the random generation */
|
||||
charset?: string
|
||||
/** Length that we generate to */
|
||||
len: number
|
||||
}
|
||||
|
||||
export type ValueSpecString = // deno-lint-ignore ban-types
|
||||
(
|
||||
| {}
|
||||
| {
|
||||
pattern: string
|
||||
"pattern-description": string
|
||||
}
|
||||
) & {
|
||||
copyable?: boolean
|
||||
masked?: boolean
|
||||
placeholder?: string
|
||||
}
|
||||
export type ValueSpecNumber = {
|
||||
/** Something like [3,6] or [0, *) */
|
||||
range?: string
|
||||
integral?: boolean
|
||||
/** Used a description of the units */
|
||||
units?: string
|
||||
placeholder?: number
|
||||
}
|
||||
export type ValueSpecBoolean = Record<string, unknown>
|
||||
export type ValueSpecAny =
|
||||
| Tag<"boolean", WithDescription<WithDefault<ValueSpecBoolean, boolean>>>
|
||||
| Tag<
|
||||
"string",
|
||||
WithDescription<
|
||||
WithNullableDefault<WithNullable<ValueSpecString>, DefaultString>
|
||||
>
|
||||
>
|
||||
| Tag<
|
||||
"number",
|
||||
WithDescription<
|
||||
WithNullableDefault<WithNullable<ValueSpecNumber>, number>
|
||||
>
|
||||
>
|
||||
| Tag<
|
||||
"enum",
|
||||
WithDescription<
|
||||
WithDefault<
|
||||
{
|
||||
values: readonly string[] | string[]
|
||||
"value-names": {
|
||||
[key: string]: string
|
||||
}
|
||||
},
|
||||
string
|
||||
>
|
||||
>
|
||||
>
|
||||
| Tag<"list", ValueSpecList>
|
||||
| Tag<"object", WithDescription<WithNullableDefault<ValueSpecObject, Config>>>
|
||||
| Tag<"union", WithOptionalDescription<WithDefault<ValueSpecUnion, string>>>
|
||||
| Tag<
|
||||
"pointer",
|
||||
WithDescription<
|
||||
| Subtype<
|
||||
"package",
|
||||
| Target<
|
||||
"tor-key",
|
||||
{
|
||||
"package-id": string
|
||||
interface: string
|
||||
}
|
||||
>
|
||||
| Target<
|
||||
"tor-address",
|
||||
{
|
||||
"package-id": string
|
||||
interface: string
|
||||
}
|
||||
>
|
||||
| Target<
|
||||
"lan-address",
|
||||
{
|
||||
"package-id": string
|
||||
interface: string
|
||||
}
|
||||
>
|
||||
| Target<
|
||||
"config",
|
||||
{
|
||||
"package-id": string
|
||||
selector: string
|
||||
multi: boolean
|
||||
}
|
||||
>
|
||||
>
|
||||
| Subtype<"system", Record<string, unknown>>
|
||||
>
|
||||
>
|
||||
export type ValueSpecUnion = {
|
||||
/** What tag for the specification, for tag unions */
|
||||
tag: {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
"variant-names": {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
/** The possible enum values */
|
||||
variants: {
|
||||
[key: string]: ConfigSpec
|
||||
}
|
||||
"display-as"?: string
|
||||
"unique-by"?: UniqueBy
|
||||
}
|
||||
export type ValueSpecObject = {
|
||||
spec: ConfigSpec
|
||||
"display-as"?: string
|
||||
"unique-by"?: UniqueBy
|
||||
}
|
||||
export type ValueSpecList =
|
||||
| Subtype<
|
||||
"boolean",
|
||||
WithDescription<WithDefault<ListSpec<ValueSpecBoolean>, boolean[]>>
|
||||
>
|
||||
| Subtype<
|
||||
"string",
|
||||
WithDescription<WithDefault<ListSpec<ValueSpecString>, string[]>>
|
||||
>
|
||||
| Subtype<
|
||||
"number",
|
||||
WithDescription<WithDefault<ListSpec<ValueSpecNumber>, number[]>>
|
||||
>
|
||||
| Subtype<
|
||||
"enum",
|
||||
WithDescription<WithDefault<ListSpec<ValueSpecEnum>, string[]>>
|
||||
>
|
||||
| Subtype<
|
||||
"object",
|
||||
WithDescription<
|
||||
WithNullableDefault<
|
||||
ListSpec<ValueSpecObject>,
|
||||
Record<string, unknown>[]
|
||||
>
|
||||
>
|
||||
>
|
||||
| Subtype<
|
||||
"union",
|
||||
WithDescription<WithDefault<ListSpec<ValueSpecUnion>, string[]>>
|
||||
>
|
||||
export type ValueSpecEnum = {
|
||||
values: string[]
|
||||
"value-names": { [key: string]: string }
|
||||
}
|
||||
|
||||
export type SetResult = {
|
||||
/** These are the unix process signals */
|
||||
signal:
|
||||
| "SIGTERM"
|
||||
| "SIGHUP"
|
||||
| "SIGINT"
|
||||
| "SIGQUIT"
|
||||
| "SIGILL"
|
||||
| "SIGTRAP"
|
||||
| "SIGABRT"
|
||||
| "SIGBUS"
|
||||
| "SIGFPE"
|
||||
| "SIGKILL"
|
||||
| "SIGUSR1"
|
||||
| "SIGSEGV"
|
||||
| "SIGUSR2"
|
||||
| "SIGPIPE"
|
||||
| "SIGALRM"
|
||||
| "SIGSTKFLT"
|
||||
| "SIGCHLD"
|
||||
| "SIGCONT"
|
||||
| "SIGSTOP"
|
||||
| "SIGTSTP"
|
||||
| "SIGTTIN"
|
||||
| "SIGTTOU"
|
||||
| "SIGURG"
|
||||
| "SIGXCPU"
|
||||
| "SIGXFSZ"
|
||||
| "SIGVTALRM"
|
||||
| "SIGPROF"
|
||||
| "SIGWINCH"
|
||||
| "SIGIO"
|
||||
| "SIGPWR"
|
||||
| "SIGSYS"
|
||||
| "SIGEMT"
|
||||
| "SIGINFO"
|
||||
"depends-on": DependsOn
|
||||
}
|
||||
|
||||
export type DependsOn = {
|
||||
[packageId: string]: string[]
|
||||
}
|
||||
|
||||
export type KnownError =
|
||||
| { error: string }
|
||||
| {
|
||||
"error-code": [number, string] | readonly [number, string]
|
||||
}
|
||||
export type ResultType<T> = KnownError | { result: T }
|
||||
|
||||
export type PackagePropertiesV2 = {
|
||||
[name: string]: PackagePropertyObject | PackagePropertyString
|
||||
}
|
||||
export type PackagePropertyString = {
|
||||
type: "string"
|
||||
description?: string
|
||||
value: string
|
||||
/** Let's the ui make this copyable button */
|
||||
copyable?: boolean
|
||||
/** Let the ui create a qr for this field */
|
||||
qr?: boolean
|
||||
/** Hiding the value unless toggled off for field */
|
||||
masked?: boolean
|
||||
}
|
||||
export type PackagePropertyObject = {
|
||||
value: PackagePropertiesV2
|
||||
type: "object"
|
||||
description: string
|
||||
}
|
||||
|
||||
export type Properties = {
|
||||
version: 2
|
||||
data: PackagePropertiesV2
|
||||
}
|
||||
|
||||
export type Dependencies = {
|
||||
/** Id is the id of the package, should be the same as the manifest */
|
||||
[id: string]: {
|
||||
/** Checks are called to make sure that our dependency is in the correct shape. If a known error is returned we know that the dependency needs modification */
|
||||
check(effects: Effects, input: Config): Promise<ResultType<void | null>>
|
||||
/** This is called after we know that the dependency package needs a new configuration, this would be a transform for defaults */
|
||||
autoConfigure(effects: Effects, input: Config): Promise<ResultType<Config>>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as oet from "./oldEmbassyTypes"
|
||||
import { Volume } from "../../../Models/Volume"
|
||||
import * as child_process from "child_process"
|
||||
import { promisify } from "util"
|
||||
import { Daemons, startSdk, T } from "@start9labs/start-sdk"
|
||||
import { HostSystemStartOs } from "../../HostSystemStartOs"
|
||||
import "isomorphic-fetch"
|
||||
import { Manifest } from "./matchManifest"
|
||||
|
||||
const execFile = promisify(child_process.execFile)
|
||||
|
||||
export class PolyfillEffects implements oet.Effects {
|
||||
constructor(
|
||||
readonly effects: HostSystemStartOs,
|
||||
private manifest: Manifest,
|
||||
) {}
|
||||
async writeFile(input: {
|
||||
path: string
|
||||
volumeId: string
|
||||
toWrite: string
|
||||
}): Promise<void> {
|
||||
await fs.writeFile(
|
||||
new Volume(input.volumeId, input.path).path,
|
||||
input.toWrite,
|
||||
)
|
||||
}
|
||||
async readFile(input: { volumeId: string; path: string }): Promise<string> {
|
||||
return (
|
||||
await fs.readFile(new Volume(input.volumeId, input.path).path)
|
||||
).toString()
|
||||
}
|
||||
async metadata(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<oet.Metadata> {
|
||||
const stats = await fs.stat(new Volume(input.volumeId, input.path).path)
|
||||
return {
|
||||
fileType: stats.isFile() ? "file" : "directory",
|
||||
gid: stats.gid,
|
||||
uid: stats.uid,
|
||||
mode: stats.mode,
|
||||
isDir: stats.isDirectory(),
|
||||
isFile: stats.isFile(),
|
||||
isSymlink: stats.isSymbolicLink(),
|
||||
len: stats.size,
|
||||
readonly: (stats.mode & 0o200) > 0,
|
||||
}
|
||||
}
|
||||
async createDir(input: { volumeId: string; path: string }): Promise<string> {
|
||||
const path = new Volume(input.volumeId, input.path).path
|
||||
await fs.mkdir(path, { recursive: true })
|
||||
return path
|
||||
}
|
||||
async readDir(input: { volumeId: string; path: string }): Promise<string[]> {
|
||||
return fs.readdir(new Volume(input.volumeId, input.path).path)
|
||||
}
|
||||
async removeDir(input: { volumeId: string; path: string }): Promise<string> {
|
||||
const path = new Volume(input.volumeId, input.path).path
|
||||
await fs.rmdir(new Volume(input.volumeId, input.path).path, {
|
||||
recursive: true,
|
||||
})
|
||||
return path
|
||||
}
|
||||
removeFile(input: { volumeId: string; path: string }): Promise<void> {
|
||||
return fs.rm(new Volume(input.volumeId, input.path).path)
|
||||
}
|
||||
async writeJsonFile(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
toWrite: Record<string, unknown>
|
||||
}): Promise<void> {
|
||||
await fs.writeFile(
|
||||
new Volume(input.volumeId, input.path).path,
|
||||
JSON.stringify(input.toWrite),
|
||||
)
|
||||
}
|
||||
async readJsonFile(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<Record<string, unknown>> {
|
||||
return JSON.parse(
|
||||
(
|
||||
await fs.readFile(new Volume(input.volumeId, input.path).path)
|
||||
).toString(),
|
||||
)
|
||||
}
|
||||
runCommand({
|
||||
command,
|
||||
args,
|
||||
timeoutMillis,
|
||||
}: {
|
||||
command: string
|
||||
args?: string[] | undefined
|
||||
timeoutMillis?: number | undefined
|
||||
}): Promise<oet.ResultType<string>> {
|
||||
return startSdk
|
||||
.runCommand(
|
||||
this.effects,
|
||||
this.manifest.main.image,
|
||||
[command, ...(args || [])],
|
||||
{},
|
||||
)
|
||||
.then((x: any) => ({
|
||||
stderr: x.stderr.toString(),
|
||||
stdout: x.stdout.toString(),
|
||||
}))
|
||||
.then((x) => (!!x.stderr ? { error: x.stderr } : { result: x.stdout }))
|
||||
}
|
||||
runDaemon(input: { command: string; args?: string[] | undefined }): {
|
||||
wait(): Promise<oet.ResultType<string>>
|
||||
term(): Promise<void>
|
||||
} {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
chown(input: { volumeId: string; path: string; uid: string }): Promise<null> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
chmod(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
mode: string
|
||||
}): Promise<null> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
sleep(timeMs: number): Promise<null> {
|
||||
return new Promise((resolve) => setTimeout(resolve, timeMs))
|
||||
}
|
||||
trace(whatToPrint: string): void {
|
||||
console.trace(whatToPrint)
|
||||
}
|
||||
warn(whatToPrint: string): void {
|
||||
console.warn(whatToPrint)
|
||||
}
|
||||
error(whatToPrint: string): void {
|
||||
console.error(whatToPrint)
|
||||
}
|
||||
debug(whatToPrint: string): void {
|
||||
console.debug(whatToPrint)
|
||||
}
|
||||
info(whatToPrint: string): void {
|
||||
console.log(false)
|
||||
}
|
||||
is_sandboxed(): boolean {
|
||||
return false
|
||||
}
|
||||
exists(input: { volumeId: string; path: string }): Promise<boolean> {
|
||||
return this.metadata(input)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
}
|
||||
bindLocal(options: {
|
||||
internalPort: number
|
||||
name: string
|
||||
externalPort: number
|
||||
}): Promise<string> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
bindTor(options: {
|
||||
internalPort: number
|
||||
name: string
|
||||
externalPort: number
|
||||
}): Promise<string> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
async fetch(
|
||||
url: string,
|
||||
options?:
|
||||
| {
|
||||
method?:
|
||||
| "GET"
|
||||
| "POST"
|
||||
| "PUT"
|
||||
| "DELETE"
|
||||
| "HEAD"
|
||||
| "PATCH"
|
||||
| undefined
|
||||
headers?: Record<string, string> | undefined
|
||||
body?: string | undefined
|
||||
}
|
||||
| undefined,
|
||||
): Promise<{
|
||||
method: string
|
||||
ok: boolean
|
||||
status: number
|
||||
headers: Record<string, string>
|
||||
body?: string | null | undefined
|
||||
text(): Promise<string>
|
||||
json(): Promise<unknown>
|
||||
}> {
|
||||
const fetched = await fetch(url, options)
|
||||
return {
|
||||
method: fetched.type,
|
||||
ok: fetched.ok,
|
||||
status: fetched.status,
|
||||
headers: Object.fromEntries(fetched.headers.entries()),
|
||||
body: await fetched.text(),
|
||||
text: () => fetched.text(),
|
||||
json: () => fetched.json(),
|
||||
}
|
||||
}
|
||||
runRsync(options: {
|
||||
srcVolume: string
|
||||
dstVolume: string
|
||||
srcPath: string
|
||||
dstPath: string
|
||||
options: oet.BackupOptions
|
||||
}): {
|
||||
id: () => Promise<string>
|
||||
wait: () => Promise<null>
|
||||
progress: () => Promise<number>
|
||||
} {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
152
container-runtime/src/Adapters/Systems/SystemForStartOs.ts
Normal file
152
container-runtime/src/Adapters/Systems/SystemForStartOs.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { ExecuteResult, System } from "../../Interfaces/System"
|
||||
import { unNestPath } from "../../Models/JsonPath"
|
||||
import { string } from "ts-matches"
|
||||
import { HostSystemStartOs } from "../HostSystemStartOs"
|
||||
import { Effects } from "../../Models/Effects"
|
||||
import { RpcResult } from "../RpcListener"
|
||||
import { duration } from "../../Models/Duration"
|
||||
const LOCATION = "/usr/lib/startos/package/startos"
|
||||
export class SystemForStartOs implements System {
|
||||
private onTerm: (() => Promise<void>) | undefined
|
||||
static of() {
|
||||
return new SystemForStartOs()
|
||||
}
|
||||
constructor() {}
|
||||
async execute(
|
||||
effects: HostSystemStartOs,
|
||||
options: {
|
||||
procedure:
|
||||
| "/init"
|
||||
| "/uninit"
|
||||
| "/main/start"
|
||||
| "/main/stop"
|
||||
| "/config/set"
|
||||
| "/config/get"
|
||||
| "/backup/create"
|
||||
| "/backup/restore"
|
||||
| "/actions/metadata"
|
||||
| `/actions/${string}/get`
|
||||
| `/actions/${string}/run`
|
||||
| `/dependencies/${string}/query`
|
||||
| `/dependencies/${string}/update`
|
||||
input: unknown
|
||||
timeout?: number | undefined
|
||||
},
|
||||
): Promise<RpcResult> {
|
||||
return { result: await this._execute(effects, options) }
|
||||
}
|
||||
async _execute(
|
||||
effects: Effects,
|
||||
options: {
|
||||
procedure:
|
||||
| "/init"
|
||||
| "/uninit"
|
||||
| "/main/start"
|
||||
| "/main/stop"
|
||||
| "/config/set"
|
||||
| "/config/get"
|
||||
| "/backup/create"
|
||||
| "/backup/restore"
|
||||
| "/actions/metadata"
|
||||
| `/actions/${string}/get`
|
||||
| `/actions/${string}/run`
|
||||
| `/dependencies/${string}/query`
|
||||
| `/dependencies/${string}/update`
|
||||
input: unknown
|
||||
timeout?: number | undefined
|
||||
},
|
||||
): Promise<unknown> {
|
||||
switch (options.procedure) {
|
||||
case "/init": {
|
||||
const path = `${LOCATION}/procedures/init`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
const previousVersion = string.optional().unsafeCast(options)
|
||||
return procedure.init({ effects, previousVersion })
|
||||
}
|
||||
case "/uninit": {
|
||||
const path = `${LOCATION}/procedures/init`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
const nextVersion = string.optional().unsafeCast(options)
|
||||
return procedure.uninit({ effects, nextVersion })
|
||||
}
|
||||
case "/main/start": {
|
||||
const path = `${LOCATION}/procedures/main`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
const started = async (onTerm: () => Promise<void>) => {
|
||||
await effects.setMainStatus({ status: "running" })
|
||||
if (this.onTerm) await this.onTerm()
|
||||
this.onTerm = onTerm
|
||||
}
|
||||
return procedure.main({ effects, started })
|
||||
}
|
||||
case "/main/stop": {
|
||||
await effects.setMainStatus({ status: "stopped" })
|
||||
if (this.onTerm) await this.onTerm()
|
||||
delete this.onTerm
|
||||
return duration(30, "s")
|
||||
}
|
||||
case "/config/set": {
|
||||
const path = `${LOCATION}/procedures/config`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
const input = options.input
|
||||
return procedure.setConfig({ effects, input })
|
||||
}
|
||||
case "/config/get": {
|
||||
const path = `${LOCATION}/procedures/config`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
return procedure.getConfig({ effects })
|
||||
}
|
||||
case "/backup/create":
|
||||
case "/backup/restore":
|
||||
throw new Error("this should be called with the init/unit")
|
||||
case "/actions/metadata": {
|
||||
const path = `${LOCATION}/procedures/actions`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
return procedure.actionsMetadata({ effects })
|
||||
}
|
||||
default:
|
||||
const procedures = unNestPath(options.procedure)
|
||||
const id = procedures[2]
|
||||
switch (true) {
|
||||
case procedures[1] === "actions" && procedures[3] === "get": {
|
||||
const path = `${LOCATION}/procedures/actions`
|
||||
const action: any = (await import(path).catch(() => require(path)))
|
||||
.actions[id]
|
||||
if (!action) throw new Error(`Action ${id} not found`)
|
||||
return action.get({ effects })
|
||||
}
|
||||
case procedures[1] === "actions" && procedures[3] === "run": {
|
||||
const path = `${LOCATION}/procedures/actions`
|
||||
const action: any = (await import(path).catch(() => require(path)))
|
||||
.actions[id]
|
||||
if (!action) throw new Error(`Action ${id} not found`)
|
||||
const input = options.input
|
||||
return action.run({ effects, input })
|
||||
}
|
||||
case procedures[1] === "dependencies" && procedures[3] === "query": {
|
||||
const path = `${LOCATION}/procedures/dependencies`
|
||||
const dependencyConfig: any = (
|
||||
await import(path).catch(() => require(path))
|
||||
).dependencyConfig[id]
|
||||
if (!dependencyConfig)
|
||||
throw new Error(`dependencyConfig ${id} not found`)
|
||||
const localConfig = options.input
|
||||
return dependencyConfig.query({ effects, localConfig })
|
||||
}
|
||||
case procedures[1] === "dependencies" && procedures[3] === "update": {
|
||||
const path = `${LOCATION}/procedures/dependencies`
|
||||
const dependencyConfig: any = (
|
||||
await import(path).catch(() => require(path))
|
||||
).dependencyConfig[id]
|
||||
if (!dependencyConfig)
|
||||
throw new Error(`dependencyConfig ${id} not found`)
|
||||
return dependencyConfig.update(options.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
exit(effects: Effects): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
6
container-runtime/src/Adapters/Systems/index.ts
Normal file
6
container-runtime/src/Adapters/Systems/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { System } from "../../Interfaces/System"
|
||||
import { SystemForEmbassy } from "./SystemForEmbassy"
|
||||
import { SystemForStartOs } from "./SystemForStartOs"
|
||||
export async function getSystem(): Promise<System> {
|
||||
return SystemForEmbassy.of()
|
||||
}
|
||||
6
container-runtime/src/Interfaces/AllGetDependencies.ts
Normal file
6
container-runtime/src/Interfaces/AllGetDependencies.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { GetDependency } from "./GetDependency"
|
||||
import { System } from "./System"
|
||||
import { GetHostSystem, HostSystem } from "./HostSystem"
|
||||
|
||||
export type AllGetDependencies = GetDependency<"system", Promise<System>> &
|
||||
GetDependency<"hostSystem", GetHostSystem>
|
||||
3
container-runtime/src/Interfaces/GetDependency.ts
Normal file
3
container-runtime/src/Interfaces/GetDependency.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type GetDependency<K extends string, T> = {
|
||||
[OtherK in K]: () => T
|
||||
}
|
||||
7
container-runtime/src/Interfaces/HostSystem.ts
Normal file
7
container-runtime/src/Interfaces/HostSystem.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { types as T } from "@start9labs/start-sdk"
|
||||
|
||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||
import { Effects } from "../Models/Effects"
|
||||
|
||||
export type HostSystem = Effects
|
||||
export type GetHostSystem = (callbackHolder: CallbackHolder) => HostSystem
|
||||
32
container-runtime/src/Interfaces/System.ts
Normal file
32
container-runtime/src/Interfaces/System.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { types as T } from "@start9labs/start-sdk"
|
||||
import { JsonPath } from "../Models/JsonPath"
|
||||
import { HostSystemStartOs } from "../Adapters/HostSystemStartOs"
|
||||
import { RpcResult } from "../Adapters/RpcListener"
|
||||
export type ExecuteResult =
|
||||
| { ok: unknown }
|
||||
| { err: { code: number; message: string } }
|
||||
export interface System {
|
||||
// init(effects: Effects): Promise<void>
|
||||
// exit(effects: Effects): Promise<void>
|
||||
// start(effects: Effects): Promise<void>
|
||||
// stop(effects: Effects, options: { timeout: number, signal?: number }): Promise<void>
|
||||
|
||||
execute(
|
||||
effects: T.Effects,
|
||||
options: {
|
||||
procedure: JsonPath
|
||||
input: unknown
|
||||
timeout?: number
|
||||
},
|
||||
): Promise<RpcResult>
|
||||
// sandbox(
|
||||
// effects: Effects,
|
||||
// options: {
|
||||
// procedure: JsonPath
|
||||
// input: unknown
|
||||
// timeout?: number
|
||||
// },
|
||||
// ): Promise<unknown>
|
||||
|
||||
exit(effects: T.Effects): Promise<void>
|
||||
}
|
||||
20
container-runtime/src/Models/CallbackHolder.ts
Normal file
20
container-runtime/src/Models/CallbackHolder.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export class CallbackHolder {
|
||||
constructor() {}
|
||||
private root = (Math.random() + 1).toString(36).substring(7)
|
||||
private inc = 0
|
||||
private callbacks = new Map<string, Function>()
|
||||
private newId() {
|
||||
return this.root + (this.inc++).toString(36)
|
||||
}
|
||||
addCallback(callback: Function) {
|
||||
const id = this.newId()
|
||||
this.callbacks.set(id, callback)
|
||||
return id
|
||||
}
|
||||
callCallback(index: string, args: any[]): Promise<unknown> {
|
||||
const callback = this.callbacks.get(index)
|
||||
if (!callback) throw new Error(`Callback ${index} does not exist`)
|
||||
this.callbacks.delete(index)
|
||||
return Promise.resolve().then(() => callback(...args))
|
||||
}
|
||||
}
|
||||
45
container-runtime/src/Models/DockerProcedure.ts
Normal file
45
container-runtime/src/Models/DockerProcedure.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
object,
|
||||
literal,
|
||||
string,
|
||||
boolean,
|
||||
array,
|
||||
dictionary,
|
||||
literals,
|
||||
number,
|
||||
Parser,
|
||||
} from "ts-matches"
|
||||
|
||||
const VolumeId = string
|
||||
const Path = string
|
||||
|
||||
export type VolumeId = string
|
||||
export type Path = string
|
||||
export const matchDockerProcedure = object(
|
||||
{
|
||||
type: literal("docker"),
|
||||
image: string,
|
||||
system: boolean,
|
||||
entrypoint: string,
|
||||
args: array(string),
|
||||
mounts: dictionary([VolumeId, Path]),
|
||||
"io-format": literals(
|
||||
"json",
|
||||
"json-pretty",
|
||||
"yaml",
|
||||
"cbor",
|
||||
"toml",
|
||||
"toml-pretty",
|
||||
),
|
||||
"sigterm-timeout": number,
|
||||
inject: boolean,
|
||||
},
|
||||
["io-format", "sigterm-timeout", "system", "args", "inject", "mounts"],
|
||||
{
|
||||
"sigterm-timeout": 30,
|
||||
inject: false,
|
||||
args: [],
|
||||
},
|
||||
)
|
||||
|
||||
export type DockerProcedure = typeof matchDockerProcedure._TYPE
|
||||
6
container-runtime/src/Models/Duration.ts
Normal file
6
container-runtime/src/Models/Duration.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type TimeUnit = "d" | "h" | "s" | "ms"
|
||||
export type Duration = `${number}${TimeUnit}`
|
||||
|
||||
export function duration(timeValue: number, timeUnit: TimeUnit = "s") {
|
||||
return `${timeValue}${timeUnit}` as Duration
|
||||
}
|
||||
5
container-runtime/src/Models/Effects.ts
Normal file
5
container-runtime/src/Models/Effects.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { types as T } from "@start9labs/start-sdk"
|
||||
|
||||
export type Effects = T.Effects & {
|
||||
setMainStatus(o: { status: "running" | "stopped" }): Promise<void>
|
||||
}
|
||||
43
container-runtime/src/Models/JsonPath.ts
Normal file
43
container-runtime/src/Models/JsonPath.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
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">
|
||||
// prettier-ignore
|
||||
type UnNestPaths<A> =
|
||||
A extends `${infer A}/${infer B}` ? [...UnNestPaths<A>, ... UnNestPaths<B>] :
|
||||
[A]
|
||||
|
||||
export function unNestPath<A extends string>(a: A): UnNestPaths<A> {
|
||||
return a.split("/") as 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")
|
||||
)
|
||||
return true
|
||||
return false
|
||||
}
|
||||
export const jsonPath = some(
|
||||
literals(
|
||||
"/init",
|
||||
"/uninit",
|
||||
"/main/start",
|
||||
"/main/stop",
|
||||
"/config/set",
|
||||
"/config/get",
|
||||
"/backup/create",
|
||||
"/backup/restore",
|
||||
"/actions/metadata",
|
||||
"/properties",
|
||||
),
|
||||
string.refine(isNestedPath, "isNestedPath"),
|
||||
)
|
||||
|
||||
export type JsonPath = typeof jsonPath._TYPE
|
||||
19
container-runtime/src/Models/Volume.ts
Normal file
19
container-runtime/src/Models/Volume.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as fs from "node:fs/promises"
|
||||
|
||||
export class Volume {
|
||||
readonly path: string
|
||||
constructor(
|
||||
readonly volumeId: string,
|
||||
_path = "",
|
||||
) {
|
||||
const path = (this.path = `/media/startos/volumes/${volumeId}${
|
||||
!_path ? "" : `/${_path}`
|
||||
}`)
|
||||
}
|
||||
async exists() {
|
||||
return fs.stat(this.path).then(
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
}
|
||||
}
|
||||
44
container-runtime/src/index.ts
Normal file
44
container-runtime/src/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { RpcListener } from "./Adapters/RpcListener"
|
||||
import { SystemForEmbassy } from "./Adapters/Systems/SystemForEmbassy"
|
||||
import { HostSystemStartOs } from "./Adapters/HostSystemStartOs"
|
||||
import { AllGetDependencies } from "./Interfaces/AllGetDependencies"
|
||||
import { getSystem } from "./Adapters/Systems"
|
||||
|
||||
const getDependencies: AllGetDependencies = {
|
||||
system: getSystem,
|
||||
hostSystem: () => HostSystemStartOs.of,
|
||||
}
|
||||
|
||||
new RpcListener(getDependencies)
|
||||
|
||||
/**
|
||||
|
||||
So, this is going to be sent into a running comtainer along with any of the other node modules that are going to be needed and used.
|
||||
|
||||
Once the container is started, we will go into a loading/ await state.
|
||||
This is the init system, and it will always be running, and it will be waiting for a command to be sent to it.
|
||||
|
||||
Each command will be a stopable promise. And an example is going to be something like an action/ main/ or just a query into the types.
|
||||
|
||||
A command will be sent an object which are the effects, and the effects will be things like the file system, the network, the process, and the os.
|
||||
|
||||
|
||||
*/
|
||||
// So OS Adapter
|
||||
// ==============
|
||||
|
||||
/**
|
||||
* Why: So when the we call from the os we enter or leave here?
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
Command: This is a command that the
|
||||
|
||||
There are
|
||||
*/
|
||||
|
||||
/**
|
||||
TODO:
|
||||
Should I seperate those adapter in/out?
|
||||
*/
|
||||
31
container-runtime/tsconfig.json
Normal file
31
container-runtime/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"include": [
|
||||
"./**/*.mjs",
|
||||
"./**/*.js",
|
||||
"src/Adapters/RpcListener.ts",
|
||||
"src/index.ts",
|
||||
"effects.ts"
|
||||
],
|
||||
"exclude": ["dist"],
|
||||
"inputs": ["./src/index.ts"],
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"strict": true,
|
||||
"outDir": "dist",
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": true,
|
||||
"target": "ES2022",
|
||||
"pretty": true,
|
||||
"declaration": true,
|
||||
"noImplicitAny": true,
|
||||
"esModuleInterop": true,
|
||||
"types": ["node"],
|
||||
"moduleResolution": "Node16",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
41
container-runtime/update-image.sh
Executable file
41
container-runtime/update-image.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
|
||||
if mountpoint tmp/combined; then sudo umount tmp/combined; fi
|
||||
if mountpoint tmp/lower; then sudo umount tmp/lower; fi
|
||||
mkdir -p tmp/lower tmp/upper tmp/work tmp/combined
|
||||
sudo mount alpine.${ARCH}.squashfs tmp/lower
|
||||
sudo mount -t overlay -olowerdir=tmp/lower,upperdir=tmp/upper,workdir=tmp/work overlay tmp/combined
|
||||
|
||||
QEMU=
|
||||
if [ "$ARCH" != "$(uname -m)" ]; then
|
||||
QEMU=/usr/bin/qemu-${ARCH}-static
|
||||
sudo cp $(which qemu-$ARCH-static) tmp/combined${QEMU}
|
||||
fi
|
||||
|
||||
echo "nameserver 8.8.8.8" | sudo tee tmp/combined/etc/resolv.conf # TODO - delegate to host resolver?
|
||||
sudo chroot tmp/combined $QEMU /sbin/apk add nodejs
|
||||
sudo mkdir -p tmp/combined/usr/lib/startos/
|
||||
sudo rsync -a --copy-unsafe-links dist/ tmp/combined/usr/lib/startos/init/
|
||||
sudo cp containerRuntime.rc tmp/combined/etc/init.d/containerRuntime
|
||||
sudo cp ../core/target/$ARCH-unknown-linux-musl/release/containerbox tmp/combined/usr/bin/start-cli
|
||||
sudo chmod +x tmp/combined/etc/init.d/containerRuntime
|
||||
sudo chroot tmp/combined $QEMU /sbin/rc-update add containerRuntime default
|
||||
|
||||
if [ -n "$QEMU" ]; then
|
||||
sudo rm tmp/combined${QEMU}
|
||||
fi
|
||||
|
||||
sudo truncate -s 0 tmp/combined/etc/resolv.conf
|
||||
sudo chown -R 0:0 tmp/combined
|
||||
rm -f rootfs.${ARCH}.squashfs
|
||||
mkdir -p ../build/lib/container-runtime
|
||||
sudo mksquashfs tmp/combined rootfs.${ARCH}.squashfs
|
||||
sudo umount tmp/combined
|
||||
sudo umount tmp/lower
|
||||
sudo rm -rf tmp
|
||||
3232
core/Cargo.lock
generated
3232
core/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,3 @@
|
||||
[workspace]
|
||||
|
||||
members = [
|
||||
"container-init",
|
||||
"helpers",
|
||||
"js-engine",
|
||||
"models",
|
||||
"snapshot-creator",
|
||||
"startos",
|
||||
]
|
||||
members = ["helpers", "models", "startos"]
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
## Structure
|
||||
|
||||
- `startos`: This contains the core library for StartOS that supports building `startbox`.
|
||||
- `container-init` (ignore: deprecated)
|
||||
- `js-engine`: This contains the library required to build `deno` to support running `.js` maintainer scripts for v0.3
|
||||
- `snapshot-creator`: This contains a binary used to build `v8` runtime snapshots, required for initializing `start-deno`
|
||||
- `helpers`: This contains utility functions used across both `startos` and `js-engine`
|
||||
- `models`: This contains types that are shared across `startos`, `js-engine`, and `helpers`
|
||||
|
||||
@@ -24,8 +21,6 @@ several different names for different behaviour:
|
||||
`startd` and control it similarly to the UI
|
||||
- `start-sdk`: This is a CLI tool that aids in building and packaging services
|
||||
you wish to deploy to StartOS
|
||||
- `start-deno`: This is a CLI tool invoked by startd to run `.js` maintainer scripts for v0.3
|
||||
- `avahi-alias`: This is a CLI tool invoked by startd to create aliases in `avahi` for mDNS
|
||||
|
||||
## Questions
|
||||
|
||||
|
||||
@@ -18,22 +18,22 @@ cd ..
|
||||
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
|
||||
RUSTFLAGS=""
|
||||
|
||||
alias 'rust-gnu-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/usr/local/cargo/registry -v "$(pwd)":/home/rust/src -w /home/rust/src -P start9/rust-arm-cross:aarch64'
|
||||
alias 'rust-musl-builder'='docker run $USE_TTY --rm -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
||||
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||
RUSTFLAGS="--cfg tokio_unstable"
|
||||
fi
|
||||
|
||||
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
||||
|
||||
set +e
|
||||
fail=
|
||||
echo "FEATURES=\"$FEATURES\""
|
||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||
if ! rust-gnu-builder sh -c "(cd core && cargo build --release --features avahi-alias,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-gnu)"; then
|
||||
if ! rust-musl-builder sh -c "(cd core && cargo build --release $(if [ -n "$FEATURES" ]; then echo "--features $FEATURES"; fi) --locked --bin startbox --target=$ARCH-unknown-linux-musl)"; then
|
||||
fail=true
|
||||
fi
|
||||
if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features container-runtime,$FEATURES --locked --bin containerbox --target=$ARCH-unknown-linux-musl)"; then
|
||||
fail=true
|
||||
fi
|
||||
for ARCH in x86_64 aarch64
|
||||
do
|
||||
if ! rust-musl-builder sh -c "(cd core && cargo build --release --locked --bin container-init)"; then
|
||||
fail=true
|
||||
fi
|
||||
done
|
||||
set -e
|
||||
cd core
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Reason for this being is that we need to create a snapshot for the deno runtime. It wants to pull 3 files from build, and during the creation it gets embedded, but for some
|
||||
# reason during the actual runtime it is looking for them. So this will create a docker in arm that creates the snaphot needed for the arm
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
set -e
|
||||
shopt -s expand_aliases
|
||||
|
||||
if [ -z "$ARCH" ]; then
|
||||
ARCH=$(uname -m)
|
||||
fi
|
||||
|
||||
USE_TTY=
|
||||
if tty -s; then
|
||||
USE_TTY="-it"
|
||||
fi
|
||||
|
||||
alias 'rust-gnu-builder'='docker run $USE_TTY --rm -v "$HOME/.cargo/registry":/usr/local/cargo/registry -v "$(pwd)":/home/rust/src -w /home/rust/src -P start9/rust-arm-cross:aarch64'
|
||||
|
||||
echo "Building "
|
||||
cd ..
|
||||
rust-gnu-builder sh -c "(cd core/ && cargo build -p snapshot_creator --release --target=${ARCH}-unknown-linux-gnu)"
|
||||
cd -
|
||||
|
||||
if [ "$ARCH" = "aarch64" ]; then
|
||||
DOCKER_ARCH='arm64/v8'
|
||||
elif [ "$ARCH" = "x86_64" ]; then
|
||||
DOCKER_ARCH='amd64'
|
||||
fi
|
||||
|
||||
echo "Creating Arm v8 Snapshot"
|
||||
docker run $USE_TTY --platform "linux/${DOCKER_ARCH}" --mount type=bind,src=$(pwd),dst=/mnt ubuntu:22.04 /bin/sh -c "cd /mnt && /mnt/target/${ARCH}-unknown-linux-gnu/release/snapshot_creator"
|
||||
sudo chown -R $USER target
|
||||
sudo chown -R $USER ~/.cargo
|
||||
sudo chown $USER JS_SNAPSHOT.bin
|
||||
sudo chmod 0644 JS_SNAPSHOT.bin
|
||||
|
||||
sudo mv -f JS_SNAPSHOT.bin ./js-engine/src/artifacts/JS_SNAPSHOT.${ARCH}.bin
|
||||
@@ -1,39 +0,0 @@
|
||||
[package]
|
||||
name = "container-init"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust = "1.66"
|
||||
|
||||
[features]
|
||||
dev = []
|
||||
metal = []
|
||||
sound = []
|
||||
unstable = []
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[dependencies]
|
||||
async-stream = "0.3"
|
||||
# cgroups-rs = "0.2"
|
||||
color-eyre = "0.6"
|
||||
futures = "0.3"
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
serde_json = "1"
|
||||
helpers = { path = "../helpers" }
|
||||
imbl = "2"
|
||||
nix = { version = "0.27", features = ["process", "signal"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-stream = { version = "0.1", features = ["io-util", "sync", "net"] }
|
||||
tracing = "0.1"
|
||||
tracing-error = "0.2"
|
||||
tracing-futures = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
yajrc = { version = "*", git = "https://github.com/dr-bonez/yajrc.git", branch = "develop" }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
procfs = "0.15"
|
||||
|
||||
[profile.test]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.backtrace]
|
||||
opt-level = 3
|
||||
@@ -1,214 +0,0 @@
|
||||
use nix::unistd::Pid;
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use yajrc::RpcMethod;
|
||||
|
||||
/// Know what the process is called
|
||||
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ProcessId(pub u32);
|
||||
impl From<ProcessId> for Pid {
|
||||
fn from(pid: ProcessId) -> Self {
|
||||
Pid::from_raw(pid.0 as i32)
|
||||
}
|
||||
}
|
||||
impl From<Pid> for ProcessId {
|
||||
fn from(pid: Pid) -> Self {
|
||||
ProcessId(pid.as_raw() as u32)
|
||||
}
|
||||
}
|
||||
impl From<i32> for ProcessId {
|
||||
fn from(pid: i32) -> Self {
|
||||
ProcessId(pid as u32)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ProcessGroupId(pub u32);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum OutputStrategy {
|
||||
Inherit,
|
||||
Collect,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RunCommand;
|
||||
impl Serialize for RunCommand {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
Serialize::serialize(Self.as_str(), serializer)
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RunCommandParams {
|
||||
pub gid: Option<ProcessGroupId>,
|
||||
pub command: String,
|
||||
pub args: Vec<String>,
|
||||
pub output: OutputStrategy,
|
||||
}
|
||||
impl RpcMethod for RunCommand {
|
||||
type Params = RunCommandParams;
|
||||
type Response = ProcessId;
|
||||
fn as_str<'a>(&'a self) -> &'a str {
|
||||
"command"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum LogLevel {
|
||||
Trace(String),
|
||||
Warn(String),
|
||||
Error(String),
|
||||
Info(String),
|
||||
Debug(String),
|
||||
}
|
||||
impl LogLevel {
|
||||
pub fn trace(&self) {
|
||||
match self {
|
||||
LogLevel::Trace(x) => tracing::trace!("{}", x),
|
||||
LogLevel::Warn(x) => tracing::warn!("{}", x),
|
||||
LogLevel::Error(x) => tracing::error!("{}", x),
|
||||
LogLevel::Info(x) => tracing::info!("{}", x),
|
||||
LogLevel::Debug(x) => tracing::debug!("{}", x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Log;
|
||||
impl Serialize for Log {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
Serialize::serialize(Self.as_str(), serializer)
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogParams {
|
||||
pub gid: Option<ProcessGroupId>,
|
||||
pub level: LogLevel,
|
||||
}
|
||||
impl RpcMethod for Log {
|
||||
type Params = LogParams;
|
||||
type Response = ();
|
||||
fn as_str<'a>(&'a self) -> &'a str {
|
||||
"log"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ReadLineStdout;
|
||||
impl Serialize for ReadLineStdout {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
Serialize::serialize(Self.as_str(), serializer)
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReadLineStdoutParams {
|
||||
pub pid: ProcessId,
|
||||
}
|
||||
impl RpcMethod for ReadLineStdout {
|
||||
type Params = ReadLineStdoutParams;
|
||||
type Response = String;
|
||||
fn as_str<'a>(&'a self) -> &'a str {
|
||||
"read-line-stdout"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ReadLineStderr;
|
||||
impl Serialize for ReadLineStderr {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
Serialize::serialize(Self.as_str(), serializer)
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReadLineStderrParams {
|
||||
pub pid: ProcessId,
|
||||
}
|
||||
impl RpcMethod for ReadLineStderr {
|
||||
type Params = ReadLineStderrParams;
|
||||
type Response = String;
|
||||
fn as_str<'a>(&'a self) -> &'a str {
|
||||
"read-line-stderr"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Output;
|
||||
impl Serialize for Output {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
Serialize::serialize(Self.as_str(), serializer)
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OutputParams {
|
||||
pub pid: ProcessId,
|
||||
}
|
||||
impl RpcMethod for Output {
|
||||
type Params = OutputParams;
|
||||
type Response = String;
|
||||
fn as_str<'a>(&'a self) -> &'a str {
|
||||
"output"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SendSignal;
|
||||
impl Serialize for SendSignal {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
Serialize::serialize(Self.as_str(), serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SendSignalParams {
|
||||
pub pid: ProcessId,
|
||||
pub signal: u32,
|
||||
}
|
||||
impl RpcMethod for SendSignal {
|
||||
type Params = SendSignalParams;
|
||||
type Response = ();
|
||||
fn as_str<'a>(&'a self) -> &'a str {
|
||||
"signal"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SignalGroup;
|
||||
impl Serialize for SignalGroup {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
Serialize::serialize(Self.as_str(), serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SignalGroupParams {
|
||||
pub gid: ProcessGroupId,
|
||||
pub signal: u32,
|
||||
}
|
||||
impl RpcMethod for SignalGroup {
|
||||
type Params = SignalGroupParams;
|
||||
type Response = ();
|
||||
fn as_str<'a>(&'a self) -> &'a str {
|
||||
"signal-group"
|
||||
}
|
||||
}
|
||||
@@ -1,428 +0,0 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::DerefMut;
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
|
||||
use container_init::{
|
||||
LogParams, OutputParams, OutputStrategy, ProcessGroupId, ProcessId, RunCommandParams,
|
||||
SendSignalParams, SignalGroupParams,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use helpers::NonDetachingJoinHandle;
|
||||
use nix::errno::Errno;
|
||||
use nix::sys::signal::Signal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::process::{Child, Command};
|
||||
use tokio::select;
|
||||
use tokio::sync::{watch, Mutex};
|
||||
use yajrc::{Id, RpcError};
|
||||
|
||||
/// Outputs embedded in the JSONRpc output of the executable.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(untagged)]
|
||||
enum Output {
|
||||
Command(ProcessId),
|
||||
ReadLineStdout(String),
|
||||
ReadLineStderr(String),
|
||||
Output(String),
|
||||
Log,
|
||||
Signal,
|
||||
SignalGroup,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "method", content = "params", rename_all = "kebab-case")]
|
||||
enum Input {
|
||||
/// Run a new command, with the args
|
||||
Command(RunCommandParams),
|
||||
/// Want to log locall on the service rather than the eos
|
||||
Log(LogParams),
|
||||
// /// Get a line of stdout from the command
|
||||
// ReadLineStdout(ReadLineStdoutParams),
|
||||
// /// Get a line of stderr from the command
|
||||
// ReadLineStderr(ReadLineStderrParams),
|
||||
/// Get output of command
|
||||
Output(OutputParams),
|
||||
/// Send the sigterm to the process
|
||||
Signal(SendSignalParams),
|
||||
/// Signal a group of processes
|
||||
SignalGroup(SignalGroupParams),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct IncomingRpc {
|
||||
id: Id,
|
||||
#[serde(flatten)]
|
||||
input: Input,
|
||||
}
|
||||
|
||||
struct ChildInfo {
|
||||
gid: Option<ProcessGroupId>,
|
||||
child: Arc<Mutex<Option<Child>>>,
|
||||
output: Option<InheritOutput>,
|
||||
}
|
||||
|
||||
struct InheritOutput {
|
||||
_thread: NonDetachingJoinHandle<()>,
|
||||
stdout: watch::Receiver<String>,
|
||||
stderr: watch::Receiver<String>,
|
||||
}
|
||||
|
||||
struct HandlerMut {
|
||||
processes: BTreeMap<ProcessId, ChildInfo>,
|
||||
// groups: BTreeMap<ProcessGroupId, Cgroup>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Handler {
|
||||
children: Arc<Mutex<HandlerMut>>,
|
||||
}
|
||||
impl Handler {
|
||||
fn new() -> Self {
|
||||
Handler {
|
||||
children: Arc::new(Mutex::new(HandlerMut {
|
||||
processes: BTreeMap::new(),
|
||||
// groups: BTreeMap::new(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
async fn handle(&self, req: Input) -> Result<Output, RpcError> {
|
||||
Ok(match req {
|
||||
Input::Command(RunCommandParams {
|
||||
gid,
|
||||
command,
|
||||
args,
|
||||
output,
|
||||
}) => Output::Command(self.command(gid, command, args, output).await?),
|
||||
// Input::ReadLineStdout(ReadLineStdoutParams { pid }) => {
|
||||
// Output::ReadLineStdout(self.read_line_stdout(pid).await?)
|
||||
// }
|
||||
// Input::ReadLineStderr(ReadLineStderrParams { pid }) => {
|
||||
// Output::ReadLineStderr(self.read_line_stderr(pid).await?)
|
||||
// }
|
||||
Input::Log(LogParams { gid: _, level }) => {
|
||||
level.trace();
|
||||
Output::Log
|
||||
}
|
||||
Input::Output(OutputParams { pid }) => Output::Output(self.output(pid).await?),
|
||||
Input::Signal(SendSignalParams { pid, signal }) => {
|
||||
self.signal(pid, signal).await?;
|
||||
Output::Signal
|
||||
}
|
||||
Input::SignalGroup(SignalGroupParams { gid, signal }) => {
|
||||
self.signal_group(gid, signal).await?;
|
||||
Output::SignalGroup
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn command(
|
||||
&self,
|
||||
gid: Option<ProcessGroupId>,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
output: OutputStrategy,
|
||||
) -> Result<ProcessId, RpcError> {
|
||||
let mut cmd = Command::new(command);
|
||||
cmd.args(args);
|
||||
cmd.kill_on_drop(true);
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped());
|
||||
let mut child = cmd.spawn().map_err(|e| {
|
||||
let mut err = yajrc::INTERNAL_ERROR.clone();
|
||||
err.data = Some(json!(e.to_string()));
|
||||
err
|
||||
})?;
|
||||
let pid = ProcessId(child.id().ok_or_else(|| {
|
||||
let mut err = yajrc::INTERNAL_ERROR.clone();
|
||||
err.data = Some(json!("Child has no pid"));
|
||||
err
|
||||
})?);
|
||||
let output = match output {
|
||||
OutputStrategy::Inherit => {
|
||||
let (stdout_send, stdout) = watch::channel(String::new());
|
||||
let (stderr_send, stderr) = watch::channel(String::new());
|
||||
if let (Some(child_stdout), Some(child_stderr)) =
|
||||
(child.stdout.take(), child.stderr.take())
|
||||
{
|
||||
Some(InheritOutput {
|
||||
_thread: tokio::spawn(async move {
|
||||
tokio::join!(
|
||||
async {
|
||||
if let Err(e) = async {
|
||||
let mut lines = BufReader::new(child_stdout).lines();
|
||||
while let Some(line) = lines.next_line().await? {
|
||||
tracing::info!("({}): {}", pid.0, line);
|
||||
let _ = stdout_send.send(line);
|
||||
}
|
||||
Ok::<_, std::io::Error>(())
|
||||
}
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Error reading stdout of pid {}: {}",
|
||||
pid.0,
|
||||
e
|
||||
);
|
||||
}
|
||||
},
|
||||
async {
|
||||
if let Err(e) = async {
|
||||
let mut lines = BufReader::new(child_stderr).lines();
|
||||
while let Some(line) = lines.next_line().await? {
|
||||
tracing::warn!("({}): {}", pid.0, line);
|
||||
let _ = stderr_send.send(line);
|
||||
}
|
||||
Ok::<_, std::io::Error>(())
|
||||
}
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Error reading stdout of pid {}: {}",
|
||||
pid.0,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
})
|
||||
.into(),
|
||||
stdout,
|
||||
stderr,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
OutputStrategy::Collect => None,
|
||||
};
|
||||
self.children.lock().await.processes.insert(
|
||||
pid,
|
||||
ChildInfo {
|
||||
gid,
|
||||
child: Arc::new(Mutex::new(Some(child))),
|
||||
output,
|
||||
},
|
||||
);
|
||||
Ok(pid)
|
||||
}
|
||||
|
||||
async fn output(&self, pid: ProcessId) -> Result<String, RpcError> {
|
||||
let not_found = || {
|
||||
let mut err = yajrc::INTERNAL_ERROR.clone();
|
||||
err.data = Some(json!(format!("Child with pid {} not found", pid.0)));
|
||||
err
|
||||
};
|
||||
let mut child = {
|
||||
self.children
|
||||
.lock()
|
||||
.await
|
||||
.processes
|
||||
.get(&pid)
|
||||
.ok_or_else(not_found)?
|
||||
.child
|
||||
.clone()
|
||||
}
|
||||
.lock_owned()
|
||||
.await;
|
||||
if let Some(child) = child.take() {
|
||||
let output = child.wait_with_output().await?;
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8(output.stdout).map_err(|_| yajrc::PARSE_ERROR)?)
|
||||
} else {
|
||||
Err(RpcError {
|
||||
code: output
|
||||
.status
|
||||
.code()
|
||||
.or_else(|| output.status.signal().map(|s| 128 + s))
|
||||
.unwrap_or(0),
|
||||
message: "Command failed".into(),
|
||||
data: Some(json!(String::from_utf8(if output.stderr.is_empty() {
|
||||
output.stdout
|
||||
} else {
|
||||
output.stderr
|
||||
})
|
||||
.map_err(|_| yajrc::PARSE_ERROR)?)),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Err(not_found())
|
||||
}
|
||||
}
|
||||
|
||||
async fn signal(&self, pid: ProcessId, signal: u32) -> Result<(), RpcError> {
|
||||
let not_found = || {
|
||||
let mut err = yajrc::INTERNAL_ERROR.clone();
|
||||
err.data = Some(json!(format!("Child with pid {} not found", pid.0)));
|
||||
err
|
||||
};
|
||||
|
||||
Self::killall(pid, Signal::try_from(signal as i32)?)?;
|
||||
|
||||
if signal == 9 {
|
||||
self.children
|
||||
.lock()
|
||||
.await
|
||||
.processes
|
||||
.remove(&pid)
|
||||
.ok_or_else(not_found)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn signal_group(&self, gid: ProcessGroupId, signal: u32) -> Result<(), RpcError> {
|
||||
let mut to_kill = Vec::new();
|
||||
{
|
||||
let mut children_ref = self.children.lock().await;
|
||||
let children = std::mem::take(&mut children_ref.deref_mut().processes);
|
||||
for (pid, child_info) in children {
|
||||
if child_info.gid == Some(gid) {
|
||||
to_kill.push(pid);
|
||||
} else {
|
||||
children_ref.processes.insert(pid, child_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
for pid in to_kill {
|
||||
tracing::info!("Killing pid {}", pid.0);
|
||||
Self::killall(pid, Signal::try_from(signal as i32)?)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn killall(pid: ProcessId, signal: Signal) -> Result<(), RpcError> {
|
||||
for proc in procfs::process::all_processes()? {
|
||||
let stat = proc?.stat()?;
|
||||
if ProcessId::from(stat.ppid) == pid {
|
||||
Self::killall(stat.pid.into(), signal)?;
|
||||
}
|
||||
}
|
||||
if let Err(e) = nix::sys::signal::kill(pid.into(), Some(signal)) {
|
||||
if e != Errno::ESRCH {
|
||||
tracing::error!("Failed to kill pid {}: {}", pid.0, e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn graceful_exit(self) {
|
||||
let kill_all = futures::stream::iter(
|
||||
std::mem::take(&mut self.children.lock().await.deref_mut().processes).into_iter(),
|
||||
)
|
||||
.for_each_concurrent(None, |(pid, child)| async move {
|
||||
let _ = Self::killall(pid, Signal::SIGTERM);
|
||||
if let Some(child) = child.child.lock().await.take() {
|
||||
let _ = child.wait_with_output().await;
|
||||
}
|
||||
});
|
||||
kill_all.await
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
let mut sigint = signal(SignalKind::interrupt()).unwrap();
|
||||
let mut sigterm = signal(SignalKind::terminate()).unwrap();
|
||||
let mut sigquit = signal(SignalKind::quit()).unwrap();
|
||||
let mut sighangup = signal(SignalKind::hangup()).unwrap();
|
||||
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
let filter_layer = EnvFilter::new("container_init=debug");
|
||||
let fmt_layer = fmt::layer().with_target(true);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(filter_layer)
|
||||
.with(fmt_layer)
|
||||
.with(ErrorLayer::default())
|
||||
.init();
|
||||
color_eyre::install().unwrap();
|
||||
|
||||
let handler = Handler::new();
|
||||
let handler_thread = async {
|
||||
let listener = tokio::net::UnixListener::bind("/start9/sockets/rpc.sock")?;
|
||||
loop {
|
||||
let (stream, _) = listener.accept().await?;
|
||||
let (r, w) = stream.into_split();
|
||||
let mut lines = BufReader::new(r).lines();
|
||||
let handler = handler.clone();
|
||||
tokio::spawn(async move {
|
||||
let w = Arc::new(Mutex::new(w));
|
||||
while let Some(line) = lines.next_line().await.transpose() {
|
||||
let handler = handler.clone();
|
||||
let w = w.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = async {
|
||||
let req = serde_json::from_str::<IncomingRpc>(&line?)?;
|
||||
match handler.handle(req.input).await {
|
||||
Ok(output) => {
|
||||
if w.lock().await.write_all(
|
||||
format!("{}\n", json!({ "id": req.id, "jsonrpc": "2.0", "result": output }))
|
||||
.as_bytes(),
|
||||
)
|
||||
.await.is_err() {
|
||||
tracing::error!("Error sending to {id:?}", id = req.id);
|
||||
}
|
||||
}
|
||||
Err(e) =>
|
||||
if w
|
||||
.lock()
|
||||
.await
|
||||
.write_all(
|
||||
format!("{}\n", json!({ "id": req.id, "jsonrpc": "2.0", "error": e }))
|
||||
.as_bytes(),
|
||||
)
|
||||
.await.is_err() {
|
||||
|
||||
tracing::error!("Handle + Error sending to {id:?}", id = req.id);
|
||||
},
|
||||
}
|
||||
Ok::<_, color_eyre::Report>(())
|
||||
}
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error parsing RPC request: {}", e);
|
||||
tracing::debug!("{:?}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok::<_, std::io::Error>(())
|
||||
});
|
||||
}
|
||||
#[allow(unreachable_code)]
|
||||
Ok::<_, std::io::Error>(())
|
||||
};
|
||||
|
||||
select! {
|
||||
res = handler_thread => {
|
||||
match res {
|
||||
Ok(()) => tracing::debug!("Done with inputs/outputs"),
|
||||
Err(e) => {
|
||||
tracing::error!("Error reading RPC input: {}", e);
|
||||
tracing::debug!("{:?}", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
_ = sigint.recv() => {
|
||||
tracing::debug!("SIGINT");
|
||||
},
|
||||
_ = sigterm.recv() => {
|
||||
tracing::debug!("SIGTERM");
|
||||
},
|
||||
_ = sigquit.recv() => {
|
||||
tracing::debug!("SIGQUIT");
|
||||
},
|
||||
_ = sighangup.recv() => {
|
||||
tracing::debug!("SIGHUP");
|
||||
}
|
||||
}
|
||||
handler.graceful_exit().await;
|
||||
::std::process::exit(0)
|
||||
}
|
||||
@@ -11,9 +11,9 @@ futures = "0.3.28"
|
||||
lazy_async_pool = "0.3.3"
|
||||
models = { path = "../models" }
|
||||
pin-project = "1.1.3"
|
||||
rpc-toolkit = "0.2.3"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-stream = { version = "0.1.14", features = ["io-util", "sync"] }
|
||||
tracing = "0.1.39"
|
||||
yajrc = { version = "*", git = "https://github.com/dr-bonez/yajrc.git", branch = "develop" }
|
||||
|
||||
@@ -11,11 +11,9 @@ use tokio::sync::oneshot;
|
||||
use tokio::task::{JoinError, JoinHandle, LocalSet};
|
||||
|
||||
mod byte_replacement_reader;
|
||||
mod rpc_client;
|
||||
mod rsync;
|
||||
mod script_dir;
|
||||
pub use byte_replacement_reader::*;
|
||||
pub use rpc_client::{RpcClient, UnixRpcClient};
|
||||
pub use rsync::*;
|
||||
pub use script_dir::*;
|
||||
|
||||
|
||||
@@ -12,7 +12,4 @@ if [ -z "$PLATFORM" ]; then
|
||||
export PLATFORM=$(uname -m)
|
||||
fi
|
||||
|
||||
cargo install --path=./startos --no-default-features --features=js_engine,sdk,cli --locked
|
||||
startbox_loc=$(which startbox)
|
||||
ln -sf $startbox_loc $(dirname $startbox_loc)/start-cli
|
||||
ln -sf $startbox_loc $(dirname $startbox_loc)/start-sdk
|
||||
cargo install --path=./startos --no-default-features --features=cli,docker --bin start-cli --locked
|
||||
@@ -1,23 +0,0 @@
|
||||
[package]
|
||||
name = "js-engine"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.74"
|
||||
dashmap = "5.5.3"
|
||||
deno_core = "=0.222.0"
|
||||
deno_ast = { version = "=0.29.5", features = ["transpiling"] }
|
||||
container-init = { path = "../container-init" }
|
||||
reqwest = { version = "0.11.22" }
|
||||
sha2 = "0.10.8"
|
||||
itertools = "0.11.0"
|
||||
lazy_static = "1.4.0"
|
||||
models = { path = "../models" }
|
||||
helpers = { path = "../helpers" }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tracing = "0.1"
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,242 +0,0 @@
|
||||
import Deno from "/deno_global.js";
|
||||
import * as mainModule from "/embassy.js";
|
||||
|
||||
function requireParam(param) {
|
||||
throw new Error(`Missing required parameter ${param}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is using the simplified json pointer spec, using no escapes and arrays
|
||||
* @param {object} obj
|
||||
* @param {string} pointer
|
||||
* @returns
|
||||
*/
|
||||
function jsonPointerValue(obj, pointer) {
|
||||
const paths = pointer.substring(1).split("/");
|
||||
for (const path of paths) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
obj = (obj || {})[path];
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function maybeDate(value) {
|
||||
if (!value) return value;
|
||||
return new Date(value);
|
||||
}
|
||||
const writeFile = (
|
||||
{
|
||||
path = requireParam("path"),
|
||||
volumeId = requireParam("volumeId"),
|
||||
toWrite = requireParam("toWrite"),
|
||||
} = requireParam("options"),
|
||||
) => Deno.core.opAsync("write_file", volumeId, path, toWrite);
|
||||
|
||||
const readFile = (
|
||||
{ volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"),
|
||||
) => Deno.core.opAsync("read_file", volumeId, path);
|
||||
|
||||
|
||||
|
||||
const runDaemon = (
|
||||
{ command = requireParam("command"), args = [] } = requireParam("options"),
|
||||
) => {
|
||||
let id = Deno.core.opAsync("start_command", command, args, "inherit", null);
|
||||
let processId = id.then(x => x.processId)
|
||||
let waitPromise = null;
|
||||
return {
|
||||
processId,
|
||||
async wait() {
|
||||
waitPromise = waitPromise || Deno.core.opAsync("wait_command", await processId)
|
||||
return waitPromise
|
||||
},
|
||||
async term(signal = 15) {
|
||||
return Deno.core.opAsync("send_signal", await processId, 15)
|
||||
}
|
||||
}
|
||||
};
|
||||
const runCommand = async (
|
||||
{ command = requireParam("command"), args = [], timeoutMillis = 30000 } = requireParam("options"),
|
||||
) => {
|
||||
let id = Deno.core.opAsync("start_command", command, args, "collect", timeoutMillis);
|
||||
let pid = id.then(x => x.processId)
|
||||
return Deno.core.opAsync("wait_command", await pid)
|
||||
};
|
||||
const signalGroup = async (
|
||||
{ gid = requireParam("gid"), signal = requireParam("signal") } = requireParam("gid and signal")
|
||||
) => {
|
||||
return Deno.core.opAsync("signal_group", gid, signal);
|
||||
};
|
||||
const sleep = (timeMs = requireParam("timeMs"),
|
||||
) => Deno.core.opAsync("sleep", timeMs);
|
||||
|
||||
const rename = (
|
||||
{
|
||||
srcVolume = requireParam("srcVolume"),
|
||||
dstVolume = requirePapram("dstVolume"),
|
||||
srcPath = requireParam("srcPath"),
|
||||
dstPath = requireParam("dstPath"),
|
||||
} = requireParam("options"),
|
||||
) => Deno.core.opAsync("rename", srcVolume, srcPath, dstVolume, dstPath);
|
||||
const metadata = async (
|
||||
{ volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"),
|
||||
) => {
|
||||
const data = await Deno.core.opAsync("metadata", volumeId, path);
|
||||
return {
|
||||
...data,
|
||||
modified: maybeDate(data.modified),
|
||||
created: maybeDate(data.created),
|
||||
accessed: maybeDate(data.accessed),
|
||||
};
|
||||
};
|
||||
const removeFile = (
|
||||
{ volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"),
|
||||
) => Deno.core.opAsync("remove_file", volumeId, path);
|
||||
const isSandboxed = () => Deno.core.ops["is_sandboxed"]();
|
||||
|
||||
const writeJsonFile = (
|
||||
{
|
||||
volumeId = requireParam("volumeId"),
|
||||
path = requireParam("path"),
|
||||
toWrite = requireParam("toWrite"),
|
||||
} = requireParam("options"),
|
||||
) =>
|
||||
writeFile({
|
||||
volumeId,
|
||||
path,
|
||||
toWrite: JSON.stringify(toWrite),
|
||||
});
|
||||
|
||||
const chown = async (
|
||||
{
|
||||
volumeId = requireParam("volumeId"),
|
||||
path = requireParam("path"),
|
||||
uid = requireParam("uid"),
|
||||
} = requireParam("options"),
|
||||
) => {
|
||||
return await Deno.core.opAsync("chown", volumeId, path, uid);
|
||||
};
|
||||
|
||||
const chmod = async (
|
||||
{
|
||||
volumeId = requireParam("volumeId"),
|
||||
path = requireParam("path"),
|
||||
mode = requireParam("mode"),
|
||||
} = requireParam("options"),
|
||||
) => {
|
||||
return await Deno.core.opAsync("chmod", volumeId, path, mode);
|
||||
};
|
||||
const readJsonFile = async (
|
||||
{ volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"),
|
||||
) => JSON.parse(await readFile({ volumeId, path }));
|
||||
const createDir = (
|
||||
{ volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"),
|
||||
) => Deno.core.opAsync("create_dir", volumeId, path);
|
||||
|
||||
const readDir = (
|
||||
{ volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"),
|
||||
) => Deno.core.opAsync("read_dir", volumeId, path);
|
||||
const removeDir = (
|
||||
{ volumeId = requireParam("volumeId"), path = requireParam("path") } = requireParam("options"),
|
||||
) => Deno.core.opAsync("remove_dir", volumeId, path);
|
||||
const trace = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opAsync("log_trace", whatToTrace);
|
||||
const warn = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opAsync("log_warn", whatToTrace);
|
||||
const error = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opAsync("log_error", whatToTrace);
|
||||
const debug = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opAsync("log_debug", whatToTrace);
|
||||
const info = (whatToTrace = requireParam('whatToTrace')) => Deno.core.opAsync("log_info", whatToTrace);
|
||||
const fetch = async (url = requireParam ('url'), options = null) => {
|
||||
const { body, ...response } = await Deno.core.opAsync("fetch", url, options);
|
||||
const textValue = Promise.resolve(body);
|
||||
return {
|
||||
...response,
|
||||
text() {
|
||||
return textValue;
|
||||
},
|
||||
json() {
|
||||
return textValue.then((x) => JSON.parse(x));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const runRsync = (
|
||||
{
|
||||
srcVolume = requireParam("srcVolume"),
|
||||
dstVolume = requireParam("dstVolume"),
|
||||
srcPath = requireParam("srcPath"),
|
||||
dstPath = requireParam("dstPath"),
|
||||
options = requireParam("options"),
|
||||
} = requireParam("options"),
|
||||
) => {
|
||||
let id = Deno.core.opAsync("rsync", srcVolume, srcPath, dstVolume, dstPath, options);
|
||||
let waitPromise = null;
|
||||
return {
|
||||
async id() {
|
||||
return id
|
||||
},
|
||||
async wait() {
|
||||
waitPromise = waitPromise || Deno.core.opAsync("rsync_wait", await id)
|
||||
return waitPromise
|
||||
},
|
||||
async progress() {
|
||||
return Deno.core.opAsync("rsync_progress", await id)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const diskUsage = async ({
|
||||
volumeId = requireParam("volumeId"),
|
||||
path = requireParam("path"),
|
||||
} = { volumeId: null, path: null }) => {
|
||||
const [used, total] = await Deno.core.opAsync("disk_usage", volumeId, path);
|
||||
return { used, total }
|
||||
}
|
||||
|
||||
const currentFunction = Deno.core.ops.current_function();
|
||||
const input = Deno.core.ops.get_input();
|
||||
const variable_args = Deno.core.ops.get_variable_args();
|
||||
const setState = (x) => Deno.core.ops.set_value(x);
|
||||
const effects = {
|
||||
chmod,
|
||||
chown,
|
||||
writeFile,
|
||||
readFile,
|
||||
writeJsonFile,
|
||||
readJsonFile,
|
||||
error,
|
||||
warn,
|
||||
debug,
|
||||
trace,
|
||||
info,
|
||||
isSandboxed,
|
||||
fetch,
|
||||
removeFile,
|
||||
createDir,
|
||||
removeDir,
|
||||
metadata,
|
||||
rename,
|
||||
runCommand,
|
||||
sleep,
|
||||
runDaemon,
|
||||
signalGroup,
|
||||
runRsync,
|
||||
readDir,
|
||||
diskUsage,
|
||||
};
|
||||
|
||||
const defaults = {
|
||||
"handleSignal": (effects, { gid, signal }) => {
|
||||
return effects.signalGroup({ gid, signal })
|
||||
}
|
||||
}
|
||||
|
||||
const runFunction = jsonPointerValue(mainModule, currentFunction) || jsonPointerValue(defaults, currentFunction);
|
||||
(async () => {
|
||||
if (typeof runFunction !== "function") {
|
||||
error(`Expecting ${currentFunction} to be a function`);
|
||||
throw new Error(`Expecting ${currentFunction} to be a function`);
|
||||
}
|
||||
const answer = await runFunction(effects, input, ...variable_args);
|
||||
setState(answer);
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ emver = { version = "0.1", git = "https://github.com/Start9Labs/emver-rs.git", f
|
||||
"serde",
|
||||
] }
|
||||
ipnet = "2.8.0"
|
||||
num_enum = "0.7.1"
|
||||
openssl = { version = "0.10.57", features = ["vendored"] }
|
||||
patch-db = { version = "*", path = "../../patch-db/patch-db", features = [
|
||||
"trace",
|
||||
@@ -31,8 +32,9 @@ sqlx = { version = "0.7.2", features = [
|
||||
"postgres",
|
||||
] }
|
||||
ssh-key = "0.6.2"
|
||||
ts-rs = { git = "https://github.com/dr-bonez/ts-rs.git", branch = "feature/top-level-type-override" } # "8"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
torut = "0.2.1"
|
||||
torut = { git = "https://github.com/Start9Labs/torut.git", branch = "update/dependencies" }
|
||||
tracing = "0.1.39"
|
||||
yasi = "0.1.5"
|
||||
|
||||
3
core/models/bindings/ServiceInterfaceId.ts
Normal file
3
core/models/bindings/ServiceInterfaceId.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ServiceInterfaceId = string;
|
||||
@@ -6,11 +6,13 @@ use color_eyre::eyre::eyre;
|
||||
use reqwest::header::CONTENT_TYPE;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
use ts_rs::TS;
|
||||
use yasi::InternedString;
|
||||
|
||||
use crate::{mime, Error, ErrorKind, ResultExt};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, TS)]
|
||||
#[ts(type = "string")]
|
||||
pub struct DataUrl<'a> {
|
||||
mime: InternedString,
|
||||
data: Cow<'a, [u8]>,
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
use std::fmt::Display;
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use num_enum::TryFromPrimitive;
|
||||
use patch_db::Revision;
|
||||
use rpc_toolkit::hyper::http::uri::InvalidUri;
|
||||
use rpc_toolkit::reqwest;
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use rpc_toolkit::yajrc::{
|
||||
RpcError, INVALID_PARAMS_ERROR, INVALID_REQUEST_ERROR, METHOD_NOT_FOUND_ERROR, PARSE_ERROR,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::InvalidId;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
|
||||
#[repr(i32)]
|
||||
pub enum ErrorKind {
|
||||
Unknown = 1,
|
||||
Filesystem = 2,
|
||||
@@ -81,6 +86,8 @@ pub enum ErrorKind {
|
||||
CpuSettings = 69,
|
||||
Firmware = 70,
|
||||
Timeout = 71,
|
||||
Lxc = 72,
|
||||
Cancelled = 73,
|
||||
}
|
||||
impl ErrorKind {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
@@ -157,6 +164,8 @@ impl ErrorKind {
|
||||
CpuSettings => "CPU Settings Error",
|
||||
Firmware => "Firmware Error",
|
||||
Timeout => "Timeout Error",
|
||||
Lxc => "LXC Error",
|
||||
Cancelled => "Cancelled",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,6 +195,22 @@ impl Error {
|
||||
revision: None,
|
||||
}
|
||||
}
|
||||
pub fn clone_output(&self) -> Self {
|
||||
Error {
|
||||
source: ErrorData {
|
||||
details: format!("{}", self.source),
|
||||
debug: format!("{:?}", self.source),
|
||||
}
|
||||
.into(),
|
||||
kind: self.kind,
|
||||
revision: self.revision.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<std::convert::Infallible> for Error {
|
||||
fn from(value: std::convert::Infallible) -> Self {
|
||||
match value {}
|
||||
}
|
||||
}
|
||||
impl From<InvalidId> for Error {
|
||||
fn from(err: InvalidId) -> Self {
|
||||
@@ -300,6 +325,53 @@ impl From<patch_db::value::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct ErrorData {
|
||||
pub details: String,
|
||||
pub debug: String,
|
||||
}
|
||||
impl Display for ErrorData {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&self.details, f)
|
||||
}
|
||||
}
|
||||
impl Debug for ErrorData {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&self.debug, f)
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ErrorData {}
|
||||
impl From<&RpcError> for ErrorData {
|
||||
fn from(value: &RpcError) -> Self {
|
||||
Self {
|
||||
details: value
|
||||
.data
|
||||
.as_ref()
|
||||
.and_then(|d| {
|
||||
d.as_object()
|
||||
.and_then(|d| {
|
||||
d.get("details")
|
||||
.and_then(|d| d.as_str().map(|s| s.to_owned()))
|
||||
})
|
||||
.or_else(|| d.as_str().map(|s| s.to_owned()))
|
||||
})
|
||||
.unwrap_or_else(|| value.message.clone().into_owned()),
|
||||
debug: value
|
||||
.data
|
||||
.as_ref()
|
||||
.and_then(|d| {
|
||||
d.as_object()
|
||||
.and_then(|d| {
|
||||
d.get("debug")
|
||||
.and_then(|d| d.as_str().map(|s| s.to_owned()))
|
||||
})
|
||||
.or_else(|| d.as_str().map(|s| s.to_owned()))
|
||||
})
|
||||
.unwrap_or_else(|| value.message.clone().into_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for RpcError {
|
||||
fn from(e: Error) -> Self {
|
||||
let mut data_object = serde_json::Map::with_capacity(3);
|
||||
@@ -318,10 +390,40 @@ impl From<Error> for RpcError {
|
||||
RpcError {
|
||||
code: e.kind as i32,
|
||||
message: e.kind.as_str().into(),
|
||||
data: Some(data_object.into()),
|
||||
data: Some(
|
||||
match serde_json::to_value(&ErrorData {
|
||||
details: format!("{}", e.source),
|
||||
debug: format!("{:?}", e.source),
|
||||
}) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::warn!("Error serializing revision for Error object: {}", e);
|
||||
serde_json::Value::Null
|
||||
}
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<RpcError> for Error {
|
||||
fn from(e: RpcError) -> Self {
|
||||
Error::new(
|
||||
ErrorData::from(&e),
|
||||
if let Ok(kind) = e.code.try_into() {
|
||||
kind
|
||||
} else if e.code == METHOD_NOT_FOUND_ERROR.code {
|
||||
ErrorKind::NotFound
|
||||
} else if e.code == PARSE_ERROR.code
|
||||
|| e.code == INVALID_PARAMS_ERROR.code
|
||||
|| e.code == INVALID_REQUEST_ERROR.code
|
||||
{
|
||||
ErrorKind::Deserialization
|
||||
} else {
|
||||
ErrorKind::Unknown
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ErrorCollection(Vec<Error>);
|
||||
@@ -377,10 +479,7 @@ where
|
||||
Self: Sized,
|
||||
{
|
||||
fn with_kind(self, kind: ErrorKind) -> Result<T, Error>;
|
||||
fn with_ctx<F: FnOnce(&E) -> (ErrorKind, D), D: Display + Send + Sync + 'static>(
|
||||
self,
|
||||
f: F,
|
||||
) -> Result<T, Error>;
|
||||
fn with_ctx<F: FnOnce(&E) -> (ErrorKind, D), D: Display>(self, f: F) -> Result<T, Error>;
|
||||
}
|
||||
impl<T, E> ResultExt<T, E> for Result<T, E>
|
||||
where
|
||||
@@ -394,10 +493,7 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
fn with_ctx<F: FnOnce(&E) -> (ErrorKind, D), D: Display + Send + Sync + 'static>(
|
||||
self,
|
||||
f: F,
|
||||
) -> Result<T, Error> {
|
||||
fn with_ctx<F: FnOnce(&E) -> (ErrorKind, D), D: Display>(self, f: F) -> Result<T, Error> {
|
||||
self.map_err(|e| {
|
||||
let (kind, ctx) = f(&e);
|
||||
let source = color_eyre::eyre::Error::from(e);
|
||||
@@ -411,6 +507,29 @@ where
|
||||
})
|
||||
}
|
||||
}
|
||||
impl<T> ResultExt<T, Error> for Result<T, Error> {
|
||||
fn with_kind(self, kind: ErrorKind) -> Result<T, Error> {
|
||||
self.map_err(|e| Error {
|
||||
source: e.source,
|
||||
kind,
|
||||
revision: e.revision,
|
||||
})
|
||||
}
|
||||
|
||||
fn with_ctx<F: FnOnce(&Error) -> (ErrorKind, D), D: Display>(self, f: F) -> Result<T, Error> {
|
||||
self.map_err(|e| {
|
||||
let (kind, ctx) = f(&e);
|
||||
let source = e.source;
|
||||
let ctx = format!("{}: {}", ctx, source);
|
||||
let source = source.wrap_err(ctx);
|
||||
Error {
|
||||
kind,
|
||||
source,
|
||||
revision: e.revision,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OptionExt<T>
|
||||
where
|
||||
|
||||
@@ -2,10 +2,12 @@ use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::{Id, InvalidId};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, TS)]
|
||||
#[ts(type = "string")]
|
||||
pub struct ActionId(Id);
|
||||
impl FromStr for ActionId {
|
||||
type Err = InvalidId;
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::Id;
|
||||
use crate::{Id, InvalidId};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, TS)]
|
||||
#[ts(type = "string")]
|
||||
pub struct HealthCheckId(Id);
|
||||
impl std::fmt::Display for HealthCheckId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", &self.0)
|
||||
}
|
||||
}
|
||||
impl FromStr for HealthCheckId {
|
||||
type Err = InvalidId;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Id::from_str(s).map(HealthCheckId)
|
||||
}
|
||||
}
|
||||
impl AsRef<str> for HealthCheckId {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
|
||||
@@ -2,52 +2,65 @@ use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use ts_rs::TS;
|
||||
use yasi::InternedString;
|
||||
|
||||
use crate::{Id, InvalidId};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
pub struct InterfaceId(Id);
|
||||
impl FromStr for InterfaceId {
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, TS)]
|
||||
#[ts(type = "string")]
|
||||
pub struct HostId(Id);
|
||||
impl FromStr for HostId {
|
||||
type Err = InvalidId;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self(Id::try_from(s.to_owned())?))
|
||||
}
|
||||
}
|
||||
impl From<Id> for InterfaceId {
|
||||
impl From<Id> for HostId {
|
||||
fn from(id: Id) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for InterfaceId {
|
||||
impl From<HostId> for Id {
|
||||
fn from(value: HostId) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
impl From<HostId> for InternedString {
|
||||
fn from(value: HostId) -> Self {
|
||||
value.0.into()
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for HostId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", &self.0)
|
||||
}
|
||||
}
|
||||
impl std::ops::Deref for InterfaceId {
|
||||
impl std::ops::Deref for HostId {
|
||||
type Target = str;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
}
|
||||
}
|
||||
impl AsRef<str> for InterfaceId {
|
||||
impl AsRef<str> for HostId {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for InterfaceId {
|
||||
impl<'de> Deserialize<'de> for HostId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(InterfaceId(Deserialize::deserialize(deserializer)?))
|
||||
Ok(HostId(Deserialize::deserialize(deserializer)?))
|
||||
}
|
||||
}
|
||||
impl AsRef<Path> for InterfaceId {
|
||||
impl AsRef<Path> for HostId {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref().as_ref()
|
||||
}
|
||||
}
|
||||
impl<'q> sqlx::Encode<'q, sqlx::Postgres> for InterfaceId {
|
||||
impl<'q> sqlx::Encode<'q, sqlx::Postgres> for HostId {
|
||||
fn encode_by_ref(
|
||||
&self,
|
||||
buf: &mut <sqlx::Postgres as sqlx::database::HasArguments<'q>>::ArgumentBuffer,
|
||||
@@ -55,7 +68,7 @@ impl<'q> sqlx::Encode<'q, sqlx::Postgres> for InterfaceId {
|
||||
<&str as sqlx::Encode<'q, sqlx::Postgres>>::encode_by_ref(&&**self, buf)
|
||||
}
|
||||
}
|
||||
impl sqlx::Type<sqlx::Postgres> for InterfaceId {
|
||||
impl sqlx::Type<sqlx::Postgres> for HostId {
|
||||
fn type_info() -> sqlx::postgres::PgTypeInfo {
|
||||
<&str as sqlx::Type<sqlx::Postgres>>::type_info()
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
use std::fmt::Debug;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::{Id, InvalidId, PackageId, Version};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, TS)]
|
||||
#[ts(type = "string")]
|
||||
pub struct ImageId(Id);
|
||||
impl AsRef<Path> for ImageId {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref().as_ref()
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for ImageId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", &self.0)
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::str::FromStr;
|
||||
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use yasi::InternedString;
|
||||
|
||||
mod action;
|
||||
mod address;
|
||||
mod health_check;
|
||||
mod host;
|
||||
mod image;
|
||||
mod interface;
|
||||
mod invalid_id;
|
||||
mod package;
|
||||
mod service_interface;
|
||||
mod volume;
|
||||
|
||||
pub use action::ActionId;
|
||||
pub use address::AddressId;
|
||||
pub use health_check::HealthCheckId;
|
||||
pub use host::HostId;
|
||||
pub use image::ImageId;
|
||||
pub use interface::InterfaceId;
|
||||
pub use invalid_id::InvalidId;
|
||||
pub use package::{PackageId, SYSTEM_PACKAGE_ID};
|
||||
pub use service_interface::ServiceInterfaceId;
|
||||
pub use volume::VolumeId;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@@ -32,7 +33,7 @@ pub struct Id(InternedString);
|
||||
impl TryFrom<InternedString> for Id {
|
||||
type Error = InvalidId;
|
||||
fn try_from(value: InternedString) -> Result<Self, Self::Error> {
|
||||
if ID_REGEX.is_match(&*value) {
|
||||
if ID_REGEX.is_match(&value) {
|
||||
Ok(Id(value))
|
||||
} else {
|
||||
Err(InvalidId)
|
||||
@@ -52,17 +53,28 @@ impl TryFrom<String> for Id {
|
||||
impl TryFrom<&str> for Id {
|
||||
type Error = InvalidId;
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
if ID_REGEX.is_match(&value) {
|
||||
if ID_REGEX.is_match(value) {
|
||||
Ok(Id(InternedString::intern(value)))
|
||||
} else {
|
||||
Err(InvalidId)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FromStr for Id {
|
||||
type Err = InvalidId;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s)
|
||||
}
|
||||
}
|
||||
impl From<Id> for InternedString {
|
||||
fn from(value: Id) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
impl std::ops::Deref for Id {
|
||||
type Target = str;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for Id {
|
||||
@@ -72,7 +84,7 @@ impl std::fmt::Display for Id {
|
||||
}
|
||||
impl AsRef<str> for Id {
|
||||
fn as_ref(&self) -> &str {
|
||||
&*self.0
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl Borrow<str> for Id {
|
||||
@@ -94,7 +106,7 @@ impl Serialize for Id {
|
||||
where
|
||||
Ser: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&*self)
|
||||
serializer.serialize_str(self)
|
||||
}
|
||||
}
|
||||
impl<'q> sqlx::Encode<'q, sqlx::Postgres> for Id {
|
||||
|
||||
@@ -3,13 +3,16 @@ use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use ts_rs::TS;
|
||||
use yasi::InternedString;
|
||||
|
||||
use crate::{Id, InvalidId, SYSTEM_ID};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref SYSTEM_PACKAGE_ID: PackageId = PackageId(SYSTEM_ID.clone());
|
||||
}
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, TS)]
|
||||
#[ts(type = "string")]
|
||||
pub struct PackageId(Id);
|
||||
impl FromStr for PackageId {
|
||||
type Err = InvalidId;
|
||||
@@ -22,10 +25,20 @@ impl From<Id> for PackageId {
|
||||
PackageId(id)
|
||||
}
|
||||
}
|
||||
impl From<PackageId> for Id {
|
||||
fn from(value: PackageId) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
impl From<PackageId> for InternedString {
|
||||
fn from(value: PackageId) -> Self {
|
||||
value.0.into()
|
||||
}
|
||||
}
|
||||
impl std::ops::Deref for PackageId {
|
||||
type Target = str;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl AsRef<PackageId> for PackageId {
|
||||
|
||||
@@ -1,46 +1,48 @@
|
||||
use std::path::Path;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::Id;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
pub struct AddressId(Id);
|
||||
impl From<Id> for AddressId {
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, TS)]
|
||||
#[ts(export, type = "string")]
|
||||
pub struct ServiceInterfaceId(Id);
|
||||
impl From<Id> for ServiceInterfaceId {
|
||||
fn from(id: Id) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for AddressId {
|
||||
impl std::fmt::Display for ServiceInterfaceId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", &self.0)
|
||||
}
|
||||
}
|
||||
impl std::ops::Deref for AddressId {
|
||||
impl std::ops::Deref for ServiceInterfaceId {
|
||||
type Target = str;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
}
|
||||
}
|
||||
impl AsRef<str> for AddressId {
|
||||
impl AsRef<str> for ServiceInterfaceId {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for AddressId {
|
||||
impl<'de> Deserialize<'de> for ServiceInterfaceId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(AddressId(Deserialize::deserialize(deserializer)?))
|
||||
Ok(ServiceInterfaceId(Deserialize::deserialize(deserializer)?))
|
||||
}
|
||||
}
|
||||
impl AsRef<Path> for AddressId {
|
||||
impl AsRef<Path> for ServiceInterfaceId {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref().as_ref()
|
||||
}
|
||||
}
|
||||
impl<'q> sqlx::Encode<'q, sqlx::Postgres> for AddressId {
|
||||
impl<'q> sqlx::Encode<'q, sqlx::Postgres> for ServiceInterfaceId {
|
||||
fn encode_by_ref(
|
||||
&self,
|
||||
buf: &mut <sqlx::Postgres as sqlx::database::HasArguments<'q>>::ArgumentBuffer,
|
||||
@@ -48,7 +50,7 @@ impl<'q> sqlx::Encode<'q, sqlx::Postgres> for AddressId {
|
||||
<&str as sqlx::Encode<'q, sqlx::Postgres>>::encode_by_ref(&&**self, buf)
|
||||
}
|
||||
}
|
||||
impl sqlx::Type<sqlx::Postgres> for AddressId {
|
||||
impl sqlx::Type<sqlx::Postgres> for ServiceInterfaceId {
|
||||
fn type_info() -> sqlx::postgres::PgTypeInfo {
|
||||
<&str as sqlx::Type<sqlx::Postgres>>::type_info()
|
||||
}
|
||||
@@ -2,10 +2,12 @@ use std::borrow::Borrow;
|
||||
use std::path::Path;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::Id;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, TS)]
|
||||
#[ts(type = "string")]
|
||||
pub enum VolumeId {
|
||||
Backup,
|
||||
Custom(Id),
|
||||
|
||||
@@ -1,57 +1,42 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{ActionId, HealthCheckId, PackageId};
|
||||
use crate::ActionId;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ProcedureName {
|
||||
Main, // Usually just run container
|
||||
CreateBackup,
|
||||
RestoreBackup,
|
||||
StartMain,
|
||||
StopMain,
|
||||
GetConfig,
|
||||
SetConfig,
|
||||
Migration,
|
||||
CreateBackup,
|
||||
Properties,
|
||||
LongRunning,
|
||||
Check(PackageId),
|
||||
AutoConfig(PackageId),
|
||||
Health(HealthCheckId),
|
||||
Action(ActionId),
|
||||
Signal,
|
||||
RestoreBackup,
|
||||
ActionMetadata,
|
||||
RunAction(ActionId),
|
||||
GetAction(ActionId),
|
||||
QueryDependency(ActionId),
|
||||
UpdateDependency(ActionId),
|
||||
Init,
|
||||
Uninit,
|
||||
}
|
||||
|
||||
impl ProcedureName {
|
||||
pub fn docker_name(&self) -> Option<String> {
|
||||
pub fn js_function_name(&self) -> String {
|
||||
match self {
|
||||
ProcedureName::Main => None,
|
||||
ProcedureName::LongRunning => None,
|
||||
ProcedureName::CreateBackup => Some("CreateBackup".to_string()),
|
||||
ProcedureName::RestoreBackup => Some("RestoreBackup".to_string()),
|
||||
ProcedureName::GetConfig => Some("GetConfig".to_string()),
|
||||
ProcedureName::SetConfig => Some("SetConfig".to_string()),
|
||||
ProcedureName::Migration => Some("Migration".to_string()),
|
||||
ProcedureName::Properties => Some(format!("Properties-{}", rand::random::<u64>())),
|
||||
ProcedureName::Health(id) => Some(format!("{}Health", id)),
|
||||
ProcedureName::Action(id) => Some(format!("{}Action", id)),
|
||||
ProcedureName::Check(_) => None,
|
||||
ProcedureName::AutoConfig(_) => None,
|
||||
ProcedureName::Signal => None,
|
||||
}
|
||||
}
|
||||
pub fn js_function_name(&self) -> Option<String> {
|
||||
match self {
|
||||
ProcedureName::Main => Some("/main".to_string()),
|
||||
ProcedureName::LongRunning => None,
|
||||
ProcedureName::CreateBackup => Some("/createBackup".to_string()),
|
||||
ProcedureName::RestoreBackup => Some("/restoreBackup".to_string()),
|
||||
ProcedureName::GetConfig => Some("/getConfig".to_string()),
|
||||
ProcedureName::SetConfig => Some("/setConfig".to_string()),
|
||||
ProcedureName::Migration => Some("/migration".to_string()),
|
||||
ProcedureName::Properties => Some("/properties".to_string()),
|
||||
ProcedureName::Health(id) => Some(format!("/health/{}", id)),
|
||||
ProcedureName::Action(id) => Some(format!("/action/{}", id)),
|
||||
ProcedureName::Check(id) => Some(format!("/dependencies/{}/check", id)),
|
||||
ProcedureName::AutoConfig(id) => Some(format!("/dependencies/{}/autoConfigure", id)),
|
||||
ProcedureName::Signal => Some("/handleSignal".to_string()),
|
||||
ProcedureName::Init => "/init".to_string(),
|
||||
ProcedureName::Uninit => "/uninit".to_string(),
|
||||
ProcedureName::StartMain => "/main/start".to_string(),
|
||||
ProcedureName::StopMain => "/main/stop".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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
[package]
|
||||
name = "snapshot_creator"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dashmap = "5.3.4"
|
||||
deno_core = "=0.222.0"
|
||||
deno_ast = { version = "=0.29.5", features = ["transpiling"] }
|
||||
@@ -1,11 +0,0 @@
|
||||
use deno_core::JsRuntimeForSnapshot;
|
||||
|
||||
fn main() {
|
||||
let runtime = JsRuntimeForSnapshot::new(Default::default());
|
||||
let snapshot = runtime.snapshot();
|
||||
|
||||
let snapshot_slice: &[u8] = &*snapshot;
|
||||
println!("Snapshot size: {}", snapshot_slice.len());
|
||||
|
||||
std::fs::write("JS_SNAPSHOT.bin", snapshot_slice).unwrap();
|
||||
}
|
||||
@@ -21,20 +21,27 @@ license = "MIT"
|
||||
name = "startos"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "containerbox"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "start-cli"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "startbox"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
avahi = ["avahi-sys"]
|
||||
avahi-alias = ["avahi"]
|
||||
cli = []
|
||||
container-runtime = []
|
||||
daemon = []
|
||||
default = ["cli", "sdk", "daemon", "js-engine"]
|
||||
default = ["cli", "daemon"]
|
||||
dev = []
|
||||
docker = []
|
||||
sdk = []
|
||||
unstable = ["console-subscriber", "tokio/tracing"]
|
||||
docker = []
|
||||
test = []
|
||||
|
||||
[dependencies]
|
||||
aes = { version = "0.7.5", features = ["ctr"] }
|
||||
@@ -45,9 +52,8 @@ async-compression = { version = "0.4.4", features = [
|
||||
] }
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.74"
|
||||
avahi-sys = { git = "https://github.com/Start9Labs/avahi-sys", version = "0.10.0", branch = "feature/dynamic-linking", features = [
|
||||
"dynamic",
|
||||
], optional = true }
|
||||
axum = { version = "0.7.3", features = ["ws"] }
|
||||
axum-server = "0.6.0"
|
||||
base32 = "0.4.0"
|
||||
base64 = "0.21.4"
|
||||
base64ct = "1.6.0"
|
||||
@@ -55,7 +61,7 @@ basic-cookies = "0.1.4"
|
||||
blake3 = "1.5.0"
|
||||
bytes = "1"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
clap = "3.2.25"
|
||||
clap = "4.4.12"
|
||||
color-eyre = "0.6.2"
|
||||
console = "0.15.7"
|
||||
console-subscriber = { version = "0.2", optional = true }
|
||||
@@ -65,14 +71,14 @@ current_platform = "0.2.0"
|
||||
digest = "0.10.7"
|
||||
divrem = "1.0.0"
|
||||
ed25519 = { version = "2.2.3", features = ["pkcs8", "pem", "alloc"] }
|
||||
ed25519-dalek = { version = "2.0.0", features = [
|
||||
ed25519-dalek = { version = "2.1.1", features = [
|
||||
"serde",
|
||||
"zeroize",
|
||||
"rand_core",
|
||||
"digest",
|
||||
"pkcs8",
|
||||
] }
|
||||
ed25519-dalek-v1 = { package = "ed25519-dalek", version = "1" }
|
||||
container-init = { path = "../container-init" }
|
||||
emver = { version = "0.1.7", git = "https://github.com/Start9Labs/emver-rs.git", features = [
|
||||
"serde",
|
||||
] }
|
||||
@@ -82,9 +88,11 @@ gpt = "3.1.0"
|
||||
helpers = { path = "../helpers" }
|
||||
hex = "0.4.3"
|
||||
hmac = "0.12.1"
|
||||
http = "0.2.9"
|
||||
hyper = { version = "0.14.27", features = ["full"] }
|
||||
hyper-ws-listener = "0.3.0"
|
||||
http = "1.0.0"
|
||||
id-pool = { version = "0.2.2", default-features = false, features = [
|
||||
"serde",
|
||||
"u16",
|
||||
] }
|
||||
imbl = "2.0.2"
|
||||
imbl-value = { git = "https://github.com/Start9Labs/imbl-value.git" }
|
||||
include_dir = "0.7.3"
|
||||
@@ -94,12 +102,13 @@ integer-encoding = { version = "4.0.0", features = ["tokio_async"] }
|
||||
ipnet = { version = "2.8.0", features = ["serde"] }
|
||||
iprange = { version = "0.6.7", features = ["serde"] }
|
||||
isocountry = "0.3.2"
|
||||
itertools = "0.11.0"
|
||||
itertools = "0.12.0"
|
||||
jaq-core = "0.10.1"
|
||||
jaq-std = "0.10.0"
|
||||
josekit = "0.8.4"
|
||||
js-engine = { path = '../js-engine', optional = true }
|
||||
jsonpath_lib = { git = "https://github.com/Start9Labs/jsonpath.git" }
|
||||
lazy_async_pool = "0.3.3"
|
||||
lazy_format = "2.0"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2.149"
|
||||
log = "0.4.20"
|
||||
@@ -110,6 +119,7 @@ nix = { version = "0.27.1", features = ["user", "process", "signal", "fs"] }
|
||||
nom = "7.1.3"
|
||||
num = "0.4.1"
|
||||
num_enum = "0.7.0"
|
||||
once_cell = "1.19.0"
|
||||
openssh-keys = "0.6.2"
|
||||
openssl = { version = "0.10.57", features = ["vendored"] }
|
||||
p256 = { version = "0.13.2", features = ["pem"] }
|
||||
@@ -124,12 +134,12 @@ proptest = "1.3.1"
|
||||
proptest-derive = "0.4.0"
|
||||
rand = { version = "0.8.5", features = ["std"] }
|
||||
regex = "1.10.2"
|
||||
reqwest = { version = "0.11.22", features = ["stream", "json", "socks"] }
|
||||
reqwest = { version = "0.11.23", features = ["stream", "json", "socks"] }
|
||||
reqwest_cookie_store = "0.6.0"
|
||||
rpassword = "7.2.0"
|
||||
rpc-toolkit = "0.2.2"
|
||||
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "refactor/traits" }
|
||||
rust-argon2 = "2.0.0"
|
||||
scopeguard = "1.1" # because avahi-sys fucks your shit up
|
||||
rustyline-async = "0.4.1"
|
||||
semver = { version = "1.0.20", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_cbor = { package = "ciborium", version = "0.2.1" }
|
||||
@@ -138,6 +148,7 @@ serde_toml = { package = "toml", version = "0.8.2" }
|
||||
serde_with = { version = "3.4.0", features = ["macros", "json"] }
|
||||
serde_yaml = "0.9.25"
|
||||
sha2 = "0.10.2"
|
||||
shell-words = "1"
|
||||
simple-logging = "2.0.2"
|
||||
sqlx = { version = "0.7.2", features = [
|
||||
"chrono",
|
||||
@@ -150,20 +161,23 @@ stderrlog = "0.5.4"
|
||||
tar = "0.4.40"
|
||||
thiserror = "1.0.49"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-rustls = "0.24.1"
|
||||
tokio-rustls = "0.25.0"
|
||||
tokio-socks = "0.5.1"
|
||||
tokio-stream = { version = "0.1.14", features = ["io-util", "sync", "net"] }
|
||||
tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" }
|
||||
tokio-tungstenite = { version = "0.20.1", features = ["native-tls"] }
|
||||
tokio-tungstenite = { version = "0.21.0", features = ["native-tls"] }
|
||||
tokio-util = { version = "0.7.9", features = ["io"] }
|
||||
torut = "0.2.1"
|
||||
torut = { git = "https://github.com/Start9Labs/torut.git", branch = "update/dependencies", features = [
|
||||
"serialize",
|
||||
] }
|
||||
tracing = "0.1.39"
|
||||
tracing-error = "0.2.0"
|
||||
tracing-futures = "0.2.5"
|
||||
tracing-journald = "0.3.0"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
trust-dns-server = "0.23.1"
|
||||
typed-builder = "0.17.0"
|
||||
ts-rs = { git = "https://github.com/dr-bonez/ts-rs.git", branch = "feature/top-level-type-override" } # "8.1.0"
|
||||
typed-builder = "0.18.0"
|
||||
url = { version = "2.4.1", features = ["serde"] }
|
||||
urlencoding = "2.1.3"
|
||||
uuid = { version = "1.4.1", features = ["v4"] }
|
||||
|
||||
3
core/startos/Effects.ts
Normal file
3
core/startos/Effects.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export interface SetStoreParams { value: any, path: string, }
|
||||
3
core/startos/bindings/ActionId.ts
Normal file
3
core/startos/bindings/ActionId.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ActionId = string;
|
||||
12
core/startos/bindings/ActionMetadata.ts
Normal file
12
core/startos/bindings/ActionMetadata.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AllowedStatuses } from "./AllowedStatuses";
|
||||
|
||||
export type ActionMetadata = {
|
||||
name: string;
|
||||
description: string;
|
||||
warning: string | null;
|
||||
input: any;
|
||||
disabled: boolean;
|
||||
allowedStatuses: AllowedStatuses;
|
||||
group: string | null;
|
||||
};
|
||||
8
core/startos/bindings/AddSslOptions.ts
Normal file
8
core/startos/bindings/AddSslOptions.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AlpnInfo } from "./AlpnInfo";
|
||||
|
||||
export type AddSslOptions = {
|
||||
scheme: string | null;
|
||||
preferredExternalPort: number;
|
||||
alpn: AlpnInfo;
|
||||
};
|
||||
10
core/startos/bindings/AddressInfo.ts
Normal file
10
core/startos/bindings/AddressInfo.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { BindOptions } from "./BindOptions";
|
||||
import type { HostId } from "./HostId";
|
||||
|
||||
export type AddressInfo = {
|
||||
username: string | null;
|
||||
hostId: HostId;
|
||||
bindOptions: BindOptions;
|
||||
suffix: string;
|
||||
};
|
||||
9
core/startos/bindings/Alerts.ts
Normal file
9
core/startos/bindings/Alerts.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Alerts = {
|
||||
install: string | null;
|
||||
uninstall: string | null;
|
||||
restore: string | null;
|
||||
start: string | null;
|
||||
stop: string | null;
|
||||
};
|
||||
3
core/startos/bindings/Algorithm.ts
Normal file
3
core/startos/bindings/Algorithm.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Algorithm = "ecdsa" | "ed25519";
|
||||
5
core/startos/bindings/AllPackageData.ts
Normal file
5
core/startos/bindings/AllPackageData.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PackageDataEntry } from "./PackageDataEntry";
|
||||
import type { PackageId } from "./PackageId";
|
||||
|
||||
export type AllPackageData = { [key: PackageId]: PackageDataEntry };
|
||||
3
core/startos/bindings/AllowedStatuses.ts
Normal file
3
core/startos/bindings/AllowedStatuses.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AllowedStatuses = "onlyRunning" | "onlyStopped" | "any";
|
||||
4
core/startos/bindings/AlpnInfo.ts
Normal file
4
core/startos/bindings/AlpnInfo.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { MaybeUtf8String } from "./MaybeUtf8String";
|
||||
|
||||
export type AlpnInfo = "reflect" | { specified: Array<MaybeUtf8String> };
|
||||
3
core/startos/bindings/BackupProgress.ts
Normal file
3
core/startos/bindings/BackupProgress.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type BackupProgress = { complete: boolean };
|
||||
4
core/startos/bindings/BindInfo.ts
Normal file
4
core/startos/bindings/BindInfo.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { BindOptions } from "./BindOptions";
|
||||
|
||||
export type BindInfo = { options: BindOptions; assignedLanPort: number | null };
|
||||
10
core/startos/bindings/BindOptions.ts
Normal file
10
core/startos/bindings/BindOptions.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AddSslOptions } from "./AddSslOptions";
|
||||
import type { Security } from "./Security";
|
||||
|
||||
export type BindOptions = {
|
||||
scheme: string | null;
|
||||
preferredExternalPort: number;
|
||||
addSsl: AddSslOptions | null;
|
||||
secure: Security | null;
|
||||
};
|
||||
15
core/startos/bindings/BindParams.ts
Normal file
15
core/startos/bindings/BindParams.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AddSslOptions } from "./AddSslOptions";
|
||||
import type { HostId } from "./HostId";
|
||||
import type { HostKind } from "./HostKind";
|
||||
import type { Security } from "./Security";
|
||||
|
||||
export type BindParams = {
|
||||
kind: HostKind;
|
||||
id: HostId;
|
||||
internalPort: number;
|
||||
scheme: string | null;
|
||||
preferredExternalPort: number;
|
||||
addSsl: AddSslOptions | null;
|
||||
secure: Security | null;
|
||||
};
|
||||
3
core/startos/bindings/Callback.ts
Normal file
3
core/startos/bindings/Callback.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Callback = () => void;
|
||||
10
core/startos/bindings/ChrootParams.ts
Normal file
10
core/startos/bindings/ChrootParams.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ChrootParams = {
|
||||
env: string | null;
|
||||
workdir: string | null;
|
||||
user: string | null;
|
||||
path: string;
|
||||
command: string;
|
||||
args: string[];
|
||||
};
|
||||
3
core/startos/bindings/CreateOverlayedImageParams.ts
Normal file
3
core/startos/bindings/CreateOverlayedImageParams.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type CreateOverlayedImageParams = { imageId: string };
|
||||
5
core/startos/bindings/CurrentDependencies.ts
Normal file
5
core/startos/bindings/CurrentDependencies.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { CurrentDependencyInfo } from "./CurrentDependencyInfo";
|
||||
import type { PackageId } from "./PackageId";
|
||||
|
||||
export type CurrentDependencies = { [key: PackageId]: CurrentDependencyInfo };
|
||||
9
core/startos/bindings/CurrentDependencyInfo.ts
Normal file
9
core/startos/bindings/CurrentDependencyInfo.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DataUrl } from "./DataUrl";
|
||||
|
||||
export type CurrentDependencyInfo = {
|
||||
title: string;
|
||||
icon: DataUrl;
|
||||
registryUrl: string;
|
||||
versionSpec: string;
|
||||
} & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] });
|
||||
3
core/startos/bindings/DataUrl.ts
Normal file
3
core/startos/bindings/DataUrl.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type DataUrl = string;
|
||||
3
core/startos/bindings/DepInfo.ts
Normal file
3
core/startos/bindings/DepInfo.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type DepInfo = { description: string | null; optional: boolean };
|
||||
5
core/startos/bindings/Dependencies.ts
Normal file
5
core/startos/bindings/Dependencies.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DepInfo } from "./DepInfo";
|
||||
import type { PackageId } from "./PackageId";
|
||||
|
||||
export type Dependencies = { [key: PackageId]: DepInfo };
|
||||
4
core/startos/bindings/DependencyConfigErrors.ts
Normal file
4
core/startos/bindings/DependencyConfigErrors.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PackageId } from "./PackageId";
|
||||
|
||||
export type DependencyConfigErrors = { [key: PackageId]: string };
|
||||
3
core/startos/bindings/DependencyKind.ts
Normal file
3
core/startos/bindings/DependencyKind.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type DependencyKind = "exists" | "running";
|
||||
11
core/startos/bindings/DependencyRequirement.ts
Normal file
11
core/startos/bindings/DependencyRequirement.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type DependencyRequirement =
|
||||
| {
|
||||
kind: "running";
|
||||
id: string;
|
||||
healthChecks: string[];
|
||||
versionSpec: string;
|
||||
registryUrl: string;
|
||||
}
|
||||
| { kind: "exists"; id: string; versionSpec: string; registryUrl: string };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user