mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-02 05:23:14 +00:00
Compare commits
64 Commits
v0.4.0-alp
...
v0.4.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0eedcf58f5 | ||
|
|
208e9a5e3a | ||
|
|
7c304eef02 | ||
|
|
3de91687f1 | ||
|
|
0d80a1c0b1 | ||
|
|
48f50e3c6c | ||
|
|
3e0bbe47ad | ||
|
|
a3575cf9a3 | ||
|
|
5443f8dab1 | ||
|
|
c6fd58f714 | ||
|
|
40ac974a0c | ||
|
|
cbcff17ebc | ||
|
|
8e8f457393 | ||
|
|
886aa5d7db | ||
|
|
653a0a1428 | ||
|
|
0b004a19ae | ||
|
|
ce1da028ce | ||
|
|
0d4dcf6c61 | ||
|
|
8359712cd9 | ||
|
|
f46cdc6ee5 | ||
|
|
c96b38f915 | ||
|
|
c1c8dc8f9c | ||
|
|
e3b7277ccd | ||
|
|
b0b4b41c42 | ||
|
|
bbbc8f7440 | ||
|
|
c7a4dd617e | ||
|
|
d6b81f3c9b | ||
|
|
879f953a9f | ||
|
|
782f2e83bf | ||
|
|
6cefc27c5f | ||
|
|
2b676808a9 | ||
|
|
7c1c15073d | ||
|
|
025d569dfa | ||
|
|
976bdf3e53 | ||
|
|
dce0f075ce | ||
|
|
f3d2782f18 | ||
|
|
8d9be64c19 | ||
|
|
9bc0fbd5b1 | ||
|
|
b7f7202e25 | ||
|
|
0719c227ee | ||
|
|
621da47990 | ||
|
|
9fa81a0c9d | ||
|
|
2dac2bb2b3 | ||
|
|
58f1dc5025 | ||
|
|
cc89023bbd | ||
|
|
7e35ad57e7 | ||
|
|
010e439d1d | ||
|
|
cdbb512cca | ||
|
|
bb2e69299e | ||
|
|
fd0dc9a5b8 | ||
|
|
e2e88f774e | ||
|
|
4bebcafdde | ||
|
|
2bb1463f4f | ||
|
|
f20ece44a1 | ||
|
|
9fddcb957f | ||
|
|
fd502cfb99 | ||
|
|
ee95eef395 | ||
|
|
aaa43ce6af | ||
|
|
e0f27281d1 | ||
|
|
ecc4703ae7 | ||
|
|
d478911311 | ||
|
|
23fe6fb663 | ||
|
|
186925065d | ||
|
|
53dff95365 |
2
.github/actions/setup-build/action.yml
vendored
2
.github/actions/setup-build/action.yml
vendored
@@ -47,7 +47,7 @@ runs:
|
|||||||
sudo rm -rf /usr/share/swift
|
sudo rm -rf /usr/share/swift
|
||||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||||
|
|
||||||
# BuildJet runners lack /opt/hostedtoolcache, which setup-python and setup-qemu expect
|
# Some runners lack /opt/hostedtoolcache, which setup-python and setup-qemu expect
|
||||||
- name: Ensure hostedtoolcache exists
|
- name: Ensure hostedtoolcache exists
|
||||||
shell: bash
|
shell: bash
|
||||||
run: sudo mkdir -p /opt/hostedtoolcache && sudo chown $USER:$USER /opt/hostedtoolcache
|
run: sudo mkdir -p /opt/hostedtoolcache && sudo chown $USER:$USER /opt/hostedtoolcache
|
||||||
|
|||||||
2
.github/workflows/start-cli.yaml
vendored
2
.github/workflows/start-cli.yaml
vendored
@@ -63,7 +63,7 @@ jobs:
|
|||||||
"ALL": ["x86_64-unknown-linux-musl", "x86_64-apple-darwin", "aarch64-unknown-linux-musl", "aarch64-apple-darwin", "riscv64gc-unknown-linux-musl"]
|
"ALL": ["x86_64-unknown-linux-musl", "x86_64-apple-darwin", "aarch64-unknown-linux-musl", "aarch64-apple-darwin", "riscv64gc-unknown-linux-musl"]
|
||||||
}')[github.event.inputs.platform || 'ALL']
|
}')[github.event.inputs.platform || 'ALL']
|
||||||
}}
|
}}
|
||||||
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
runs-on: ${{ fromJson('["ubuntu-latest", "ubuntu-24.04-32-cores"]')[github.event.inputs.runner == 'fast'] }}
|
||||||
steps:
|
steps:
|
||||||
- name: Mount tmpfs
|
- name: Mount tmpfs
|
||||||
if: ${{ github.event.inputs.runner == 'fast' }}
|
if: ${{ github.event.inputs.runner == 'fast' }}
|
||||||
|
|||||||
4
.github/workflows/start-registry.yaml
vendored
4
.github/workflows/start-registry.yaml
vendored
@@ -59,7 +59,7 @@ jobs:
|
|||||||
"ALL": ["x86_64", "aarch64", "riscv64"]
|
"ALL": ["x86_64", "aarch64", "riscv64"]
|
||||||
}')[github.event.inputs.platform || 'ALL']
|
}')[github.event.inputs.platform || 'ALL']
|
||||||
}}
|
}}
|
||||||
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
runs-on: ${{ fromJson('["ubuntu-latest", "ubuntu-24.04-32-cores"]')[github.event.inputs.runner == 'fast'] }}
|
||||||
steps:
|
steps:
|
||||||
- name: Mount tmpfs
|
- name: Mount tmpfs
|
||||||
if: ${{ github.event.inputs.runner == 'fast' }}
|
if: ${{ github.event.inputs.runner == 'fast' }}
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
runs-on: ${{ fromJson('["ubuntu-latest", "ubuntu-24.04-32-cores"]')[github.event.inputs.runner == 'fast'] }}
|
||||||
steps:
|
steps:
|
||||||
- name: Cleaning up unnecessary files
|
- name: Cleaning up unnecessary files
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
2
.github/workflows/start-tunnel.yaml
vendored
2
.github/workflows/start-tunnel.yaml
vendored
@@ -59,7 +59,7 @@ jobs:
|
|||||||
"ALL": ["x86_64", "aarch64", "riscv64"]
|
"ALL": ["x86_64", "aarch64", "riscv64"]
|
||||||
}')[github.event.inputs.platform || 'ALL']
|
}')[github.event.inputs.platform || 'ALL']
|
||||||
}}
|
}}
|
||||||
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
runs-on: ${{ fromJson('["ubuntu-latest", "ubuntu-24.04-32-cores"]')[github.event.inputs.runner == 'fast'] }}
|
||||||
steps:
|
steps:
|
||||||
- name: Mount tmpfs
|
- name: Mount tmpfs
|
||||||
if: ${{ github.event.inputs.runner == 'fast' }}
|
if: ${{ github.event.inputs.runner == 'fast' }}
|
||||||
|
|||||||
148
.github/workflows/startos-iso.yaml
vendored
148
.github/workflows/startos-iso.yaml
vendored
@@ -29,7 +29,7 @@ on:
|
|||||||
- aarch64
|
- aarch64
|
||||||
- aarch64-nonfree
|
- aarch64-nonfree
|
||||||
- aarch64-nvidia
|
- aarch64-nvidia
|
||||||
# - raspberrypi
|
- raspberrypi
|
||||||
- riscv64
|
- riscv64
|
||||||
- riscv64-nonfree
|
- riscv64-nonfree
|
||||||
deploy:
|
deploy:
|
||||||
@@ -89,9 +89,9 @@ jobs:
|
|||||||
"riscv64": "ubuntu-latest"
|
"riscv64": "ubuntu-latest"
|
||||||
}')[matrix.arch],
|
}')[matrix.arch],
|
||||||
fromJson('{
|
fromJson('{
|
||||||
"x86_64": "buildjet-32vcpu-ubuntu-2204",
|
"x86_64": "amd64-fast",
|
||||||
"aarch64": "buildjet-32vcpu-ubuntu-2204-arm",
|
"aarch64": "aarch64-fast",
|
||||||
"riscv64": "buildjet-32vcpu-ubuntu-2204"
|
"riscv64": "amd64-fast"
|
||||||
}')[matrix.arch]
|
}')[matrix.arch]
|
||||||
)
|
)
|
||||||
)[github.event.inputs.runner == 'fast']
|
)[github.event.inputs.runner == 'fast']
|
||||||
@@ -153,15 +153,15 @@ jobs:
|
|||||||
"riscv64-nonfree": "ubuntu-24.04-arm",
|
"riscv64-nonfree": "ubuntu-24.04-arm",
|
||||||
}')[matrix.platform],
|
}')[matrix.platform],
|
||||||
fromJson('{
|
fromJson('{
|
||||||
"x86_64": "buildjet-8vcpu-ubuntu-2204",
|
"x86_64": "amd64-fast",
|
||||||
"x86_64-nonfree": "buildjet-8vcpu-ubuntu-2204",
|
"x86_64-nonfree": "amd64-fast",
|
||||||
"x86_64-nvidia": "buildjet-8vcpu-ubuntu-2204",
|
"x86_64-nvidia": "amd64-fast",
|
||||||
"aarch64": "buildjet-8vcpu-ubuntu-2204-arm",
|
"aarch64": "aarch64-fast",
|
||||||
"aarch64-nonfree": "buildjet-8vcpu-ubuntu-2204-arm",
|
"aarch64-nonfree": "aarch64-fast",
|
||||||
"aarch64-nvidia": "buildjet-8vcpu-ubuntu-2204-arm",
|
"aarch64-nvidia": "aarch64-fast",
|
||||||
"raspberrypi": "buildjet-8vcpu-ubuntu-2204-arm",
|
"raspberrypi": "aarch64-fast",
|
||||||
"riscv64": "buildjet-8vcpu-ubuntu-2204",
|
"riscv64": "amd64-fast",
|
||||||
"riscv64-nonfree": "buildjet-8vcpu-ubuntu-2204",
|
"riscv64-nonfree": "amd64-fast",
|
||||||
}')[matrix.platform]
|
}')[matrix.platform]
|
||||||
)
|
)
|
||||||
)[github.event.inputs.runner == 'fast']
|
)[github.event.inputs.runner == 'fast']
|
||||||
@@ -203,7 +203,7 @@ jobs:
|
|||||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY" # Pre-cached tool cache (Go, Node, etc.)
|
sudo rm -rf "$AGENT_TOOLSDIRECTORY" # Pre-cached tool cache (Go, Node, etc.)
|
||||||
if: ${{ github.event.inputs.runner != 'fast' }}
|
if: ${{ github.event.inputs.runner != 'fast' }}
|
||||||
|
|
||||||
# BuildJet runners lack /opt/hostedtoolcache, which setup-qemu expects
|
# Some runners lack /opt/hostedtoolcache, which setup-qemu expects
|
||||||
- name: Ensure hostedtoolcache exists
|
- name: Ensure hostedtoolcache exists
|
||||||
run: sudo mkdir -p /opt/hostedtoolcache && sudo chown $USER:$USER /opt/hostedtoolcache
|
run: sudo mkdir -p /opt/hostedtoolcache && sudo chown $USER:$USER /opt/hostedtoolcache
|
||||||
|
|
||||||
@@ -268,3 +268,123 @@ jobs:
|
|||||||
name: ${{ matrix.platform }}.img
|
name: ${{ matrix.platform }}.img
|
||||||
path: results/*.img
|
path: results/*.img
|
||||||
if: ${{ matrix.platform == 'raspberrypi' }}
|
if: ${{ matrix.platform == 'raspberrypi' }}
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
needs: [image]
|
||||||
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.deploy != 'NONE'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
REGISTRY: >-
|
||||||
|
${{
|
||||||
|
fromJson('{
|
||||||
|
"alpha": "https://alpha-registry-x.start9.com",
|
||||||
|
"beta": "https://beta-registry.start9.com"
|
||||||
|
}')[github.event.inputs.deploy]
|
||||||
|
}}
|
||||||
|
S3_BUCKET: s3://startos-images
|
||||||
|
S3_CDN: https://startos-images.nyc3.cdn.digitaloceanspaces.com
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
sparse-checkout: web/package.json
|
||||||
|
|
||||||
|
- name: Determine version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' web/package.json | head -1)
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
|
- name: Determine platforms
|
||||||
|
id: platforms
|
||||||
|
run: |
|
||||||
|
INPUT="${{ github.event.inputs.platform }}"
|
||||||
|
if [ "$INPUT" = "ALL" ]; then
|
||||||
|
PLATFORMS="x86_64 x86_64-nonfree x86_64-nvidia aarch64 aarch64-nonfree aarch64-nvidia riscv64 riscv64-nonfree"
|
||||||
|
else
|
||||||
|
PLATFORMS="$INPUT"
|
||||||
|
fi
|
||||||
|
echo "list=$PLATFORMS" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Platforms: $PLATFORMS"
|
||||||
|
|
||||||
|
- name: Download squashfs artifacts
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
pattern: "*.squashfs"
|
||||||
|
path: artifacts/
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Download ISO artifacts
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
pattern: "*.iso"
|
||||||
|
path: artifacts/
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Install start-cli
|
||||||
|
run: |
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
|
ASSET_NAME="start-cli_${ARCH}-${OS}"
|
||||||
|
DOWNLOAD_URL=$(curl -fsS \
|
||||||
|
-H "Authorization: token ${{ github.token }}" \
|
||||||
|
https://api.github.com/repos/Start9Labs/start-os/releases \
|
||||||
|
| jq -r '[.[].assets[] | select(.name=="'"$ASSET_NAME"'")] | first | .browser_download_url')
|
||||||
|
curl -fsSL \
|
||||||
|
-H "Authorization: token ${{ github.token }}" \
|
||||||
|
-H "Accept: application/octet-stream" \
|
||||||
|
"$DOWNLOAD_URL" -o /tmp/start-cli
|
||||||
|
sudo install -m 755 /tmp/start-cli /usr/local/bin/start-cli
|
||||||
|
echo "start-cli: $(start-cli --version)"
|
||||||
|
|
||||||
|
- name: Configure S3
|
||||||
|
run: |
|
||||||
|
sudo apt-get install -y -qq s3cmd > /dev/null
|
||||||
|
cat > ~/.s3cfg <<EOF
|
||||||
|
[default]
|
||||||
|
access_key = ${{ secrets.S3_ACCESS_KEY }}
|
||||||
|
secret_key = ${{ secrets.S3_SECRET_KEY }}
|
||||||
|
host_base = nyc3.digitaloceanspaces.com
|
||||||
|
host_bucket = %(bucket)s.nyc3.digitaloceanspaces.com
|
||||||
|
use_https = True
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Set up developer key
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.startos
|
||||||
|
printf '%s' "${{ secrets.DEV_KEY }}" > ~/.startos/developer.key.pem
|
||||||
|
|
||||||
|
- name: Upload to S3
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
cd artifacts
|
||||||
|
for PLATFORM in ${{ steps.platforms.outputs.list }}; do
|
||||||
|
for file in *_${PLATFORM}.squashfs *_${PLATFORM}.iso; do
|
||||||
|
[ -f "$file" ] || continue
|
||||||
|
echo "Uploading $file..."
|
||||||
|
s3cmd put -P "$file" "${{ env.S3_BUCKET }}/v${VERSION}/$file"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Register OS version
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
start-cli --registry="${{ env.REGISTRY }}" registry os version add \
|
||||||
|
"$VERSION" "v${VERSION}" '' ">=0.3.5 <=${VERSION}"
|
||||||
|
|
||||||
|
- name: Index assets in registry
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
cd artifacts
|
||||||
|
for PLATFORM in ${{ steps.platforms.outputs.list }}; do
|
||||||
|
for file in *_${PLATFORM}.squashfs *_${PLATFORM}.iso; do
|
||||||
|
[ -f "$file" ] || continue
|
||||||
|
echo "Indexing $file for platform $PLATFORM..."
|
||||||
|
start-cli --registry="${{ env.REGISTRY }}" registry os asset add \
|
||||||
|
--platform="$PLATFORM" \
|
||||||
|
--version="$VERSION" \
|
||||||
|
"$file" \
|
||||||
|
"${{ env.S3_CDN }}/v${VERSION}/$file"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,3 +23,4 @@ tmp
|
|||||||
web/.i18n-checked
|
web/.i18n-checked
|
||||||
docs/USER.md
|
docs/USER.md
|
||||||
*.s9pk
|
*.s9pk
|
||||||
|
/build/lib/migration-images
|
||||||
|
|||||||
7
Makefile
7
Makefile
@@ -15,7 +15,7 @@ IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo
|
|||||||
WEB_UIS := web/dist/raw/ui/index.html web/dist/raw/setup-wizard/index.html
|
WEB_UIS := web/dist/raw/ui/index.html web/dist/raw/setup-wizard/index.html
|
||||||
COMPRESSED_WEB_UIS := web/dist/static/ui/index.html web/dist/static/setup-wizard/index.html
|
COMPRESSED_WEB_UIS := web/dist/static/ui/index.html web/dist/static/setup-wizard/index.html
|
||||||
FIRMWARE_ROMS := build/lib/firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./build/lib/firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json)
|
FIRMWARE_ROMS := build/lib/firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./build/lib/firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json)
|
||||||
BUILD_SRC := $(call ls-files, build/lib) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS)
|
BUILD_SRC := $(call ls-files, build/lib) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS) build/lib/migration-images/.done
|
||||||
IMAGE_RECIPE_SRC := $(call ls-files, build/image-recipe/)
|
IMAGE_RECIPE_SRC := $(call ls-files, build/image-recipe/)
|
||||||
STARTD_SRC := core/startd.service $(BUILD_SRC)
|
STARTD_SRC := core/startd.service $(BUILD_SRC)
|
||||||
CORE_SRC := $(call ls-files, core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE)
|
CORE_SRC := $(call ls-files, core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE)
|
||||||
@@ -89,6 +89,7 @@ clean:
|
|||||||
rm -rf container-runtime/node_modules
|
rm -rf container-runtime/node_modules
|
||||||
rm -f container-runtime/*.squashfs
|
rm -f container-runtime/*.squashfs
|
||||||
(cd sdk && make clean)
|
(cd sdk && make clean)
|
||||||
|
rm -rf build/lib/migration-images
|
||||||
rm -f env/*.txt
|
rm -f env/*.txt
|
||||||
|
|
||||||
format:
|
format:
|
||||||
@@ -105,6 +106,10 @@ test-sdk: $(call ls-files, sdk) sdk/base/lib/osBindings/index.ts
|
|||||||
test-container-runtime: container-runtime/node_modules/.package-lock.json $(call ls-files, container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json
|
test-container-runtime: container-runtime/node_modules/.package-lock.json $(call ls-files, container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json
|
||||||
cd container-runtime && npm test
|
cd container-runtime && npm test
|
||||||
|
|
||||||
|
build/lib/migration-images/.done: build/save-migration-images.sh
|
||||||
|
ARCH=$(ARCH) ./build/save-migration-images.sh build/lib/migration-images
|
||||||
|
touch $@
|
||||||
|
|
||||||
install-cli: $(GIT_HASH_FILE)
|
install-cli: $(GIT_HASH_FILE)
|
||||||
./core/build/build-cli.sh --install
|
./core/build/build-cli.sh --install
|
||||||
|
|
||||||
|
|||||||
@@ -83,11 +83,16 @@ if [ ${#DEB_FILES[@]} -eq 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy each deb to the pool, renaming to standard format
|
# Copy each deb to the pool, removing old versions of the same package+arch
|
||||||
for deb in "${DEB_FILES[@]}"; do
|
for deb in "${DEB_FILES[@]}"; do
|
||||||
PKG_NAME="$(dpkg-deb --field "$deb" Package)"
|
PKG_NAME="$(dpkg-deb --field "$deb" Package)"
|
||||||
|
PKG_ARCH="$(dpkg-deb --field "$deb" Architecture)"
|
||||||
POOL_DIR="$REPO_DIR/pool/${COMPONENT}/${PKG_NAME:0:1}/${PKG_NAME}"
|
POOL_DIR="$REPO_DIR/pool/${COMPONENT}/${PKG_NAME:0:1}/${PKG_NAME}"
|
||||||
mkdir -p "$POOL_DIR"
|
mkdir -p "$POOL_DIR"
|
||||||
|
# Remove old versions for the same architecture
|
||||||
|
for old in "$POOL_DIR"/${PKG_NAME}_*_${PKG_ARCH}.deb; do
|
||||||
|
[ -f "$old" ] && rm -v "$old"
|
||||||
|
done
|
||||||
cp "$deb" "$POOL_DIR/"
|
cp "$deb" "$POOL_DIR/"
|
||||||
dpkg-name -o "$POOL_DIR/$(basename "$deb")" 2>/dev/null || true
|
dpkg-name -o "$POOL_DIR/$(basename "$deb")" 2>/dev/null || true
|
||||||
echo "Added: $(basename "$deb") -> pool/${COMPONENT}/${PKG_NAME:0:1}/${PKG_NAME}/"
|
echo "Added: $(basename "$deb") -> pool/${COMPONENT}/${PKG_NAME:0:1}/${PKG_NAME}/"
|
||||||
|
|||||||
@@ -58,15 +58,18 @@ iptables -t nat -A ${NAME}_OUTPUT -d "$sip" -p udp --dport "$sport" -j DNAT --to
|
|||||||
iptables -A ${NAME}_FORWARD -d $dip -p tcp --dport $dport -m state --state NEW -j ACCEPT
|
iptables -A ${NAME}_FORWARD -d $dip -p tcp --dport $dport -m state --state NEW -j ACCEPT
|
||||||
iptables -A ${NAME}_FORWARD -d $dip -p udp --dport $dport -m state --state NEW -j ACCEPT
|
iptables -A ${NAME}_FORWARD -d $dip -p udp --dport $dport -m state --state NEW -j ACCEPT
|
||||||
|
|
||||||
# NAT hairpin: masquerade traffic from the bridge subnet or host to the DNAT
|
# NAT hairpin: masquerade so replies route back through this host for proper
|
||||||
# target, so replies route back through the host for proper NAT reversal.
|
# NAT reversal instead of taking a direct path that bypasses conntrack.
|
||||||
# Container-to-container hairpin (source is on the bridge subnet)
|
# Host-to-target hairpin: locally-originated packets whose original destination
|
||||||
if [ -n "$bridge_subnet" ]; then
|
# was sip (before OUTPUT DNAT rewrote it to dip). Using --ctorigdst ties the
|
||||||
iptables -t nat -A ${NAME}_POSTROUTING -s "$bridge_subnet" -d "$dip" -p tcp --dport "$dport" -j MASQUERADE
|
# rule to this specific sip, so multiple WAN IPs forwarding the same port to
|
||||||
iptables -t nat -A ${NAME}_POSTROUTING -s "$bridge_subnet" -d "$dip" -p udp --dport "$dport" -j MASQUERADE
|
# different targets each get their own masquerade.
|
||||||
fi
|
iptables -t nat -A ${NAME}_POSTROUTING -m addrtype --src-type LOCAL -m conntrack --ctorigdst "$sip" -d "$dip" -p tcp --dport "$dport" -j MASQUERADE
|
||||||
# Host-to-container hairpin (host connects to its own gateway IP, source is sip)
|
iptables -t nat -A ${NAME}_POSTROUTING -m addrtype --src-type LOCAL -m conntrack --ctorigdst "$sip" -d "$dip" -p udp --dport "$dport" -j MASQUERADE
|
||||||
iptables -t nat -A ${NAME}_POSTROUTING -s "$sip" -d "$dip" -p tcp --dport "$dport" -j MASQUERADE
|
# Same-subnet hairpin: when traffic originates from the same subnet as the DNAT
|
||||||
iptables -t nat -A ${NAME}_POSTROUTING -s "$sip" -d "$dip" -p udp --dport "$dport" -j MASQUERADE
|
# target (e.g. a container reaching another container, or a WireGuard peer
|
||||||
|
# connecting to itself via the tunnel's public IP).
|
||||||
|
iptables -t nat -A ${NAME}_POSTROUTING -s "$dip/$dprefix" -d "$dip" -p tcp --dport "$dport" -j MASQUERADE
|
||||||
|
iptables -t nat -A ${NAME}_POSTROUTING -s "$dip/$dprefix" -d "$dip" -p udp --dport "$dport" -j MASQUERADE
|
||||||
|
|
||||||
exit $err
|
exit $err
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ S3_BUCKET="s3://startos-images"
|
|||||||
S3_CDN="https://startos-images.nyc3.cdn.digitaloceanspaces.com"
|
S3_CDN="https://startos-images.nyc3.cdn.digitaloceanspaces.com"
|
||||||
START9_GPG_KEY="2D63C217"
|
START9_GPG_KEY="2D63C217"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
ARCHES="aarch64 aarch64-nonfree aarch64-nvidia riscv64 riscv64-nonfree x86_64 x86_64-nonfree x86_64-nvidia"
|
ARCHES="aarch64 aarch64-nonfree aarch64-nvidia riscv64 riscv64-nonfree x86_64 x86_64-nonfree x86_64-nvidia"
|
||||||
CLI_ARCHES="aarch64 riscv64 x86_64"
|
CLI_ARCHES="aarch64 riscv64 x86_64"
|
||||||
|
|
||||||
@@ -83,16 +84,21 @@ resolve_gh_user() {
|
|||||||
GH_GPG_KEY=$(git config user.signingkey 2>/dev/null || true)
|
GH_GPG_KEY=$(git config user.signingkey 2>/dev/null || true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Fetch the URL for an OS asset from the registry index.
|
||||||
|
# Usage: registry_url <iso|squashfs|img> <platform>
|
||||||
|
registry_url() {
|
||||||
|
local ext=$1 platform=$2
|
||||||
|
if [ -z "${_REGISTRY_INDEX:-}" ]; then
|
||||||
|
_REGISTRY_INDEX=$(start-cli --registry=$REGISTRY registry os index)
|
||||||
|
fi
|
||||||
|
echo "$_REGISTRY_INDEX" | jq -r ".versions[\"$VERSION\"].$ext[\"$platform\"].urls[0]"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Subcommands ---
|
# --- Subcommands ---
|
||||||
|
|
||||||
cmd_download() {
|
cmd_download() {
|
||||||
require_version
|
require_version
|
||||||
|
|
||||||
if [ -z "${RUN_ID:-}" ]; then
|
|
||||||
read -rp "RUN_ID (OS images, leave blank to skip): " RUN_ID
|
|
||||||
fi
|
|
||||||
RUN_ID=$(parse_run_id "${RUN_ID:-}")
|
|
||||||
|
|
||||||
if [ -z "${ST_RUN_ID:-}" ]; then
|
if [ -z "${ST_RUN_ID:-}" ]; then
|
||||||
read -rp "ST_RUN_ID (start-tunnel, leave blank to skip): " ST_RUN_ID
|
read -rp "ST_RUN_ID (start-tunnel, leave blank to skip): " ST_RUN_ID
|
||||||
fi
|
fi
|
||||||
@@ -105,14 +111,14 @@ cmd_download() {
|
|||||||
|
|
||||||
ensure_release_dir
|
ensure_release_dir
|
||||||
|
|
||||||
if [ -n "$RUN_ID" ]; then
|
# Download OS images from registry (deployed by GitHub workflow)
|
||||||
for arch in $ARCHES; do
|
echo "Downloading OS images from registry..."
|
||||||
while ! gh run download -R $REPO "$RUN_ID" -n "$arch.squashfs" -D "$(pwd)"; do sleep 1; done
|
for arch in $ARCHES; do
|
||||||
|
for ext in squashfs iso; do
|
||||||
|
echo " $ext $arch"
|
||||||
|
start-cli --registry=$REGISTRY registry os asset get "$ext" "$VERSION" "$arch" -d "$(pwd)"
|
||||||
done
|
done
|
||||||
for arch in $ARCHES; do
|
done
|
||||||
while ! gh run download -R $REPO "$RUN_ID" -n "$arch.iso" -D "$(pwd)"; do sleep 1; done
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$ST_RUN_ID" ]; then
|
if [ -n "$ST_RUN_ID" ]; then
|
||||||
for arch in $CLI_ARCHES; do
|
for arch in $CLI_ARCHES; do
|
||||||
@@ -143,19 +149,12 @@ cmd_pull() {
|
|||||||
gh release download -R $REPO "v$VERSION" -p "$file" -D "$(pwd)" --clobber
|
gh release download -R $REPO "v$VERSION" -p "$file" -D "$(pwd)" --clobber
|
||||||
done
|
done
|
||||||
|
|
||||||
# Download ISOs and squashfs from S3 CDN
|
# Download ISOs and squashfs from registry
|
||||||
|
echo "Downloading OS images from registry..."
|
||||||
for arch in $ARCHES; do
|
for arch in $ARCHES; do
|
||||||
for ext in squashfs iso; do
|
for ext in squashfs iso; do
|
||||||
# Get the actual filename from the GH release asset list or body
|
echo " $ext $arch"
|
||||||
local filename
|
start-cli --registry=$REGISTRY registry os asset get "$ext" "$VERSION" "$arch" -d "$(pwd)"
|
||||||
filename=$(gh release view -R $REPO "v$VERSION" --json assets -q ".assets[].name" | grep "_${arch}\\.${ext}$" || true)
|
|
||||||
if [ -z "$filename" ]; then
|
|
||||||
filename=$(gh release view -R $REPO "v$VERSION" --json body -q .body | grep -oP "[^ ]*_${arch}\\.${ext}" | head -1 || true)
|
|
||||||
fi
|
|
||||||
if [ -n "$filename" ]; then
|
|
||||||
echo "Downloading $filename from S3..."
|
|
||||||
curl -fSL -o "$filename" "$S3_CDN/v$VERSION/$filename"
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
@@ -170,14 +169,12 @@ cmd_upload() {
|
|||||||
require_version
|
require_version
|
||||||
enter_release_dir
|
enter_release_dir
|
||||||
|
|
||||||
|
# OS images (iso/squashfs) are already on S3 via the GitHub workflow.
|
||||||
|
# Upload only debs and CLI binaries to the GitHub Release.
|
||||||
for file in $(release_files); do
|
for file in $(release_files); do
|
||||||
case "$file" in
|
case "$file" in
|
||||||
*.iso|*.squashfs)
|
*.iso|*.squashfs) ;;
|
||||||
s3cmd put -P "$file" "$S3_BUCKET/v$VERSION/$file"
|
*) gh release upload -R $REPO "v$VERSION" "$file" ;;
|
||||||
;;
|
|
||||||
*)
|
|
||||||
gh release upload -R $REPO "v$VERSION" "$file"
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
@@ -248,6 +245,24 @@ cmd_cosign() {
|
|||||||
echo "Done. Personal signatures for $GH_USER added to v$VERSION."
|
echo "Done. Personal signatures for $GH_USER added to v$VERSION."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd_publish_tunnel() {
|
||||||
|
require_version
|
||||||
|
enter_release_dir
|
||||||
|
|
||||||
|
local tunnel_debs=()
|
||||||
|
for file in start-tunnel*.deb; do
|
||||||
|
[ -f "$file" ] && tunnel_debs+=("$file")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#tunnel_debs[@]} -eq 0 ]; then
|
||||||
|
>&2 echo "No start-tunnel .deb files found in release directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Publishing start-tunnel debs to apt repository..."
|
||||||
|
"$SCRIPT_DIR/apt/publish-deb.sh" "${tunnel_debs[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
cmd_notes() {
|
cmd_notes() {
|
||||||
require_version
|
require_version
|
||||||
enter_release_dir
|
enter_release_dir
|
||||||
@@ -255,14 +270,14 @@ cmd_notes() {
|
|||||||
cat << EOF
|
cat << EOF
|
||||||
# ISO Downloads
|
# ISO Downloads
|
||||||
|
|
||||||
- [x86_64/AMD64]($S3_CDN/v$VERSION/$(ls *_x86_64-nonfree.iso))
|
- [x86_64/AMD64]($(registry_url iso x86_64-nonfree))
|
||||||
- [x86_64/AMD64 + NVIDIA]($S3_CDN/v$VERSION/$(ls *_x86_64-nvidia.iso))
|
- [x86_64/AMD64 + NVIDIA]($(registry_url iso x86_64-nvidia))
|
||||||
- [x86_64/AMD64-slim (FOSS-only)]($S3_CDN/v$VERSION/$(ls *_x86_64.iso) "Without proprietary software or drivers")
|
- [x86_64/AMD64-slim (FOSS-only)]($(registry_url iso x86_64) "Without proprietary software or drivers")
|
||||||
- [aarch64/ARM64]($S3_CDN/v$VERSION/$(ls *_aarch64-nonfree.iso))
|
- [aarch64/ARM64]($(registry_url iso aarch64-nonfree))
|
||||||
- [aarch64/ARM64 + NVIDIA]($S3_CDN/v$VERSION/$(ls *_aarch64-nvidia.iso))
|
- [aarch64/ARM64 + NVIDIA]($(registry_url iso aarch64-nvidia))
|
||||||
- [aarch64/ARM64-slim (FOSS-Only)]($S3_CDN/v$VERSION/$(ls *_aarch64.iso) "Without proprietary software or drivers")
|
- [aarch64/ARM64-slim (FOSS-Only)]($(registry_url iso aarch64) "Without proprietary software or drivers")
|
||||||
- [RISCV64 (RVA23)]($S3_CDN/v$VERSION/$(ls *_riscv64-nonfree.iso))
|
- [RISCV64 (RVA23)]($(registry_url iso riscv64-nonfree))
|
||||||
- [RISCV64 (RVA23)-slim (FOSS-only)]($S3_CDN/v$VERSION/$(ls *_riscv64.iso) "Without proprietary software or drivers")
|
- [RISCV64 (RVA23)-slim (FOSS-only)]($(registry_url iso riscv64) "Without proprietary software or drivers")
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
cat << 'EOF'
|
cat << 'EOF'
|
||||||
@@ -318,9 +333,8 @@ EOF
|
|||||||
|
|
||||||
cmd_full_release() {
|
cmd_full_release() {
|
||||||
cmd_download
|
cmd_download
|
||||||
cmd_register
|
|
||||||
cmd_upload
|
cmd_upload
|
||||||
cmd_index
|
cmd_publish_tunnel
|
||||||
cmd_sign
|
cmd_sign
|
||||||
cmd_notes
|
cmd_notes
|
||||||
}
|
}
|
||||||
@@ -330,22 +344,23 @@ usage() {
|
|||||||
Usage: manage-release.sh <subcommand>
|
Usage: manage-release.sh <subcommand>
|
||||||
|
|
||||||
Subcommands:
|
Subcommands:
|
||||||
download Download artifacts from GitHub Actions runs
|
download Download OS images from registry + other artifacts from GH Actions
|
||||||
Requires: RUN_ID, ST_RUN_ID, CLI_RUN_ID (any combination)
|
OS images are pulled via start-cli from the registry (deployed by GH workflow)
|
||||||
pull Download an existing release from the GH tag and S3
|
Requires: ST_RUN_ID, CLI_RUN_ID (any combination)
|
||||||
register Register the version in the Start9 registry
|
pull Download an existing release from the GH tag and S3
|
||||||
upload Upload artifacts to GitHub Releases and S3
|
register Register the version in the Start9 registry
|
||||||
index Add assets to the registry index
|
upload Upload artifacts to GitHub Releases and S3
|
||||||
sign Sign all artifacts with Start9 org key (+ personal key if available)
|
index Add assets to the registry index
|
||||||
and upload signatures.tar.gz
|
publish-tunnel Publish start-tunnel .deb files to the apt repository
|
||||||
cosign Add personal GPG signature to an existing release's signatures
|
sign Sign all artifacts with Start9 org key (+ personal key if available)
|
||||||
(requires 'pull' first so you can verify assets before signing)
|
and upload signatures.tar.gz
|
||||||
notes Print release notes with download links and checksums
|
cosign Add personal GPG signature to an existing release's signatures
|
||||||
full-release Run: download → register → upload → index → sign → notes
|
(requires 'pull' first so you can verify assets before signing)
|
||||||
|
notes Print release notes with download links and checksums
|
||||||
|
full-release Run: download → register → upload → publish-tunnel → sign → notes
|
||||||
|
|
||||||
Environment variables:
|
Environment variables:
|
||||||
VERSION (required) Release version
|
VERSION (required) Release version
|
||||||
RUN_ID GitHub Actions run ID for OS images (download subcommand)
|
|
||||||
ST_RUN_ID GitHub Actions run ID for start-tunnel (download subcommand)
|
ST_RUN_ID GitHub Actions run ID for start-tunnel (download subcommand)
|
||||||
CLI_RUN_ID GitHub Actions run ID for start-cli (download subcommand)
|
CLI_RUN_ID GitHub Actions run ID for start-cli (download subcommand)
|
||||||
GH_USER Override GitHub username (default: autodetected via gh cli)
|
GH_USER Override GitHub username (default: autodetected via gh cli)
|
||||||
@@ -354,14 +369,15 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
download) cmd_download ;;
|
download) cmd_download ;;
|
||||||
pull) cmd_pull ;;
|
pull) cmd_pull ;;
|
||||||
register) cmd_register ;;
|
register) cmd_register ;;
|
||||||
upload) cmd_upload ;;
|
upload) cmd_upload ;;
|
||||||
index) cmd_index ;;
|
index) cmd_index ;;
|
||||||
sign) cmd_sign ;;
|
publish-tunnel) cmd_publish_tunnel ;;
|
||||||
cosign) cmd_cosign ;;
|
sign) cmd_sign ;;
|
||||||
notes) cmd_notes ;;
|
cosign) cmd_cosign ;;
|
||||||
full-release) cmd_full_release ;;
|
notes) cmd_notes ;;
|
||||||
*) usage; exit 1 ;;
|
full-release) cmd_full_release ;;
|
||||||
|
*) usage; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
36
build/save-migration-images.sh
Executable file
36
build/save-migration-images.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Save Docker images needed by the 0.3.6-alpha.0 migration as tarballs
|
||||||
|
# so they can be bundled into the OS and loaded without internet access.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
ARCH="${ARCH:-x86_64}"
|
||||||
|
DESTDIR="${1:-build/lib/migration-images}"
|
||||||
|
|
||||||
|
if [ "$ARCH" = "x86_64" ]; then
|
||||||
|
DOCKER_PLATFORM="linux/amd64"
|
||||||
|
elif [ "$ARCH" = "aarch64" ]; then
|
||||||
|
DOCKER_PLATFORM="linux/arm64"
|
||||||
|
else
|
||||||
|
DOCKER_PLATFORM="linux/$ARCH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
IMAGES=("tonistiigi/binfmt:latest")
|
||||||
|
if [ "$ARCH" != "riscv64" ]; then
|
||||||
|
IMAGES=("start9/compat:latest" "start9/utils:latest" "${IMAGES[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$DESTDIR"
|
||||||
|
|
||||||
|
for IMAGE in "${IMAGES[@]}"; do
|
||||||
|
FILENAME=$(echo "$IMAGE" | sed 's|/|_|g; s/:/_/g').tar
|
||||||
|
if [ -f "$DESTDIR/$FILENAME" ]; then
|
||||||
|
echo "Skipping $IMAGE (already saved)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "Pulling $IMAGE for $DOCKER_PLATFORM..."
|
||||||
|
docker pull --platform "$DOCKER_PLATFORM" "$IMAGE"
|
||||||
|
echo "Saving $IMAGE to $DESTDIR/$FILENAME..."
|
||||||
|
docker save "$IMAGE" -o "$DESTDIR/$FILENAME"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Migration images saved to $DESTDIR"
|
||||||
2
container-runtime/package-lock.json
generated
2
container-runtime/package-lock.json
generated
@@ -37,7 +37,7 @@
|
|||||||
},
|
},
|
||||||
"../sdk/dist": {
|
"../sdk/dist": {
|
||||||
"name": "@start9labs/start-sdk",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "0.4.0-beta.64",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iarna/toml": "^3.0.0",
|
"@iarna/toml": "^3.0.0",
|
||||||
|
|||||||
@@ -396,6 +396,12 @@ export class SystemForEmbassy implements System {
|
|||||||
if (this.manifest.id === "nostr") {
|
if (this.manifest.id === "nostr") {
|
||||||
this.manifest.id = "nostr-rs-relay"
|
this.manifest.id = "nostr-rs-relay"
|
||||||
}
|
}
|
||||||
|
if (this.manifest.id === "ghost") {
|
||||||
|
this.manifest.id = "ghost-legacy"
|
||||||
|
}
|
||||||
|
if (this.manifest.id === "synapse") {
|
||||||
|
this.manifest.id = "synapse-legacy"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(
|
async init(
|
||||||
@@ -494,7 +500,7 @@ export class SystemForEmbassy implements System {
|
|||||||
const host = new MultiHost({ effects, id })
|
const host = new MultiHost({ effects, id })
|
||||||
const internalPorts = new Set(
|
const internalPorts = new Set(
|
||||||
Object.values(interfaceValue["tor-config"]?.["port-mapping"] ?? {})
|
Object.values(interfaceValue["tor-config"]?.["port-mapping"] ?? {})
|
||||||
.map(Number.parseInt)
|
.map((v) => parseInt(v))
|
||||||
.concat(
|
.concat(
|
||||||
...Object.values(interfaceValue["lan-config"] ?? {}).map(
|
...Object.values(interfaceValue["lan-config"] ?? {}).map(
|
||||||
(c) => c.internal,
|
(c) => c.internal,
|
||||||
|
|||||||
40
core/Cargo.lock
generated
40
core/Cargo.lock
generated
@@ -1967,6 +1967,18 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-iterator"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-streaming-iterator"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -3562,6 +3574,17 @@ dependencies = [
|
|||||||
"redox_syscall 0.7.3",
|
"redox_syscall 0.7.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsqlite3-sys"
|
||||||
|
version = "0.32.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libyml"
|
name = "libyml"
|
||||||
version = "0.0.5"
|
version = "0.0.5"
|
||||||
@@ -5444,6 +5467,20 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusqlite"
|
||||||
|
version = "0.34.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
"fallible-iterator",
|
||||||
|
"fallible-streaming-iterator",
|
||||||
|
"hashlink",
|
||||||
|
"libsqlite3-sys",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-argon2"
|
name = "rust-argon2"
|
||||||
version = "3.0.0"
|
version = "3.0.0"
|
||||||
@@ -6439,7 +6476,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "start-os"
|
name = "start-os"
|
||||||
version = "0.4.0-alpha.22"
|
version = "0.4.0-beta.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"async-acme",
|
"async-acme",
|
||||||
@@ -6536,6 +6573,7 @@ dependencies = [
|
|||||||
"reqwest_cookie_store",
|
"reqwest_cookie_store",
|
||||||
"rpassword",
|
"rpassword",
|
||||||
"rpc-toolkit",
|
"rpc-toolkit",
|
||||||
|
"rusqlite",
|
||||||
"rust-argon2",
|
"rust-argon2",
|
||||||
"rust-i18n",
|
"rust-i18n",
|
||||||
"semver",
|
"semver",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ license = "MIT"
|
|||||||
name = "start-os"
|
name = "start-os"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/Start9Labs/start-os"
|
repository = "https://github.com/Start9Labs/start-os"
|
||||||
version = "0.4.0-alpha.22" # VERSION_BUMP
|
version = "0.4.0-beta.0" # VERSION_BUMP
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "startos"
|
name = "startos"
|
||||||
@@ -182,6 +182,7 @@ qrcode = "0.14.1"
|
|||||||
r3bl_tui = "0.7.6"
|
r3bl_tui = "0.7.6"
|
||||||
rand = "0.9.2"
|
rand = "0.9.2"
|
||||||
regex = "1.10.2"
|
regex = "1.10.2"
|
||||||
|
rusqlite = { version = "0.34", features = ["bundled"] }
|
||||||
reqwest = { version = "0.12.25", features = [
|
reqwest = { version = "0.12.25", features = [
|
||||||
"http2",
|
"http2",
|
||||||
"json",
|
"json",
|
||||||
@@ -250,5 +251,3 @@ opt-level = 3
|
|||||||
[profile.dev.package.backtrace]
|
[profile.dev.package.backtrace]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
||||||
[profile.dev.package.sqlx-macros]
|
|
||||||
opt-level = 3
|
|
||||||
|
|||||||
@@ -1721,6 +1721,14 @@ lxc.mod.cleaned-up-containers:
|
|||||||
fr_FR: "Conteneurs LXC orphelins nettoyés avec succès"
|
fr_FR: "Conteneurs LXC orphelins nettoyés avec succès"
|
||||||
pl_PL: "Pomyślnie wyczyszczono wiszące kontenery LXC"
|
pl_PL: "Pomyślnie wyczyszczono wiszące kontenery LXC"
|
||||||
|
|
||||||
|
# version/v0_3_6_alpha_0.rs
|
||||||
|
migration.migrating-package:
|
||||||
|
en_US: "Migrating package %{package}..."
|
||||||
|
de_DE: "Paket %{package} wird migriert..."
|
||||||
|
es_ES: "Migrando paquete %{package}..."
|
||||||
|
fr_FR: "Migration du paquet %{package}..."
|
||||||
|
pl_PL: "Migracja pakietu %{package}..."
|
||||||
|
|
||||||
# registry/admin.rs
|
# registry/admin.rs
|
||||||
registry.admin.unknown-signer:
|
registry.admin.unknown-signer:
|
||||||
en_US: "Unknown signer"
|
en_US: "Unknown signer"
|
||||||
@@ -1818,6 +1826,21 @@ registry.os.version.signer-not-authorized:
|
|||||||
fr_FR: "Le signataire %{signer} n'est pas autorisé à signer pour v%{version}"
|
fr_FR: "Le signataire %{signer} n'est pas autorisé à signer pour v%{version}"
|
||||||
pl_PL: "Sygnatariusz %{signer} nie jest autoryzowany do podpisywania v%{version}"
|
pl_PL: "Sygnatariusz %{signer} nie jest autoryzowany do podpisywania v%{version}"
|
||||||
|
|
||||||
|
# registry/os/promote.rs
|
||||||
|
registry.os.promote.need-from-or-to:
|
||||||
|
en_US: "At least one of --from or --to must be specified"
|
||||||
|
de_DE: "Mindestens --from oder --to muss angegeben werden"
|
||||||
|
es_ES: "Se debe especificar al menos --from o --to"
|
||||||
|
fr_FR: "Au moins --from ou --to doit être spécifié"
|
||||||
|
pl_PL: "Należy podać przynajmniej --from lub --to"
|
||||||
|
|
||||||
|
registry.os.promote.version-not-found:
|
||||||
|
en_US: "OS version %{version} not found on source registry"
|
||||||
|
de_DE: "OS-Version %{version} nicht in der Quell-Registry gefunden"
|
||||||
|
es_ES: "Versión del SO %{version} no encontrada en el registro de origen"
|
||||||
|
fr_FR: "Version OS %{version} introuvable dans le registre source"
|
||||||
|
pl_PL: "Wersja OS %{version} nie znaleziona w rejestrze źródłowym"
|
||||||
|
|
||||||
# registry/package/mod.rs
|
# registry/package/mod.rs
|
||||||
registry.package.remove-not-exist:
|
registry.package.remove-not-exist:
|
||||||
en_US: "%{id}@%{version}%{sighash} does not exist, so not removed"
|
en_US: "%{id}@%{version}%{sighash} does not exist, so not removed"
|
||||||
@@ -1862,6 +1885,20 @@ registry.package.add-mirror.unauthorized:
|
|||||||
fr_FR: "Non autorisé"
|
fr_FR: "Non autorisé"
|
||||||
pl_PL: "Brak autoryzacji"
|
pl_PL: "Brak autoryzacji"
|
||||||
|
|
||||||
|
registry.package.promote.need-from-or-to:
|
||||||
|
en_US: "At least one of --from or --to must be specified"
|
||||||
|
de_DE: "Mindestens --from oder --to muss angegeben werden"
|
||||||
|
es_ES: "Se debe especificar al menos --from o --to"
|
||||||
|
fr_FR: "Au moins --from ou --to doit être spécifié"
|
||||||
|
pl_PL: "Należy podać przynajmniej --from lub --to"
|
||||||
|
|
||||||
|
registry.package.promote.version-not-found:
|
||||||
|
en_US: "Version %{version} of %{id} not found on source registry"
|
||||||
|
de_DE: "Version %{version} von %{id} nicht in der Quell-Registry gefunden"
|
||||||
|
es_ES: "Versión %{version} de %{id} no encontrada en el registro de origen"
|
||||||
|
fr_FR: "Version %{version} de %{id} introuvable dans le registre source"
|
||||||
|
pl_PL: "Wersja %{version} pakietu %{id} nie znaleziona w rejestrze źródłowym"
|
||||||
|
|
||||||
registry.package.cannot-remove-last-mirror:
|
registry.package.cannot-remove-last-mirror:
|
||||||
en_US: "Cannot remove last mirror from an s9pk"
|
en_US: "Cannot remove last mirror from an s9pk"
|
||||||
de_DE: "Letzter Spiegel kann nicht aus einem s9pk entfernt werden"
|
de_DE: "Letzter Spiegel kann nicht aus einem s9pk entfernt werden"
|
||||||
@@ -2649,6 +2686,13 @@ help.arg.add-signer-key:
|
|||||||
fr_FR: "Ajouter une clé publique au signataire"
|
fr_FR: "Ajouter une clé publique au signataire"
|
||||||
pl_PL: "Dodaj klucz publiczny do sygnatariusza"
|
pl_PL: "Dodaj klucz publiczny do sygnatariusza"
|
||||||
|
|
||||||
|
help.arg.address:
|
||||||
|
en_US: "Network address"
|
||||||
|
de_DE: "Netzwerkadresse"
|
||||||
|
es_ES: "Dirección de red"
|
||||||
|
fr_FR: "Adresse réseau"
|
||||||
|
pl_PL: "Adres sieciowy"
|
||||||
|
|
||||||
help.arg.allow-model-mismatch:
|
help.arg.allow-model-mismatch:
|
||||||
en_US: "Allow database model mismatch"
|
en_US: "Allow database model mismatch"
|
||||||
de_DE: "Datenbankmodell-Abweichung erlauben"
|
de_DE: "Datenbankmodell-Abweichung erlauben"
|
||||||
@@ -2803,6 +2847,13 @@ help.arg.echoip-urls:
|
|||||||
fr_FR: "URLs du service Echo IP pour la détection d'IP externe"
|
fr_FR: "URLs du service Echo IP pour la détection d'IP externe"
|
||||||
pl_PL: "Adresy URL usługi Echo IP do wykrywania zewnętrznego IP"
|
pl_PL: "Adresy URL usługi Echo IP do wykrywania zewnętrznego IP"
|
||||||
|
|
||||||
|
help.arg.ed25519:
|
||||||
|
en_US: "Use Ed25519 instead of NIST P-256"
|
||||||
|
de_DE: "Ed25519 anstelle von NIST P-256 verwenden"
|
||||||
|
es_ES: "Usar Ed25519 en lugar de NIST P-256"
|
||||||
|
fr_FR: "Utiliser Ed25519 au lieu de NIST P-256"
|
||||||
|
pl_PL: "Użyj Ed25519 zamiast NIST P-256"
|
||||||
|
|
||||||
help.arg.emulate-missing-arch:
|
help.arg.emulate-missing-arch:
|
||||||
en_US: "Emulate missing architecture using this one"
|
en_US: "Emulate missing architecture using this one"
|
||||||
de_DE: "Fehlende Architektur mit dieser emulieren"
|
de_DE: "Fehlende Architektur mit dieser emulieren"
|
||||||
@@ -2880,6 +2931,13 @@ help.arg.force-uninstall:
|
|||||||
fr_FR: "Ignorer les erreurs dans le script de désinitialisation du service"
|
fr_FR: "Ignorer les erreurs dans le script de désinitialisation du service"
|
||||||
pl_PL: "Ignoruj błędy w skrypcie deinicjalizacji usługi"
|
pl_PL: "Ignoruj błędy w skrypcie deinicjalizacji usługi"
|
||||||
|
|
||||||
|
help.arg.from-registry-url:
|
||||||
|
en_US: "Source registry URL to promote from"
|
||||||
|
de_DE: "Quell-Registry-URL zum Heraufstufen"
|
||||||
|
es_ES: "URL del registro de origen para promover"
|
||||||
|
fr_FR: "URL du registre source pour la promotion"
|
||||||
|
pl_PL: "URL rejestru źródłowego do promowania"
|
||||||
|
|
||||||
help.arg.host-url:
|
help.arg.host-url:
|
||||||
en_US: "URL of the StartOS server"
|
en_US: "URL of the StartOS server"
|
||||||
de_DE: "URL des StartOS-Servers"
|
de_DE: "URL des StartOS-Servers"
|
||||||
@@ -2887,6 +2945,13 @@ help.arg.host-url:
|
|||||||
fr_FR: "URL du serveur StartOS"
|
fr_FR: "URL du serveur StartOS"
|
||||||
pl_PL: "URL serwera StartOS"
|
pl_PL: "URL serwera StartOS"
|
||||||
|
|
||||||
|
help.arg.hostnames:
|
||||||
|
en_US: "Hostnames to include in the certificate"
|
||||||
|
de_DE: "Hostnamen, die in das Zertifikat aufgenommen werden sollen"
|
||||||
|
es_ES: "Nombres de host para incluir en el certificado"
|
||||||
|
fr_FR: "Noms d'hôtes à inclure dans le certificat"
|
||||||
|
pl_PL: "Nazwy hostów do uwzględnienia w certyfikacie"
|
||||||
|
|
||||||
help.arg.icon-path:
|
help.arg.icon-path:
|
||||||
en_US: "Path to service icon file"
|
en_US: "Path to service icon file"
|
||||||
de_DE: "Pfad zur Service-Icon-Datei"
|
de_DE: "Pfad zur Service-Icon-Datei"
|
||||||
@@ -2894,6 +2959,13 @@ help.arg.icon-path:
|
|||||||
fr_FR: "Chemin vers le fichier d'icône du service"
|
fr_FR: "Chemin vers le fichier d'icône du service"
|
||||||
pl_PL: "Ścieżka do pliku ikony usługi"
|
pl_PL: "Ścieżka do pliku ikony usługi"
|
||||||
|
|
||||||
|
help.arg.icon-source:
|
||||||
|
en_US: "Icon source: file path, file:// URL, http(s):// URL, or data: URL"
|
||||||
|
de_DE: "Icon-Quelle: Dateipfad, file://-URL, http(s)://-URL oder data:-URL"
|
||||||
|
es_ES: "Fuente del icono: ruta de archivo, URL file://, URL http(s):// o URL data:"
|
||||||
|
fr_FR: "Source de l'icône : chemin de fichier, URL file://, URL http(s):// ou URL data:"
|
||||||
|
pl_PL: "Źródło ikony: ścieżka pliku, URL file://, URL http(s):// lub URL data:"
|
||||||
|
|
||||||
help.arg.image-id:
|
help.arg.image-id:
|
||||||
en_US: "Docker image identifier"
|
en_US: "Docker image identifier"
|
||||||
de_DE: "Docker-Image-Kennung"
|
de_DE: "Docker-Image-Kennung"
|
||||||
@@ -2978,6 +3050,34 @@ help.arg.merge:
|
|||||||
fr_FR: "Fusionner avec la plage de versions existante au lieu de remplacer"
|
fr_FR: "Fusionner avec la plage de versions existante au lieu de remplacer"
|
||||||
pl_PL: "Połącz z istniejącym zakresem wersji zamiast zastępować"
|
pl_PL: "Połącz z istniejącym zakresem wersji zamiast zastępować"
|
||||||
|
|
||||||
|
help.arg.metrics-after:
|
||||||
|
en_US: "Start of time range (RFC 3339 timestamp)"
|
||||||
|
de_DE: "Beginn des Zeitraums (RFC 3339 Zeitstempel)"
|
||||||
|
es_ES: "Inicio del rango de tiempo (marca de tiempo RFC 3339)"
|
||||||
|
fr_FR: "Début de la plage temporelle (horodatage RFC 3339)"
|
||||||
|
pl_PL: "Początek zakresu czasu (znacznik czasu RFC 3339)"
|
||||||
|
|
||||||
|
help.arg.metrics-before:
|
||||||
|
en_US: "End of time range (RFC 3339 timestamp)"
|
||||||
|
de_DE: "Ende des Zeitraums (RFC 3339 Zeitstempel)"
|
||||||
|
es_ES: "Fin del rango de tiempo (marca de tiempo RFC 3339)"
|
||||||
|
fr_FR: "Fin de la plage temporelle (horodatage RFC 3339)"
|
||||||
|
pl_PL: "Koniec zakresu czasu (znacznik czasu RFC 3339)"
|
||||||
|
|
||||||
|
help.arg.metrics-pkg-id:
|
||||||
|
en_US: "Filter by package ID"
|
||||||
|
de_DE: "Nach Paket-ID filtern"
|
||||||
|
es_ES: "Filtrar por ID de paquete"
|
||||||
|
fr_FR: "Filtrer par identifiant de paquet"
|
||||||
|
pl_PL: "Filtruj według identyfikatora pakietu"
|
||||||
|
|
||||||
|
help.arg.metrics-version:
|
||||||
|
en_US: "Filter by version"
|
||||||
|
de_DE: "Nach Version filtern"
|
||||||
|
es_ES: "Filtrar por versión"
|
||||||
|
fr_FR: "Filtrer par version"
|
||||||
|
pl_PL: "Filtruj według wersji"
|
||||||
|
|
||||||
help.arg.mirror-url:
|
help.arg.mirror-url:
|
||||||
en_US: "URL of the mirror"
|
en_US: "URL of the mirror"
|
||||||
de_DE: "URL des Spiegels"
|
de_DE: "URL des Spiegels"
|
||||||
@@ -3090,12 +3190,12 @@ help.arg.platform:
|
|||||||
fr_FR: "Identifiant de la plateforme cible"
|
fr_FR: "Identifiant de la plateforme cible"
|
||||||
pl_PL: "Identyfikator platformy docelowej"
|
pl_PL: "Identyfikator platformy docelowej"
|
||||||
|
|
||||||
help.arg.postgres-connection-url:
|
help.arg.port:
|
||||||
en_US: "PostgreSQL connection URL"
|
en_US: "Port number"
|
||||||
de_DE: "PostgreSQL-Verbindungs-URL"
|
de_DE: "Portnummer"
|
||||||
es_ES: "URL de conexión PostgreSQL"
|
es_ES: "Número de puerto"
|
||||||
fr_FR: "URL de connexion PostgreSQL"
|
fr_FR: "Numéro de port"
|
||||||
pl_PL: "URL połączenia PostgreSQL"
|
pl_PL: "Numer portu"
|
||||||
|
|
||||||
help.arg.proxy-url:
|
help.arg.proxy-url:
|
||||||
en_US: "HTTP/SOCKS proxy URL"
|
en_US: "HTTP/SOCKS proxy URL"
|
||||||
@@ -3174,6 +3274,13 @@ help.arg.server-id:
|
|||||||
fr_FR: "Identifiant unique du serveur"
|
fr_FR: "Identifiant unique du serveur"
|
||||||
pl_PL: "Unikalny identyfikator serwera"
|
pl_PL: "Unikalny identyfikator serwera"
|
||||||
|
|
||||||
|
help.arg.set-as-default-outbound:
|
||||||
|
en_US: "Set as the default outbound gateway"
|
||||||
|
de_DE: "Als Standard-Ausgangs-Gateway festlegen"
|
||||||
|
es_ES: "Establecer como puerta de enlace de salida predeterminada"
|
||||||
|
fr_FR: "Définir comme passerelle de sortie par défaut"
|
||||||
|
pl_PL: "Ustaw jako domyślną bramę wychodzącą"
|
||||||
|
|
||||||
help.arg.set-signer-name:
|
help.arg.set-signer-name:
|
||||||
en_US: "Set the signer name"
|
en_US: "Set the signer name"
|
||||||
de_DE: "Unterzeichnernamen festlegen"
|
de_DE: "Unterzeichnernamen festlegen"
|
||||||
@@ -3314,6 +3421,13 @@ help.arg.target-version-range:
|
|||||||
fr_FR: "Contrainte de plage de version cible"
|
fr_FR: "Contrainte de plage de version cible"
|
||||||
pl_PL: "Ograniczenie zakresu wersji docelowej"
|
pl_PL: "Ograniczenie zakresu wersji docelowej"
|
||||||
|
|
||||||
|
help.arg.to-registry-url:
|
||||||
|
en_US: "Destination registry URL to promote to"
|
||||||
|
de_DE: "Ziel-Registry-URL zum Heraufstufen"
|
||||||
|
es_ES: "URL del registro de destino para promover"
|
||||||
|
fr_FR: "URL du registre de destination pour la promotion"
|
||||||
|
pl_PL: "URL rejestru docelowego do promowania"
|
||||||
|
|
||||||
help.arg.tor-proxy-url:
|
help.arg.tor-proxy-url:
|
||||||
en_US: "Tor SOCKS proxy URL"
|
en_US: "Tor SOCKS proxy URL"
|
||||||
de_DE: "Tor-SOCKS-Proxy-URL"
|
de_DE: "Tor-SOCKS-Proxy-URL"
|
||||||
@@ -3538,6 +3652,13 @@ help.arg.gateway-name:
|
|||||||
fr_FR: "Nom de la passerelle"
|
fr_FR: "Nom de la passerelle"
|
||||||
pl_PL: "Nazwa bramy"
|
pl_PL: "Nazwa bramy"
|
||||||
|
|
||||||
|
help.arg.gateway-type:
|
||||||
|
en_US: "Type of gateway"
|
||||||
|
de_DE: "Typ des Gateways"
|
||||||
|
es_ES: "Tipo de puerta de enlace"
|
||||||
|
fr_FR: "Type de passerelle"
|
||||||
|
pl_PL: "Typ bramy"
|
||||||
|
|
||||||
help.arg.governor-name:
|
help.arg.governor-name:
|
||||||
en_US: "CPU governor name"
|
en_US: "CPU governor name"
|
||||||
de_DE: "CPU-Governor-Name"
|
de_DE: "CPU-Governor-Name"
|
||||||
@@ -4064,6 +4185,13 @@ about.add-version-signer:
|
|||||||
fr_FR: "Ajouter un signataire de version"
|
fr_FR: "Ajouter un signataire de version"
|
||||||
pl_PL: "Dodaj sygnatariusza wersji"
|
pl_PL: "Dodaj sygnatariusza wersji"
|
||||||
|
|
||||||
|
about.add-vhost-passthrough:
|
||||||
|
en_US: "Add vhost passthrough"
|
||||||
|
de_DE: "Vhost-Passthrough hinzufügen"
|
||||||
|
es_ES: "Agregar passthrough de vhost"
|
||||||
|
fr_FR: "Ajouter un passthrough vhost"
|
||||||
|
pl_PL: "Dodaj passthrough vhost"
|
||||||
|
|
||||||
about.add-wifi-ssid-password:
|
about.add-wifi-ssid-password:
|
||||||
en_US: "Add wifi ssid and password"
|
en_US: "Add wifi ssid and password"
|
||||||
de_DE: "WLAN-SSID und Passwort hinzufügen"
|
de_DE: "WLAN-SSID und Passwort hinzufügen"
|
||||||
@@ -4113,6 +4241,13 @@ about.check-for-updates:
|
|||||||
fr_FR: "Vérifier les mises à jour disponibles"
|
fr_FR: "Vérifier les mises à jour disponibles"
|
||||||
pl_PL: "Sprawdź dostępne aktualizacje"
|
pl_PL: "Sprawdź dostępne aktualizacje"
|
||||||
|
|
||||||
|
about.check-port-reachability:
|
||||||
|
en_US: "Check if a port is reachable from the WAN"
|
||||||
|
de_DE: "Prüfen, ob ein Port vom WAN erreichbar ist"
|
||||||
|
es_ES: "Comprobar si un puerto es accesible desde la WAN"
|
||||||
|
fr_FR: "Vérifier si un port est accessible depuis le WAN"
|
||||||
|
pl_PL: "Sprawdź, czy port jest osiągalny z WAN"
|
||||||
|
|
||||||
about.check-update-startos:
|
about.check-update-startos:
|
||||||
en_US: "Check a given registry for StartOS updates and update if available"
|
en_US: "Check a given registry for StartOS updates and update if available"
|
||||||
de_DE: "Ein bestimmtes Registry auf StartOS-Updates prüfen und bei Verfügbarkeit aktualisieren"
|
de_DE: "Ein bestimmtes Registry auf StartOS-Updates prüfen und bei Verfügbarkeit aktualisieren"
|
||||||
@@ -4211,6 +4346,13 @@ about.commands-authentication:
|
|||||||
fr_FR: "Commandes liées à l'authentification, comme connexion, déconnexion"
|
fr_FR: "Commandes liées à l'authentification, comme connexion, déconnexion"
|
||||||
pl_PL: "Polecenia związane z uwierzytelnianiem, np. logowanie, wylogowanie"
|
pl_PL: "Polecenia związane z uwierzytelnianiem, np. logowanie, wylogowanie"
|
||||||
|
|
||||||
|
about.commands-authorized-keys:
|
||||||
|
en_US: "Commands for managing authorized keys"
|
||||||
|
de_DE: "Befehle zur Verwaltung autorisierter Schlüssel"
|
||||||
|
es_ES: "Comandos para gestionar claves autorizadas"
|
||||||
|
fr_FR: "Commandes pour gérer les clés autorisées"
|
||||||
|
pl_PL: "Polecenia do zarządzania autoryzowanymi kluczami"
|
||||||
|
|
||||||
about.commands-backup:
|
about.commands-backup:
|
||||||
en_US: "Commands related to backup creation and backup targets"
|
en_US: "Commands related to backup creation and backup targets"
|
||||||
de_DE: "Befehle zur Backup-Erstellung und Backup-Zielen"
|
de_DE: "Befehle zur Backup-Erstellung und Backup-Zielen"
|
||||||
@@ -4274,6 +4416,41 @@ about.commands-experimental:
|
|||||||
fr_FR: "Commandes liées à la configuration d'options expérimentales comme zram et le gouverneur CPU"
|
fr_FR: "Commandes liées à la configuration d'options expérimentales comme zram et le gouverneur CPU"
|
||||||
pl_PL: "Polecenia konfiguracji opcji eksperymentalnych jak zram i regulator CPU"
|
pl_PL: "Polecenia konfiguracji opcji eksperymentalnych jak zram i regulator CPU"
|
||||||
|
|
||||||
|
about.commands-host-address-domain:
|
||||||
|
en_US: "Commands for managing host address domains"
|
||||||
|
de_DE: "Befehle zur Verwaltung von Host-Adressdomänen"
|
||||||
|
es_ES: "Comandos para gestionar dominios de direcciones del host"
|
||||||
|
fr_FR: "Commandes pour gérer les domaines d'adresses de l'hôte"
|
||||||
|
pl_PL: "Polecenia do zarządzania domenami adresów hosta"
|
||||||
|
|
||||||
|
about.commands-host-addresses:
|
||||||
|
en_US: "Commands for managing host addresses"
|
||||||
|
de_DE: "Befehle zur Verwaltung von Host-Adressen"
|
||||||
|
es_ES: "Comandos para gestionar direcciones del host"
|
||||||
|
fr_FR: "Commandes pour gérer les adresses de l'hôte"
|
||||||
|
pl_PL: "Polecenia do zarządzania adresami hosta"
|
||||||
|
|
||||||
|
about.commands-host-bindings:
|
||||||
|
en_US: "Commands for managing host bindings"
|
||||||
|
de_DE: "Befehle zur Verwaltung von Host-Bindungen"
|
||||||
|
es_ES: "Comandos para gestionar vínculos del host"
|
||||||
|
fr_FR: "Commandes pour gérer les liaisons de l'hôte"
|
||||||
|
pl_PL: "Polecenia do zarządzania powiązaniami hosta"
|
||||||
|
|
||||||
|
about.commands-host-private-domain:
|
||||||
|
en_US: "Commands for managing private domains for a host"
|
||||||
|
de_DE: "Befehle zur Verwaltung privater Domänen für einen Host"
|
||||||
|
es_ES: "Comandos para gestionar dominios privados de un host"
|
||||||
|
fr_FR: "Commandes pour gérer les domaines privés d'un hôte"
|
||||||
|
pl_PL: "Polecenia do zarządzania prywatnymi domenami hosta"
|
||||||
|
|
||||||
|
about.commands-host-public-domain:
|
||||||
|
en_US: "Commands for managing public domains for a host"
|
||||||
|
de_DE: "Befehle zur Verwaltung öffentlicher Domänen für einen Host"
|
||||||
|
es_ES: "Comandos para gestionar dominios públicos de un host"
|
||||||
|
fr_FR: "Commandes pour gérer les domaines publics d'un hôte"
|
||||||
|
pl_PL: "Polecenia do zarządzania publicznymi domenami hosta"
|
||||||
|
|
||||||
about.commands-host-system-ui:
|
about.commands-host-system-ui:
|
||||||
en_US: "Commands for modifying the host for the system ui"
|
en_US: "Commands for modifying the host for the system ui"
|
||||||
de_DE: "Befehle zum Ändern des Hosts für die System-UI"
|
de_DE: "Befehle zum Ändern des Hosts für die System-UI"
|
||||||
@@ -4330,6 +4507,13 @@ about.commands-packages:
|
|||||||
fr_FR: "Commandes liées aux paquets"
|
fr_FR: "Commandes liées aux paquets"
|
||||||
pl_PL: "Polecenia związane z pakietami"
|
pl_PL: "Polecenia związane z pakietami"
|
||||||
|
|
||||||
|
about.commands-port-forward:
|
||||||
|
en_US: "Commands for managing port forwards"
|
||||||
|
de_DE: "Befehle zur Verwaltung von Portweiterleitungen"
|
||||||
|
es_ES: "Comandos para gestionar reenvíos de puertos"
|
||||||
|
fr_FR: "Commandes pour gérer les redirections de ports"
|
||||||
|
pl_PL: "Polecenia do zarządzania przekierowaniami portów"
|
||||||
|
|
||||||
about.commands-registry:
|
about.commands-registry:
|
||||||
en_US: "Commands related to the registry"
|
en_US: "Commands related to the registry"
|
||||||
de_DE: "Befehle zum Registry"
|
de_DE: "Befehle zum Registry"
|
||||||
@@ -4344,6 +4528,20 @@ about.commands-registry-db:
|
|||||||
fr_FR: "Commandes pour interagir avec la base de données, comme dump et apply"
|
fr_FR: "Commandes pour interagir avec la base de données, comme dump et apply"
|
||||||
pl_PL: "Polecenia interakcji z bazą danych, np. dump i apply"
|
pl_PL: "Polecenia interakcji z bazą danych, np. dump i apply"
|
||||||
|
|
||||||
|
about.commands-registry-metrics:
|
||||||
|
en_US: "Query registry usage metrics"
|
||||||
|
de_DE: "Registry-Nutzungsmetriken abfragen"
|
||||||
|
es_ES: "Consultar métricas de uso del registro"
|
||||||
|
fr_FR: "Consulter les métriques d'utilisation du registre"
|
||||||
|
pl_PL: "Zapytaj o metryki użycia rejestru"
|
||||||
|
|
||||||
|
about.commands-registry-info:
|
||||||
|
en_US: "View or edit registry information"
|
||||||
|
de_DE: "Registry-Informationen anzeigen oder bearbeiten"
|
||||||
|
es_ES: "Ver o editar información del registro"
|
||||||
|
fr_FR: "Afficher ou modifier les informations du registre"
|
||||||
|
pl_PL: "Wyświetl lub edytuj informacje rejestru"
|
||||||
|
|
||||||
about.commands-restore-backup:
|
about.commands-restore-backup:
|
||||||
en_US: "Commands for restoring package(s) from backup"
|
en_US: "Commands for restoring package(s) from backup"
|
||||||
de_DE: "Befehle zum Wiederherstellen von Paketen aus dem Backup"
|
de_DE: "Befehle zum Wiederherstellen von Paketen aus dem Backup"
|
||||||
@@ -4386,6 +4584,20 @@ about.commands-tunnel:
|
|||||||
fr_FR: "Commandes liées à StartTunnel"
|
fr_FR: "Commandes liées à StartTunnel"
|
||||||
pl_PL: "Polecenia związane z StartTunnel"
|
pl_PL: "Polecenia związane z StartTunnel"
|
||||||
|
|
||||||
|
about.commands-tunnel-update:
|
||||||
|
en_US: "Commands for checking and applying tunnel updates"
|
||||||
|
de_DE: "Befehle zum Prüfen und Anwenden von Tunnel-Updates"
|
||||||
|
es_ES: "Comandos para verificar y aplicar actualizaciones del túnel"
|
||||||
|
fr_FR: "Commandes pour vérifier et appliquer les mises à jour du tunnel"
|
||||||
|
pl_PL: "Polecenia do sprawdzania i stosowania aktualizacji tunelu"
|
||||||
|
|
||||||
|
about.commands-tunnel-web:
|
||||||
|
en_US: "Commands for managing the tunnel web interface"
|
||||||
|
de_DE: "Befehle zur Verwaltung der Tunnel-Weboberfläche"
|
||||||
|
es_ES: "Comandos para gestionar la interfaz web del túnel"
|
||||||
|
fr_FR: "Commandes pour gérer l'interface web du tunnel"
|
||||||
|
pl_PL: "Polecenia do zarządzania interfejsem webowym tunelu"
|
||||||
|
|
||||||
about.commands-wifi:
|
about.commands-wifi:
|
||||||
en_US: "Commands related to wifi networks i.e. add, connect, delete"
|
en_US: "Commands related to wifi networks i.e. add, connect, delete"
|
||||||
de_DE: "Befehle zu WLAN-Netzwerken, z.B. hinzufügen, verbinden, löschen"
|
de_DE: "Befehle zu WLAN-Netzwerken, z.B. hinzufügen, verbinden, löschen"
|
||||||
@@ -4526,6 +4738,13 @@ about.display-s9pk-manifest:
|
|||||||
fr_FR: "Afficher le manifeste s9pk"
|
fr_FR: "Afficher le manifeste s9pk"
|
||||||
pl_PL: "Wyświetl manifest s9pk"
|
pl_PL: "Wyświetl manifest s9pk"
|
||||||
|
|
||||||
|
about.display-s9pk-root-sighash-and-maxsize:
|
||||||
|
en_US: "Display the s9pk root signature hash and max size"
|
||||||
|
de_DE: "Den s9pk-Root-Signaturhash und die maximale Größe anzeigen"
|
||||||
|
es_ES: "Mostrar el hash de firma raíz y el tamaño máximo del s9pk"
|
||||||
|
fr_FR: "Afficher le hachage de signature racine et la taille maximale du s9pk"
|
||||||
|
pl_PL: "Wyświetl hash podpisu głównego i maksymalny rozmiar s9pk"
|
||||||
|
|
||||||
about.display-server-metrics:
|
about.display-server-metrics:
|
||||||
en_US: "Display server metrics"
|
en_US: "Display server metrics"
|
||||||
de_DE: "Server-Metriken anzeigen"
|
de_DE: "Server-Metriken anzeigen"
|
||||||
@@ -4589,6 +4808,20 @@ about.dump-address-resolution-table:
|
|||||||
fr_FR: "Exporter la table de résolution d'adresses"
|
fr_FR: "Exporter la table de résolution d'adresses"
|
||||||
pl_PL: "Zrzuć tabelę rozpoznawania adresów"
|
pl_PL: "Zrzuć tabelę rozpoznawania adresów"
|
||||||
|
|
||||||
|
about.dump-port-forward-table:
|
||||||
|
en_US: "Dump port forward table"
|
||||||
|
de_DE: "Portweiterleitungstabelle ausgeben"
|
||||||
|
es_ES: "Volcar tabla de reenvío de puertos"
|
||||||
|
fr_FR: "Exporter la table de redirection de ports"
|
||||||
|
pl_PL: "Zrzuć tabelę przekierowań portów"
|
||||||
|
|
||||||
|
about.dump-vhost-proxy-table:
|
||||||
|
en_US: "Dump vhost proxy table"
|
||||||
|
de_DE: "Vhost-Proxy-Tabelle ausgeben"
|
||||||
|
es_ES: "Volcar tabla de proxy vhost"
|
||||||
|
fr_FR: "Exporter la table de proxy vhost"
|
||||||
|
pl_PL: "Zrzuć tabelę proxy vhost"
|
||||||
|
|
||||||
about.echo-message:
|
about.echo-message:
|
||||||
en_US: "Echo a message back"
|
en_US: "Echo a message back"
|
||||||
de_DE: "Eine Nachricht zurückgeben"
|
de_DE: "Eine Nachricht zurückgeben"
|
||||||
@@ -4624,6 +4857,13 @@ about.enable-kiosk-mode:
|
|||||||
fr_FR: "Activer le mode kiosque"
|
fr_FR: "Activer le mode kiosque"
|
||||||
pl_PL: "Włącz tryb kiosku"
|
pl_PL: "Włącz tryb kiosku"
|
||||||
|
|
||||||
|
about.enable-or-disable-port-forward:
|
||||||
|
en_US: "Enable or disable a port forward"
|
||||||
|
de_DE: "Portweiterleitung aktivieren oder deaktivieren"
|
||||||
|
es_ES: "Habilitar o deshabilitar un reenvío de puerto"
|
||||||
|
fr_FR: "Activer ou désactiver une redirection de port"
|
||||||
|
pl_PL: "Włącz lub wyłącz przekierowanie portu"
|
||||||
|
|
||||||
about.enable-webserver:
|
about.enable-webserver:
|
||||||
en_US: "Enable the webserver"
|
en_US: "Enable the webserver"
|
||||||
de_DE: "Webserver aktivieren"
|
de_DE: "Webserver aktivieren"
|
||||||
@@ -4715,6 +4955,13 @@ about.get-developer-pubkey:
|
|||||||
fr_FR: "Obtenir la clé publique du développeur"
|
fr_FR: "Obtenir la clé publique du développeur"
|
||||||
pl_PL: "Pobierz klucz publiczny dewelopera"
|
pl_PL: "Pobierz klucz publiczny dewelopera"
|
||||||
|
|
||||||
|
about.get-device-info:
|
||||||
|
en_US: "Display device information"
|
||||||
|
de_DE: "Geräteinformationen anzeigen"
|
||||||
|
es_ES: "Mostrar información del dispositivo"
|
||||||
|
fr_FR: "Afficher les informations de l'appareil"
|
||||||
|
pl_PL: "Wyświetl informacje o urządzeniu"
|
||||||
|
|
||||||
about.get-initialization-progress:
|
about.get-initialization-progress:
|
||||||
en_US: "Get initialization progress"
|
en_US: "Get initialization progress"
|
||||||
de_DE: "Initialisierungsfortschritt abrufen"
|
de_DE: "Initialisierungsfortschritt abrufen"
|
||||||
@@ -4729,6 +4976,27 @@ about.get-listen-address-for-webserver:
|
|||||||
fr_FR: "Obtenir l'adresse d'écoute du serveur web"
|
fr_FR: "Obtenir l'adresse d'écoute du serveur web"
|
||||||
pl_PL: "Pobierz adres nasłuchiwania serwera internetowego"
|
pl_PL: "Pobierz adres nasłuchiwania serwera internetowego"
|
||||||
|
|
||||||
|
about.get-metrics-downloads:
|
||||||
|
en_US: "Count package download requests with optional filters"
|
||||||
|
de_DE: "Paket-Download-Anfragen mit optionalen Filtern zählen"
|
||||||
|
es_ES: "Contar solicitudes de descarga de paquetes con filtros opcionales"
|
||||||
|
fr_FR: "Compter les demandes de téléchargement de paquets avec filtres optionnels"
|
||||||
|
pl_PL: "Zlicz żądania pobrania pakietów z opcjonalnymi filtrami"
|
||||||
|
|
||||||
|
about.get-metrics-summary:
|
||||||
|
en_US: "Get a summary of registry usage metrics"
|
||||||
|
de_DE: "Zusammenfassung der Registry-Nutzungsmetriken abrufen"
|
||||||
|
es_ES: "Obtener un resumen de las métricas de uso del registro"
|
||||||
|
fr_FR: "Obtenir un résumé des métriques d'utilisation du registre"
|
||||||
|
pl_PL: "Pobierz podsumowanie metryk użycia rejestru"
|
||||||
|
|
||||||
|
about.get-metrics-users:
|
||||||
|
en_US: "Count unique active users within a time range"
|
||||||
|
de_DE: "Eindeutige aktive Benutzer in einem Zeitraum zählen"
|
||||||
|
es_ES: "Contar usuarios activos únicos dentro de un rango de tiempo"
|
||||||
|
fr_FR: "Compter les utilisateurs actifs uniques dans un intervalle de temps"
|
||||||
|
pl_PL: "Zlicz unikalnych aktywnych użytkowników w zakresie czasu"
|
||||||
|
|
||||||
about.get-os-versions-info:
|
about.get-os-versions-info:
|
||||||
en_US: "Get OS versions info"
|
en_US: "Get OS versions info"
|
||||||
de_DE: "Informationen zu Betriebssystemversionen abrufen"
|
de_DE: "Informationen zu Betriebssystemversionen abrufen"
|
||||||
@@ -4904,6 +5172,13 @@ about.list-paths-of-package-ingredients:
|
|||||||
fr_FR: "Lister les chemins des composants du package"
|
fr_FR: "Lister les chemins des composants du package"
|
||||||
pl_PL: "Wyświetl ścieżki składników pakietu"
|
pl_PL: "Wyświetl ścieżki składników pakietu"
|
||||||
|
|
||||||
|
about.list-registry-categories:
|
||||||
|
en_US: "List registry categories"
|
||||||
|
de_DE: "Registry-Kategorien auflisten"
|
||||||
|
es_ES: "Listar categorías del registro"
|
||||||
|
fr_FR: "Lister les catégories du registre"
|
||||||
|
pl_PL: "Wyświetl kategorie rejestru"
|
||||||
|
|
||||||
about.list-registry-info-packages:
|
about.list-registry-info-packages:
|
||||||
en_US: "List registry info and packages"
|
en_US: "List registry info and packages"
|
||||||
de_DE: "Registry-Informationen und Pakete auflisten"
|
de_DE: "Registry-Informationen und Pakete auflisten"
|
||||||
@@ -4932,6 +5207,13 @@ about.list-version-signers:
|
|||||||
fr_FR: "Lister les signataires de versions"
|
fr_FR: "Lister les signataires de versions"
|
||||||
pl_PL: "Wyświetl sygnatariuszy wersji"
|
pl_PL: "Wyświetl sygnatariuszy wersji"
|
||||||
|
|
||||||
|
about.list-vhost-passthrough:
|
||||||
|
en_US: "List vhost passthroughs"
|
||||||
|
de_DE: "Vhost-Passthroughs auflisten"
|
||||||
|
es_ES: "Listar passthroughs de vhost"
|
||||||
|
fr_FR: "Lister les passthroughs vhost"
|
||||||
|
pl_PL: "Wyświetl passthrough vhost"
|
||||||
|
|
||||||
about.list-wifi-info:
|
about.list-wifi-info:
|
||||||
en_US: "List wifi information"
|
en_US: "List wifi information"
|
||||||
de_DE: "WLAN-Informationen auflisten"
|
de_DE: "WLAN-Informationen auflisten"
|
||||||
@@ -4981,6 +5263,13 @@ about.manage-query-dns:
|
|||||||
fr_FR: "Gérer et interroger le DNS"
|
fr_FR: "Gérer et interroger le DNS"
|
||||||
pl_PL: "Zarządzaj i odpytuj DNS"
|
pl_PL: "Zarządzaj i odpytuj DNS"
|
||||||
|
|
||||||
|
about.manage-ssl-certificates:
|
||||||
|
en_US: "Manage SSL certificates"
|
||||||
|
de_DE: "SSL-Zertifikate verwalten"
|
||||||
|
es_ES: "Gestionar certificados SSL"
|
||||||
|
fr_FR: "Gérer les certificats SSL"
|
||||||
|
pl_PL: "Zarządzaj certyfikatami SSL"
|
||||||
|
|
||||||
about.manage-ssl-vhost-proxy:
|
about.manage-ssl-vhost-proxy:
|
||||||
en_US: "Manage SSL vhost proxy"
|
en_US: "Manage SSL vhost proxy"
|
||||||
de_DE: "SSL-vhost-Proxy verwalten"
|
de_DE: "SSL-vhost-Proxy verwalten"
|
||||||
@@ -5044,6 +5333,20 @@ about.persist-new-notification:
|
|||||||
fr_FR: "Persister une nouvelle notification"
|
fr_FR: "Persister une nouvelle notification"
|
||||||
pl_PL: "Utrwal nowe powiadomienie"
|
pl_PL: "Utrwal nowe powiadomienie"
|
||||||
|
|
||||||
|
about.promote-os-registry:
|
||||||
|
en_US: "Promote an OS version from one registry to another"
|
||||||
|
de_DE: "Eine OS-Version von einer Registry in eine andere heraufstufen"
|
||||||
|
es_ES: "Promover una versión del SO de un registro a otro"
|
||||||
|
fr_FR: "Promouvoir une version OS d'un registre à un autre"
|
||||||
|
pl_PL: "Promuj wersję OS z jednego rejestru do drugiego"
|
||||||
|
|
||||||
|
about.promote-package-registry:
|
||||||
|
en_US: "Promote a package from one registry to another"
|
||||||
|
de_DE: "Ein Paket von einer Registry in eine andere heraufstufen"
|
||||||
|
es_ES: "Promover un paquete de un registro a otro"
|
||||||
|
fr_FR: "Promouvoir un paquet d'un registre à un autre"
|
||||||
|
pl_PL: "Promuj pakiet z jednego rejestru do drugiego"
|
||||||
|
|
||||||
about.publish-s9pk:
|
about.publish-s9pk:
|
||||||
en_US: "Publish s9pk to S3 bucket and index on registry"
|
en_US: "Publish s9pk to S3 bucket and index on registry"
|
||||||
de_DE: "S9pk in S3-Bucket veröffentlichen und in Registry indizieren"
|
de_DE: "S9pk in S3-Bucket veröffentlichen und in Registry indizieren"
|
||||||
@@ -5226,6 +5529,13 @@ about.remove-version-signer:
|
|||||||
fr_FR: "Supprimer le signataire de version"
|
fr_FR: "Supprimer le signataire de version"
|
||||||
pl_PL: "Usuń sygnatariusza wersji"
|
pl_PL: "Usuń sygnatariusza wersji"
|
||||||
|
|
||||||
|
about.remove-vhost-passthrough:
|
||||||
|
en_US: "Remove vhost passthrough"
|
||||||
|
de_DE: "Vhost-Passthrough entfernen"
|
||||||
|
es_ES: "Eliminar passthrough de vhost"
|
||||||
|
fr_FR: "Supprimer un passthrough vhost"
|
||||||
|
pl_PL: "Usuń passthrough vhost"
|
||||||
|
|
||||||
about.remove-wifi-network:
|
about.remove-wifi-network:
|
||||||
en_US: "Remove a wifi network"
|
en_US: "Remove a wifi network"
|
||||||
de_DE: "Ein WLAN-Netzwerk entfernen"
|
de_DE: "Ein WLAN-Netzwerk entfernen"
|
||||||
@@ -5310,6 +5620,13 @@ about.run-service-action:
|
|||||||
fr_FR: "Exécuter une action de service"
|
fr_FR: "Exécuter une action de service"
|
||||||
pl_PL: "Uruchom akcję usługi"
|
pl_PL: "Uruchom akcję usługi"
|
||||||
|
|
||||||
|
about.set-address-enabled-for-binding:
|
||||||
|
en_US: "Set a gateway address enabled for a binding"
|
||||||
|
de_DE: "Gateway-Adresse für eine Bindung aktivieren"
|
||||||
|
es_ES: "Establecer una dirección de gateway habilitada para un vínculo"
|
||||||
|
fr_FR: "Définir une adresse de passerelle activée pour une liaison"
|
||||||
|
pl_PL: "Ustaw adres bramy jako włączony dla powiązania"
|
||||||
|
|
||||||
about.set-country:
|
about.set-country:
|
||||||
en_US: "Set the country"
|
en_US: "Set the country"
|
||||||
de_DE: "Das Land festlegen"
|
de_DE: "Das Land festlegen"
|
||||||
@@ -5317,6 +5634,13 @@ about.set-country:
|
|||||||
fr_FR: "Définir le pays"
|
fr_FR: "Définir le pays"
|
||||||
pl_PL: "Ustaw kraj"
|
pl_PL: "Ustaw kraj"
|
||||||
|
|
||||||
|
about.set-default-outbound-gateway:
|
||||||
|
en_US: "Set the default outbound gateway"
|
||||||
|
de_DE: "Standard-Ausgangs-Gateway festlegen"
|
||||||
|
es_ES: "Establecer la puerta de enlace de salida predeterminada"
|
||||||
|
fr_FR: "Définir la passerelle sortante par défaut"
|
||||||
|
pl_PL: "Ustaw domyślną bramę wychodzącą"
|
||||||
|
|
||||||
about.set-echoip-urls:
|
about.set-echoip-urls:
|
||||||
en_US: "Set the Echo IP service URLs"
|
en_US: "Set the Echo IP service URLs"
|
||||||
de_DE: "Die Echo-IP-Dienst-URLs festlegen"
|
de_DE: "Die Echo-IP-Dienst-URLs festlegen"
|
||||||
@@ -5359,6 +5683,13 @@ about.set-listen-address-for-webserver:
|
|||||||
fr_FR: "Définir l'adresse d'écoute du serveur web"
|
fr_FR: "Définir l'adresse d'écoute du serveur web"
|
||||||
pl_PL: "Ustaw adres nasłuchiwania serwera internetowego"
|
pl_PL: "Ustaw adres nasłuchiwania serwera internetowego"
|
||||||
|
|
||||||
|
about.set-outbound-gateway-package:
|
||||||
|
en_US: "Set the outbound gateway for a package"
|
||||||
|
de_DE: "Ausgangs-Gateway für ein Paket festlegen"
|
||||||
|
es_ES: "Establecer la puerta de enlace de salida para un paquete"
|
||||||
|
fr_FR: "Définir la passerelle sortante pour un package"
|
||||||
|
pl_PL: "Ustaw bramę wychodzącą dla pakietu"
|
||||||
|
|
||||||
about.set-registry-icon:
|
about.set-registry-icon:
|
||||||
en_US: "Set the registry icon"
|
en_US: "Set the registry icon"
|
||||||
de_DE: "Das Registry-Symbol festlegen"
|
de_DE: "Das Registry-Symbol festlegen"
|
||||||
@@ -5457,6 +5788,13 @@ about.stop-service:
|
|||||||
fr_FR: "Arrêter un service"
|
fr_FR: "Arrêter un service"
|
||||||
pl_PL: "Zatrzymaj usługę"
|
pl_PL: "Zatrzymaj usługę"
|
||||||
|
|
||||||
|
about.ssl-generate-certificate:
|
||||||
|
en_US: "Generate an SSL certificate from the system root CA"
|
||||||
|
de_DE: "SSL-Zertifikat von der System-Root-CA generieren"
|
||||||
|
es_ES: "Generar un certificado SSL desde la CA raíz del sistema"
|
||||||
|
fr_FR: "Générer un certificat SSL depuis l'autorité racine du système"
|
||||||
|
pl_PL: "Wygeneruj certyfikat SSL z głównego CA systemu"
|
||||||
|
|
||||||
about.teardown-rebuild-containers:
|
about.teardown-rebuild-containers:
|
||||||
en_US: "Teardown and rebuild containers"
|
en_US: "Teardown and rebuild containers"
|
||||||
de_DE: "Container abbauen und neu erstellen"
|
de_DE: "Container abbauen und neu erstellen"
|
||||||
@@ -5527,6 +5865,13 @@ about.update-firmware:
|
|||||||
fr_FR: "Mettre à jour le firmware"
|
fr_FR: "Mettre à jour le firmware"
|
||||||
pl_PL: "Zaktualizuj oprogramowanie układowe"
|
pl_PL: "Zaktualizuj oprogramowanie układowe"
|
||||||
|
|
||||||
|
about.update-port-forward-label:
|
||||||
|
en_US: "Update the label of a port forward"
|
||||||
|
de_DE: "Bezeichnung einer Portweiterleitung aktualisieren"
|
||||||
|
es_ES: "Actualizar la etiqueta de un reenvío de puerto"
|
||||||
|
fr_FR: "Mettre à jour le libellé d'une redirection de port"
|
||||||
|
pl_PL: "Zaktualizuj etykietę przekierowania portu"
|
||||||
|
|
||||||
about.view-edit-gateway-configs:
|
about.view-edit-gateway-configs:
|
||||||
en_US: "View and edit gateway configurations"
|
en_US: "View and edit gateway configurations"
|
||||||
de_DE: "Gateway-Konfigurationen anzeigen und bearbeiten"
|
de_DE: "Gateway-Konfigurationen anzeigen und bearbeiten"
|
||||||
|
|||||||
@@ -148,6 +148,15 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
|
|||||||
.build()
|
.build()
|
||||||
.expect(&t!("bins.startd.failed-to-initialize-runtime"));
|
.expect(&t!("bins.startd.failed-to-initialize-runtime"));
|
||||||
let res = rt.block_on(async {
|
let res = rt.block_on(async {
|
||||||
|
// Periodically wake a worker thread from a non-tokio OS thread to
|
||||||
|
// prevent tokio I/O driver starvation (all workers parked on
|
||||||
|
// condvar with no driver). See tokio-rs/tokio#4730.
|
||||||
|
let rt_handle = tokio::runtime::Handle::current();
|
||||||
|
std::thread::spawn(move || loop {
|
||||||
|
std::thread::sleep(Duration::from_secs(30));
|
||||||
|
rt_handle.spawn(async {});
|
||||||
|
});
|
||||||
|
|
||||||
let mut server = WebServer::new(Acceptor::new(WildcardListener::new(80)?), refresher());
|
let mut server = WebServer::new(Acceptor::new(WildcardListener::new(80)?), refresher());
|
||||||
match inner_main(&mut server, &config).await {
|
match inner_main(&mut server, &config).await {
|
||||||
Ok(a) => {
|
Ok(a) => {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ impl DiagnosticContext {
|
|||||||
shutdown,
|
shutdown,
|
||||||
disk_guid,
|
disk_guid,
|
||||||
error: Arc::new(error.into()),
|
error: Arc::new(error.into()),
|
||||||
rpc_continuations: RpcContinuations::new(None),
|
rpc_continuations: RpcContinuations::new(),
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ impl InitContext {
|
|||||||
error: watch::channel(None).0,
|
error: watch::channel(None).0,
|
||||||
progress,
|
progress,
|
||||||
shutdown,
|
shutdown,
|
||||||
rpc_continuations: RpcContinuations::new(None),
|
rpc_continuations: RpcContinuations::new(),
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ impl RpcContext {
|
|||||||
services,
|
services,
|
||||||
cancellable_installs: SyncMutex::new(BTreeMap::new()),
|
cancellable_installs: SyncMutex::new(BTreeMap::new()),
|
||||||
metrics_cache,
|
metrics_cache,
|
||||||
rpc_continuations: RpcContinuations::new(Some(shutdown.clone())),
|
rpc_continuations: RpcContinuations::new(),
|
||||||
shutdown,
|
shutdown,
|
||||||
lxc_manager: Arc::new(LxcManager::new()),
|
lxc_manager: Arc::new(LxcManager::new()),
|
||||||
open_authed_continuations: OpenAuthedContinuations::new(),
|
open_authed_continuations: OpenAuthedContinuations::new(),
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ impl SetupContext {
|
|||||||
result: OnceCell::new(),
|
result: OnceCell::new(),
|
||||||
disk_guid: OnceCell::new(),
|
disk_guid: OnceCell::new(),
|
||||||
shutdown,
|
shutdown,
|
||||||
rpc_continuations: RpcContinuations::new(None),
|
rpc_continuations: RpcContinuations::new(),
|
||||||
install_rootfs: SyncMutex::new(None),
|
install_rootfs: SyncMutex::new(None),
|
||||||
language: SyncMutex::new(None),
|
language: SyncMutex::new(None),
|
||||||
keyboard: SyncMutex::new(None),
|
keyboard: SyncMutex::new(None),
|
||||||
|
|||||||
@@ -55,8 +55,6 @@ impl Public {
|
|||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
server_info: ServerInfo {
|
server_info: ServerInfo {
|
||||||
arch: get_arch(),
|
|
||||||
platform: get_platform(),
|
|
||||||
id: account.server_id.clone(),
|
id: account.server_id.clone(),
|
||||||
version: Current::default().semver(),
|
version: Current::default().semver(),
|
||||||
name: account.hostname.name.clone(),
|
name: account.hostname.name.clone(),
|
||||||
@@ -125,10 +123,10 @@ impl Public {
|
|||||||
},
|
},
|
||||||
status_info: ServerStatus {
|
status_info: ServerStatus {
|
||||||
backup_progress: None,
|
backup_progress: None,
|
||||||
updated: false,
|
|
||||||
update_progress: None,
|
update_progress: None,
|
||||||
shutting_down: false,
|
shutting_down: false,
|
||||||
restarting: false,
|
restarting: false,
|
||||||
|
restart: None,
|
||||||
},
|
},
|
||||||
unread_notification_count: 0,
|
unread_notification_count: 0,
|
||||||
password_hash: account.password.clone(),
|
password_hash: account.password.clone(),
|
||||||
@@ -160,14 +158,6 @@ impl Public {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_arch() -> InternedString {
|
|
||||||
(*ARCH).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_platform() -> InternedString {
|
|
||||||
(&*PLATFORM).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_echoip_urls() -> Vec<Url> {
|
pub fn default_echoip_urls() -> Vec<Url> {
|
||||||
vec![
|
vec![
|
||||||
"https://ipconfig.io".parse().unwrap(),
|
"https://ipconfig.io".parse().unwrap(),
|
||||||
@@ -180,10 +170,6 @@ pub fn default_echoip_urls() -> Vec<Url> {
|
|||||||
#[model = "Model<Self>"]
|
#[model = "Model<Self>"]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct ServerInfo {
|
pub struct ServerInfo {
|
||||||
#[serde(default = "get_arch")]
|
|
||||||
pub arch: InternedString,
|
|
||||||
#[serde(default = "get_platform")]
|
|
||||||
pub platform: InternedString,
|
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: InternedString,
|
pub name: InternedString,
|
||||||
pub hostname: InternedString,
|
pub hostname: InternedString,
|
||||||
@@ -220,6 +206,16 @@ pub struct ServerInfo {
|
|||||||
pub keyboard: Option<KeyboardOptions>,
|
pub keyboard: Option<KeyboardOptions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, TS)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub enum RestartReason {
|
||||||
|
Mdns,
|
||||||
|
Language,
|
||||||
|
Kiosk,
|
||||||
|
Update,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[model = "Model<Self>"]
|
#[model = "Model<Self>"]
|
||||||
@@ -364,12 +360,13 @@ pub struct BackupProgress {
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct ServerStatus {
|
pub struct ServerStatus {
|
||||||
pub backup_progress: Option<BTreeMap<PackageId, BackupProgress>>,
|
pub backup_progress: Option<BTreeMap<PackageId, BackupProgress>>,
|
||||||
pub updated: bool,
|
|
||||||
pub update_progress: Option<FullProgress>,
|
pub update_progress: Option<FullProgress>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub shutting_down: bool,
|
pub shutting_down: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub restarting: bool,
|
pub restarting: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub restart: Option<RestartReason>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||||
|
|||||||
@@ -2,24 +2,11 @@ use std::io::Cursor;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tracing::instrument;
|
|
||||||
|
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::disk::fsck::RequiresReboot;
|
use crate::disk::fsck::RequiresReboot;
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
|
||||||
pub async fn btrfs_check_readonly(logicalname: impl AsRef<Path>) -> Result<RequiresReboot, Error> {
|
|
||||||
Command::new("btrfs")
|
|
||||||
.arg("check")
|
|
||||||
.arg("--readonly")
|
|
||||||
.arg(logicalname.as_ref())
|
|
||||||
.invoke(crate::ErrorKind::DiskManagement)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(RequiresReboot(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn btrfs_check_repair(logicalname: impl AsRef<Path>) -> Result<RequiresReboot, Error> {
|
pub async fn btrfs_check_repair(logicalname: impl AsRef<Path>) -> Result<RequiresReboot, Error> {
|
||||||
Command::new("btrfs")
|
Command::new("btrfs")
|
||||||
.arg("check")
|
.arg("check")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use color_eyre::eyre::eyre;
|
|||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::disk::fsck::btrfs::{btrfs_check_readonly, btrfs_check_repair};
|
use crate::disk::fsck::btrfs::btrfs_check_repair;
|
||||||
use crate::disk::fsck::ext4::{e2fsck_aggressive, e2fsck_preen};
|
use crate::disk::fsck::ext4::{e2fsck_aggressive, e2fsck_preen};
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ impl RepairStrategy {
|
|||||||
logicalname: impl AsRef<Path> + std::fmt::Debug,
|
logicalname: impl AsRef<Path> + std::fmt::Debug,
|
||||||
) -> Result<RequiresReboot, Error> {
|
) -> Result<RequiresReboot, Error> {
|
||||||
match self {
|
match self {
|
||||||
RepairStrategy::Preen => btrfs_check_readonly(logicalname).await,
|
RepairStrategy::Preen => Ok(RequiresReboot(false)),
|
||||||
RepairStrategy::Aggressive => btrfs_check_repair(logicalname).await,
|
RepairStrategy::Aggressive => btrfs_check_repair(logicalname).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,12 +344,17 @@ pub async fn mount_fs<P: AsRef<Path>>(
|
|||||||
.arg(&blockdev_path)
|
.arg(&blockdev_path)
|
||||||
.invoke(ErrorKind::DiskManagement)
|
.invoke(ErrorKind::DiskManagement)
|
||||||
.await?;
|
.await?;
|
||||||
// Defragment after conversion for optimal performance
|
// Delete ext2_saved subvolume and defragment after conversion
|
||||||
let tmp_mount = datadir.as_ref().join(format!("{name}.convert-tmp"));
|
let tmp_mount = datadir.as_ref().join(format!("{name}.convert-tmp"));
|
||||||
tokio::fs::create_dir_all(&tmp_mount).await?;
|
tokio::fs::create_dir_all(&tmp_mount).await?;
|
||||||
BlockDev::new(&blockdev_path)
|
BlockDev::new(&blockdev_path)
|
||||||
.mount(&tmp_mount, ReadWrite)
|
.mount(&tmp_mount, ReadWrite)
|
||||||
.await?;
|
.await?;
|
||||||
|
Command::new("btrfs")
|
||||||
|
.args(["subvolume", "delete"])
|
||||||
|
.arg(tmp_mount.join("ext2_saved"))
|
||||||
|
.invoke(ErrorKind::DiskManagement)
|
||||||
|
.await?;
|
||||||
Command::new("btrfs")
|
Command::new("btrfs")
|
||||||
.args(["filesystem", "defragment", "-r"])
|
.args(["filesystem", "defragment", "-r"])
|
||||||
.arg(&tmp_mount)
|
.arg(&tmp_mount)
|
||||||
@@ -409,6 +414,18 @@ pub async fn mount_all_fs<P: AsRef<Path>>(
|
|||||||
/// filesystem type. Returns `None` if probing fails (e.g. LV doesn't exist).
|
/// filesystem type. Returns `None` if probing fails (e.g. LV doesn't exist).
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn probe_package_data_fs(guid: &str) -> Result<Option<String>, Error> {
|
pub async fn probe_package_data_fs(guid: &str) -> Result<Option<String>, Error> {
|
||||||
|
// If the target block device is already accessible (e.g. this is the
|
||||||
|
// currently active system VG), probe it directly without any
|
||||||
|
// import/activate/open/cleanup steps.
|
||||||
|
let blockdev_path = if !guid.ends_with("_UNENC") {
|
||||||
|
PathBuf::from(format!("/dev/mapper/{guid}_package-data"))
|
||||||
|
} else {
|
||||||
|
Path::new("/dev").join(guid).join("package-data")
|
||||||
|
};
|
||||||
|
if tokio::fs::metadata(&blockdev_path).await.is_ok() {
|
||||||
|
return detect_filesystem(&blockdev_path).await.map(Some);
|
||||||
|
}
|
||||||
|
|
||||||
// Import and activate the VG
|
// Import and activate the VG
|
||||||
match Command::new("vgimport")
|
match Command::new("vgimport")
|
||||||
.arg(guid)
|
.arg(guid)
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ impl OsPartitionInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const BIOS_BOOT_TYPE_GUID: &str = "21686148-6449-6e6f-744e-656564726548";
|
const BIOS_BOOT_TYPE_GUID: &str = "21686148-6449-6E6F-744E-656564454649";
|
||||||
|
|
||||||
/// Find the BIOS boot partition on the same disk as `known_part`.
|
/// Find the BIOS boot partition on the same disk as `known_part`.
|
||||||
async fn find_bios_boot_partition(known_part: &Path) -> Result<Option<PathBuf>, Error> {
|
async fn find_bios_boot_partition(known_part: &Path) -> Result<Option<PathBuf>, Error> {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use tracing::instrument;
|
|||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::context::RpcContext;
|
use crate::context::RpcContext;
|
||||||
use crate::db::model::public::ServerInfo;
|
use crate::db::model::public::{RestartReason, ServerInfo};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
|
|
||||||
@@ -272,6 +272,7 @@ pub async fn set_hostname_rpc(
|
|||||||
}
|
}
|
||||||
if let Some(hostname) = &hostname {
|
if let Some(hostname) = &hostname {
|
||||||
hostname.save(server_info)?;
|
hostname.save(server_info)?;
|
||||||
|
server_info.as_status_info_mut().as_restart_mut().ser(&Some(RestartReason::Mdns))?;
|
||||||
}
|
}
|
||||||
ServerHostnameInfo::load(server_info)
|
ServerHostnameInfo::load(server_info)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ pub async fn init(
|
|||||||
.arg("journalctl")
|
.arg("journalctl")
|
||||||
.invoke(crate::ErrorKind::Journald)
|
.invoke(crate::ErrorKind::Journald)
|
||||||
.await
|
.await
|
||||||
.log_err();
|
.ok();
|
||||||
mount_logs.complete();
|
mount_logs.complete();
|
||||||
tokio::io::copy(
|
tokio::io::copy(
|
||||||
&mut open_file("/run/startos/init.log").await?,
|
&mut open_file("/run/startos/init.log").await?,
|
||||||
@@ -371,11 +371,11 @@ pub async fn init(
|
|||||||
let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024;
|
let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024;
|
||||||
let devices = lshw().await?;
|
let devices = lshw().await?;
|
||||||
let status_info = ServerStatus {
|
let status_info = ServerStatus {
|
||||||
updated: false,
|
|
||||||
update_progress: None,
|
update_progress: None,
|
||||||
backup_progress: None,
|
backup_progress: None,
|
||||||
shutting_down: false,
|
shutting_down: false,
|
||||||
restarting: false,
|
restarting: false,
|
||||||
|
restart: None,
|
||||||
};
|
};
|
||||||
db.mutate(|v| {
|
db.mutate(|v| {
|
||||||
let server_info = v.as_public_mut().as_server_info_mut();
|
let server_info = v.as_public_mut().as_server_info_mut();
|
||||||
|
|||||||
@@ -572,7 +572,9 @@ pub fn package<C: Context>() -> ParentHandler<C> {
|
|||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"attach",
|
"attach",
|
||||||
from_fn_async_local(service::cli_attach).no_display(),
|
from_fn_async_local(service::cli_attach)
|
||||||
|
.no_display()
|
||||||
|
.with_about("about.execute-commands-container"),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"host",
|
"host",
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ pub fn forward_api<C: Context>() -> ParentHandler<C> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
.with_about("about.dump-port-forward-table")
|
||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,11 +241,19 @@ pub async fn check_port(
|
|||||||
.await
|
.await
|
||||||
.map_or(false, |r| r.is_ok());
|
.map_or(false, |r| r.is_ok());
|
||||||
|
|
||||||
|
let local_ipv4 = ip_info
|
||||||
|
.subnets
|
||||||
|
.iter()
|
||||||
|
.find_map(|s| match s.addr() {
|
||||||
|
IpAddr::V4(v4) => Some(v4),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or(Ipv4Addr::UNSPECIFIED);
|
||||||
let client = reqwest::Client::builder();
|
let client = reqwest::Client::builder();
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let client = client
|
let client = client
|
||||||
.interface(gateway.as_str())
|
.interface(gateway.as_str())
|
||||||
.local_address(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
|
.local_address(IpAddr::V4(local_ipv4));
|
||||||
let client = client.build()?;
|
let client = client.build()?;
|
||||||
|
|
||||||
let mut res = None;
|
let mut res = None;
|
||||||
@@ -282,12 +290,7 @@ pub async fn check_port(
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
let hairpinning = tokio::time::timeout(
|
let hairpinning = check_hairpin(gateway, local_ipv4, ip, port).await;
|
||||||
Duration::from_secs(5),
|
|
||||||
tokio::net::TcpStream::connect(SocketAddr::new(ip.into(), port)),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_or(false, |r| r.is_ok());
|
|
||||||
|
|
||||||
Ok(CheckPortRes {
|
Ok(CheckPortRes {
|
||||||
ip,
|
ip,
|
||||||
@@ -298,6 +301,30 @@ pub async fn check_port(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
async fn check_hairpin(gateway: GatewayId, local_ipv4: Ipv4Addr, ip: Ipv4Addr, port: u16) -> bool {
|
||||||
|
let hairpinning = tokio::time::timeout(Duration::from_secs(5), async {
|
||||||
|
let dest = SocketAddr::new(ip.into(), port);
|
||||||
|
let socket = socket2::Socket::new(socket2::Domain::IPV4, socket2::Type::STREAM, None)?;
|
||||||
|
socket.bind_device(Some(gateway.as_str().as_bytes()))?;
|
||||||
|
socket.bind(&SocketAddr::new(IpAddr::V4(local_ipv4), 0).into())?;
|
||||||
|
socket.set_nonblocking(true)?;
|
||||||
|
let socket = unsafe {
|
||||||
|
use std::os::fd::{FromRawFd, IntoRawFd};
|
||||||
|
tokio::net::TcpSocket::from_raw_fd(socket.into_raw_fd())
|
||||||
|
};
|
||||||
|
socket.connect(dest).await.map(|_| ())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_or(false, |r| r.is_ok());
|
||||||
|
hairpinning
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
async fn check_hairpin(_: GatewayId, _: Ipv4Addr, _: Ipv4Addr, _: u16) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
|
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
|
||||||
#[group(skip)]
|
#[group(skip)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -765,7 +792,10 @@ async fn watcher(
|
|||||||
}
|
}
|
||||||
changed
|
changed
|
||||||
});
|
});
|
||||||
futures::future::try_join_all(jobs).await?;
|
gc_policy_routing(&ifaces).await;
|
||||||
|
for result in futures::future::join_all(jobs).await {
|
||||||
|
result.log_err();
|
||||||
|
}
|
||||||
|
|
||||||
Ok::<_, Error>(())
|
Ok::<_, Error>(())
|
||||||
})
|
})
|
||||||
@@ -781,12 +811,16 @@ async fn watcher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_wan_ipv4(iface: &str, base_url: &Url) -> Result<Option<Ipv4Addr>, Error> {
|
async fn get_wan_ipv4(
|
||||||
|
iface: &str,
|
||||||
|
base_url: &Url,
|
||||||
|
local_ipv4: Ipv4Addr,
|
||||||
|
) -> Result<Option<Ipv4Addr>, Error> {
|
||||||
let client = reqwest::Client::builder();
|
let client = reqwest::Client::builder();
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let client = client
|
let client = client
|
||||||
.interface(iface)
|
.interface(iface)
|
||||||
.local_address(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
|
.local_address(IpAddr::V4(local_ipv4));
|
||||||
let url = base_url.join("/ip").with_kind(ErrorKind::ParseUrl)?;
|
let url = base_url.join("/ip").with_kind(ErrorKind::ParseUrl)?;
|
||||||
let text = client
|
let text = client
|
||||||
.build()?
|
.build()?
|
||||||
@@ -804,15 +838,43 @@ async fn get_wan_ipv4(iface: &str, base_url: &Url) -> Result<Option<Ipv4Addr>, E
|
|||||||
Ok(Some(trimmed.parse()?))
|
Ok(Some(trimmed.parse()?))
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PolicyRoutingCleanup {
|
struct PolicyRoutingGuard {
|
||||||
table_id: u32,
|
table_id: u32,
|
||||||
iface: String,
|
|
||||||
}
|
}
|
||||||
impl Drop for PolicyRoutingCleanup {
|
|
||||||
fn drop(&mut self) {
|
/// Remove stale per-interface policy-routing state (fwmark rules, routing
|
||||||
let table_str = self.table_id.to_string();
|
/// tables, iptables CONNMARK rules) for interfaces that no longer exist.
|
||||||
let iface = std::mem::take(&mut self.iface);
|
async fn gc_policy_routing(active_ifaces: &BTreeSet<GatewayId>) {
|
||||||
tokio::spawn(async move {
|
let active_tables: BTreeSet<u32> = active_ifaces
|
||||||
|
.iter()
|
||||||
|
.filter_map(|iface| if_nametoindex(iface.as_str()).ok().map(|idx| 1000 + idx))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// GC fwmark ip rules at priority 50 and their routing tables.
|
||||||
|
if let Ok(rules) = Command::new("ip")
|
||||||
|
.arg("rule")
|
||||||
|
.arg("show")
|
||||||
|
.invoke(ErrorKind::Network)
|
||||||
|
.await
|
||||||
|
.and_then(|b| String::from_utf8(b).with_kind(ErrorKind::Utf8))
|
||||||
|
{
|
||||||
|
for line in rules.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if !line.starts_with("50:") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(pos) = line.find("lookup ") else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let token = line[pos + 7..].split_whitespace().next().unwrap_or("");
|
||||||
|
let Ok(table_id) = token.parse::<u32>() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if table_id < 1000 || active_tables.contains(&table_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let table_str = table_id.to_string();
|
||||||
|
tracing::debug!("gc_policy_routing: removing stale table {table_id}");
|
||||||
Command::new("ip")
|
Command::new("ip")
|
||||||
.arg("rule")
|
.arg("rule")
|
||||||
.arg("del")
|
.arg("del")
|
||||||
@@ -824,7 +886,7 @@ impl Drop for PolicyRoutingCleanup {
|
|||||||
.arg("50")
|
.arg("50")
|
||||||
.invoke(ErrorKind::Network)
|
.invoke(ErrorKind::Network)
|
||||||
.await
|
.await
|
||||||
.log_err();
|
.ok();
|
||||||
Command::new("ip")
|
Command::new("ip")
|
||||||
.arg("route")
|
.arg("route")
|
||||||
.arg("flush")
|
.arg("flush")
|
||||||
@@ -832,26 +894,47 @@ impl Drop for PolicyRoutingCleanup {
|
|||||||
.arg(&table_str)
|
.arg(&table_str)
|
||||||
.invoke(ErrorKind::Network)
|
.invoke(ErrorKind::Network)
|
||||||
.await
|
.await
|
||||||
.log_err();
|
.ok();
|
||||||
Command::new("iptables")
|
}
|
||||||
.arg("-t")
|
}
|
||||||
.arg("mangle")
|
|
||||||
.arg("-D")
|
// GC iptables CONNMARK set-mark rules for defunct interfaces.
|
||||||
.arg("PREROUTING")
|
if let Ok(rules) = Command::new("iptables")
|
||||||
.arg("-i")
|
.arg("-t")
|
||||||
.arg(&iface)
|
.arg("mangle")
|
||||||
.arg("-m")
|
.arg("-S")
|
||||||
.arg("conntrack")
|
.arg("PREROUTING")
|
||||||
.arg("--ctstate")
|
.invoke(ErrorKind::Network)
|
||||||
.arg("NEW")
|
.await
|
||||||
.arg("-j")
|
.and_then(|b| String::from_utf8(b).with_kind(ErrorKind::Utf8))
|
||||||
.arg("CONNMARK")
|
{
|
||||||
.arg("--set-mark")
|
// Rules look like:
|
||||||
.arg(&table_str)
|
// -A PREROUTING -i wg0 -m conntrack --ctstate NEW -j CONNMARK --set-mark 1005
|
||||||
.invoke(ErrorKind::Network)
|
for line in rules.lines() {
|
||||||
.await
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
.log_err();
|
if parts.first() != Some(&"-A") {
|
||||||
});
|
continue;
|
||||||
|
}
|
||||||
|
if !parts.contains(&"--set-mark") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(iface_idx) = parts.iter().position(|&p| p == "-i") else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(&iface) = parts.get(iface_idx + 1) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if active_ifaces.contains(&GatewayId::from(InternedString::intern(iface))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tracing::debug!("gc_policy_routing: removing stale iptables rule for {iface}");
|
||||||
|
let mut cmd = Command::new("iptables");
|
||||||
|
cmd.arg("-t").arg("mangle").arg("-D");
|
||||||
|
for &arg in &parts[1..] {
|
||||||
|
cmd.arg(arg);
|
||||||
|
}
|
||||||
|
cmd.invoke(ErrorKind::Network).await.ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -983,11 +1066,8 @@ async fn watch_ip(
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let policy_guard: Option<PolicyRoutingCleanup> =
|
let policy_guard: Option<PolicyRoutingGuard> =
|
||||||
policy_table_id.map(|t| PolicyRoutingCleanup {
|
policy_table_id.map(|t| PolicyRoutingGuard { table_id: t });
|
||||||
table_id: t,
|
|
||||||
iface: iface.as_str().to_owned(),
|
|
||||||
});
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
until
|
until
|
||||||
@@ -1014,7 +1094,7 @@ async fn watch_ip(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn apply_policy_routing(
|
async fn apply_policy_routing(
|
||||||
guard: &PolicyRoutingCleanup,
|
guard: &PolicyRoutingGuard,
|
||||||
iface: &GatewayId,
|
iface: &GatewayId,
|
||||||
lan_ip: &OrdSet<IpAddr>,
|
lan_ip: &OrdSet<IpAddr>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
@@ -1067,7 +1147,17 @@ async fn apply_policy_routing(
|
|||||||
cmd.arg(part);
|
cmd.arg(part);
|
||||||
}
|
}
|
||||||
cmd.arg("table").arg(&table_str);
|
cmd.arg("table").arg(&table_str);
|
||||||
cmd.invoke(ErrorKind::Network).await.log_err();
|
if let Err(e) = cmd.invoke(ErrorKind::Network).await {
|
||||||
|
// Transient interfaces (podman, wg-quick, etc.) may
|
||||||
|
// vanish between reading the main table and replaying
|
||||||
|
// the route — demote to debug to avoid log noise.
|
||||||
|
if e.source.to_string().contains("No such file or directory") {
|
||||||
|
tracing::trace!("ip route replace (transient device): {e}");
|
||||||
|
} else {
|
||||||
|
tracing::error!("{e}");
|
||||||
|
tracing::debug!("{e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1238,7 +1328,7 @@ async fn poll_ip_info(
|
|||||||
ip4_proxy: &Ip4ConfigProxy<'_>,
|
ip4_proxy: &Ip4ConfigProxy<'_>,
|
||||||
ip6_proxy: &Ip6ConfigProxy<'_>,
|
ip6_proxy: &Ip6ConfigProxy<'_>,
|
||||||
dhcp4_proxy: &Option<Dhcp4ConfigProxy<'_>>,
|
dhcp4_proxy: &Option<Dhcp4ConfigProxy<'_>>,
|
||||||
policy_guard: &Option<PolicyRoutingCleanup>,
|
policy_guard: &Option<PolicyRoutingGuard>,
|
||||||
iface: &GatewayId,
|
iface: &GatewayId,
|
||||||
echoip_ratelimit_state: &mut BTreeMap<Url, Instant>,
|
echoip_ratelimit_state: &mut BTreeMap<Url, Instant>,
|
||||||
db: Option<&TypedPatchDb<Database>>,
|
db: Option<&TypedPatchDb<Database>>,
|
||||||
@@ -1287,57 +1377,18 @@ async fn poll_ip_info(
|
|||||||
apply_policy_routing(guard, iface, &lan_ip).await?;
|
apply_policy_routing(guard, iface, &lan_ip).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let echoip_urls = if let Some(db) = db {
|
// Write IP info to the watch immediately so the gateway appears in the
|
||||||
db.peek()
|
// DB without waiting for the (slow) WAN IP fetch. The echoip HTTP
|
||||||
.await
|
// request has a 5-second timeout per URL and is easily cancelled by
|
||||||
.as_public()
|
// D-Bus signals via the Until mechanism, which would prevent the
|
||||||
.as_server_info()
|
// gateway from ever appearing if we waited.
|
||||||
.as_echoip_urls()
|
|
||||||
.de()
|
|
||||||
.unwrap_or_else(|_| crate::db::model::public::default_echoip_urls())
|
|
||||||
} else {
|
|
||||||
crate::db::model::public::default_echoip_urls()
|
|
||||||
};
|
|
||||||
let mut wan_ip = None;
|
|
||||||
for echoip_url in echoip_urls {
|
|
||||||
if echoip_ratelimit_state
|
|
||||||
.get(&echoip_url)
|
|
||||||
.map_or(true, |i| i.elapsed() > Duration::from_secs(300))
|
|
||||||
&& !subnets.is_empty()
|
|
||||||
&& !matches!(
|
|
||||||
device_type,
|
|
||||||
Some(NetworkInterfaceType::Bridge | NetworkInterfaceType::Loopback)
|
|
||||||
)
|
|
||||||
{
|
|
||||||
match get_wan_ipv4(iface.as_str(), &echoip_url).await {
|
|
||||||
Ok(a) => {
|
|
||||||
wan_ip = a;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!(
|
|
||||||
"{}",
|
|
||||||
t!(
|
|
||||||
"net.gateway.failed-to-determine-wan-ip",
|
|
||||||
iface = iface.to_string(),
|
|
||||||
error = e.to_string()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
tracing::debug!("{e:?}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
echoip_ratelimit_state.insert(echoip_url, Instant::now());
|
|
||||||
if wan_ip.is_some() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let mut ip_info = IpInfo {
|
let mut ip_info = IpInfo {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
scope_id,
|
scope_id,
|
||||||
device_type,
|
device_type,
|
||||||
subnets,
|
subnets: subnets.clone(),
|
||||||
lan_ip,
|
lan_ip,
|
||||||
wan_ip,
|
wan_ip: None,
|
||||||
ntp_servers,
|
ntp_servers,
|
||||||
dns_servers,
|
dns_servers,
|
||||||
};
|
};
|
||||||
@@ -1352,7 +1403,7 @@ async fn poll_ip_info(
|
|||||||
i.ip_info.as_ref().and_then(|i| i.wan_ip),
|
i.ip_info.as_ref().and_then(|i| i.wan_ip),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
ip_info.wan_ip = ip_info.wan_ip.or(prev_wan_ip);
|
ip_info.wan_ip = prev_wan_ip;
|
||||||
let ip_info = Arc::new(ip_info);
|
let ip_info = Arc::new(ip_info);
|
||||||
m.insert(
|
m.insert(
|
||||||
iface.clone(),
|
iface.clone(),
|
||||||
@@ -1367,6 +1418,85 @@ async fn poll_ip_info(
|
|||||||
.is_none()
|
.is_none()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Now fetch the WAN IP in a second pass. Even if this is slow or
|
||||||
|
// gets cancelled, the gateway already has valid ip_info above.
|
||||||
|
let echoip_urls = if let Some(db) = db {
|
||||||
|
db.peek()
|
||||||
|
.await
|
||||||
|
.as_public()
|
||||||
|
.as_server_info()
|
||||||
|
.as_echoip_urls()
|
||||||
|
.de()
|
||||||
|
.unwrap_or_else(|_| crate::db::model::public::default_echoip_urls())
|
||||||
|
} else {
|
||||||
|
crate::db::model::public::default_echoip_urls()
|
||||||
|
};
|
||||||
|
let mut wan_ip = None;
|
||||||
|
let mut err = None;
|
||||||
|
for echoip_url in echoip_urls {
|
||||||
|
if echoip_ratelimit_state
|
||||||
|
.get(&echoip_url)
|
||||||
|
.map_or(true, |i| i.elapsed() > Duration::from_secs(300))
|
||||||
|
&& !subnets.is_empty()
|
||||||
|
&& !matches!(
|
||||||
|
device_type,
|
||||||
|
Some(NetworkInterfaceType::Bridge | NetworkInterfaceType::Loopback)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
let local_ipv4 = subnets
|
||||||
|
.iter()
|
||||||
|
.find_map(|s| match s.addr() {
|
||||||
|
IpAddr::V4(v4) => Some(v4),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or(Ipv4Addr::UNSPECIFIED);
|
||||||
|
match get_wan_ipv4(iface.as_str(), &echoip_url, local_ipv4).await {
|
||||||
|
Ok(a) => {
|
||||||
|
wan_ip = a;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
err = Some(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
echoip_ratelimit_state.insert(echoip_url, Instant::now());
|
||||||
|
if wan_ip.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if wan_ip.is_none()
|
||||||
|
&& let Some(e) = err
|
||||||
|
{
|
||||||
|
tracing::error!(
|
||||||
|
"{}",
|
||||||
|
t!(
|
||||||
|
"net.gateway.failed-to-determine-wan-ip",
|
||||||
|
iface = iface.to_string(),
|
||||||
|
error = e.to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
tracing::debug!("{e:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update with WAN IP if we obtained one
|
||||||
|
if wan_ip.is_some() {
|
||||||
|
write_to.send_if_modified(|m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| {
|
||||||
|
let Some(entry) = m.get_mut(iface) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(ref existing_ip) = entry.ip_info else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if existing_ip.wan_ip == wan_ip {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut updated = (**existing_ip).clone();
|
||||||
|
updated.wan_ip = wan_ip;
|
||||||
|
entry.ip_info = Some(Arc::new(updated));
|
||||||
|
true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1470,22 +1600,12 @@ impl NetworkInterfaceController {
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
tracing::debug!("syncronizing {info:?} to db");
|
tracing::debug!("syncronizing {info:?} to db");
|
||||||
|
|
||||||
let mut wifi_iface = info
|
let wifi_iface = find_wifi_iface()
|
||||||
.iter()
|
.await
|
||||||
.find(|(_, info)| {
|
.ok()
|
||||||
info.ip_info.as_ref().map_or(false, |i| {
|
.and_then(|a| a)
|
||||||
i.device_type == Some(NetworkInterfaceType::Wireless)
|
.map(InternedString::from)
|
||||||
})
|
.map(GatewayId::from);
|
||||||
})
|
|
||||||
.map(|(id, _)| id.clone());
|
|
||||||
if wifi_iface.is_none() {
|
|
||||||
wifi_iface = find_wifi_iface()
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.and_then(|a| a)
|
|
||||||
.map(InternedString::from)
|
|
||||||
.map(GatewayId::from);
|
|
||||||
}
|
|
||||||
|
|
||||||
db.mutate(|db| {
|
db.mutate(|db| {
|
||||||
let network = db.as_public_mut().as_server_info_mut().as_network_mut();
|
let network = db.as_public_mut().as_server_info_mut().as_network_mut();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use crate::hostname::ServerHostname;
|
|||||||
use crate::net::acme::AcmeProvider;
|
use crate::net::acme::AcmeProvider;
|
||||||
use crate::net::gateway::{CheckDnsParams, CheckPortParams, CheckPortRes, check_dns, check_port};
|
use crate::net::gateway::{CheckDnsParams, CheckPortParams, CheckPortRes, check_dns, check_port};
|
||||||
use crate::net::host::{HostApiKind, all_hosts};
|
use crate::net::host::{HostApiKind, all_hosts};
|
||||||
|
use crate::net::service_interface::HostnameMetadata;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||||
|
|
||||||
@@ -108,6 +109,7 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
|
|||||||
.with_about("about.remove-public-domain-from-host")
|
.with_about("about.remove-public-domain-from-host")
|
||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
)
|
)
|
||||||
|
.with_about("about.commands-host-public-domain")
|
||||||
.with_inherited(|_, a| a),
|
.with_inherited(|_, a| a),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
@@ -131,8 +133,10 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
|
|||||||
.with_about("about.remove-private-domain-from-host")
|
.with_about("about.remove-private-domain-from-host")
|
||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
)
|
)
|
||||||
|
.with_about("about.commands-host-private-domain")
|
||||||
.with_inherited(|_, a| a),
|
.with_inherited(|_, a| a),
|
||||||
)
|
)
|
||||||
|
.with_about("about.commands-host-address-domain")
|
||||||
.with_inherited(Kind::inheritance),
|
.with_inherited(Kind::inheritance),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
@@ -243,8 +247,50 @@ pub async fn add_public_domain<Kind: HostApiKind>(
|
|||||||
.and_then(|a| a.port)
|
.and_then(|a| a.port)
|
||||||
.ok_or_else(|| Error::new(eyre!("no public address found for {fqdn} on port {internal_port}"), ErrorKind::NotFound))?;
|
.ok_or_else(|| Error::new(eyre!("no public address found for {fqdn} on port {internal_port}"), ErrorKind::NotFound))?;
|
||||||
|
|
||||||
// Disable the domain on all other bindings
|
// On the target binding, enable the WAN IPv4 and all
|
||||||
|
// public domains on the same gateway+port (no SNI without SSL).
|
||||||
host.as_bindings_mut().mutate(|b| {
|
host.as_bindings_mut().mutate(|b| {
|
||||||
|
if let Some(bind) = b.get_mut(&internal_port) {
|
||||||
|
let non_ssl_port = bind.addresses.available.iter().find_map(|a| {
|
||||||
|
if a.ssl || !a.public || a.hostname != fqdn {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if let HostnameMetadata::PublicDomain { gateway: gw } = &a.metadata {
|
||||||
|
if *gw == gateway {
|
||||||
|
return a.port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
});
|
||||||
|
if let Some(dp) = non_ssl_port {
|
||||||
|
for a in &bind.addresses.available {
|
||||||
|
if a.ssl || !a.public {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let HostnameMetadata::Ipv4 { gateway: gw } = &a.metadata {
|
||||||
|
if *gw == gateway {
|
||||||
|
if let Some(sa) = a.to_socket_addr() {
|
||||||
|
if sa.port() == dp {
|
||||||
|
bind.addresses.enabled.insert(sa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for a in &bind.addresses.available {
|
||||||
|
if a.ssl {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let HostnameMetadata::PublicDomain { gateway: gw } = &a.metadata {
|
||||||
|
if *gw == gateway && a.port == Some(dp) {
|
||||||
|
bind.addresses.disabled.remove(&(a.hostname.clone(), dp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable the domain on all other bindings
|
||||||
for (&port, bind) in b.iter_mut() {
|
for (&port, bind) in b.iter_mut() {
|
||||||
if port == internal_port {
|
if port == internal_port {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use crate::context::{CliContext, RpcContext};
|
|||||||
use crate::db::prelude::Map;
|
use crate::db::prelude::Map;
|
||||||
use crate::net::forward::AvailablePorts;
|
use crate::net::forward::AvailablePorts;
|
||||||
use crate::net::host::HostApiKind;
|
use crate::net::host::HostApiKind;
|
||||||
use crate::net::service_interface::HostnameInfo;
|
use crate::net::service_interface::{HostnameInfo, HostnameMetadata};
|
||||||
use crate::net::vhost::AlpnInfo;
|
use crate::net::vhost::AlpnInfo;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::FromStrParser;
|
use crate::util::FromStrParser;
|
||||||
@@ -344,6 +344,41 @@ pub async fn set_address_enabled<Kind: HostApiKind>(
|
|||||||
} else {
|
} else {
|
||||||
bind.addresses.enabled.remove(&sa);
|
bind.addresses.enabled.remove(&sa);
|
||||||
}
|
}
|
||||||
|
// Non-SSL Ipv4: cascade to PublicDomains on same gateway
|
||||||
|
if !address.ssl {
|
||||||
|
if let HostnameMetadata::Ipv4 { gateway } =
|
||||||
|
&address.metadata
|
||||||
|
{
|
||||||
|
let port = sa.port();
|
||||||
|
for a in &bind.addresses.available {
|
||||||
|
if a.ssl {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let HostnameMetadata::PublicDomain {
|
||||||
|
gateway: gw,
|
||||||
|
} = &a.metadata
|
||||||
|
{
|
||||||
|
if gw == gateway
|
||||||
|
&& a.port.unwrap_or(80) == port
|
||||||
|
{
|
||||||
|
let k = (
|
||||||
|
a.hostname.clone(),
|
||||||
|
a.port.unwrap_or(80),
|
||||||
|
);
|
||||||
|
if enabled {
|
||||||
|
bind.addresses
|
||||||
|
.disabled
|
||||||
|
.remove(&k);
|
||||||
|
} else {
|
||||||
|
bind.addresses
|
||||||
|
.disabled
|
||||||
|
.insert(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Domains and private IPs: toggle via (host, port) in `disabled` set
|
// Domains and private IPs: toggle via (host, port) in `disabled` set
|
||||||
let port = address.port.unwrap_or(if address.ssl { 443 } else { 80 });
|
let port = address.port.unwrap_or(if address.ssl { 443 } else { 80 });
|
||||||
@@ -353,6 +388,61 @@ pub async fn set_address_enabled<Kind: HostApiKind>(
|
|||||||
} else {
|
} else {
|
||||||
bind.addresses.disabled.insert(key);
|
bind.addresses.disabled.insert(key);
|
||||||
}
|
}
|
||||||
|
// Non-SSL PublicDomain: cascade to Ipv4 + other PublicDomains on same gateway
|
||||||
|
if !address.ssl {
|
||||||
|
if let HostnameMetadata::PublicDomain { gateway } =
|
||||||
|
&address.metadata
|
||||||
|
{
|
||||||
|
for a in &bind.addresses.available {
|
||||||
|
if a.ssl {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match &a.metadata {
|
||||||
|
HostnameMetadata::Ipv4 { gateway: gw }
|
||||||
|
if a.public
|
||||||
|
&& gw == gateway =>
|
||||||
|
{
|
||||||
|
if let Some(sa) =
|
||||||
|
a.to_socket_addr()
|
||||||
|
{
|
||||||
|
if sa.port() == port {
|
||||||
|
if enabled {
|
||||||
|
bind.addresses
|
||||||
|
.enabled
|
||||||
|
.insert(sa);
|
||||||
|
} else {
|
||||||
|
bind.addresses
|
||||||
|
.enabled
|
||||||
|
.remove(&sa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HostnameMetadata::PublicDomain {
|
||||||
|
gateway: gw,
|
||||||
|
} if gw == gateway => {
|
||||||
|
let dp = a.port.unwrap_or(80);
|
||||||
|
if dp == port {
|
||||||
|
let k = (
|
||||||
|
a.hostname.clone(),
|
||||||
|
dp,
|
||||||
|
);
|
||||||
|
if enabled {
|
||||||
|
bind.addresses
|
||||||
|
.disabled
|
||||||
|
.remove(&k);
|
||||||
|
} else {
|
||||||
|
bind.addresses
|
||||||
|
.disabled
|
||||||
|
.insert(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -521,18 +521,27 @@ pub fn host_api<C: Context>() -> ParentHandler<C, RequiresPackageId> {
|
|||||||
.subcommand(
|
.subcommand(
|
||||||
"address",
|
"address",
|
||||||
address_api::<C, ForPackage>()
|
address_api::<C, ForPackage>()
|
||||||
.with_inherited(|RequiresPackageId { package }, _| package),
|
.with_inherited(|RequiresPackageId { package }, _| package)
|
||||||
|
.with_about("about.commands-host-addresses"),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"binding",
|
"binding",
|
||||||
binding::<C, ForPackage>().with_inherited(|RequiresPackageId { package }, _| package),
|
binding::<C, ForPackage>()
|
||||||
|
.with_inherited(|RequiresPackageId { package }, _| package)
|
||||||
|
.with_about("about.commands-host-bindings"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn server_host_api<C: Context>() -> ParentHandler<C> {
|
pub fn server_host_api<C: Context>() -> ParentHandler<C> {
|
||||||
ParentHandler::<C>::new()
|
ParentHandler::<C>::new()
|
||||||
.subcommand("address", address_api::<C, ForServer>())
|
.subcommand(
|
||||||
.subcommand("binding", binding::<C, ForServer>())
|
"address",
|
||||||
|
address_api::<C, ForServer>().with_about("about.commands-host-addresses"),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
"binding",
|
||||||
|
binding::<C, ForServer>().with_about("about.commands-host-bindings"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_hosts(
|
pub async fn list_hosts(
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ pub fn net_api<C: Context>() -> ParentHandler<C> {
|
|||||||
"tunnel",
|
"tunnel",
|
||||||
tunnel::tunnel_api::<C>().with_about("about.manage-tunnels"),
|
tunnel::tunnel_api::<C>().with_about("about.manage-tunnels"),
|
||||||
)
|
)
|
||||||
|
.subcommand(
|
||||||
|
"ssl",
|
||||||
|
ssl::ssl_api::<C>().with_about("about.manage-ssl-certificates"),
|
||||||
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"vhost",
|
"vhost",
|
||||||
vhost::vhost_api::<C>().with_about("about.manage-ssl-vhost-proxy"),
|
vhost::vhost_api::<C>().with_about("about.manage-ssl-vhost-proxy"),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::path::Path;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
use libc::time_t;
|
use libc::time_t;
|
||||||
@@ -21,16 +22,19 @@ use openssl::x509::extension::{
|
|||||||
use openssl::x509::{X509, X509Builder, X509NameBuilder, X509Ref};
|
use openssl::x509::{X509, X509Builder, X509NameBuilder, X509Ref};
|
||||||
use openssl::*;
|
use openssl::*;
|
||||||
use patch_db::HasModel;
|
use patch_db::HasModel;
|
||||||
|
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio_rustls::rustls::ServerConfig;
|
use tokio_rustls::rustls::ServerConfig;
|
||||||
use tokio_rustls::rustls::crypto::CryptoProvider;
|
use tokio_rustls::rustls::crypto::CryptoProvider;
|
||||||
use tokio_rustls::rustls::pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer};
|
use tokio_rustls::rustls::pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer};
|
||||||
use tokio_rustls::rustls::server::ClientHello;
|
use tokio_rustls::rustls::server::ClientHello;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
use ts_rs::TS;
|
||||||
use visit_rs::Visit;
|
use visit_rs::Visit;
|
||||||
|
|
||||||
use crate::SOURCE_DATE;
|
use crate::SOURCE_DATE;
|
||||||
use crate::account::AccountInfo;
|
use crate::account::AccountInfo;
|
||||||
|
use crate::context::{CliContext, RpcContext};
|
||||||
use crate::db::model::Database;
|
use crate::db::model::Database;
|
||||||
use crate::db::{DbAccess, DbAccessMut};
|
use crate::db::{DbAccess, DbAccessMut};
|
||||||
use crate::hostname::ServerHostname;
|
use crate::hostname::ServerHostname;
|
||||||
@@ -39,7 +43,7 @@ use crate::net::gateway::GatewayInfo;
|
|||||||
use crate::net::tls::{TlsHandler, TlsHandlerAction};
|
use crate::net::tls::{TlsHandler, TlsHandlerAction};
|
||||||
use crate::net::web_server::{Accept, ExtractVisitor, TcpMetadata, extract};
|
use crate::net::web_server::{Accept, ExtractVisitor, TcpMetadata, extract};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::serde::Pem;
|
use crate::util::serde::{HandlerExtSerde, Pem};
|
||||||
|
|
||||||
pub fn should_use_cert(cert: &X509Ref) -> Result<bool, ErrorStack> {
|
pub fn should_use_cert(cert: &X509Ref) -> Result<bool, ErrorStack> {
|
||||||
Ok(cert
|
Ok(cert
|
||||||
@@ -592,6 +596,85 @@ pub fn make_self_signed(applicant: (&PKey<Private>, &SANInfo)) -> Result<X509, E
|
|||||||
Ok(cert)
|
Ok(cert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ssl_api<C: Context>() -> ParentHandler<C> {
|
||||||
|
ParentHandler::new().subcommand(
|
||||||
|
"generate-certificate",
|
||||||
|
from_fn_async(generate_certificate)
|
||||||
|
.with_display_serializable()
|
||||||
|
.with_custom_display_fn(|_, res: GenerateCertificateResponse| {
|
||||||
|
println!("Private Key:");
|
||||||
|
print!("{}", res.key);
|
||||||
|
println!("\nCertificate Chain:");
|
||||||
|
print!("{}", res.fullchain);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.with_about("about.ssl-generate-certificate")
|
||||||
|
.with_call_remote::<CliContext>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[command(rename_all = "kebab-case")]
|
||||||
|
#[group(skip)]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct GenerateCertificateParams {
|
||||||
|
#[arg(help = "help.arg.hostnames")]
|
||||||
|
pub hostnames: Vec<String>,
|
||||||
|
#[arg(long, help = "help.arg.ed25519")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub ed25519: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct GenerateCertificateResponse {
|
||||||
|
pub key: String,
|
||||||
|
pub fullchain: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_certificate(
|
||||||
|
ctx: RpcContext,
|
||||||
|
GenerateCertificateParams { hostnames, ed25519 }: GenerateCertificateParams,
|
||||||
|
) -> Result<GenerateCertificateResponse, Error> {
|
||||||
|
let peek = ctx.db.peek().await;
|
||||||
|
let cert_store = peek.as_private().as_key_store().as_local_certs();
|
||||||
|
let int_key = cert_store.as_int_key().de()?.0;
|
||||||
|
let int_cert = cert_store.as_int_cert().de()?.0;
|
||||||
|
let root_cert = cert_store.as_root_cert().de()?.0;
|
||||||
|
drop(peek);
|
||||||
|
|
||||||
|
let hostnames: BTreeSet<InternedString> = hostnames.into_iter().map(InternedString::from).collect();
|
||||||
|
let san_info = SANInfo::new(&hostnames);
|
||||||
|
|
||||||
|
let (key, cert) = if ed25519 {
|
||||||
|
let key = PKey::generate_ed25519()?;
|
||||||
|
let cert = make_leaf_cert((&int_key, &int_cert), (&key, &san_info))?;
|
||||||
|
(key, cert)
|
||||||
|
} else {
|
||||||
|
let key = gen_nistp256()?;
|
||||||
|
let cert = make_leaf_cert((&int_key, &int_cert), (&key, &san_info))?;
|
||||||
|
(key, cert)
|
||||||
|
};
|
||||||
|
|
||||||
|
let key_pem =
|
||||||
|
String::from_utf8(key.private_key_to_pem_pkcs8()?).with_kind(ErrorKind::Utf8)?;
|
||||||
|
let fullchain_pem = String::from_utf8(
|
||||||
|
[&cert, &int_cert, &root_cert]
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| c.to_pem())
|
||||||
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
|
.concat(),
|
||||||
|
)
|
||||||
|
.with_kind(ErrorKind::Utf8)?;
|
||||||
|
|
||||||
|
Ok(GenerateCertificateResponse {
|
||||||
|
key: key_pem,
|
||||||
|
fullchain: fullchain_pem,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub struct RootCaTlsHandler<M: HasModel> {
|
pub struct RootCaTlsHandler<M: HasModel> {
|
||||||
pub db: TypedPatchDb<M>,
|
pub db: TypedPatchDb<M>,
|
||||||
pub crypto_provider: Arc<CryptoProvider>,
|
pub crypto_provider: Arc<CryptoProvider>,
|
||||||
|
|||||||
@@ -126,24 +126,28 @@ pub fn vhost_api<C: Context>() -> ParentHandler<C> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
.with_about("about.dump-vhost-proxy-table")
|
||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"add-passthrough",
|
"add-passthrough",
|
||||||
from_fn_async(add_passthrough)
|
from_fn_async(add_passthrough)
|
||||||
.no_display()
|
.no_display()
|
||||||
|
.with_about("about.add-vhost-passthrough")
|
||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"remove-passthrough",
|
"remove-passthrough",
|
||||||
from_fn_async(remove_passthrough)
|
from_fn_async(remove_passthrough)
|
||||||
.no_display()
|
.no_display()
|
||||||
|
.with_about("about.remove-vhost-passthrough")
|
||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"list-passthrough",
|
"list-passthrough",
|
||||||
from_fn(list_passthrough)
|
from_fn(list_passthrough)
|
||||||
.with_display_serializable()
|
.with_display_serializable()
|
||||||
|
.with_about("about.list-vhost-passthrough")
|
||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,19 +180,14 @@ pub async fn set_enabled(
|
|||||||
.invoke(ErrorKind::Wifi)
|
.invoke(ErrorKind::Wifi)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
let iface = if let Some(man) = ctx.wifi_manager.read().await.as_ref().filter(|_| enabled) {
|
|
||||||
Some(man.interface.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|d| {
|
.mutate(|d| {
|
||||||
d.as_public_mut()
|
d.as_public_mut()
|
||||||
.as_server_info_mut()
|
.as_server_info_mut()
|
||||||
.as_network_mut()
|
.as_network_mut()
|
||||||
.as_wifi_mut()
|
.as_wifi_mut()
|
||||||
.as_interface_mut()
|
.as_enabled_mut()
|
||||||
.ser(&iface)
|
.ser(&enabled)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ use patch_db::json_ptr::ROOT;
|
|||||||
use reqwest::{Client, Proxy};
|
use reqwest::{Client, Proxy};
|
||||||
use rpc_toolkit::yajrc::RpcError;
|
use rpc_toolkit::yajrc::RpcError;
|
||||||
use rpc_toolkit::{CallRemote, Context, Empty, RpcRequest};
|
use rpc_toolkit::{CallRemote, Context, Empty, RpcRequest};
|
||||||
|
use rusqlite::Connection;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::PgPool;
|
|
||||||
use tokio::sync::broadcast::Sender;
|
use tokio::sync::broadcast::Sender;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
@@ -34,6 +34,7 @@ use crate::registry::signer::SignerInfo;
|
|||||||
use crate::rpc_continuations::RpcContinuations;
|
use crate::rpc_continuations::RpcContinuations;
|
||||||
use crate::sign::AnyVerifyingKey;
|
use crate::sign::AnyVerifyingKey;
|
||||||
use crate::util::io::{append_file, read_file_to_string};
|
use crate::util::io::{append_file, read_file_to_string};
|
||||||
|
use crate::util::sync::SyncMutex;
|
||||||
|
|
||||||
const DEFAULT_REGISTRY_LISTEN: SocketAddr =
|
const DEFAULT_REGISTRY_LISTEN: SocketAddr =
|
||||||
SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), 5959);
|
SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), 5959);
|
||||||
@@ -57,12 +58,6 @@ pub struct RegistryConfig {
|
|||||||
pub tor_proxy: Option<Url>,
|
pub tor_proxy: Option<Url>,
|
||||||
#[arg(short = 'd', long = "datadir", help = "help.arg.data-directory")]
|
#[arg(short = 'd', long = "datadir", help = "help.arg.data-directory")]
|
||||||
pub datadir: Option<PathBuf>,
|
pub datadir: Option<PathBuf>,
|
||||||
#[arg(
|
|
||||||
short = 'u',
|
|
||||||
long = "pg-connection-url",
|
|
||||||
help = "help.arg.postgres-connection-url"
|
|
||||||
)]
|
|
||||||
pub pg_connection_url: Option<String>,
|
|
||||||
}
|
}
|
||||||
impl ContextConfig for RegistryConfig {
|
impl ContextConfig for RegistryConfig {
|
||||||
fn next(&mut self) -> Option<PathBuf> {
|
fn next(&mut self) -> Option<PathBuf> {
|
||||||
@@ -93,7 +88,7 @@ pub struct RegistryContextSeed {
|
|||||||
pub rpc_continuations: RpcContinuations,
|
pub rpc_continuations: RpcContinuations,
|
||||||
pub client: Client,
|
pub client: Client,
|
||||||
pub shutdown: Sender<()>,
|
pub shutdown: Sender<()>,
|
||||||
pub pool: Option<PgPool>,
|
pub metrics_db: SyncMutex<Connection>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -123,14 +118,30 @@ impl RegistryContext {
|
|||||||
.tor_proxy
|
.tor_proxy
|
||||||
.clone()
|
.clone()
|
||||||
.map(Ok)
|
.map(Ok)
|
||||||
.unwrap_or_else(|| "socks5h://localhost:9050".parse())?;
|
.unwrap_or_else(|| "socks5h://tor.startos:9050".parse())?;
|
||||||
let pool: Option<PgPool> = match &config.pg_connection_url {
|
let metrics_db_path = datadir.join("metrics.db");
|
||||||
Some(url) => match PgPool::connect(url.as_str()).await {
|
let metrics_db = Connection::open(&metrics_db_path).with_kind(ErrorKind::Database)?;
|
||||||
Ok(pool) => Some(pool),
|
metrics_db
|
||||||
Err(_) => None,
|
.execute_batch(
|
||||||
},
|
"CREATE TABLE IF NOT EXISTS user_activity (
|
||||||
None => None,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
};
|
created_at TEXT NOT NULL,
|
||||||
|
server_id TEXT NOT NULL,
|
||||||
|
arch TEXT,
|
||||||
|
os_version TEXT
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS package_request (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
pkg_id TEXT NOT NULL,
|
||||||
|
version TEXT
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_activity_created_at ON user_activity(created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_package_request_created_at ON package_request(created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_package_request_pkg_id ON package_request(pkg_id);",
|
||||||
|
)
|
||||||
|
.with_kind(ErrorKind::Database)?;
|
||||||
|
let metrics_db = SyncMutex::new(metrics_db);
|
||||||
if config.registry_hostname.is_empty() {
|
if config.registry_hostname.is_empty() {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!("{}", t!("registry.context.missing-hostname")),
|
eyre!("{}", t!("registry.context.missing-hostname")),
|
||||||
@@ -142,7 +153,7 @@ impl RegistryContext {
|
|||||||
listen: config.registry_listen.unwrap_or(DEFAULT_REGISTRY_LISTEN),
|
listen: config.registry_listen.unwrap_or(DEFAULT_REGISTRY_LISTEN),
|
||||||
db,
|
db,
|
||||||
datadir,
|
datadir,
|
||||||
rpc_continuations: RpcContinuations::new(None),
|
rpc_continuations: RpcContinuations::new(),
|
||||||
client: Client::builder()
|
client: Client::builder()
|
||||||
.proxy(Proxy::custom(move |url| {
|
.proxy(Proxy::custom(move |url| {
|
||||||
if url.host_str().map_or(false, |h| h.ends_with(".onion")) {
|
if url.host_str().map_or(false, |h| h.ends_with(".onion")) {
|
||||||
@@ -154,7 +165,7 @@ impl RegistryContext {
|
|||||||
.build()
|
.build()
|
||||||
.with_kind(crate::ErrorKind::ParseUrl)?,
|
.with_kind(crate::ErrorKind::ParseUrl)?,
|
||||||
shutdown,
|
shutdown,
|
||||||
pool,
|
metrics_db,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ impl HardwareInfo {
|
|||||||
pub async fn load(ctx: &RpcContext) -> Result<Self, Error> {
|
pub async fn load(ctx: &RpcContext) -> Result<Self, Error> {
|
||||||
let s = ctx.db.peek().await.into_public().into_server_info();
|
let s = ctx.db.peek().await.into_public().into_server_info();
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
arch: s.as_arch().de()?,
|
arch: InternedString::intern(&*crate::ARCH),
|
||||||
ram: s.as_ram().de()?,
|
ram: s.as_ram().de()?,
|
||||||
devices: Some(s.as_devices().de()?),
|
devices: Some(s.as_devices().de()?),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
@@ -107,8 +106,8 @@ pub async fn set_icon(
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct CliSetIconParams {
|
pub struct CliSetIconParams {
|
||||||
#[arg(help = "help.arg.icon-path")]
|
#[arg(help = "help.arg.icon-source")]
|
||||||
pub icon: PathBuf,
|
pub icon: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cli_set_icon(
|
pub async fn cli_set_icon(
|
||||||
@@ -120,7 +119,23 @@ pub async fn cli_set_icon(
|
|||||||
..
|
..
|
||||||
}: HandlerArgs<CliContext, CliSetIconParams>,
|
}: HandlerArgs<CliContext, CliSetIconParams>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let data_url = DataUrl::from_path(icon).await?;
|
let data_url = if icon.starts_with("data:") {
|
||||||
|
icon.parse::<DataUrl<'static>>()
|
||||||
|
.with_kind(ErrorKind::ParseUrl)?
|
||||||
|
} else if icon.starts_with("https://") || icon.starts_with("http://") {
|
||||||
|
let res = ctx
|
||||||
|
.client
|
||||||
|
.get(&icon)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.with_kind(ErrorKind::Network)?;
|
||||||
|
DataUrl::from_response(res).await?
|
||||||
|
} else {
|
||||||
|
let path = icon
|
||||||
|
.strip_prefix("file://")
|
||||||
|
.unwrap_or(&icon);
|
||||||
|
DataUrl::from_path(path).await?
|
||||||
|
};
|
||||||
ctx.call_remote::<RegistryContext>(
|
ctx.call_remote::<RegistryContext>(
|
||||||
&parent_method.into_iter().chain(method).join("."),
|
&parent_method.into_iter().chain(method).join("."),
|
||||||
imbl_value::json!({
|
imbl_value::json!({
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
|
||||||
TMP_DIR=$(mktemp -d)
|
|
||||||
mkdir $TMP_DIR/pgdata
|
|
||||||
docker run -d --rm --name=tmp_postgres -e POSTGRES_PASSWORD=password -v $TMP_DIR/pgdata:/var/lib/postgresql/data postgres
|
|
||||||
|
|
||||||
(
|
|
||||||
set -e
|
|
||||||
ctr=0
|
|
||||||
until docker exec tmp_postgres psql -U postgres 2> /dev/null || [ $ctr -ge 5 ]; do
|
|
||||||
ctr=$[ctr + 1]
|
|
||||||
sleep 5;
|
|
||||||
done
|
|
||||||
|
|
||||||
PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' tmp_postgres)
|
|
||||||
|
|
||||||
cat "./registry_schema.sql" | docker exec -i tmp_postgres psql -U postgres -d postgres -f-
|
|
||||||
cd ../../..
|
|
||||||
DATABASE_URL=postgres://postgres:password@$PG_IP/postgres PLATFORM=$(uname -m) cargo sqlx prepare -- --lib --profile=test --workspace
|
|
||||||
echo "Subscript Complete"
|
|
||||||
)
|
|
||||||
|
|
||||||
docker stop tmp_postgres
|
|
||||||
sudo rm -rf $TMP_DIR
|
|
||||||
@@ -1,828 +0,0 @@
|
|||||||
--
|
|
||||||
-- PostgreSQL database dump
|
|
||||||
--
|
|
||||||
|
|
||||||
-- Dumped from database version 14.12 (Ubuntu 14.12-0ubuntu0.22.04.1)
|
|
||||||
-- Dumped by pg_dump version 14.12 (Ubuntu 14.12-0ubuntu0.22.04.1)
|
|
||||||
|
|
||||||
SET statement_timeout = 0;
|
|
||||||
SET lock_timeout = 0;
|
|
||||||
SET idle_in_transaction_session_timeout = 0;
|
|
||||||
SET client_encoding = 'UTF8';
|
|
||||||
SET standard_conforming_strings = on;
|
|
||||||
SELECT pg_catalog.set_config('search_path', '', false);
|
|
||||||
SET check_function_bodies = false;
|
|
||||||
SET xmloption = content;
|
|
||||||
SET client_min_messages = warning;
|
|
||||||
SET row_security = off;
|
|
||||||
|
|
||||||
SET default_tablespace = '';
|
|
||||||
|
|
||||||
SET default_table_access_method = heap;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: admin; Type: TABLE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE public.admin (
|
|
||||||
id character varying NOT NULL,
|
|
||||||
created_at timestamp with time zone NOT NULL,
|
|
||||||
pass_hash character varying NOT NULL,
|
|
||||||
deleted_at timestamp with time zone
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.admin OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: admin_pkgs; Type: TABLE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE public.admin_pkgs (
|
|
||||||
id bigint NOT NULL,
|
|
||||||
admin character varying NOT NULL,
|
|
||||||
pkg_id character varying NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.admin_pkgs OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: admin_pkgs_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE SEQUENCE public.admin_pkgs_id_seq
|
|
||||||
START WITH 1
|
|
||||||
INCREMENT BY 1
|
|
||||||
NO MINVALUE
|
|
||||||
NO MAXVALUE
|
|
||||||
CACHE 1;
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.admin_pkgs_id_seq OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: admin_pkgs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER SEQUENCE public.admin_pkgs_id_seq OWNED BY public.admin_pkgs.id;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: category; Type: TABLE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE public.category (
|
|
||||||
id bigint NOT NULL,
|
|
||||||
created_at timestamp with time zone NOT NULL,
|
|
||||||
name character varying NOT NULL,
|
|
||||||
description character varying NOT NULL,
|
|
||||||
priority bigint DEFAULT 0 NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.category OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: category_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE SEQUENCE public.category_id_seq
|
|
||||||
START WITH 1
|
|
||||||
INCREMENT BY 1
|
|
||||||
NO MINVALUE
|
|
||||||
NO MAXVALUE
|
|
||||||
CACHE 1;
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.category_id_seq OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: category_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER SEQUENCE public.category_id_seq OWNED BY public.category.id;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: eos_hash; Type: TABLE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE public.eos_hash (
|
|
||||||
id bigint NOT NULL,
|
|
||||||
version character varying NOT NULL,
|
|
||||||
hash character varying NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.eos_hash OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: eos_hash_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE SEQUENCE public.eos_hash_id_seq
|
|
||||||
START WITH 1
|
|
||||||
INCREMENT BY 1
|
|
||||||
NO MINVALUE
|
|
||||||
NO MAXVALUE
|
|
||||||
CACHE 1;
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.eos_hash_id_seq OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: eos_hash_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER SEQUENCE public.eos_hash_id_seq OWNED BY public.eos_hash.id;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: error_log_record; Type: TABLE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE public.error_log_record (
|
|
||||||
id bigint NOT NULL,
|
|
||||||
created_at timestamp with time zone NOT NULL,
|
|
||||||
epoch character varying NOT NULL,
|
|
||||||
commit_hash character varying NOT NULL,
|
|
||||||
source_file character varying NOT NULL,
|
|
||||||
line bigint NOT NULL,
|
|
||||||
target character varying NOT NULL,
|
|
||||||
level character varying NOT NULL,
|
|
||||||
message character varying NOT NULL,
|
|
||||||
incidents bigint NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.error_log_record OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: error_log_record_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE SEQUENCE public.error_log_record_id_seq
|
|
||||||
START WITH 1
|
|
||||||
INCREMENT BY 1
|
|
||||||
NO MINVALUE
|
|
||||||
NO MAXVALUE
|
|
||||||
CACHE 1;
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.error_log_record_id_seq OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: error_log_record_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER SEQUENCE public.error_log_record_id_seq OWNED BY public.error_log_record.id;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: metric; Type: TABLE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE public.metric (
|
|
||||||
id bigint NOT NULL,
|
|
||||||
created_at timestamp with time zone NOT NULL,
|
|
||||||
version character varying NOT NULL,
|
|
||||||
pkg_id character varying NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.metric OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: metric_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE SEQUENCE public.metric_id_seq
|
|
||||||
START WITH 1
|
|
||||||
INCREMENT BY 1
|
|
||||||
NO MINVALUE
|
|
||||||
NO MAXVALUE
|
|
||||||
CACHE 1;
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.metric_id_seq OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: metric_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER SEQUENCE public.metric_id_seq OWNED BY public.metric.id;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: os_version; Type: TABLE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE public.os_version (
|
|
||||||
id bigint NOT NULL,
|
|
||||||
created_at timestamp with time zone NOT NULL,
|
|
||||||
updated_at timestamp with time zone NOT NULL,
|
|
||||||
number character varying NOT NULL,
|
|
||||||
headline character varying NOT NULL,
|
|
||||||
release_notes character varying NOT NULL,
|
|
||||||
arch character varying
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.os_version OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: os_version_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE SEQUENCE public.os_version_id_seq
|
|
||||||
START WITH 1
|
|
||||||
INCREMENT BY 1
|
|
||||||
NO MINVALUE
|
|
||||||
NO MAXVALUE
|
|
||||||
CACHE 1;
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.os_version_id_seq OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: os_version_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER SEQUENCE public.os_version_id_seq OWNED BY public.os_version.id;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: persistent_migration; Type: TABLE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE public.persistent_migration (
|
|
||||||
id integer NOT NULL,
|
|
||||||
version integer NOT NULL,
|
|
||||||
label character varying,
|
|
||||||
"timestamp" timestamp with time zone NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.persistent_migration OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: persistent_migration_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE SEQUENCE public.persistent_migration_id_seq
|
|
||||||
AS integer
|
|
||||||
START WITH 1
|
|
||||||
INCREMENT BY 1
|
|
||||||
NO MINVALUE
|
|
||||||
NO MAXVALUE
|
|
||||||
CACHE 1;
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.persistent_migration_id_seq OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: persistent_migration_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER SEQUENCE public.persistent_migration_id_seq OWNED BY public.persistent_migration.id;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: pkg_category; Type: TABLE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE public.pkg_category (
|
|
||||||
id bigint NOT NULL,
|
|
||||||
created_at timestamp with time zone NOT NULL,
|
|
||||||
category_id bigint NOT NULL,
|
|
||||||
pkg_id character varying NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.pkg_category OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: pkg_dependency; Type: TABLE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE public.pkg_dependency (
|
|
||||||
id bigint NOT NULL,
|
|
||||||
created_at timestamp with time zone NOT NULL,
|
|
||||||
pkg_id character varying NOT NULL,
|
|
||||||
pkg_version character varying NOT NULL,
|
|
||||||
dep_id character varying NOT NULL,
|
|
||||||
dep_version_range character varying NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.pkg_dependency OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: pkg_dependency_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE SEQUENCE public.pkg_dependency_id_seq
|
|
||||||
START WITH 1
|
|
||||||
INCREMENT BY 1
|
|
||||||
NO MINVALUE
|
|
||||||
NO MAXVALUE
|
|
||||||
CACHE 1;
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.pkg_dependency_id_seq OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: pkg_dependency_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER SEQUENCE public.pkg_dependency_id_seq OWNED BY public.pkg_dependency.id;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: pkg_record; Type: TABLE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE public.pkg_record (
|
|
||||||
created_at timestamp with time zone NOT NULL,
|
|
||||||
updated_at timestamp with time zone,
|
|
||||||
pkg_id character varying NOT NULL,
|
|
||||||
hidden boolean DEFAULT false NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.pkg_record OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: service_category_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE SEQUENCE public.service_category_id_seq
|
|
||||||
START WITH 1
|
|
||||||
INCREMENT BY 1
|
|
||||||
NO MINVALUE
|
|
||||||
NO MAXVALUE
|
|
||||||
CACHE 1;
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.service_category_id_seq OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: service_category_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER SEQUENCE public.service_category_id_seq OWNED BY public.pkg_category.id;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: upload; Type: TABLE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE public.upload (
|
|
||||||
id bigint NOT NULL,
|
|
||||||
uploader character varying NOT NULL,
|
|
||||||
pkg_id character varying NOT NULL,
|
|
||||||
pkg_version character varying NOT NULL,
|
|
||||||
created_at timestamp with time zone NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.upload OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: upload_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE SEQUENCE public.upload_id_seq
|
|
||||||
START WITH 1
|
|
||||||
INCREMENT BY 1
|
|
||||||
NO MINVALUE
|
|
||||||
NO MAXVALUE
|
|
||||||
CACHE 1;
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.upload_id_seq OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: upload_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER SEQUENCE public.upload_id_seq OWNED BY public.upload.id;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: user_activity; Type: TABLE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE public.user_activity (
|
|
||||||
id bigint NOT NULL,
|
|
||||||
created_at timestamp with time zone NOT NULL,
|
|
||||||
server_id character varying NOT NULL,
|
|
||||||
os_version character varying,
|
|
||||||
arch character varying
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.user_activity OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: user_activity_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE SEQUENCE public.user_activity_id_seq
|
|
||||||
START WITH 1
|
|
||||||
INCREMENT BY 1
|
|
||||||
NO MINVALUE
|
|
||||||
NO MAXVALUE
|
|
||||||
CACHE 1;
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.user_activity_id_seq OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: user_activity_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER SEQUENCE public.user_activity_id_seq OWNED BY public.user_activity.id;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: version; Type: TABLE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE public.version (
|
|
||||||
created_at timestamp with time zone NOT NULL,
|
|
||||||
updated_at timestamp with time zone,
|
|
||||||
number character varying NOT NULL,
|
|
||||||
release_notes character varying NOT NULL,
|
|
||||||
os_version character varying NOT NULL,
|
|
||||||
pkg_id character varying NOT NULL,
|
|
||||||
title character varying NOT NULL,
|
|
||||||
desc_short character varying NOT NULL,
|
|
||||||
desc_long character varying NOT NULL,
|
|
||||||
icon_type character varying NOT NULL,
|
|
||||||
deprecated_at timestamp with time zone
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.version OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: version_platform; Type: TABLE; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE TABLE public.version_platform (
|
|
||||||
created_at timestamp with time zone NOT NULL,
|
|
||||||
updated_at timestamp with time zone,
|
|
||||||
pkg_id character varying NOT NULL,
|
|
||||||
version_number character varying NOT NULL,
|
|
||||||
arch character varying NOT NULL,
|
|
||||||
ram bigint,
|
|
||||||
device jsonb
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.version_platform OWNER TO alpha_admin;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: admin_pkgs id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.admin_pkgs ALTER COLUMN id SET DEFAULT nextval('public.admin_pkgs_id_seq'::regclass);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: category id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.category ALTER COLUMN id SET DEFAULT nextval('public.category_id_seq'::regclass);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: eos_hash id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.eos_hash ALTER COLUMN id SET DEFAULT nextval('public.eos_hash_id_seq'::regclass);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: error_log_record id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.error_log_record ALTER COLUMN id SET DEFAULT nextval('public.error_log_record_id_seq'::regclass);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: metric id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.metric ALTER COLUMN id SET DEFAULT nextval('public.metric_id_seq'::regclass);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: os_version id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.os_version ALTER COLUMN id SET DEFAULT nextval('public.os_version_id_seq'::regclass);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: persistent_migration id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.persistent_migration ALTER COLUMN id SET DEFAULT nextval('public.persistent_migration_id_seq'::regclass);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: pkg_category id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.pkg_category ALTER COLUMN id SET DEFAULT nextval('public.service_category_id_seq'::regclass);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: pkg_dependency id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.pkg_dependency ALTER COLUMN id SET DEFAULT nextval('public.pkg_dependency_id_seq'::regclass);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: upload id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.upload ALTER COLUMN id SET DEFAULT nextval('public.upload_id_seq'::regclass);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: user_activity id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.user_activity ALTER COLUMN id SET DEFAULT nextval('public.user_activity_id_seq'::regclass);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: admin admin_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.admin
|
|
||||||
ADD CONSTRAINT admin_pkey PRIMARY KEY (id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: admin_pkgs admin_pkgs_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.admin_pkgs
|
|
||||||
ADD CONSTRAINT admin_pkgs_pkey PRIMARY KEY (id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: category category_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.category
|
|
||||||
ADD CONSTRAINT category_pkey PRIMARY KEY (id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: eos_hash eos_hash_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.eos_hash
|
|
||||||
ADD CONSTRAINT eos_hash_pkey PRIMARY KEY (id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: error_log_record error_log_record_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.error_log_record
|
|
||||||
ADD CONSTRAINT error_log_record_pkey PRIMARY KEY (id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: metric metric_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.metric
|
|
||||||
ADD CONSTRAINT metric_pkey PRIMARY KEY (id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: os_version os_version_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.os_version
|
|
||||||
ADD CONSTRAINT os_version_pkey PRIMARY KEY (id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: persistent_migration persistent_migration_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.persistent_migration
|
|
||||||
ADD CONSTRAINT persistent_migration_pkey PRIMARY KEY (id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: pkg_category pkg_category_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.pkg_category
|
|
||||||
ADD CONSTRAINT pkg_category_pkey PRIMARY KEY (id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: pkg_dependency pkg_dependency_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.pkg_dependency
|
|
||||||
ADD CONSTRAINT pkg_dependency_pkey PRIMARY KEY (id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: admin_pkgs unique_admin_pkg; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.admin_pkgs
|
|
||||||
ADD CONSTRAINT unique_admin_pkg UNIQUE (pkg_id, admin);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: error_log_record unique_log_record; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.error_log_record
|
|
||||||
ADD CONSTRAINT unique_log_record UNIQUE (epoch, commit_hash, source_file, line, target, level, message);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: category unique_name; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.category
|
|
||||||
ADD CONSTRAINT unique_name UNIQUE (name);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: pkg_category unique_pkg_category; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.pkg_category
|
|
||||||
ADD CONSTRAINT unique_pkg_category UNIQUE (pkg_id, category_id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: pkg_dependency unique_pkg_dep_version; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.pkg_dependency
|
|
||||||
ADD CONSTRAINT unique_pkg_dep_version UNIQUE (pkg_id, pkg_version, dep_id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: eos_hash unique_version; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.eos_hash
|
|
||||||
ADD CONSTRAINT unique_version UNIQUE (version);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: upload upload_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.upload
|
|
||||||
ADD CONSTRAINT upload_pkey PRIMARY KEY (id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: user_activity user_activity_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.user_activity
|
|
||||||
ADD CONSTRAINT user_activity_pkey PRIMARY KEY (id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: version version_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.version
|
|
||||||
ADD CONSTRAINT version_pkey PRIMARY KEY (pkg_id, number);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: version_platform version_platform_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.version_platform
|
|
||||||
ADD CONSTRAINT version_platform_pkey PRIMARY KEY (pkg_id, version_number, arch);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: category_name_idx; Type: INDEX; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX category_name_idx ON public.category USING btree (name);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: pkg_record_pkg_id_idx; Type: INDEX; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX pkg_record_pkg_id_idx ON public.pkg_record USING btree (pkg_id);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: version_number_idx; Type: INDEX; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
CREATE INDEX version_number_idx ON public.version USING btree (number);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: admin_pkgs admin_pkgs_admin_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.admin_pkgs
|
|
||||||
ADD CONSTRAINT admin_pkgs_admin_fkey FOREIGN KEY (admin) REFERENCES public.admin(id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: metric metric_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.metric
|
|
||||||
ADD CONSTRAINT metric_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: pkg_category pkg_category_category_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.pkg_category
|
|
||||||
ADD CONSTRAINT pkg_category_category_id_fkey FOREIGN KEY (category_id) REFERENCES public.category(id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: pkg_category pkg_category_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.pkg_category
|
|
||||||
ADD CONSTRAINT pkg_category_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: pkg_dependency pkg_dependency_dep_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.pkg_dependency
|
|
||||||
ADD CONSTRAINT pkg_dependency_dep_id_fkey FOREIGN KEY (dep_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: pkg_dependency pkg_dependency_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.pkg_dependency
|
|
||||||
ADD CONSTRAINT pkg_dependency_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: upload upload_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.upload
|
|
||||||
ADD CONSTRAINT upload_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: upload upload_uploader_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.upload
|
|
||||||
ADD CONSTRAINT upload_uploader_fkey FOREIGN KEY (uploader) REFERENCES public.admin(id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: version version_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.version
|
|
||||||
ADD CONSTRAINT version_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Name: version_platform version_platform_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.version_platform
|
|
||||||
ADD CONSTRAINT version_platform_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- PostgreSQL database dump complete
|
|
||||||
--
|
|
||||||
|
|
||||||
436
core/src/registry/metrics.rs
Normal file
436
core/src/registry/metrics.rs
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
|
use crate::context::CliContext;
|
||||||
|
use crate::prelude::*;
|
||||||
|
use crate::registry::context::RegistryContext;
|
||||||
|
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
|
||||||
|
|
||||||
|
pub fn metrics_api<C: Context>() -> ParentHandler<C> {
|
||||||
|
ParentHandler::new()
|
||||||
|
.subcommand(
|
||||||
|
"summary",
|
||||||
|
from_fn_async(get_summary)
|
||||||
|
.with_metadata("admin", Value::Bool(true))
|
||||||
|
.with_display_serializable()
|
||||||
|
.with_custom_display_fn(|handle, result| display_summary(handle.params, result))
|
||||||
|
.with_about("about.get-metrics-summary")
|
||||||
|
.with_call_remote::<CliContext>(),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
"users",
|
||||||
|
from_fn_async(get_users)
|
||||||
|
.with_metadata("admin", Value::Bool(true))
|
||||||
|
.with_display_serializable()
|
||||||
|
.with_custom_display_fn(|handle, result| display_users(handle.params, result))
|
||||||
|
.with_about("about.get-metrics-users")
|
||||||
|
.with_call_remote::<CliContext>(),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
"downloads",
|
||||||
|
from_fn_async(get_downloads)
|
||||||
|
.with_metadata("admin", Value::Bool(true))
|
||||||
|
.with_display_serializable()
|
||||||
|
.with_custom_display_fn(|handle, result| {
|
||||||
|
display_downloads(handle.params, result)
|
||||||
|
})
|
||||||
|
.with_about("about.get-metrics-downloads")
|
||||||
|
.with_call_remote::<CliContext>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- summary ---
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct CountEntry {
|
||||||
|
pub label: String,
|
||||||
|
pub count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct MetricsSummary {
|
||||||
|
pub total_checkins: u64,
|
||||||
|
pub unique_servers: u64,
|
||||||
|
pub total_package_requests: u64,
|
||||||
|
pub by_arch: Vec<CountEntry>,
|
||||||
|
pub by_os_version: Vec<CountEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_summary(ctx: RegistryContext) -> Result<MetricsSummary, Error> {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
ctx.metrics_db.peek(|conn| {
|
||||||
|
let total_checkins: u64 = conn
|
||||||
|
.query_row("SELECT COUNT(*) FROM user_activity", [], |row| row.get(0))
|
||||||
|
.with_kind(ErrorKind::Database)?;
|
||||||
|
|
||||||
|
let unique_servers: u64 = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(DISTINCT server_id) FROM user_activity",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.with_kind(ErrorKind::Database)?;
|
||||||
|
|
||||||
|
let total_package_requests: u64 = conn
|
||||||
|
.query_row("SELECT COUNT(*) FROM package_request", [], |row| row.get(0))
|
||||||
|
.with_kind(ErrorKind::Database)?;
|
||||||
|
|
||||||
|
let by_arch = query_count_entries(
|
||||||
|
conn,
|
||||||
|
"SELECT COALESCE(arch, 'unknown'), COUNT(*) FROM user_activity GROUP BY arch ORDER BY COUNT(*) DESC",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let by_os_version = query_count_entries(
|
||||||
|
conn,
|
||||||
|
"SELECT COALESCE(os_version, 'unknown'), COUNT(*) FROM user_activity GROUP BY os_version ORDER BY COUNT(*) DESC",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(MetricsSummary {
|
||||||
|
total_checkins,
|
||||||
|
unique_servers,
|
||||||
|
total_package_requests,
|
||||||
|
by_arch,
|
||||||
|
by_os_version,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.with_kind(ErrorKind::Unknown)?
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_summary<T>(params: WithIoFormat<T>, summary: MetricsSummary) -> Result<(), Error> {
|
||||||
|
use prettytable::*;
|
||||||
|
|
||||||
|
if let Some(format) = params.format {
|
||||||
|
return display_serializable(format, summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Total check-ins: {}", summary.total_checkins);
|
||||||
|
println!("Unique servers: {}", summary.unique_servers);
|
||||||
|
println!("Total package requests: {}", summary.total_package_requests);
|
||||||
|
|
||||||
|
if !summary.by_arch.is_empty() {
|
||||||
|
println!();
|
||||||
|
let mut table = Table::new();
|
||||||
|
table.add_row(row![bc => "ARCHITECTURE", "COUNT"]);
|
||||||
|
for entry in &summary.by_arch {
|
||||||
|
table.add_row(row![&entry.label, entry.count]);
|
||||||
|
}
|
||||||
|
table.print_tty(false)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !summary.by_os_version.is_empty() {
|
||||||
|
println!();
|
||||||
|
let mut table = Table::new();
|
||||||
|
table.add_row(row![bc => "OS VERSION", "COUNT"]);
|
||||||
|
for entry in &summary.by_os_version {
|
||||||
|
table.add_row(row![&entry.label, entry.count]);
|
||||||
|
}
|
||||||
|
table.print_tty(false)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- users ---
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||||
|
#[group(skip)]
|
||||||
|
#[command(rename_all = "kebab-case")]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct GetUsersParams {
|
||||||
|
/// Start of time range (RFC 3339)
|
||||||
|
#[ts(type = "string | null")]
|
||||||
|
#[arg(long, help = "help.arg.metrics-after")]
|
||||||
|
pub after: Option<String>,
|
||||||
|
/// End of time range (RFC 3339)
|
||||||
|
#[ts(type = "string | null")]
|
||||||
|
#[arg(long, help = "help.arg.metrics-before")]
|
||||||
|
pub before: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct UsersResponse {
|
||||||
|
pub unique_servers: u64,
|
||||||
|
pub total_checkins: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_users(
|
||||||
|
ctx: RegistryContext,
|
||||||
|
GetUsersParams { after, before }: GetUsersParams,
|
||||||
|
) -> Result<UsersResponse, Error> {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
ctx.metrics_db.peek(|conn| {
|
||||||
|
let (where_clause, params) = time_range_where(&after, &before);
|
||||||
|
|
||||||
|
let unique_servers: u64 = conn
|
||||||
|
.query_row(
|
||||||
|
&format!("SELECT COUNT(DISTINCT server_id) FROM user_activity{where_clause}"),
|
||||||
|
rusqlite::params_from_iter(¶ms),
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.with_kind(ErrorKind::Database)?;
|
||||||
|
|
||||||
|
let total_checkins: u64 = conn
|
||||||
|
.query_row(
|
||||||
|
&format!("SELECT COUNT(*) FROM user_activity{where_clause}"),
|
||||||
|
rusqlite::params_from_iter(¶ms),
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.with_kind(ErrorKind::Database)?;
|
||||||
|
|
||||||
|
Ok(UsersResponse {
|
||||||
|
unique_servers,
|
||||||
|
total_checkins,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.with_kind(ErrorKind::Unknown)?
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_users<T>(params: WithIoFormat<T>, response: UsersResponse) -> Result<(), Error> {
|
||||||
|
if let Some(format) = params.format {
|
||||||
|
return display_serializable(format, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Unique servers: {}", response.unique_servers);
|
||||||
|
println!("Total check-ins: {}", response.total_checkins);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- downloads ---
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||||
|
#[group(skip)]
|
||||||
|
#[command(rename_all = "kebab-case")]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct GetDownloadsParams {
|
||||||
|
/// Filter by package ID
|
||||||
|
#[ts(type = "string | null")]
|
||||||
|
#[arg(long, help = "help.arg.metrics-pkg-id")]
|
||||||
|
pub pkg_id: Option<String>,
|
||||||
|
/// Filter by version
|
||||||
|
#[ts(type = "string | null")]
|
||||||
|
#[arg(long, help = "help.arg.metrics-version")]
|
||||||
|
pub version: Option<String>,
|
||||||
|
/// Start of time range (RFC 3339)
|
||||||
|
#[ts(type = "string | null")]
|
||||||
|
#[arg(long, help = "help.arg.metrics-after")]
|
||||||
|
pub after: Option<String>,
|
||||||
|
/// End of time range (RFC 3339)
|
||||||
|
#[ts(type = "string | null")]
|
||||||
|
#[arg(long, help = "help.arg.metrics-before")]
|
||||||
|
pub before: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct PackageVersionCount {
|
||||||
|
pub pkg_id: String,
|
||||||
|
pub version: String,
|
||||||
|
pub count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct DownloadsResponse {
|
||||||
|
pub total_requests: u64,
|
||||||
|
pub by_package: Vec<CountEntry>,
|
||||||
|
pub by_package_version: Vec<PackageVersionCount>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_downloads(
|
||||||
|
ctx: RegistryContext,
|
||||||
|
GetDownloadsParams {
|
||||||
|
pkg_id,
|
||||||
|
version,
|
||||||
|
after,
|
||||||
|
before,
|
||||||
|
}: GetDownloadsParams,
|
||||||
|
) -> Result<DownloadsResponse, Error> {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
ctx.metrics_db.peek(|conn| {
|
||||||
|
let (where_clause, params) =
|
||||||
|
downloads_where(&pkg_id, &version, &after, &before);
|
||||||
|
|
||||||
|
let total_requests: u64 = conn
|
||||||
|
.query_row(
|
||||||
|
&format!("SELECT COUNT(*) FROM package_request{where_clause}"),
|
||||||
|
rusqlite::params_from_iter(¶ms),
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.with_kind(ErrorKind::Database)?;
|
||||||
|
|
||||||
|
let by_package = query_count_entries_with_params(
|
||||||
|
conn,
|
||||||
|
&format!(
|
||||||
|
"SELECT pkg_id, COUNT(*) FROM package_request{where_clause} GROUP BY pkg_id ORDER BY COUNT(*) DESC"
|
||||||
|
),
|
||||||
|
¶ms,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let by_package_version = {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(&format!(
|
||||||
|
"SELECT pkg_id, COALESCE(version, 'unknown'), COUNT(*) FROM package_request{where_clause} GROUP BY pkg_id, version ORDER BY pkg_id, COUNT(*) DESC"
|
||||||
|
))
|
||||||
|
.with_kind(ErrorKind::Database)?;
|
||||||
|
let rows = stmt
|
||||||
|
.query_map(rusqlite::params_from_iter(¶ms), |row| {
|
||||||
|
Ok(PackageVersionCount {
|
||||||
|
pkg_id: row.get(0)?,
|
||||||
|
version: row.get(1)?,
|
||||||
|
count: row.get(2)?,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.with_kind(ErrorKind::Database)?;
|
||||||
|
rows.map(|r| r.with_kind(ErrorKind::Database))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(DownloadsResponse {
|
||||||
|
total_requests,
|
||||||
|
by_package,
|
||||||
|
by_package_version,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.with_kind(ErrorKind::Unknown)?
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_downloads(
|
||||||
|
params: WithIoFormat<GetDownloadsParams>,
|
||||||
|
response: DownloadsResponse,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
use prettytable::*;
|
||||||
|
|
||||||
|
if let Some(format) = params.format {
|
||||||
|
return display_serializable(format, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Total requests: {}", response.total_requests);
|
||||||
|
|
||||||
|
if !response.by_package.is_empty() {
|
||||||
|
println!();
|
||||||
|
let mut table = Table::new();
|
||||||
|
table.add_row(row![bc => "PACKAGE", "COUNT"]);
|
||||||
|
for entry in &response.by_package {
|
||||||
|
table.add_row(row![&entry.label, entry.count]);
|
||||||
|
}
|
||||||
|
table.print_tty(false)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !response.by_package_version.is_empty() {
|
||||||
|
println!();
|
||||||
|
let mut table = Table::new();
|
||||||
|
table.add_row(row![bc => "PACKAGE", "VERSION", "COUNT"]);
|
||||||
|
for entry in &response.by_package_version {
|
||||||
|
table.add_row(row![&entry.pkg_id, &entry.version, entry.count]);
|
||||||
|
}
|
||||||
|
table.print_tty(false)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- helpers ---
|
||||||
|
|
||||||
|
fn query_count_entries(
|
||||||
|
conn: &rusqlite::Connection,
|
||||||
|
sql: &str,
|
||||||
|
) -> Result<Vec<CountEntry>, Error> {
|
||||||
|
query_count_entries_with_params(conn, sql, &[])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_count_entries_with_params(
|
||||||
|
conn: &rusqlite::Connection,
|
||||||
|
sql: &str,
|
||||||
|
params: &[String],
|
||||||
|
) -> Result<Vec<CountEntry>, Error> {
|
||||||
|
let mut stmt = conn.prepare(sql).with_kind(ErrorKind::Database)?;
|
||||||
|
let rows = stmt
|
||||||
|
.query_map(rusqlite::params_from_iter(params), |row| {
|
||||||
|
Ok(CountEntry {
|
||||||
|
label: row.get(0)?,
|
||||||
|
count: row.get(1)?,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.with_kind(ErrorKind::Database)?;
|
||||||
|
rows.map(|r| r.with_kind(ErrorKind::Database))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn time_range_where(
|
||||||
|
after: &Option<String>,
|
||||||
|
before: &Option<String>,
|
||||||
|
) -> (String, Vec<String>) {
|
||||||
|
let mut conditions = Vec::new();
|
||||||
|
let mut params = Vec::new();
|
||||||
|
|
||||||
|
if let Some(a) = after {
|
||||||
|
params.push(a.clone());
|
||||||
|
conditions.push(format!("created_at >= ?{}", params.len()));
|
||||||
|
}
|
||||||
|
if let Some(b) = before {
|
||||||
|
params.push(b.clone());
|
||||||
|
conditions.push(format!("created_at < ?{}", params.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let clause = if conditions.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(" WHERE {}", conditions.join(" AND "))
|
||||||
|
};
|
||||||
|
|
||||||
|
(clause, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn downloads_where(
|
||||||
|
pkg_id: &Option<String>,
|
||||||
|
version: &Option<String>,
|
||||||
|
after: &Option<String>,
|
||||||
|
before: &Option<String>,
|
||||||
|
) -> (String, Vec<String>) {
|
||||||
|
let mut conditions = Vec::new();
|
||||||
|
let mut params = Vec::new();
|
||||||
|
|
||||||
|
if let Some(id) = pkg_id {
|
||||||
|
params.push(id.clone());
|
||||||
|
conditions.push(format!("pkg_id = ?{}", params.len()));
|
||||||
|
}
|
||||||
|
if let Some(v) = version {
|
||||||
|
params.push(v.clone());
|
||||||
|
conditions.push(format!("version = ?{}", params.len()));
|
||||||
|
}
|
||||||
|
if let Some(a) = after {
|
||||||
|
params.push(a.clone());
|
||||||
|
conditions.push(format!("created_at >= ?{}", params.len()));
|
||||||
|
}
|
||||||
|
if let Some(b) = before {
|
||||||
|
params.push(b.clone());
|
||||||
|
conditions.push(format!("created_at < ?{}", params.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let clause = if conditions.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(" WHERE {}", conditions.join(" AND "))
|
||||||
|
};
|
||||||
|
|
||||||
|
(clause, params)
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ pub mod context;
|
|||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod device_info;
|
pub mod device_info;
|
||||||
pub mod info;
|
pub mod info;
|
||||||
|
pub mod metrics;
|
||||||
mod migrations;
|
mod migrations;
|
||||||
pub mod os;
|
pub mod os;
|
||||||
pub mod package;
|
pub mod package;
|
||||||
@@ -79,7 +80,10 @@ pub fn registry_api<C: Context>() -> ParentHandler<C> {
|
|||||||
.with_about("about.list-registry-info-packages")
|
.with_about("about.list-registry-info-packages")
|
||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
)
|
)
|
||||||
.subcommand("info", info::info_api::<C>())
|
.subcommand(
|
||||||
|
"info",
|
||||||
|
info::info_api::<C>().with_about("about.commands-registry-info"),
|
||||||
|
)
|
||||||
// set info and categories
|
// set info and categories
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"os",
|
"os",
|
||||||
@@ -97,6 +101,10 @@ pub fn registry_api<C: Context>() -> ParentHandler<C> {
|
|||||||
"db",
|
"db",
|
||||||
db::db_api::<C>().with_about("about.commands-registry-db"),
|
db::db_api::<C>().with_about("about.commands-registry-db"),
|
||||||
)
|
)
|
||||||
|
.subcommand(
|
||||||
|
"metrics",
|
||||||
|
metrics::metrics_api::<C>().with_about("about.commands-registry-metrics"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn registry_router(ctx: RegistryContext) -> Router {
|
pub fn registry_router(ctx: RegistryContext) -> Router {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub const SIG_CONTEXT: &str = "startos";
|
|||||||
|
|
||||||
pub mod asset;
|
pub mod asset;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
|
pub mod promote;
|
||||||
pub mod version;
|
pub mod version;
|
||||||
|
|
||||||
pub fn os_api<C: Context>() -> ParentHandler<C> {
|
pub fn os_api<C: Context>() -> ParentHandler<C> {
|
||||||
@@ -28,4 +29,10 @@ pub fn os_api<C: Context>() -> ParentHandler<C> {
|
|||||||
"version",
|
"version",
|
||||||
version::version_api::<C>().with_about("about.commands-add-remove-list-versions"),
|
version::version_api::<C>().with_about("about.commands-add-remove-list-versions"),
|
||||||
)
|
)
|
||||||
|
.subcommand(
|
||||||
|
"promote",
|
||||||
|
from_fn_async(promote::cli_os_promote)
|
||||||
|
.no_display()
|
||||||
|
.with_about("about.promote-os-registry"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
114
core/src/registry/os/promote.rs
Normal file
114
core/src/registry/os/promote.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use exver::Version;
|
||||||
|
use imbl_value::InternedString;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::context::CliContext;
|
||||||
|
use crate::prelude::*;
|
||||||
|
use crate::registry::os::SIG_CONTEXT;
|
||||||
|
use crate::registry::os::index::OsIndex;
|
||||||
|
use crate::registry::package::promote::{call_registry, resolve_registry_url};
|
||||||
|
use crate::sign::commitment::blake3::Blake3Commitment;
|
||||||
|
use crate::sign::ed25519::Ed25519;
|
||||||
|
use crate::sign::{AnySignature, SignatureScheme};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Parser)]
|
||||||
|
#[group(skip)]
|
||||||
|
#[command(rename_all = "kebab-case")]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CliOsPromoteParams {
|
||||||
|
#[arg(long, help = "help.arg.from-registry-url")]
|
||||||
|
pub from: Option<Url>,
|
||||||
|
#[arg(long, help = "help.arg.to-registry-url")]
|
||||||
|
pub to: Option<Url>,
|
||||||
|
#[arg(help = "help.arg.os-version")]
|
||||||
|
pub version: Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cli_os_promote(
|
||||||
|
ctx: CliContext,
|
||||||
|
CliOsPromoteParams { from, to, version }: CliOsPromoteParams,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if from.is_none() && to.is_none() {
|
||||||
|
return Err(Error::new(
|
||||||
|
eyre!("{}", t!("registry.os.promote.need-from-or-to")),
|
||||||
|
ErrorKind::InvalidRequest,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let from_url = resolve_registry_url(from.as_ref(), &ctx)?;
|
||||||
|
let to_url = resolve_registry_url(to.as_ref(), &ctx)?;
|
||||||
|
|
||||||
|
// Fetch OS index from source registry
|
||||||
|
let res: Value = call_registry(&ctx, from_url, "os.index", imbl_value::json!({})).await?;
|
||||||
|
let os_index: OsIndex = from_value(res)?;
|
||||||
|
|
||||||
|
// Find the target version
|
||||||
|
let version_info = os_index
|
||||||
|
.versions
|
||||||
|
.0
|
||||||
|
.get(&version)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::new(
|
||||||
|
eyre!(
|
||||||
|
"{}",
|
||||||
|
t!(
|
||||||
|
"registry.os.promote.version-not-found",
|
||||||
|
version = &version
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ErrorKind::NotFound,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Add the version to the target registry
|
||||||
|
call_registry(
|
||||||
|
&ctx,
|
||||||
|
to_url.clone(),
|
||||||
|
"os.version.add",
|
||||||
|
imbl_value::json!({
|
||||||
|
"version": &version,
|
||||||
|
"headline": &version_info.headline,
|
||||||
|
"releaseNotes": &version_info.release_notes,
|
||||||
|
"sourceVersion": &version_info.source_version,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Promote all assets for each type and platform
|
||||||
|
promote_assets(&ctx, &to_url, &version, &version_info.iso, "os.asset.add.iso").await?;
|
||||||
|
promote_assets(&ctx, &to_url, &version, &version_info.squashfs, "os.asset.add.squashfs").await?;
|
||||||
|
promote_assets(&ctx, &to_url, &version, &version_info.img, "os.asset.add.img").await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn promote_assets(
|
||||||
|
ctx: &CliContext,
|
||||||
|
to_url: &Url,
|
||||||
|
version: &Version,
|
||||||
|
assets: &std::collections::BTreeMap<InternedString, crate::registry::asset::RegistryAsset<Blake3Commitment>>,
|
||||||
|
method: &str,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
for (platform, asset) in assets {
|
||||||
|
let commitment = &asset.commitment;
|
||||||
|
let signature =
|
||||||
|
AnySignature::Ed25519(Ed25519.sign_commitment(ctx.developer_key()?, commitment, SIG_CONTEXT)?);
|
||||||
|
|
||||||
|
call_registry(
|
||||||
|
ctx,
|
||||||
|
to_url.clone(),
|
||||||
|
method,
|
||||||
|
imbl_value::json!({
|
||||||
|
"version": version,
|
||||||
|
"platform": platform,
|
||||||
|
"url": &asset.urls[0],
|
||||||
|
"signature": signature,
|
||||||
|
"commitment": commitment,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
|
use chrono::Utc;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use exver::{Version, VersionRange};
|
use exver::{Version, VersionRange};
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
|
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
|
||||||
|
use rusqlite::params;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::warn;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::context::CliContext;
|
use crate::context::CliContext;
|
||||||
@@ -159,33 +161,6 @@ pub struct GetOsVersionParams {
|
|||||||
pub device_info: Option<DeviceInfo>,
|
pub device_info: Option<DeviceInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PgDateTime(DateTime<Utc>);
|
|
||||||
impl sqlx::Type<sqlx::Postgres> for PgDateTime {
|
|
||||||
fn type_info() -> <sqlx::Postgres as sqlx::Database>::TypeInfo {
|
|
||||||
sqlx::postgres::PgTypeInfo::with_oid(sqlx::postgres::types::Oid(1184))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl sqlx::Encode<'_, sqlx::Postgres> for PgDateTime {
|
|
||||||
fn encode_by_ref(
|
|
||||||
&self,
|
|
||||||
buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer<'_>,
|
|
||||||
) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
|
|
||||||
fn postgres_epoch_datetime() -> NaiveDateTime {
|
|
||||||
NaiveDate::from_ymd_opt(2000, 1, 1)
|
|
||||||
.expect("expected 2000-01-01 to be a valid NaiveDate")
|
|
||||||
.and_hms_opt(0, 0, 0)
|
|
||||||
.expect("expected 2000-01-01T00:00:00 to be a valid NaiveDateTime")
|
|
||||||
}
|
|
||||||
let micros = (self.0.naive_utc() - postgres_epoch_datetime())
|
|
||||||
.num_microseconds()
|
|
||||||
.ok_or_else(|| format!("NaiveDateTime out of range for Postgres: {:?}", self.0))?;
|
|
||||||
micros.encode(buf)
|
|
||||||
}
|
|
||||||
fn size_hint(&self) -> usize {
|
|
||||||
std::mem::size_of::<i64>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_version(
|
pub async fn get_version(
|
||||||
ctx: RegistryContext,
|
ctx: RegistryContext,
|
||||||
GetOsVersionParams {
|
GetOsVersionParams {
|
||||||
@@ -199,16 +174,28 @@ pub async fn get_version(
|
|||||||
{
|
{
|
||||||
let source = source.or_else(|| device_info.as_ref().map(|d| d.os.version.clone()));
|
let source = source.or_else(|| device_info.as_ref().map(|d| d.os.version.clone()));
|
||||||
let platform = platform.or_else(|| device_info.as_ref().map(|d| d.os.platform.clone()));
|
let platform = platform.or_else(|| device_info.as_ref().map(|d| d.os.platform.clone()));
|
||||||
if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, &platform) {
|
if let (Some(server_id), Some(arch)) = (server_id, &platform) {
|
||||||
let created_at = Utc::now();
|
const MAX_SERVER_ID_LEN: usize = 256;
|
||||||
|
if server_id.len() <= MAX_SERVER_ID_LEN {
|
||||||
sqlx::query("INSERT INTO user_activity (created_at, server_id, arch) VALUES ($1, $2, $3)")
|
let created_at = Utc::now().to_rfc3339();
|
||||||
.bind(PgDateTime(created_at))
|
let arch = arch.to_string();
|
||||||
.bind(server_id)
|
let os_version = source.as_ref().map(|v| v.to_string());
|
||||||
.bind(&**arch)
|
let ctx = ctx.clone();
|
||||||
.execute(pool)
|
tokio::task::spawn_blocking(move || {
|
||||||
.await
|
ctx.metrics_db.mutate(|conn| {
|
||||||
.with_kind(ErrorKind::Database)?;
|
if let Err(e) = conn.execute(
|
||||||
|
concat!(
|
||||||
|
"INSERT INTO user_activity ",
|
||||||
|
"(created_at, server_id, arch, os_version) ",
|
||||||
|
"VALUES (?1, ?2, ?3, ?4)"
|
||||||
|
),
|
||||||
|
params![created_at, server_id, arch, os_version],
|
||||||
|
) {
|
||||||
|
warn!("failed to record user activity metric: {e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let target = target.unwrap_or(VersionRange::Any);
|
let target = target.unwrap_or(VersionRange::Any);
|
||||||
let mut res = to_value::<BTreeMap<Version, OsVersionInfo>>(
|
let mut res = to_value::<BTreeMap<Version, OsVersionInfo>>(
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ pub fn category_api<C: Context>() -> ParentHandler<C> {
|
|||||||
.with_custom_display_fn(|params, categories| {
|
.with_custom_display_fn(|params, categories| {
|
||||||
display_categories(params.params, categories)
|
display_categories(params.params, categories)
|
||||||
})
|
})
|
||||||
|
.with_about("about.list-registry-categories")
|
||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ use itertools::Itertools;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
use rusqlite::params;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::PackageId;
|
use crate::PackageId;
|
||||||
use crate::context::CliContext;
|
use crate::context::CliContext;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
@@ -233,6 +237,25 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(id) = ¶ms.id {
|
if let Some(id) = ¶ms.id {
|
||||||
|
if params.target_version.is_some() {
|
||||||
|
let created_at = Utc::now().to_rfc3339();
|
||||||
|
let pkg_id = id.to_string();
|
||||||
|
let version = best
|
||||||
|
.get(id)
|
||||||
|
.and_then(|b| b.keys().last())
|
||||||
|
.map(|v| v.to_string());
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
ctx.metrics_db.mutate(|conn| {
|
||||||
|
if let Err(e) = conn.execute(
|
||||||
|
"INSERT INTO package_request (created_at, pkg_id, version) VALUES (?1, ?2, ?3)",
|
||||||
|
params![created_at, pkg_id, version],
|
||||||
|
) {
|
||||||
|
warn!("failed to record package request metric: {e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
let categories = peek
|
let categories = peek
|
||||||
.as_index()
|
.as_index()
|
||||||
.as_package()
|
.as_package()
|
||||||
@@ -615,6 +638,7 @@ fn check_matching_info_short() {
|
|||||||
sdk_version: None,
|
sdk_version: None,
|
||||||
hardware_acceleration: false,
|
hardware_acceleration: false,
|
||||||
plugins: BTreeSet::new(),
|
plugins: BTreeSet::new(),
|
||||||
|
satisfies: BTreeSet::new(),
|
||||||
},
|
},
|
||||||
icon: DataUrl::from_vec("image/png", vec![]),
|
icon: DataUrl::from_vec("image/png", vec![]),
|
||||||
dependency_metadata: BTreeMap::new(),
|
dependency_metadata: BTreeMap::new(),
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ pub struct PackageMetadata {
|
|||||||
pub hardware_acceleration: bool,
|
pub hardware_acceleration: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub plugins: BTreeSet<PluginId>,
|
pub plugins: BTreeSet<PluginId>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub satisfies: BTreeSet<VersionString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub mod add;
|
|||||||
pub mod category;
|
pub mod category;
|
||||||
pub mod get;
|
pub mod get;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
|
pub mod promote;
|
||||||
pub mod signer;
|
pub mod signer;
|
||||||
|
|
||||||
pub fn package_api<C: Context>() -> ParentHandler<C> {
|
pub fn package_api<C: Context>() -> ParentHandler<C> {
|
||||||
@@ -98,6 +99,12 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
|
|||||||
.no_display()
|
.no_display()
|
||||||
.with_about("about.download-s9pk"),
|
.with_about("about.download-s9pk"),
|
||||||
)
|
)
|
||||||
|
.subcommand(
|
||||||
|
"promote",
|
||||||
|
from_fn_async(promote::cli_promote)
|
||||||
|
.no_display()
|
||||||
|
.with_about("about.promote-package-registry"),
|
||||||
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"category",
|
"category",
|
||||||
category::category_api::<C>().with_about("about.update-categories-registry"),
|
category::category_api::<C>().with_about("about.update-categories-registry"),
|
||||||
|
|||||||
144
core/src/registry/package/promote.rs
Normal file
144
core/src/registry/package/promote.rs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use http::HeaderMap;
|
||||||
|
use imbl_value::InternedString;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::PackageId;
|
||||||
|
use crate::context::CliContext;
|
||||||
|
use crate::prelude::*;
|
||||||
|
use crate::registry::package::get::{GetPackageResponseFull, PackageDetailLevel};
|
||||||
|
use crate::s9pk::v2::SIG_CONTEXT;
|
||||||
|
use crate::sign::ed25519::Ed25519;
|
||||||
|
use crate::sign::{AnySignature, SignatureScheme};
|
||||||
|
use crate::util::VersionString;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Parser)]
|
||||||
|
#[group(skip)]
|
||||||
|
#[command(rename_all = "kebab-case")]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CliPromoteParams {
|
||||||
|
#[arg(long, help = "help.arg.from-registry-url")]
|
||||||
|
pub from: Option<Url>,
|
||||||
|
#[arg(long, help = "help.arg.to-registry-url")]
|
||||||
|
pub to: Option<Url>,
|
||||||
|
#[arg(help = "help.arg.package-id")]
|
||||||
|
pub id: PackageId,
|
||||||
|
#[arg(help = "help.arg.package-version")]
|
||||||
|
pub version: VersionString,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn registry_rpc_url(url: &Url) -> Result<Url, Error> {
|
||||||
|
let mut url = url.clone();
|
||||||
|
url.path_segments_mut()
|
||||||
|
.map_err(|_| eyre!("Url cannot be base"))
|
||||||
|
.with_kind(ErrorKind::ParseUrl)?
|
||||||
|
.push("rpc")
|
||||||
|
.push("v0");
|
||||||
|
Ok(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_registry_url(explicit: Option<&Url>, ctx: &CliContext) -> Result<Url, Error> {
|
||||||
|
if let Some(url) = explicit {
|
||||||
|
registry_rpc_url(url)
|
||||||
|
} else if let Some(url) = &ctx.registry_url {
|
||||||
|
Ok(url.clone())
|
||||||
|
} else {
|
||||||
|
Err(Error::new(
|
||||||
|
eyre!("{}", t!("registry.context.registry-required")),
|
||||||
|
ErrorKind::InvalidRequest,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn call_registry(
|
||||||
|
ctx: &CliContext,
|
||||||
|
url: Url,
|
||||||
|
method: &str,
|
||||||
|
params: Value,
|
||||||
|
) -> Result<Value, Error> {
|
||||||
|
let sig_context = url.host().as_ref().map(InternedString::from_display);
|
||||||
|
crate::middleware::auth::signature::call_remote(
|
||||||
|
ctx,
|
||||||
|
url,
|
||||||
|
HeaderMap::new(),
|
||||||
|
sig_context.as_deref(),
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cli_promote(
|
||||||
|
ctx: CliContext,
|
||||||
|
CliPromoteParams {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
id,
|
||||||
|
version,
|
||||||
|
}: CliPromoteParams,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if from.is_none() && to.is_none() {
|
||||||
|
return Err(Error::new(
|
||||||
|
eyre!("{}", t!("registry.package.promote.need-from-or-to")),
|
||||||
|
ErrorKind::InvalidRequest,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let from_url = resolve_registry_url(from.as_ref(), &ctx)?;
|
||||||
|
let to_url = resolve_registry_url(to.as_ref(), &ctx)?;
|
||||||
|
|
||||||
|
// Fetch package info from source registry
|
||||||
|
let res: Value = call_registry(
|
||||||
|
&ctx,
|
||||||
|
from_url,
|
||||||
|
"package.get",
|
||||||
|
imbl_value::json!({
|
||||||
|
"id": &id,
|
||||||
|
"otherVersions": PackageDetailLevel::Full,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let response: GetPackageResponseFull = from_value(res)?;
|
||||||
|
|
||||||
|
// Find the target version
|
||||||
|
let version_info = response
|
||||||
|
.best
|
||||||
|
.get(&version)
|
||||||
|
.or_else(|| response.other_versions.get(&version))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::new(
|
||||||
|
eyre!(
|
||||||
|
"{}",
|
||||||
|
t!(
|
||||||
|
"registry.package.promote.version-not-found",
|
||||||
|
id = &id,
|
||||||
|
version = &version
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ErrorKind::NotFound,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Promote each s9pk variant to the target registry
|
||||||
|
for (_, asset) in &version_info.s9pks {
|
||||||
|
let commitment = &asset.commitment;
|
||||||
|
let signature = Ed25519.sign_commitment(ctx.developer_key()?, commitment, SIG_CONTEXT)?;
|
||||||
|
|
||||||
|
call_registry(
|
||||||
|
&ctx,
|
||||||
|
to_url.clone(),
|
||||||
|
"package.add",
|
||||||
|
imbl_value::json!({
|
||||||
|
"urls": &asset.urls,
|
||||||
|
"signature": AnySignature::Ed25519(signature),
|
||||||
|
"commitment": commitment,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -17,7 +17,6 @@ use ts_rs::TS;
|
|||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::shutdown::Shutdown;
|
|
||||||
use crate::util::future::TimedResource;
|
use crate::util::future::TimedResource;
|
||||||
use crate::util::net::WebSocket;
|
use crate::util::net::WebSocket;
|
||||||
use crate::util::{FromStrParser, new_guid};
|
use crate::util::{FromStrParser, new_guid};
|
||||||
@@ -99,15 +98,12 @@ pub type RestHandler = Box<dyn FnOnce(Request) -> RestFuture + Send>;
|
|||||||
|
|
||||||
pub struct WebSocketFuture {
|
pub struct WebSocketFuture {
|
||||||
kill: Option<broadcast::Receiver<()>>,
|
kill: Option<broadcast::Receiver<()>>,
|
||||||
shutdown: Option<broadcast::Receiver<Option<Shutdown>>>,
|
|
||||||
fut: BoxFuture<'static, ()>,
|
fut: BoxFuture<'static, ()>,
|
||||||
}
|
}
|
||||||
impl Future for WebSocketFuture {
|
impl Future for WebSocketFuture {
|
||||||
type Output = ();
|
type Output = ();
|
||||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
if self.kill.as_ref().map_or(false, |k| !k.is_empty())
|
if self.kill.as_ref().map_or(false, |k| !k.is_empty()) {
|
||||||
|| self.shutdown.as_ref().map_or(false, |s| !s.is_empty())
|
|
||||||
{
|
|
||||||
Poll::Ready(())
|
Poll::Ready(())
|
||||||
} else {
|
} else {
|
||||||
self.fut.poll_unpin(cx)
|
self.fut.poll_unpin(cx)
|
||||||
@@ -142,7 +138,6 @@ impl RpcContinuation {
|
|||||||
RpcContinuation::WebSocket(TimedResource::new(
|
RpcContinuation::WebSocket(TimedResource::new(
|
||||||
Box::new(|ws| WebSocketFuture {
|
Box::new(|ws| WebSocketFuture {
|
||||||
kill: None,
|
kill: None,
|
||||||
shutdown: None,
|
|
||||||
fut: handler(ws.into()).boxed(),
|
fut: handler(ws.into()).boxed(),
|
||||||
}),
|
}),
|
||||||
timeout,
|
timeout,
|
||||||
@@ -175,7 +170,6 @@ impl RpcContinuation {
|
|||||||
RpcContinuation::WebSocket(TimedResource::new(
|
RpcContinuation::WebSocket(TimedResource::new(
|
||||||
Box::new(|ws| WebSocketFuture {
|
Box::new(|ws| WebSocketFuture {
|
||||||
kill,
|
kill,
|
||||||
shutdown: None,
|
|
||||||
fut: handler(ws.into()).boxed(),
|
fut: handler(ws.into()).boxed(),
|
||||||
}),
|
}),
|
||||||
timeout,
|
timeout,
|
||||||
@@ -189,21 +183,15 @@ impl RpcContinuation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RpcContinuations {
|
pub struct RpcContinuations(AsyncMutex<BTreeMap<Guid, RpcContinuation>>);
|
||||||
continuations: AsyncMutex<BTreeMap<Guid, RpcContinuation>>,
|
|
||||||
shutdown: Option<broadcast::Sender<Option<Shutdown>>>,
|
|
||||||
}
|
|
||||||
impl RpcContinuations {
|
impl RpcContinuations {
|
||||||
pub fn new(shutdown: Option<broadcast::Sender<Option<Shutdown>>>) -> Self {
|
pub fn new() -> Self {
|
||||||
RpcContinuations {
|
RpcContinuations(AsyncMutex::new(BTreeMap::new()))
|
||||||
continuations: AsyncMutex::new(BTreeMap::new()),
|
|
||||||
shutdown,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn clean(&self) {
|
pub async fn clean(&self) {
|
||||||
let mut continuations = self.continuations.lock().await;
|
let mut continuations = self.0.lock().await;
|
||||||
let mut to_remove = Vec::new();
|
let mut to_remove = Vec::new();
|
||||||
for (guid, cont) in &*continuations {
|
for (guid, cont) in &*continuations {
|
||||||
if cont.is_timed_out() {
|
if cont.is_timed_out() {
|
||||||
@@ -218,28 +206,23 @@ impl RpcContinuations {
|
|||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn add(&self, guid: Guid, handler: RpcContinuation) {
|
pub async fn add(&self, guid: Guid, handler: RpcContinuation) {
|
||||||
self.clean().await;
|
self.clean().await;
|
||||||
self.continuations.lock().await.insert(guid, handler);
|
self.0.lock().await.insert(guid, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_ws_handler(&self, guid: &Guid) -> Option<WebSocketHandler> {
|
pub async fn get_ws_handler(&self, guid: &Guid) -> Option<WebSocketHandler> {
|
||||||
let mut continuations = self.continuations.lock().await;
|
let mut continuations = self.0.lock().await;
|
||||||
if !matches!(continuations.get(guid), Some(RpcContinuation::WebSocket(_))) {
|
if !matches!(continuations.get(guid), Some(RpcContinuation::WebSocket(_))) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let Some(RpcContinuation::WebSocket(x)) = continuations.remove(guid) else {
|
let Some(RpcContinuation::WebSocket(x)) = continuations.remove(guid) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let handler = x.get().await?;
|
x.get().await
|
||||||
let shutdown = self.shutdown.as_ref().map(|s| s.subscribe());
|
|
||||||
Some(Box::new(move |ws| {
|
|
||||||
let mut fut = handler(ws);
|
|
||||||
fut.shutdown = shutdown;
|
|
||||||
fut
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_rest_handler(&self, guid: &Guid) -> Option<RestHandler> {
|
pub async fn get_rest_handler(&self, guid: &Guid) -> Option<RestHandler> {
|
||||||
let mut continuations = self.continuations.lock().await;
|
let mut continuations: tokio::sync::MutexGuard<'_, BTreeMap<Guid, RpcContinuation>> =
|
||||||
|
self.0.lock().await;
|
||||||
if !matches!(continuations.get(guid), Some(RpcContinuation::Rest(_))) {
|
if !matches!(continuations.get(guid), Some(RpcContinuation::Rest(_))) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,10 +194,15 @@ impl TryFrom<ManifestV1> for Manifest {
|
|||||||
if &*value.id == "nostr" {
|
if &*value.id == "nostr" {
|
||||||
value.id = "nostr-rs-relay".parse()?;
|
value.id = "nostr-rs-relay".parse()?;
|
||||||
}
|
}
|
||||||
|
if &*value.id == "ghost" {
|
||||||
|
value.id = "ghost-legacy".parse()?;
|
||||||
|
}
|
||||||
|
if &*value.id == "synapse" {
|
||||||
|
value.id = "synapse-legacy".parse()?;
|
||||||
|
}
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: value.id,
|
id: value.id,
|
||||||
version: version.into(),
|
version: version.into(),
|
||||||
satisfies: BTreeSet::new(),
|
|
||||||
can_migrate_from: VersionRange::any(),
|
can_migrate_from: VersionRange::any(),
|
||||||
can_migrate_to: VersionRange::none(),
|
can_migrate_to: VersionRange::none(),
|
||||||
metadata: PackageMetadata {
|
metadata: PackageMetadata {
|
||||||
@@ -219,6 +224,7 @@ impl TryFrom<ManifestV1> for Manifest {
|
|||||||
PackageProcedure::Script(_) => false,
|
PackageProcedure::Script(_) => false,
|
||||||
},
|
},
|
||||||
plugins: BTreeSet::new(),
|
plugins: BTreeSet::new(),
|
||||||
|
satisfies: BTreeSet::new(),
|
||||||
},
|
},
|
||||||
images: BTreeMap::new(),
|
images: BTreeMap::new(),
|
||||||
volumes: value
|
volumes: value
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ pub(crate) fn current_version() -> Version {
|
|||||||
pub struct Manifest {
|
pub struct Manifest {
|
||||||
pub id: PackageId,
|
pub id: PackageId,
|
||||||
pub version: VersionString,
|
pub version: VersionString,
|
||||||
pub satisfies: BTreeSet<VersionString>,
|
|
||||||
#[ts(type = "string")]
|
#[ts(type = "string")]
|
||||||
pub can_migrate_to: VersionRange,
|
pub can_migrate_to: VersionRange,
|
||||||
#[ts(type = "string")]
|
#[ts(type = "string")]
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ pub async fn check_dependencies(
|
|||||||
};
|
};
|
||||||
let manifest = package.as_state_info().as_manifest(ManifestPreference::New);
|
let manifest = package.as_state_info().as_manifest(ManifestPreference::New);
|
||||||
let installed_version = manifest.as_version().de()?.into_version();
|
let installed_version = manifest.as_version().de()?.into_version();
|
||||||
let satisfies = manifest.as_satisfies().de()?;
|
let satisfies = manifest.as_metadata().as_satisfies().de()?;
|
||||||
let installed_version = Some(installed_version.clone().into());
|
let installed_version = Some(installed_version.clone().into());
|
||||||
let is_running = package
|
let is_running = package
|
||||||
.as_status_info()
|
.as_status_info()
|
||||||
|
|||||||
@@ -134,8 +134,9 @@ pub async fn list_service_interfaces(
|
|||||||
.expect("valid json pointer");
|
.expect("valid json pointer");
|
||||||
let mut watch = context.seed.ctx.db.watch(ptr).await;
|
let mut watch = context.seed.ctx.db.watch(ptr).await;
|
||||||
|
|
||||||
let res = imbl_value::from_value(watch.peek_and_mark_seen()?)
|
let Some(res) = from_value(watch.peek_and_mark_seen()?)? else {
|
||||||
.unwrap_or_default();
|
return Ok(BTreeMap::new());
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(callback) = callback {
|
if let Some(callback) = callback {
|
||||||
let callback = callback.register(&context.seed.persistent_container);
|
let callback = callback.register(&context.seed.persistent_container);
|
||||||
@@ -174,9 +175,7 @@ pub async fn clear_service_interfaces(
|
|||||||
.as_idx_mut(&package_id)
|
.as_idx_mut(&package_id)
|
||||||
.or_not_found(&package_id)?
|
.or_not_found(&package_id)?
|
||||||
.as_service_interfaces_mut()
|
.as_service_interfaces_mut()
|
||||||
.mutate(|s| {
|
.mutate(|s| Ok(s.retain(|id, _| except.contains(id))))
|
||||||
Ok(s.retain(|id, _| except.contains(id)))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|||||||
@@ -269,6 +269,13 @@ impl ExecParams {
|
|||||||
|
|
||||||
std::os::unix::fs::chroot(chroot)
|
std::os::unix::fs::chroot(chroot)
|
||||||
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("chroot {chroot:?}")))?;
|
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("chroot {chroot:?}")))?;
|
||||||
|
if let Ok(uid) = uid {
|
||||||
|
if uid != 0 {
|
||||||
|
std::os::unix::fs::chown("/proc/self/fd/0", Some(uid), gid.ok()).ok();
|
||||||
|
std::os::unix::fs::chown("/proc/self/fd/1", Some(uid), gid.ok()).ok();
|
||||||
|
std::os::unix::fs::chown("/proc/self/fd/2", Some(uid), gid.ok()).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
// Handle credential changes in pre_exec to control the order:
|
// Handle credential changes in pre_exec to control the order:
|
||||||
// setgroups must happen before setgid/setuid (requires CAP_SETGID)
|
// setgroups must happen before setgid/setuid (requires CAP_SETGID)
|
||||||
{
|
{
|
||||||
@@ -276,10 +283,16 @@ impl ExecParams {
|
|||||||
let set_gid = gid.ok();
|
let set_gid = gid.ok();
|
||||||
unsafe {
|
unsafe {
|
||||||
cmd.pre_exec(move || {
|
cmd.pre_exec(move || {
|
||||||
// Create a new process group so entrypoint scripts that do
|
// Create a new session so entrypoint scripts that do
|
||||||
// kill(0, SIGTERM) don't cascade to other subcontainers.
|
// kill(0, SIGTERM) don't cascade to other subcontainers.
|
||||||
nix::unistd::setsid()
|
// EPERM means we're already a session leader (e.g. pty_process
|
||||||
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
|
// called setsid() for us), which is fine.
|
||||||
|
match nix::unistd::setsid() {
|
||||||
|
Ok(_) | Err(Errno::EPERM) => {}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(std::io::Error::from_raw_os_error(e as i32));
|
||||||
|
}
|
||||||
|
}
|
||||||
if !groups.is_empty() {
|
if !groups.is_empty() {
|
||||||
nix::unistd::setgroups(&groups)
|
nix::unistd::setgroups(&groups)
|
||||||
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
|
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ pub async fn get_data_version(id: &PackageId) -> Result<Option<String>, Error> {
|
|||||||
.join(id)
|
.join(id)
|
||||||
.join("data")
|
.join("data")
|
||||||
.join(".version");
|
.join(".version");
|
||||||
maybe_read_file_to_string(&path).await
|
let s = maybe_read_file_to_string(&path).await?;
|
||||||
|
Ok(s.map(|s| s.trim().to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RootCommand(pub String);
|
struct RootCommand(pub String);
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ impl ServiceActorSeed {
|
|||||||
ErrorKind::Cancelled,
|
ErrorKind::Cancelled,
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
let backup_succeeded = res.is_ok();
|
|
||||||
let id = &self.id;
|
let id = &self.id;
|
||||||
self.ctx
|
self.ctx
|
||||||
.db
|
.db
|
||||||
@@ -52,16 +51,14 @@ impl ServiceActorSeed {
|
|||||||
x => x,
|
x => x,
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
if backup_succeeded {
|
if let Some(progress) = db
|
||||||
if let Some(progress) = db
|
.as_public_mut()
|
||||||
.as_public_mut()
|
.as_server_info_mut()
|
||||||
.as_server_info_mut()
|
.as_status_info_mut()
|
||||||
.as_status_info_mut()
|
.as_backup_progress_mut()
|
||||||
.as_backup_progress_mut()
|
.transpose_mut()
|
||||||
.transpose_mut()
|
{
|
||||||
{
|
progress.insert(id, &BackupProgress { complete: true })?;
|
||||||
progress.insert(id, &BackupProgress { complete: true })?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -557,6 +557,39 @@ pub async fn execute_inner(
|
|||||||
hostname: Option<ServerHostnameInfo>,
|
hostname: Option<ServerHostnameInfo>,
|
||||||
) -> Result<(SetupResult, RpcContext), Error> {
|
) -> Result<(SetupResult, RpcContext), Error> {
|
||||||
let progress = &ctx.progress;
|
let progress = &ctx.progress;
|
||||||
|
|
||||||
|
if !crate::disk::mount::util::is_mountpoint(Path::new(DATA_DIR).join("main")).await? {
|
||||||
|
let mut disk_phase =
|
||||||
|
progress.add_phase(t!("setup.opening-data-drive").into(), Some(10));
|
||||||
|
disk_phase.start();
|
||||||
|
let requires_reboot = crate::disk::main::import(
|
||||||
|
&*guid,
|
||||||
|
DATA_DIR,
|
||||||
|
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
|
||||||
|
RepairStrategy::Aggressive
|
||||||
|
} else {
|
||||||
|
RepairStrategy::Preen
|
||||||
|
},
|
||||||
|
if guid.ends_with("_UNENC") {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(DEFAULT_PASSWORD)
|
||||||
|
},
|
||||||
|
Some(progress),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let _ = ctx.disk_guid.set(guid.clone());
|
||||||
|
crate::util::io::delete_file(REPAIR_DISK_PATH).await?;
|
||||||
|
if requires_reboot.0 {
|
||||||
|
crate::disk::main::export(&*guid, DATA_DIR).await?;
|
||||||
|
return Err(Error::new(
|
||||||
|
eyre!("{}", t!("setup.disk-errors-corrected-restart-required")),
|
||||||
|
ErrorKind::DiskManagement,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
disk_phase.complete();
|
||||||
|
}
|
||||||
|
|
||||||
let restore_phase = match recovery_source.as_ref() {
|
let restore_phase = match recovery_source.as_ref() {
|
||||||
Some(RecoverySource::Backup { .. }) => {
|
Some(RecoverySource::Backup { .. }) => {
|
||||||
Some(progress.add_phase(t!("setup.restoring-backup").into(), Some(100)))
|
Some(progress.add_phase(t!("setup.restoring-backup").into(), Some(100)))
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use ts_rs::TS;
|
|||||||
|
|
||||||
use crate::bins::set_locale;
|
use crate::bins::set_locale;
|
||||||
use crate::context::{CliContext, RpcContext};
|
use crate::context::{CliContext, RpcContext};
|
||||||
|
use crate::db::model::public::RestartReason;
|
||||||
use crate::disk::util::{get_available, get_used};
|
use crate::disk::util::{get_available, get_used};
|
||||||
use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT};
|
use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
@@ -351,10 +352,9 @@ pub fn kiosk<C: Context>() -> ParentHandler<C> {
|
|||||||
from_fn_async(|ctx: RpcContext| async move {
|
from_fn_async(|ctx: RpcContext| async move {
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
db.as_public_mut()
|
let server_info = db.as_public_mut().as_server_info_mut();
|
||||||
.as_server_info_mut()
|
server_info.as_kiosk_mut().ser(&Some(true))?;
|
||||||
.as_kiosk_mut()
|
server_info.as_status_info_mut().as_restart_mut().ser(&Some(RestartReason::Kiosk))
|
||||||
.ser(&Some(true))
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
@@ -369,10 +369,9 @@ pub fn kiosk<C: Context>() -> ParentHandler<C> {
|
|||||||
from_fn_async(|ctx: RpcContext| async move {
|
from_fn_async(|ctx: RpcContext| async move {
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
db.as_public_mut()
|
let server_info = db.as_public_mut().as_server_info_mut();
|
||||||
.as_server_info_mut()
|
server_info.as_kiosk_mut().ser(&Some(false))?;
|
||||||
.as_kiosk_mut()
|
server_info.as_status_info_mut().as_restart_mut().ser(&Some(RestartReason::Kiosk))
|
||||||
.ser(&Some(false))
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
@@ -1367,10 +1366,11 @@ pub async fn set_language(
|
|||||||
save_language(&*language).await?;
|
save_language(&*language).await?;
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
db.as_public_mut()
|
let server_info = db.as_public_mut().as_server_info_mut();
|
||||||
.as_server_info_mut()
|
server_info
|
||||||
.as_language_mut()
|
.as_language_mut()
|
||||||
.ser(&Some(language.clone()))
|
.ser(&Some(language.clone()))?;
|
||||||
|
server_info.as_status_info_mut().as_restart_mut().ser(&Some(RestartReason::Language))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ use crate::util::serde::{HandlerExtSerde, display_serializable};
|
|||||||
|
|
||||||
pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
|
pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
|
||||||
ParentHandler::new()
|
ParentHandler::new()
|
||||||
.subcommand("web", super::web::web_api::<C>())
|
.subcommand(
|
||||||
|
"web",
|
||||||
|
super::web::web_api::<C>().with_about("about.commands-tunnel-web"),
|
||||||
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"db",
|
"db",
|
||||||
super::db::db_api::<C>().with_about("about.commands-interact-with-db-dump-apply"),
|
super::db::db_api::<C>().with_about("about.commands-interact-with-db-dump-apply"),
|
||||||
@@ -69,7 +72,8 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
|
|||||||
.no_display()
|
.no_display()
|
||||||
.with_about("about.enable-or-disable-port-forward")
|
.with_about("about.enable-or-disable-port-forward")
|
||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
),
|
)
|
||||||
|
.with_about("about.commands-port-forward"),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"restart",
|
"restart",
|
||||||
@@ -94,7 +98,8 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
|
|||||||
.with_display_serializable()
|
.with_display_serializable()
|
||||||
.with_about("about.apply-available-update")
|
.with_about("about.apply-available-update")
|
||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
),
|
)
|
||||||
|
.with_about("about.commands-tunnel-update"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,8 @@ pub fn auth_api<C: Context>() -> ParentHandler<C> {
|
|||||||
})
|
})
|
||||||
.with_about("about.list-authorized-keys")
|
.with_about("about.list-authorized-keys")
|
||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
),
|
)
|
||||||
|
.with_about("about.commands-authorized-keys"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ impl TunnelContext {
|
|||||||
listen,
|
listen,
|
||||||
db,
|
db,
|
||||||
datadir,
|
datadir,
|
||||||
rpc_continuations: RpcContinuations::new(None),
|
rpc_continuations: RpcContinuations::new(),
|
||||||
open_authed_continuations: OpenAuthedContinuations::new(),
|
open_authed_continuations: OpenAuthedContinuations::new(),
|
||||||
ephemeral_sessions: SyncMutex::new(Sessions::new()),
|
ephemeral_sessions: SyncMutex::new(Sessions::new()),
|
||||||
net_iface,
|
net_iface,
|
||||||
|
|||||||
@@ -521,7 +521,7 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
|
|||||||
.or_not_found("certificate in chain")?;
|
.or_not_found("certificate in chain")?;
|
||||||
println!("📝 Root CA:");
|
println!("📝 Root CA:");
|
||||||
print!("{cert}\n");
|
print!("{cert}\n");
|
||||||
println!("Follow instructions to trust your Root CA (recommended): https://docs.start9.com/start-tunnel/installing/index.html#trust-your-root-ca");
|
println!("Follow instructions to trust your Root CA (recommended): https://docs.start9.com/start-tunnel/installing.html#trust-your-root-ca");
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use ts_rs::TS;
|
|||||||
|
|
||||||
use crate::PLATFORM;
|
use crate::PLATFORM;
|
||||||
use crate::context::{CliContext, RpcContext};
|
use crate::context::{CliContext, RpcContext};
|
||||||
|
use crate::db::model::public::RestartReason;
|
||||||
use crate::notifications::{NotificationLevel, notify};
|
use crate::notifications::{NotificationLevel, notify};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::progress::{
|
use crate::progress::{
|
||||||
@@ -81,8 +82,9 @@ pub async fn update_system(
|
|||||||
.into_public()
|
.into_public()
|
||||||
.into_server_info()
|
.into_server_info()
|
||||||
.into_status_info()
|
.into_status_info()
|
||||||
.into_updated()
|
.into_restart()
|
||||||
.de()?
|
.de()?
|
||||||
|
== Some(RestartReason::Update)
|
||||||
{
|
{
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!("{}", t!("update.already-updated-restart-required")),
|
eyre!("{}", t!("update.already-updated-restart-required")),
|
||||||
@@ -281,10 +283,18 @@ async fn maybe_do_update(
|
|||||||
|
|
||||||
let start_progress = progress.snapshot();
|
let start_progress = progress.snapshot();
|
||||||
|
|
||||||
let status = ctx
|
ctx.db
|
||||||
.db
|
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
let mut status = peeked.as_public().as_server_info().as_status_info().de()?;
|
let server_info = db.as_public_mut().as_server_info_mut();
|
||||||
|
|
||||||
|
if server_info.as_status_info().as_restart().de()?.is_some() {
|
||||||
|
return Err(Error::new(
|
||||||
|
eyre!("{}", t!("update.already-updated-restart-required")),
|
||||||
|
crate::ErrorKind::InvalidRequest,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut status = server_info.as_status_info().de()?;
|
||||||
if status.update_progress.is_some() {
|
if status.update_progress.is_some() {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!("{}", t!("update.already-updating")),
|
eyre!("{}", t!("update.already-updating")),
|
||||||
@@ -293,22 +303,12 @@ async fn maybe_do_update(
|
|||||||
}
|
}
|
||||||
|
|
||||||
status.update_progress = Some(start_progress);
|
status.update_progress = Some(start_progress);
|
||||||
db.as_public_mut()
|
server_info.as_status_info_mut().ser(&status)?;
|
||||||
.as_server_info_mut()
|
Ok(())
|
||||||
.as_status_info_mut()
|
|
||||||
.ser(&status)?;
|
|
||||||
Ok(status)
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|
||||||
if status.updated {
|
|
||||||
return Err(Error::new(
|
|
||||||
eyre!("{}", t!("update.already-updated-restart-required")),
|
|
||||||
crate::ErrorKind::InvalidRequest,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let progress_task = NonDetachingJoinHandle::from(tokio::spawn(progress.clone().sync_to_db(
|
let progress_task = NonDetachingJoinHandle::from(tokio::spawn(progress.clone().sync_to_db(
|
||||||
ctx.db.clone(),
|
ctx.db.clone(),
|
||||||
|db| {
|
|db| {
|
||||||
@@ -338,10 +338,15 @@ async fn maybe_do_update(
|
|||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
let status_info =
|
let server_info = db.as_public_mut().as_server_info_mut();
|
||||||
db.as_public_mut().as_server_info_mut().as_status_info_mut();
|
server_info
|
||||||
status_info.as_update_progress_mut().ser(&None)?;
|
.as_status_info_mut()
|
||||||
status_info.as_updated_mut().ser(&true)
|
.as_update_progress_mut()
|
||||||
|
.ser(&None)?;
|
||||||
|
server_info
|
||||||
|
.as_status_info_mut()
|
||||||
|
.as_restart_mut()
|
||||||
|
.ser(&Some(RestartReason::Update))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use tokio::io::AsyncWrite;
|
|||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
|
|
||||||
const BLOCK_SIZE: usize = 4096;
|
const BLOCK_SIZE: usize = 4096;
|
||||||
const BUF_CAP: usize = 256 * 1024; // 256KB
|
const BUF_CAP: usize = 1024 * 1024; // 1MiB
|
||||||
|
|
||||||
/// Aligned buffer for O_DIRECT I/O.
|
/// Aligned buffer for O_DIRECT I/O.
|
||||||
struct AlignedBuf {
|
struct AlignedBuf {
|
||||||
|
|||||||
@@ -26,6 +26,30 @@ impl<'a> MakeWriter<'a> for LogFile {
|
|||||||
struct TeeWriter<'a>(MutexGuard<'a, Option<File>>);
|
struct TeeWriter<'a>(MutexGuard<'a, Option<File>>);
|
||||||
impl<'a> Write for TeeWriter<'a> {
|
impl<'a> Write for TeeWriter<'a> {
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
// Blocking file+stderr I/O on a tokio worker thread can
|
||||||
|
// starve the I/O driver (tokio-rs/tokio#4730).
|
||||||
|
// block_in_place tells the runtime to hand off driver
|
||||||
|
// duties before we block. Only available on the
|
||||||
|
// multi-thread runtime; falls back to a direct write on
|
||||||
|
// current-thread runtimes (CLI) or outside a runtime.
|
||||||
|
if matches!(
|
||||||
|
tokio::runtime::Handle::try_current().map(|h| h.runtime_flavor()),
|
||||||
|
Ok(tokio::runtime::RuntimeFlavor::MultiThread),
|
||||||
|
) {
|
||||||
|
tokio::task::block_in_place(|| self.write_inner(buf))
|
||||||
|
} else {
|
||||||
|
self.write_inner(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
if let Some(f) = &mut *self.0 {
|
||||||
|
f.flush()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> TeeWriter<'a> {
|
||||||
|
fn write_inner(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
let n = if let Some(f) = &mut *self.0 {
|
let n = if let Some(f) = &mut *self.0 {
|
||||||
f.write(buf)?
|
f.write(buf)?
|
||||||
} else {
|
} else {
|
||||||
@@ -34,12 +58,6 @@ impl<'a> MakeWriter<'a> for LogFile {
|
|||||||
io::stderr().write_all(&buf[..n])?;
|
io::stderr().write_all(&buf[..n])?;
|
||||||
Ok(n)
|
Ok(n)
|
||||||
}
|
}
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
|
||||||
if let Some(f) = &mut *self.0 {
|
|
||||||
f.flush()?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Box::new(TeeWriter(f))
|
Box::new(TeeWriter(f))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -62,8 +62,10 @@ mod v0_4_0_alpha_19;
|
|||||||
mod v0_4_0_alpha_20;
|
mod v0_4_0_alpha_20;
|
||||||
mod v0_4_0_alpha_21;
|
mod v0_4_0_alpha_21;
|
||||||
mod v0_4_0_alpha_22;
|
mod v0_4_0_alpha_22;
|
||||||
|
mod v0_4_0_alpha_23;
|
||||||
|
mod v0_4_0_beta_0;
|
||||||
|
|
||||||
pub type Current = v0_4_0_alpha_22::Version; // VERSION_BUMP
|
pub type Current = v0_4_0_beta_0::Version; // VERSION_BUMP
|
||||||
|
|
||||||
impl Current {
|
impl Current {
|
||||||
#[instrument(skip(self, db))]
|
#[instrument(skip(self, db))]
|
||||||
@@ -193,7 +195,9 @@ enum Version {
|
|||||||
V0_4_0_alpha_19(Wrapper<v0_4_0_alpha_19::Version>),
|
V0_4_0_alpha_19(Wrapper<v0_4_0_alpha_19::Version>),
|
||||||
V0_4_0_alpha_20(Wrapper<v0_4_0_alpha_20::Version>),
|
V0_4_0_alpha_20(Wrapper<v0_4_0_alpha_20::Version>),
|
||||||
V0_4_0_alpha_21(Wrapper<v0_4_0_alpha_21::Version>),
|
V0_4_0_alpha_21(Wrapper<v0_4_0_alpha_21::Version>),
|
||||||
V0_4_0_alpha_22(Wrapper<v0_4_0_alpha_22::Version>), // VERSION_BUMP
|
V0_4_0_alpha_22(Wrapper<v0_4_0_alpha_22::Version>),
|
||||||
|
V0_4_0_alpha_23(Wrapper<v0_4_0_alpha_23::Version>),
|
||||||
|
V0_4_0_beta_0(Wrapper<v0_4_0_beta_0::Version>), // VERSION_BUMP
|
||||||
Other(exver::Version),
|
Other(exver::Version),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +262,9 @@ impl Version {
|
|||||||
Self::V0_4_0_alpha_19(v) => DynVersion(Box::new(v.0)),
|
Self::V0_4_0_alpha_19(v) => DynVersion(Box::new(v.0)),
|
||||||
Self::V0_4_0_alpha_20(v) => DynVersion(Box::new(v.0)),
|
Self::V0_4_0_alpha_20(v) => DynVersion(Box::new(v.0)),
|
||||||
Self::V0_4_0_alpha_21(v) => DynVersion(Box::new(v.0)),
|
Self::V0_4_0_alpha_21(v) => DynVersion(Box::new(v.0)),
|
||||||
Self::V0_4_0_alpha_22(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
|
Self::V0_4_0_alpha_22(v) => DynVersion(Box::new(v.0)),
|
||||||
|
Self::V0_4_0_alpha_23(v) => DynVersion(Box::new(v.0)),
|
||||||
|
Self::V0_4_0_beta_0(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
|
||||||
Self::Other(v) => {
|
Self::Other(v) => {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!("unknown version {v}"),
|
eyre!("unknown version {v}"),
|
||||||
@@ -315,7 +321,9 @@ impl Version {
|
|||||||
Version::V0_4_0_alpha_19(Wrapper(x)) => x.semver(),
|
Version::V0_4_0_alpha_19(Wrapper(x)) => x.semver(),
|
||||||
Version::V0_4_0_alpha_20(Wrapper(x)) => x.semver(),
|
Version::V0_4_0_alpha_20(Wrapper(x)) => x.semver(),
|
||||||
Version::V0_4_0_alpha_21(Wrapper(x)) => x.semver(),
|
Version::V0_4_0_alpha_21(Wrapper(x)) => x.semver(),
|
||||||
Version::V0_4_0_alpha_22(Wrapper(x)) => x.semver(), // VERSION_BUMP
|
Version::V0_4_0_alpha_22(Wrapper(x)) => x.semver(),
|
||||||
|
Version::V0_4_0_alpha_23(Wrapper(x)) => x.semver(),
|
||||||
|
Version::V0_4_0_beta_0(Wrapper(x)) => x.semver(), // VERSION_BUMP
|
||||||
Version::Other(x) => x.clone(),
|
Version::Other(x) => x.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,108 +8,120 @@ Previous backups are incompatible with v0.4.0. It is strongly recommended that y
|
|||||||
|
|
||||||
A server is not a toy. It is a critical component of the computing paradigm, and its failure can be catastrophic, resulting in downtime or loss of data. From the beginning, Start9 has taken a "security and reliability first" approach to the development of StartOS, favoring soundness over speed, and prioritizing essential features such as encrypted network connections, simple backups, and a reliable container runtime over nice-to-haves like custom theming and more services.
|
A server is not a toy. It is a critical component of the computing paradigm, and its failure can be catastrophic, resulting in downtime or loss of data. From the beginning, Start9 has taken a "security and reliability first" approach to the development of StartOS, favoring soundness over speed, and prioritizing essential features such as encrypted network connections, simple backups, and a reliable container runtime over nice-to-haves like custom theming and more services.
|
||||||
|
|
||||||
Start9 is paving new ground with StartOS, trying to create what most developers and IT professionals thought impossible; namely, an OS and user experience that affords a normal person the same independent control over their data and communications as an experienced Linux sysadmin.
|
Start9 is paving new ground with StartOS, trying to create what most developers and IT professionals thought impossible: an OS and user experience that affords a normal person the same independent control over their data and communications as an experienced Linux sysadmin.
|
||||||
|
|
||||||
The difficulty of our endeavor requires making mistakes; and our integrity and dedication to excellence require that we correct them. This means a willingness to discard bad ideas and broken parts, and if absolutely necessary, to tear it all down and start over. That is exactly what we did with StartOS v0.2.0 in 2020. It is what we did with StartOS v0.3.0 in 2022. And we are doing it now with StartOS v0.4.0 in 2026.
|
The difficulty of our endeavor requires making mistakes, and our integrity and dedication to excellence require that we correct them. This means a willingness to discard bad ideas and broken parts, and if absolutely necessary, to tear it all down and start over. That is exactly what we did with StartOS v0.2.0 in 2020. It is what we did with StartOS v0.3.0 in 2022. And we are doing it now with StartOS v0.4.0 in 2026.
|
||||||
|
|
||||||
v0.4.0 is a complete rewrite of StartOS, almost nothing survived. After nearly six years of building StartOS, we believe that we have finally arrived at the correct architecture and foundation that will allow us to deliver on the promise of sovereign computing.
|
v0.4.0 is a complete rewrite of StartOS — almost nothing survived. After nearly six years of building StartOS, we believe we have finally arrived at the correct architecture and foundation to deliver on the promise of sovereign computing.
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
### New User interface
|
### User Experience
|
||||||
|
|
||||||
We re-wrote the StartOS UI to be more performant, more intuitive, and better looking on both mobile and desktop. Enjoy.
|
#### New User Interface
|
||||||
|
|
||||||
### Translations
|
The StartOS UI has been rewritten to be more performant, more intuitive, and better looking on both mobile and desktop.
|
||||||
|
|
||||||
StartOS v0.4.0 supports multiple languages and also makes it easy to add more later on.
|
#### Internationalization
|
||||||
|
|
||||||
### LXC Container Runtime
|
StartOS v0.4.0 and available services now support multiple languages and keyboard layouts.
|
||||||
|
|
||||||
Neither Docker nor Podman offer the reliability and flexibility needed for StartOS. Instead, v0.4.0 uses a nested container paradigm based on LXC for the outer container and Linux namespaces for sub containers. This architecture naturally supports multi container setups.
|
#### Improved Actions
|
||||||
|
|
||||||
### Hardware Acceleration
|
Actions accept arbitrary form input and return arbitrary responses, replacing the old "Config" and "Properties" concepts, which have been removed. The new Actions API gives package developers the ability to break configuration and properties into smaller, more specific forms — or to exclude them entirely without polluting the UI. Improved form design and new input types round out the experience.
|
||||||
|
|
||||||
Services can take advantage of (and require) the presence of certain hardware modules, such as Nvidia GPUs, for transcoding or inference purposes. For example, StartOS and Ollama can run natively on The Nvidia DGX Spark and take full advantage of the hardware/firmware stack to perform local inference against open source models.
|
#### Progress Reporting
|
||||||
|
|
||||||
### New S9PK archive format
|
A new progress reporting API enables package developers to define custom phases and provide real-time progress updates for operations such as installing, updating, or backing up a service.
|
||||||
|
|
||||||
The S9PK archive format has been overhauled to allow for signature verification of partial downloads, and allow direct mounting of container images without unpacking the s9pk.
|
#### Email Notifications via SMTP
|
||||||
|
|
||||||
### Improved Actions
|
You can now add your Gmail, SES, or other SMTP credentials to StartOS to deliver email notifications from StartOS and from installed services that support SMTP.
|
||||||
|
|
||||||
Actions take arbitrary form input and return arbitrary responses, thus satisfying the needs of both "Config" and "Properties", which have now been removed. The new actions API gives package developers the ability to break up Config and Properties into smaller, more specific formats, or to exclude them entirely without polluting the UI. Improved form design and new input types round out the new actions experience.
|
### Networking & Connectivity
|
||||||
|
|
||||||
### Squashfs Images for OS Updates
|
#### LAN Port Forwarding
|
||||||
|
|
||||||
StartOS now uses squashfs images instead of rsync for OS updates. This allows for better update verification and improved reliability.
|
Perhaps the biggest complaint with prior versions of StartOS was the use of unique `.local` URLs for service interfaces. This has been corrected. Service interfaces are now available on unique ports, supporting non-HTTP traffic on the LAN as well as remote access via VPN.
|
||||||
|
|
||||||
### Typescript Package API and SDK
|
#### Gateways
|
||||||
|
|
||||||
Package developers can now take advantage of StartOS APIs using the new start-sdk, available in Typescript. A barebones StartOS package (s9pk) can be produced in minutes with minimal knowledge or skill. More advanced developers can use the SDK to create highly customized user experiences for their service.
|
Gateways connect your server to the Internet, facilitating inbound and outbound traffic. It is now possible to add Wireguard VPN gateways to your server to control how devices outside the LAN connect to your server and how your server connects out to the Internet. Outbound traffic can also be overridden on a per-service basis.
|
||||||
|
|
||||||
### Removed PostgresSQL
|
#### Private Domains
|
||||||
|
|
||||||
StartOS itself has miniscule data persistence needs. PostgresSQL was overkill and has been removed in favor of lightweight PatchDB.
|
A private domain is like your server's `.local` address, except it also works over VPN, and it can be _anything_ — a real domain you control, a made-up domain, or even a domain controlled by someone else.
|
||||||
|
|
||||||
### Sending Emails via SMTP
|
Like your local domain, private domains can only be accessed when connected to the same LAN as your server, either physically or via VPN, and they require trusting your server's Root CA.
|
||||||
|
|
||||||
You can now add your Gmail, SES, or other SMTP credentials to StartOS in order to send deliver email notifications from StartOS and from installed services that support SMTP.
|
#### Public Domains (Clearnet)
|
||||||
|
|
||||||
### SSH password auth
|
It is now easy to expose service interfaces to the public Internet on a domain you control. There are two options:
|
||||||
|
|
||||||
|
1. **Open ports on your router.** This option is free and supported by all routers. The drawback is that your home IP address is revealed to anyone accessing an exposed interface.
|
||||||
|
|
||||||
|
2. **Use a Wireguard reverse tunnel**, such as [StartTunnel](#start-tunnel), to proxy web traffic. This option requires renting a $5–$10/month VPS and installing StartTunnel (or similar). The result is a virtual router in the cloud that you can use to expose service interfaces instead of your real router, hiding your IP address from visitors.
|
||||||
|
|
||||||
|
#### Let's Encrypt
|
||||||
|
|
||||||
|
StartOS now supports Let's Encrypt to automatically obtain SSL/TLS certificates for public domains. Visitors to your public websites and APIs will no longer need to download and trust your server's Root CA.
|
||||||
|
|
||||||
|
#### Internal DNS Server
|
||||||
|
|
||||||
|
StartOS runs its own DNS server and automatically adds records for your private domains. You can configure your router or other gateway to use the StartOS DNS server to resolve these domains locally.
|
||||||
|
|
||||||
|
#### Static DNS Servers
|
||||||
|
|
||||||
|
By default, StartOS uses the DNS servers it receives via DHCP from its gateway(s). It is now possible to override these with custom, static DNS servers.
|
||||||
|
|
||||||
|
#### Tor as a Plugin
|
||||||
|
|
||||||
|
With the expanded networking capabilities of StartOS v0.4.0, Tor is now an optional plugin that can be installed from the Marketplace. Users can run their own Tor relay, route outbound connections through Tor, and generate hidden service URLs for any service interface, including vanity addresses.
|
||||||
|
|
||||||
|
#### Tor Address Management
|
||||||
|
|
||||||
|
StartOS v0.4.0 supports adding and removing Tor addresses for both StartOS itself and all service interfaces. You can even provide your own private key instead of using one auto-generated by StartOS, enabling vanity addresses.
|
||||||
|
|
||||||
|
### System & Infrastructure
|
||||||
|
|
||||||
|
#### LXC Container Runtime
|
||||||
|
|
||||||
|
Neither Docker nor Podman offer the reliability and flexibility needed for StartOS. Instead, v0.4.0 uses a nested container paradigm based on LXC for the outer container and Linux namespaces for sub-containers. This architecture naturally supports multi-container setups.
|
||||||
|
|
||||||
|
#### Hardware Acceleration
|
||||||
|
|
||||||
|
Services can take advantage of — and require — the presence of certain hardware modules, such as Nvidia GPUs, for transcoding or inference. For example, StartOS and Ollama can run natively on the Nvidia DGX Spark and take full advantage of its hardware and firmware stack to perform local inference against open source models.
|
||||||
|
|
||||||
|
#### Squashfs Images for OS Updates
|
||||||
|
|
||||||
|
StartOS now uses squashfs images instead of rsync for OS updates, enabling better update verification and improved reliability.
|
||||||
|
|
||||||
|
#### Replaced PostgreSQL with PatchDB
|
||||||
|
|
||||||
|
StartOS itself has minimal data persistence needs. PostgreSQL was overkill and has been replaced with the lightweight PatchDB.
|
||||||
|
|
||||||
|
#### Improved Backups
|
||||||
|
|
||||||
|
The new `start-fs` FUSE module unifies filesystem expectations across platforms, enabling more reliable backups. The system now defaults to rsync differential backups instead of incremental backups, which is both faster and more space-efficient — files deleted from the server are also deleted from the backup.
|
||||||
|
|
||||||
|
#### SSH Password Authentication
|
||||||
|
|
||||||
You can now SSH into your server using your master password. SSH public key authentication is still supported as well.
|
You can now SSH into your server using your master password. SSH public key authentication is still supported as well.
|
||||||
|
|
||||||
### Tor Address Management
|
### Developer Experience
|
||||||
|
|
||||||
StartOS v0.4.0 supports adding and removing Tor addresses for StartOS and all service interfaces. You can even provide your own private key instead of using one auto-generated by StartOS. This has the added benefit of permitting vanity addresses.
|
#### New S9PK Archive Format
|
||||||
|
|
||||||
### Progress Reporting
|
The S9PK archive format has been overhauled to support signature verification of partial downloads and direct mounting of container images without unpacking the archive.
|
||||||
|
|
||||||
A new progress reporting API enabled package developers to create unique phases and provide real-time progress reporting for actions such as installing, updating, or backing up a service.
|
#### TypeScript Package API and SDK
|
||||||
|
|
||||||
### Registry Protocol
|
Package developers can now interact with StartOS APIs using the new `start-sdk`, available in TypeScript. A barebones StartOS package (S9PK) can be produced in minutes with minimal knowledge or skill. More advanced developers can use the SDK to create highly customized user experiences for their services.
|
||||||
|
|
||||||
The new registry protocol bifurcates package indexing (listing/validating) and package hosting (downloading). Registries are now simple indexes of packages that reference binaries hosted in arbitrary locations, locally or externally. For example, when someone visits the Start9 Registry, the curated list of packages comes from Start9. But when someone installs a listed service, the package binary is being downloaded from Github. The registry also validates the binary. This makes it much easier to host a custom registry, since it is just a curated list of services tat reference package binaries hosted on Github or elsewhere.
|
#### Registry Protocol
|
||||||
|
|
||||||
### LAN port forwarding
|
The new registry protocol separates package indexing (listing and validation) from package hosting (downloading). Registries are now simple indexes that reference binaries hosted in arbitrary locations, locally or externally. For example, when someone visits the Start9 Registry, the curated list of packages comes from Start9, but when they install a service, the binary is downloaded from GitHub. The registry also validates the binary. This makes it much easier to host a custom registry, since it is just a curated list of services that reference package binaries hosted on GitHub or elsewhere.
|
||||||
|
|
||||||
Perhaps the biggest complaint with prior version of StartOS was use of unique .local URLs for service interfaces. This has been corrected. Service interfaces are now available on unique ports, allowing for non-http traffic on the LAN as well as remote access via VPN.
|
#### Exver and Service Flavors
|
||||||
|
|
||||||
### Improved Backups
|
StartOS now uses Extended Versioning (Exver), which consists of three parts: (1) a semver-compliant upstream version, (2) a semver-compliant wrapper version, and (3) an optional "flavor" prefix. Flavors are alternative implementations of a service where a user would typically want only one installed, and data can be migrated between them. Flavors commonly satisfy the same dependency API for downstream packages, though this is not strictly required. A valid Exver looks like: `#knots:29.0:1.0-beta.1` — the first beta release of StartOS wrapper version 1.0 of Bitcoin Knots version 29.0.
|
||||||
|
|
||||||
The new start-fs fuse module unifies file system expectations for various platforms, enabling more reliable backups. The new system also defaults to using rsync differential backups instead of incremental backups, which is faster and saves on disk space by also deleting from the backup files that were deleted from the server.
|
|
||||||
|
|
||||||
### Exver
|
|
||||||
|
|
||||||
StartOS now uses Extended Versioning (Exver), which consists of three parts: (1) a Semver-compliant upstream version, (2) a Semver-compliant wrapper version, and (3) an optional "flavor" prefix. Flavors can be thought of as alternative implementations of services, where a user would only want one or the other installed, and data can feasibly be migrating between the two. Another common characteristic of flavors is that they satisfy the same API requirement of dependents, though this is not strictly necessary. A valid Exver looks something like this: `#knots:29.0:1.0-beta.1`. This would translate to "the first beta release of StartOS wrapper version 1.0 of Bitcoin Knots version 29.0".
|
|
||||||
|
|
||||||
### Let's Encrypt
|
|
||||||
|
|
||||||
StartOS now supports Let's Encrypt to automatically obtain SSL/TLS certificates for public domains. This means people visiting your public websites and APIs will not need to download and trust your server's Root CA.
|
|
||||||
|
|
||||||
### Gateways
|
|
||||||
|
|
||||||
Gateways connect your server to the Internet, facilitating inbound and outbound traffic. Your router is a gateway. It is now possible to add Wireguard VPN gateways to your server to control how devices outside the LAN connect to your server and how your server connects out to the Internet.
|
|
||||||
|
|
||||||
### Static DNS Servers
|
|
||||||
|
|
||||||
By default, StartOS uses the DNS servers it receives via DHCP from its gateway(s). It is now possible to override these DNS servers with custom, static ones.
|
|
||||||
|
|
||||||
### Internal DNS Server
|
|
||||||
|
|
||||||
StartOS runs its own DNS server and automatically adds records for your private domains. You can update your router or other gateway to use StartOS DNS server in order to resolve these domains locally.
|
|
||||||
|
|
||||||
### Private Domains
|
|
||||||
|
|
||||||
A private domain is like to your server's .local, except it also works for VPN connectivity, and it can be _anything_. It can be a real domain you control, a made up domain, or even a domain controlled by someone else.
|
|
||||||
|
|
||||||
Similar to your local domain, private domains can only be accessed when connected to the same LAN as your server, either physically or via VPN, and they require trusting your server's Root CA.
|
|
||||||
|
|
||||||
### Public Domains (Clearnet)
|
|
||||||
|
|
||||||
It is now easy to expose service interfaces to the public Internet on a public domain you control. There are two options, both of which are easy to accomplish:
|
|
||||||
|
|
||||||
1. Open ports on your router. This option is free and supported by all routers. The drawback is that your home IP address is revealed to anyone accessing an exposed interface.
|
|
||||||
|
|
||||||
2. Use a Wireguard reverse tunnel, such as [StartTunnel](#start-tunnel) to proxy web traffic. This option requires renting a $5-$10/month VPS and installing StartTunnel (or similar). The result is a new gateway, a virtual router in the cloud, that you can use to expose service interfaces instead of your real router, thereby hiding your IP address from visitors.
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ use crate::net::keys::KeyStore;
|
|||||||
use crate::notifications::Notifications;
|
use crate::notifications::Notifications;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||||
|
use crate::s9pk::v2::pack::CONTAINER_TOOL;
|
||||||
use crate::ssh::{SshKeys, SshPubKey};
|
use crate::ssh::{SshKeys, SshPubKey};
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
use crate::util::serde::Pem;
|
use crate::util::serde::Pem;
|
||||||
@@ -39,6 +40,22 @@ lazy_static::lazy_static! {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// All pre-0.4.0 StartOS images were initialized with the en_GB.UTF-8 locale.
|
||||||
|
/// The current trixie image does not ship it. Without it PostgreSQL starts
|
||||||
|
/// but refuses connections, breaking the migration.
|
||||||
|
async fn ensure_en_gb_locale() -> Result<(), Error> {
|
||||||
|
Command::new("localedef")
|
||||||
|
.arg("-i")
|
||||||
|
.arg("en_GB")
|
||||||
|
.arg("-c")
|
||||||
|
.arg("-f")
|
||||||
|
.arg("UTF-8")
|
||||||
|
.arg("en_GB.UTF-8")
|
||||||
|
.invoke(crate::ErrorKind::Database)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
async fn init_postgres(datadir: impl AsRef<Path>) -> Result<PgPool, Error> {
|
async fn init_postgres(datadir: impl AsRef<Path>) -> Result<PgPool, Error> {
|
||||||
let db_dir = datadir.as_ref().join("main/postgresql");
|
let db_dir = datadir.as_ref().join("main/postgresql");
|
||||||
@@ -90,6 +107,12 @@ async fn init_postgres(datadir: impl AsRef<Path>) -> Result<PgPool, Error> {
|
|||||||
|
|
||||||
crate::disk::mount::util::bind(&db_dir, "/var/lib/postgresql", false).await?;
|
crate::disk::mount::util::bind(&db_dir, "/var/lib/postgresql", false).await?;
|
||||||
|
|
||||||
|
// The cluster may have been created with a locale not present on the
|
||||||
|
// current image (e.g. en_GB.UTF-8 on a server that predates the trixie
|
||||||
|
// image). Detect and generate it before starting PostgreSQL, otherwise
|
||||||
|
// PG will start but refuse connections.
|
||||||
|
ensure_en_gb_locale().await?;
|
||||||
|
|
||||||
Command::new("systemctl")
|
Command::new("systemctl")
|
||||||
.arg("start")
|
.arg("start")
|
||||||
.arg(format!("postgresql@{pg_version}-main.service"))
|
.arg(format!("postgresql@{pg_version}-main.service"))
|
||||||
@@ -143,12 +166,11 @@ pub struct Version;
|
|||||||
|
|
||||||
impl VersionT for Version {
|
impl VersionT for Version {
|
||||||
type Previous = v0_3_5_2::Version;
|
type Previous = v0_3_5_2::Version;
|
||||||
/// (package_id, host_id, expanded_key)
|
|
||||||
type PreUpRes = (
|
type PreUpRes = (
|
||||||
AccountInfo,
|
AccountInfo,
|
||||||
SshKeys,
|
SshKeys,
|
||||||
CifsTargets,
|
CifsTargets,
|
||||||
Vec<(String, String, [u8; 64])>,
|
BTreeMap<(String, String), [u8; 64]>,
|
||||||
);
|
);
|
||||||
fn semver(self) -> exver::Version {
|
fn semver(self) -> exver::Version {
|
||||||
V0_3_6_alpha_0.clone()
|
V0_3_6_alpha_0.clone()
|
||||||
@@ -250,7 +272,7 @@ impl VersionT for Version {
|
|||||||
let mut onion_map: Value = json!({});
|
let mut onion_map: Value = json!({});
|
||||||
let onion_obj = onion_map.as_object_mut().unwrap();
|
let onion_obj = onion_map.as_object_mut().unwrap();
|
||||||
let mut tor_migration = imbl::Vector::<Value>::new();
|
let mut tor_migration = imbl::Vector::<Value>::new();
|
||||||
for (package_id, host_id, key_bytes) in &tor_keys {
|
for ((package_id, host_id), key_bytes) in &tor_keys {
|
||||||
let onion_addr = onion_address_from_key(key_bytes);
|
let onion_addr = onion_address_from_key(key_bytes);
|
||||||
let encoded_key =
|
let encoded_key =
|
||||||
base64::Engine::encode(&crate::util::serde::BASE64, key_bytes);
|
base64::Engine::encode(&crate::util::serde::BASE64, key_bytes);
|
||||||
@@ -326,7 +348,66 @@ impl VersionT for Version {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tokio::fs::metadata("/media/startos/data/package-data/volumes/ghost")
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
tokio::fs::rename(
|
||||||
|
"/media/startos/data/package-data/volumes/ghost",
|
||||||
|
"/media/startos/data/package-data/volumes/ghost-legacy",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokio::fs::metadata("/media/startos/data/package-data/volumes/synapse")
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
tokio::fs::rename(
|
||||||
|
"/media/startos/data/package-data/volumes/synapse",
|
||||||
|
"/media/startos/data/package-data/volumes/synapse-legacy",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load bundled migration images (start9/compat, start9/utils,
|
||||||
|
// tonistiigi/binfmt) so the v1->v2 s9pk conversion doesn't need
|
||||||
|
// internet access.
|
||||||
|
let migration_images_dir = Path::new("/usr/lib/startos/migration-images");
|
||||||
|
if let Ok(mut entries) = tokio::fs::read_dir(migration_images_dir).await {
|
||||||
|
while let Some(entry) = entries.next_entry().await? {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension() == Some(OsStr::new("tar")) {
|
||||||
|
tracing::info!("Loading migration image: {}", path.display());
|
||||||
|
Command::new(*CONTAINER_TOOL)
|
||||||
|
.arg("load")
|
||||||
|
.arg("-i")
|
||||||
|
.arg(&path)
|
||||||
|
.invoke(crate::ErrorKind::Docker)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Should be the name of the package
|
// Should be the name of the package
|
||||||
|
let current_package: std::sync::Arc<tokio::sync::watch::Sender<Option<PackageId>>> =
|
||||||
|
std::sync::Arc::new(tokio::sync::watch::channel(None).0);
|
||||||
|
let progress_logger = {
|
||||||
|
let current_package = current_package.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut interval = tokio::time::interval(std::time::Duration::from_secs(30));
|
||||||
|
interval.tick().await; // skip immediate first tick
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
if let Some(ref id) = *current_package.borrow() {
|
||||||
|
tracing::info!(
|
||||||
|
"{}",
|
||||||
|
t!("migration.migrating-package", package = id.to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
let mut paths = tokio::fs::read_dir(path).await?;
|
let mut paths = tokio::fs::read_dir(path).await?;
|
||||||
while let Some(path) = paths.next_entry().await? {
|
while let Some(path) = paths.next_entry().await? {
|
||||||
let Ok(id) = path.file_name().to_string_lossy().parse::<PackageId>() else {
|
let Ok(id) = path.file_name().to_string_lossy().parse::<PackageId>() else {
|
||||||
@@ -367,6 +448,12 @@ impl VersionT for Version {
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"{}",
|
||||||
|
t!("migration.migrating-package", package = id.to_string())
|
||||||
|
);
|
||||||
|
current_package.send_replace(Some(id.clone()));
|
||||||
|
|
||||||
if let Err(e) = async {
|
if let Err(e) = async {
|
||||||
let package_s9pk = tokio::fs::File::open(path).await?;
|
let package_s9pk = tokio::fs::File::open(path).await?;
|
||||||
let file = MultiCursorFile::open(&package_s9pk).await?;
|
let file = MultiCursorFile::open(&package_s9pk).await?;
|
||||||
@@ -411,6 +498,7 @@ impl VersionT for Version {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
progress_logger.abort();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -534,13 +622,16 @@ async fn previous_ssh_keys(pg: &sqlx::Pool<sqlx::Postgres>) -> Result<SshKeys, E
|
|||||||
Ok(ssh_keys)
|
Ok(ssh_keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `Vec<(package_id, host_id, expanded_key)>`.
|
/// Returns deduplicated map of `(package_id, host_id) -> expanded_key`.
|
||||||
/// Server key uses `("STARTOS", "STARTOS")`.
|
/// Server key uses `("STARTOS", "STARTOS")`.
|
||||||
|
/// When the same (package, interface) exists in both the `network_keys` and
|
||||||
|
/// `tor` tables, the `tor` table entry wins because it contains the actual
|
||||||
|
/// expanded key that was used by tor.
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
async fn previous_tor_keys(
|
async fn previous_tor_keys(
|
||||||
pg: &sqlx::Pool<sqlx::Postgres>,
|
pg: &sqlx::Pool<sqlx::Postgres>,
|
||||||
) -> Result<Vec<(String, String, [u8; 64])>, Error> {
|
) -> Result<BTreeMap<(String, String), [u8; 64]>, Error> {
|
||||||
let mut keys = Vec::new();
|
let mut keys = BTreeMap::new();
|
||||||
|
|
||||||
// Server tor key from the account table.
|
// Server tor key from the account table.
|
||||||
// Older installs have tor_key (64 bytes). Newer installs (post-NetworkKeys migration)
|
// Older installs have tor_key (64 bytes). Newer installs (post-NetworkKeys migration)
|
||||||
@@ -551,15 +642,14 @@ async fn previous_tor_keys(
|
|||||||
.with_kind(ErrorKind::Database)?;
|
.with_kind(ErrorKind::Database)?;
|
||||||
if let Ok(tor_key) = row.try_get::<Vec<u8>, _>("tor_key") {
|
if let Ok(tor_key) = row.try_get::<Vec<u8>, _>("tor_key") {
|
||||||
if let Ok(key) = <[u8; 64]>::try_from(tor_key) {
|
if let Ok(key) = <[u8; 64]>::try_from(tor_key) {
|
||||||
keys.push(("STARTOS".to_owned(), "STARTOS".to_owned(), key));
|
keys.insert(("STARTOS".to_owned(), "STARTOS".to_owned()), key);
|
||||||
}
|
}
|
||||||
} else if let Ok(net_key) = row.try_get::<Vec<u8>, _>("network_key") {
|
} else if let Ok(net_key) = row.try_get::<Vec<u8>, _>("network_key") {
|
||||||
if let Ok(seed) = <[u8; 32]>::try_from(net_key) {
|
if let Ok(seed) = <[u8; 32]>::try_from(net_key) {
|
||||||
keys.push((
|
keys.insert(
|
||||||
"STARTOS".to_owned(),
|
("STARTOS".to_owned(), "STARTOS".to_owned()),
|
||||||
"STARTOS".to_owned(),
|
|
||||||
crate::util::crypto::ed25519_expand_key(&seed),
|
crate::util::crypto::ed25519_expand_key(&seed),
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,16 +669,17 @@ async fn previous_tor_keys(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if let Ok(seed) = <[u8; 32]>::try_from(key_bytes) {
|
if let Ok(seed) = <[u8; 32]>::try_from(key_bytes) {
|
||||||
keys.push((
|
keys.insert(
|
||||||
package,
|
(package, interface),
|
||||||
interface,
|
|
||||||
crate::util::crypto::ed25519_expand_key(&seed),
|
crate::util::crypto::ed25519_expand_key(&seed),
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Package tor keys from the tor table (already 64-byte expanded keys)
|
// Package tor keys from the tor table (already 64-byte expanded keys).
|
||||||
|
// These overwrite network_keys entries for the same (package, interface)
|
||||||
|
// because the tor table has the actual expanded key used by tor.
|
||||||
if let Ok(rows) = sqlx::query(r#"SELECT package, interface, key FROM tor"#)
|
if let Ok(rows) = sqlx::query(r#"SELECT package, interface, key FROM tor"#)
|
||||||
.fetch_all(pg)
|
.fetch_all(pg)
|
||||||
.await
|
.await
|
||||||
@@ -604,7 +695,7 @@ async fn previous_tor_keys(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if let Ok(key) = <[u8; 64]>::try_from(key_bytes) {
|
if let Ok(key) = <[u8; 64]>::try_from(key_bytes) {
|
||||||
keys.push((package, interface, key));
|
keys.insert((package, interface), key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
core/src/version/v0_4_0_alpha_23.rs
Normal file
44
core/src/version/v0_4_0_alpha_23.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use exver::{PreReleaseSegment, VersionRange};
|
||||||
|
|
||||||
|
use super::v0_3_5::V0_3_0_COMPAT;
|
||||||
|
use super::{VersionT, v0_4_0_alpha_22};
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref V0_4_0_alpha_23: exver::Version = exver::Version::new(
|
||||||
|
[0, 4, 0],
|
||||||
|
[PreReleaseSegment::String("alpha".into()), 23.into()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct Version;
|
||||||
|
|
||||||
|
impl VersionT for Version {
|
||||||
|
type Previous = v0_4_0_alpha_22::Version;
|
||||||
|
type PreUpRes = ();
|
||||||
|
|
||||||
|
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn semver(self) -> exver::Version {
|
||||||
|
V0_4_0_alpha_23.clone()
|
||||||
|
}
|
||||||
|
fn compat(self) -> &'static VersionRange {
|
||||||
|
&V0_3_0_COMPAT
|
||||||
|
}
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
|
||||||
|
let status_info = db["public"]["serverInfo"]["statusInfo"]
|
||||||
|
.as_object_mut();
|
||||||
|
if let Some(m) = status_info {
|
||||||
|
m.remove("updated");
|
||||||
|
m.insert("restart".into(), Value::Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Null)
|
||||||
|
}
|
||||||
|
fn down(self, _db: &mut Value) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
37
core/src/version/v0_4_0_beta_0.rs
Normal file
37
core/src/version/v0_4_0_beta_0.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
use exver::{PreReleaseSegment, VersionRange};
|
||||||
|
|
||||||
|
use super::v0_3_5::V0_3_0_COMPAT;
|
||||||
|
use super::{VersionT, v0_4_0_alpha_23};
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref V0_4_0_beta_0: exver::Version = exver::Version::new(
|
||||||
|
[0, 4, 0],
|
||||||
|
[PreReleaseSegment::String("beta".into()), 0.into()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct Version;
|
||||||
|
|
||||||
|
impl VersionT for Version {
|
||||||
|
type Previous = v0_4_0_alpha_23::Version;
|
||||||
|
type PreUpRes = ();
|
||||||
|
|
||||||
|
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn semver(self) -> exver::Version {
|
||||||
|
V0_4_0_beta_0.clone()
|
||||||
|
}
|
||||||
|
fn compat(self) -> &'static VersionRange {
|
||||||
|
&V0_3_0_COMPAT
|
||||||
|
}
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
|
||||||
|
Ok(Value::Null)
|
||||||
|
}
|
||||||
|
fn down(self, _db: &mut Value) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.4.0-beta.66 (2026-03-24)
|
||||||
|
|
||||||
|
- **Breaking:** `withPgDump()` replaces `pgdata` with required `mountpoint` + `pgdataPath`
|
||||||
|
- Passwordless/trust auth support for `withPgDump()` and `withMysqlDump()`
|
||||||
|
- New options: `pgOptions` for postgres, `mysqldOptions` for mysql/mariadb
|
||||||
|
- Fixed MariaDB backup/restore support
|
||||||
|
|
||||||
## 0.4.0-beta.65 (2026-03-23)
|
## 0.4.0-beta.65 (2026-03-23)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
3
sdk/base/lib/osBindings/CountEntry.ts
Normal file
3
sdk/base/lib/osBindings/CountEntry.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 CountEntry = { label: string; count: bigint }
|
||||||
9
sdk/base/lib/osBindings/DownloadsResponse.ts
Normal file
9
sdk/base/lib/osBindings/DownloadsResponse.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 { CountEntry } from './CountEntry'
|
||||||
|
import type { PackageVersionCount } from './PackageVersionCount'
|
||||||
|
|
||||||
|
export type DownloadsResponse = {
|
||||||
|
totalRequests: bigint
|
||||||
|
byPackage: Array<CountEntry>
|
||||||
|
byPackageVersion: Array<PackageVersionCount>
|
||||||
|
}
|
||||||
6
sdk/base/lib/osBindings/GenerateCertificateParams.ts
Normal file
6
sdk/base/lib/osBindings/GenerateCertificateParams.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type GenerateCertificateParams = {
|
||||||
|
hostnames: Array<string>
|
||||||
|
ed25519: boolean
|
||||||
|
}
|
||||||
3
sdk/base/lib/osBindings/GenerateCertificateResponse.ts
Normal file
3
sdk/base/lib/osBindings/GenerateCertificateResponse.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 GenerateCertificateResponse = { key: string; fullchain: string }
|
||||||
20
sdk/base/lib/osBindings/GetDownloadsParams.ts
Normal file
20
sdk/base/lib/osBindings/GetDownloadsParams.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type GetDownloadsParams = {
|
||||||
|
/**
|
||||||
|
* Filter by package ID
|
||||||
|
*/
|
||||||
|
pkgId: string | null
|
||||||
|
/**
|
||||||
|
* Filter by version
|
||||||
|
*/
|
||||||
|
version: string | null
|
||||||
|
/**
|
||||||
|
* Start of time range (RFC 3339)
|
||||||
|
*/
|
||||||
|
after: string | null
|
||||||
|
/**
|
||||||
|
* End of time range (RFC 3339)
|
||||||
|
*/
|
||||||
|
before: string | null
|
||||||
|
}
|
||||||
12
sdk/base/lib/osBindings/GetUsersParams.ts
Normal file
12
sdk/base/lib/osBindings/GetUsersParams.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.
|
||||||
|
|
||||||
|
export type GetUsersParams = {
|
||||||
|
/**
|
||||||
|
* Start of time range (RFC 3339)
|
||||||
|
*/
|
||||||
|
after: string | null
|
||||||
|
/**
|
||||||
|
* End of time range (RFC 3339)
|
||||||
|
*/
|
||||||
|
before: string | null
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@ import type { VolumeId } from './VolumeId'
|
|||||||
export type Manifest = {
|
export type Manifest = {
|
||||||
id: PackageId
|
id: PackageId
|
||||||
version: Version
|
version: Version
|
||||||
satisfies: Array<Version>
|
|
||||||
canMigrateTo: string
|
canMigrateTo: string
|
||||||
canMigrateFrom: string
|
canMigrateFrom: string
|
||||||
images: { [key: ImageId]: ImageConfig }
|
images: { [key: ImageId]: ImageConfig }
|
||||||
@@ -37,4 +36,5 @@ export type Manifest = {
|
|||||||
sdkVersion: string | null
|
sdkVersion: string | null
|
||||||
hardwareAcceleration: boolean
|
hardwareAcceleration: boolean
|
||||||
plugins: Array<PluginId>
|
plugins: Array<PluginId>
|
||||||
|
satisfies: Array<Version>
|
||||||
}
|
}
|
||||||
|
|||||||
10
sdk/base/lib/osBindings/MetricsSummary.ts
Normal file
10
sdk/base/lib/osBindings/MetricsSummary.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 { CountEntry } from './CountEntry'
|
||||||
|
|
||||||
|
export type MetricsSummary = {
|
||||||
|
totalCheckins: bigint
|
||||||
|
uniqueServers: bigint
|
||||||
|
totalPackageRequests: bigint
|
||||||
|
byArch: Array<CountEntry>
|
||||||
|
byOsVersion: Array<CountEntry>
|
||||||
|
}
|
||||||
7
sdk/base/lib/osBindings/PackageVersionCount.ts
Normal file
7
sdk/base/lib/osBindings/PackageVersionCount.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type PackageVersionCount = {
|
||||||
|
pkgId: string
|
||||||
|
version: string
|
||||||
|
count: bigint
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import type { MerkleArchiveCommitment } from './MerkleArchiveCommitment'
|
|||||||
import type { PackageId } from './PackageId'
|
import type { PackageId } from './PackageId'
|
||||||
import type { PluginId } from './PluginId'
|
import type { PluginId } from './PluginId'
|
||||||
import type { RegistryAsset } from './RegistryAsset'
|
import type { RegistryAsset } from './RegistryAsset'
|
||||||
|
import type { Version } from './Version'
|
||||||
|
|
||||||
export type PackageVersionInfo = {
|
export type PackageVersionInfo = {
|
||||||
icon: DataUrl
|
icon: DataUrl
|
||||||
@@ -31,4 +32,5 @@ export type PackageVersionInfo = {
|
|||||||
sdkVersion: string | null
|
sdkVersion: string | null
|
||||||
hardwareAcceleration: boolean
|
hardwareAcceleration: boolean
|
||||||
plugins: Array<PluginId>
|
plugins: Array<PluginId>
|
||||||
|
satisfies: Array<Version>
|
||||||
}
|
}
|
||||||
|
|||||||
3
sdk/base/lib/osBindings/RestartReason.ts
Normal file
3
sdk/base/lib/osBindings/RestartReason.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 RestartReason = 'mdns' | 'language' | 'kiosk' | 'update'
|
||||||
@@ -7,8 +7,6 @@ import type { ServerStatus } from './ServerStatus'
|
|||||||
import type { SmtpValue } from './SmtpValue'
|
import type { SmtpValue } from './SmtpValue'
|
||||||
|
|
||||||
export type ServerInfo = {
|
export type ServerInfo = {
|
||||||
arch: string
|
|
||||||
platform: string
|
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
hostname: string
|
hostname: string
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
import type { BackupProgress } from './BackupProgress'
|
import type { BackupProgress } from './BackupProgress'
|
||||||
import type { FullProgress } from './FullProgress'
|
import type { FullProgress } from './FullProgress'
|
||||||
import type { PackageId } from './PackageId'
|
import type { PackageId } from './PackageId'
|
||||||
|
import type { RestartReason } from './RestartReason'
|
||||||
|
|
||||||
export type ServerStatus = {
|
export type ServerStatus = {
|
||||||
backupProgress: { [key: PackageId]: BackupProgress } | null
|
backupProgress: { [key: PackageId]: BackupProgress } | null
|
||||||
updated: boolean
|
|
||||||
updateProgress: FullProgress | null
|
updateProgress: FullProgress | null
|
||||||
shuttingDown: boolean
|
shuttingDown: boolean
|
||||||
restarting: boolean
|
restarting: boolean
|
||||||
|
restart: RestartReason | null
|
||||||
}
|
}
|
||||||
|
|||||||
3
sdk/base/lib/osBindings/UsersResponse.ts
Normal file
3
sdk/base/lib/osBindings/UsersResponse.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 UsersResponse = { uniqueServers: bigint; totalCheckins: bigint }
|
||||||
@@ -74,6 +74,7 @@ export { ClearTasksParams } from './ClearTasksParams'
|
|||||||
export { CliSetIconParams } from './CliSetIconParams'
|
export { CliSetIconParams } from './CliSetIconParams'
|
||||||
export { ContactInfo } from './ContactInfo'
|
export { ContactInfo } from './ContactInfo'
|
||||||
export { ControlParams } from './ControlParams'
|
export { ControlParams } from './ControlParams'
|
||||||
|
export { CountEntry } from './CountEntry'
|
||||||
export { CreateSubcontainerFsParams } from './CreateSubcontainerFsParams'
|
export { CreateSubcontainerFsParams } from './CreateSubcontainerFsParams'
|
||||||
export { CreateTaskParams } from './CreateTaskParams'
|
export { CreateTaskParams } from './CreateTaskParams'
|
||||||
export { CurrentDependencies } from './CurrentDependencies'
|
export { CurrentDependencies } from './CurrentDependencies'
|
||||||
@@ -90,6 +91,7 @@ export { DestroySubcontainerFsParams } from './DestroySubcontainerFsParams'
|
|||||||
export { DeviceFilter } from './DeviceFilter'
|
export { DeviceFilter } from './DeviceFilter'
|
||||||
export { DnsSettings } from './DnsSettings'
|
export { DnsSettings } from './DnsSettings'
|
||||||
export { DomainSettings } from './DomainSettings'
|
export { DomainSettings } from './DomainSettings'
|
||||||
|
export { DownloadsResponse } from './DownloadsResponse'
|
||||||
export { Duration } from './Duration'
|
export { Duration } from './Duration'
|
||||||
export { EchoParams } from './EchoParams'
|
export { EchoParams } from './EchoParams'
|
||||||
export { EditSignerParams } from './EditSignerParams'
|
export { EditSignerParams } from './EditSignerParams'
|
||||||
@@ -106,8 +108,11 @@ export { FullProgress } from './FullProgress'
|
|||||||
export { GatewayId } from './GatewayId'
|
export { GatewayId } from './GatewayId'
|
||||||
export { GatewayInfo } from './GatewayInfo'
|
export { GatewayInfo } from './GatewayInfo'
|
||||||
export { GatewayType } from './GatewayType'
|
export { GatewayType } from './GatewayType'
|
||||||
|
export { GenerateCertificateParams } from './GenerateCertificateParams'
|
||||||
|
export { GenerateCertificateResponse } from './GenerateCertificateResponse'
|
||||||
export { GetActionInputParams } from './GetActionInputParams'
|
export { GetActionInputParams } from './GetActionInputParams'
|
||||||
export { GetContainerIpParams } from './GetContainerIpParams'
|
export { GetContainerIpParams } from './GetContainerIpParams'
|
||||||
|
export { GetDownloadsParams } from './GetDownloadsParams'
|
||||||
export { GetHostInfoParams } from './GetHostInfoParams'
|
export { GetHostInfoParams } from './GetHostInfoParams'
|
||||||
export { GetOsAssetParams } from './GetOsAssetParams'
|
export { GetOsAssetParams } from './GetOsAssetParams'
|
||||||
export { GetOsVersionParams } from './GetOsVersionParams'
|
export { GetOsVersionParams } from './GetOsVersionParams'
|
||||||
@@ -122,6 +127,7 @@ export { GetSslCertificateParams } from './GetSslCertificateParams'
|
|||||||
export { GetSslKeyParams } from './GetSslKeyParams'
|
export { GetSslKeyParams } from './GetSslKeyParams'
|
||||||
export { GetStatusParams } from './GetStatusParams'
|
export { GetStatusParams } from './GetStatusParams'
|
||||||
export { GetSystemSmtpParams } from './GetSystemSmtpParams'
|
export { GetSystemSmtpParams } from './GetSystemSmtpParams'
|
||||||
|
export { GetUsersParams } from './GetUsersParams'
|
||||||
export { GigaBytes } from './GigaBytes'
|
export { GigaBytes } from './GigaBytes'
|
||||||
export { GitHash } from './GitHash'
|
export { GitHash } from './GitHash'
|
||||||
export { Governor } from './Governor'
|
export { Governor } from './Governor'
|
||||||
@@ -173,6 +179,7 @@ export { MetricsDisk } from './MetricsDisk'
|
|||||||
export { MetricsFollowResponse } from './MetricsFollowResponse'
|
export { MetricsFollowResponse } from './MetricsFollowResponse'
|
||||||
export { MetricsGeneral } from './MetricsGeneral'
|
export { MetricsGeneral } from './MetricsGeneral'
|
||||||
export { MetricsMemory } from './MetricsMemory'
|
export { MetricsMemory } from './MetricsMemory'
|
||||||
|
export { MetricsSummary } from './MetricsSummary'
|
||||||
export { Metrics } from './Metrics'
|
export { Metrics } from './Metrics'
|
||||||
export { ModifyNotificationBeforeParams } from './ModifyNotificationBeforeParams'
|
export { ModifyNotificationBeforeParams } from './ModifyNotificationBeforeParams'
|
||||||
export { ModifyNotificationParams } from './ModifyNotificationParams'
|
export { ModifyNotificationParams } from './ModifyNotificationParams'
|
||||||
@@ -200,6 +207,7 @@ export { PackageInfoShort } from './PackageInfoShort'
|
|||||||
export { PackageInfo } from './PackageInfo'
|
export { PackageInfo } from './PackageInfo'
|
||||||
export { PackagePlugin } from './PackagePlugin'
|
export { PackagePlugin } from './PackagePlugin'
|
||||||
export { PackageState } from './PackageState'
|
export { PackageState } from './PackageState'
|
||||||
|
export { PackageVersionCount } from './PackageVersionCount'
|
||||||
export { PackageVersionInfo } from './PackageVersionInfo'
|
export { PackageVersionInfo } from './PackageVersionInfo'
|
||||||
export { PartitionInfo } from './PartitionInfo'
|
export { PartitionInfo } from './PartitionInfo'
|
||||||
export { PassthroughInfo } from './PassthroughInfo'
|
export { PassthroughInfo } from './PassthroughInfo'
|
||||||
@@ -234,6 +242,7 @@ export { RenameGatewayParams } from './RenameGatewayParams'
|
|||||||
export { ReplayId } from './ReplayId'
|
export { ReplayId } from './ReplayId'
|
||||||
export { RequestCommitment } from './RequestCommitment'
|
export { RequestCommitment } from './RequestCommitment'
|
||||||
export { ResetPasswordParams } from './ResetPasswordParams'
|
export { ResetPasswordParams } from './ResetPasswordParams'
|
||||||
|
export { RestartReason } from './RestartReason'
|
||||||
export { RestorePackageParams } from './RestorePackageParams'
|
export { RestorePackageParams } from './RestorePackageParams'
|
||||||
export { RunActionParams } from './RunActionParams'
|
export { RunActionParams } from './RunActionParams'
|
||||||
export { Security } from './Security'
|
export { Security } from './Security'
|
||||||
@@ -297,6 +306,7 @@ export { UrlPluginClearUrlsParams } from './UrlPluginClearUrlsParams'
|
|||||||
export { UrlPluginExportUrlParams } from './UrlPluginExportUrlParams'
|
export { UrlPluginExportUrlParams } from './UrlPluginExportUrlParams'
|
||||||
export { UrlPluginRegisterParams } from './UrlPluginRegisterParams'
|
export { UrlPluginRegisterParams } from './UrlPluginRegisterParams'
|
||||||
export { UrlPluginRegistration } from './UrlPluginRegistration'
|
export { UrlPluginRegistration } from './UrlPluginRegistration'
|
||||||
|
export { UsersResponse } from './UsersResponse'
|
||||||
export { VerifyCifsParams } from './VerifyCifsParams'
|
export { VerifyCifsParams } from './VerifyCifsParams'
|
||||||
export { VersionSignerParams } from './VersionSignerParams'
|
export { VersionSignerParams } from './VersionSignerParams'
|
||||||
export { Version } from './Version'
|
export { Version } from './Version'
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Performs a deep structural equality check across all provided arguments.
|
* Performs a deep structural equality check across all provided arguments.
|
||||||
* Returns true only if every argument is deeply equal to every other argument.
|
* Returns true only if every argument is deeply equal to every other argument.
|
||||||
* Handles primitives, arrays, and plain objects recursively.
|
* Handles primitives, arrays, and plain objects (JSON-like) recursively.
|
||||||
|
*
|
||||||
|
* Non-plain objects (Set, Map, Date, etc.) are compared by reference only,
|
||||||
|
* since Object.keys() does not enumerate their contents.
|
||||||
*
|
*
|
||||||
* @param args - Two or more values to compare for deep equality
|
* @param args - Two or more values to compare for deep equality
|
||||||
* @returns True if all arguments are deeply equal
|
* @returns True if all arguments are deeply equal
|
||||||
@@ -23,6 +26,18 @@ export function deepEqual(...args: unknown[]) {
|
|||||||
}
|
}
|
||||||
if (objects.length !== args.length) return false
|
if (objects.length !== args.length) return false
|
||||||
if (objects.some(Array.isArray) && !objects.every(Array.isArray)) return false
|
if (objects.some(Array.isArray) && !objects.every(Array.isArray)) return false
|
||||||
|
if (
|
||||||
|
objects.some(
|
||||||
|
(x) => !Array.isArray(x) && Object.getPrototypeOf(x) !== Object.prototype,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
objects.reduce<object | null>(
|
||||||
|
(a, b) => (a === b ? a : null),
|
||||||
|
objects[0],
|
||||||
|
) !== null
|
||||||
|
)
|
||||||
|
}
|
||||||
const allKeys = new Set(objects.flatMap((x) => Object.keys(x)))
|
const allKeys = new Set(objects.flatMap((x) => Object.keys(x)))
|
||||||
for (const key of allKeys) {
|
for (const key of allKeys) {
|
||||||
for (const x of objects) {
|
for (const x of objects) {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ import { createVolumes } from './util/Volume'
|
|||||||
import { getDataVersion, setDataVersion } from './version'
|
import { getDataVersion, setDataVersion } from './version'
|
||||||
|
|
||||||
/** The minimum StartOS version required by this SDK release */
|
/** The minimum StartOS version required by this SDK release */
|
||||||
export const OSVersion = testTypeVersion('0.4.0-alpha.22')
|
export const OSVersion = testTypeVersion('0.4.0-beta.0')
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
type AnyNeverCond<T extends any[], Then, Else> =
|
type AnyNeverCond<T extends any[], Then, Else> =
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ const BACKUP_HOST_PATH = '/media/startos/backup'
|
|||||||
const BACKUP_CONTAINER_MOUNT = '/backup-target'
|
const BACKUP_CONTAINER_MOUNT = '/backup-target'
|
||||||
|
|
||||||
/** A password value, or a function that returns one. Functions are resolved lazily (only during restore). */
|
/** A password value, or a function that returns one. Functions are resolved lazily (only during restore). */
|
||||||
export type LazyPassword = string | (() => string | Promise<string>)
|
export type LazyPassword = string | (() => string | Promise<string>) | null
|
||||||
|
|
||||||
async function resolvePassword(pw: LazyPassword): Promise<string> {
|
async function resolvePassword(pw: LazyPassword): Promise<string | null> {
|
||||||
|
if (pw === null) return null
|
||||||
return typeof pw === 'function' ? pw() : pw
|
return typeof pw === 'function' ? pw() : pw
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,16 +23,20 @@ export type PgDumpConfig<M extends T.SDKManifest> = {
|
|||||||
imageId: keyof M['images'] & T.ImageId
|
imageId: keyof M['images'] & T.ImageId
|
||||||
/** Volume ID containing the PostgreSQL data directory */
|
/** Volume ID containing the PostgreSQL data directory */
|
||||||
dbVolume: M['volumes'][number]
|
dbVolume: M['volumes'][number]
|
||||||
/** Path to PGDATA within the container (e.g. '/var/lib/postgresql/data') */
|
/** Volume mountpoint (e.g. '/var/lib/postgresql') */
|
||||||
pgdata: string
|
mountpoint: string
|
||||||
|
/** Subpath from mountpoint to PGDATA (e.g. '/data', '/18/docker') */
|
||||||
|
pgdataPath: string
|
||||||
/** PostgreSQL database name to dump */
|
/** PostgreSQL database name to dump */
|
||||||
database: string
|
database: string
|
||||||
/** PostgreSQL user */
|
/** PostgreSQL user */
|
||||||
user: string
|
user: string
|
||||||
/** PostgreSQL password (for restore). Can be a string or a function that returns one — functions are resolved lazily after volumes are restored. */
|
/** PostgreSQL password (for restore). Can be a string, a function that returns one (resolved lazily after volumes are restored), or null for trust auth. */
|
||||||
password: LazyPassword
|
password: LazyPassword
|
||||||
/** Additional initdb arguments (e.g. ['--data-checksums']) */
|
/** Additional initdb arguments (e.g. ['--data-checksums']) */
|
||||||
initdbArgs?: string[]
|
initdbArgs?: string[]
|
||||||
|
/** Additional options passed to `pg_ctl start -o` (e.g. '-c shared_preload_libraries=vectorchord'). Appended after `-c listen_addresses=`. */
|
||||||
|
pgOptions?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Configuration for MySQL/MariaDB dump-based backup */
|
/** Configuration for MySQL/MariaDB dump-based backup */
|
||||||
@@ -52,6 +57,8 @@ export type MysqlDumpConfig<M extends T.SDKManifest> = {
|
|||||||
engine: 'mysql' | 'mariadb'
|
engine: 'mysql' | 'mariadb'
|
||||||
/** Custom readiness check command (default: ['mysqladmin', 'ping', ...]) */
|
/** Custom readiness check command (default: ['mysqladmin', 'ping', ...]) */
|
||||||
readyCommand?: string[]
|
readyCommand?: string[]
|
||||||
|
/** Additional options passed to `mysqld` on startup (e.g. '--innodb-buffer-pool-size=256M'). Appended after `--bind-address=127.0.0.1`. */
|
||||||
|
mysqldOptions?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Bind-mount the backup target into a SubContainer's rootfs */
|
/** Bind-mount the backup target into a SubContainer's rootfs */
|
||||||
@@ -154,19 +161,21 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
|||||||
const {
|
const {
|
||||||
imageId,
|
imageId,
|
||||||
dbVolume,
|
dbVolume,
|
||||||
pgdata,
|
mountpoint,
|
||||||
|
pgdataPath,
|
||||||
database,
|
database,
|
||||||
user,
|
user,
|
||||||
password,
|
password,
|
||||||
initdbArgs = [],
|
initdbArgs = [],
|
||||||
|
pgOptions,
|
||||||
} = config
|
} = config
|
||||||
|
const pgdata = `${mountpoint}${pgdataPath}`
|
||||||
const dumpFile = `${BACKUP_CONTAINER_MOUNT}/${database}-db.dump`
|
const dumpFile = `${BACKUP_CONTAINER_MOUNT}/${database}-db.dump`
|
||||||
const pgMountpoint = pgdata.replace(/\/data$/, '') || pgdata
|
|
||||||
|
|
||||||
function dbMounts() {
|
function dbMounts() {
|
||||||
return Mounts.of<M>().mountVolume({
|
return Mounts.of<M>().mountVolume({
|
||||||
volumeId: dbVolume,
|
volumeId: dbVolume,
|
||||||
mountpoint: pgMountpoint,
|
mountpoint: mountpoint,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
subpath: null,
|
subpath: null,
|
||||||
})
|
})
|
||||||
@@ -193,10 +202,12 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
|||||||
user: 'root',
|
user: 'root',
|
||||||
})
|
})
|
||||||
console.log(`[${label}] starting postgres`)
|
console.log(`[${label}] starting postgres`)
|
||||||
await sub.execFail(
|
const pgStartOpts = pgOptions
|
||||||
['pg_ctl', 'start', '-D', pgdata, '-o', '-c listen_addresses='],
|
? `-c listen_addresses= ${pgOptions}`
|
||||||
{ user: 'postgres' },
|
: '-c listen_addresses='
|
||||||
)
|
await sub.execFail(['pg_ctl', 'start', '-D', pgdata, '-o', pgStartOpts], {
|
||||||
|
user: 'postgres',
|
||||||
|
})
|
||||||
for (let i = 0; i < 60; i++) {
|
for (let i = 0; i < 60; i++) {
|
||||||
const { exitCode } = await sub.exec(['pg_isready', '-U', user], {
|
const { exitCode } = await sub.exec(['pg_isready', '-U', user], {
|
||||||
user: 'postgres',
|
user: 'postgres',
|
||||||
@@ -249,7 +260,7 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
|||||||
async (sub) => {
|
async (sub) => {
|
||||||
await mountBackupTarget(sub.rootfs)
|
await mountBackupTarget(sub.rootfs)
|
||||||
await sub.execFail(
|
await sub.execFail(
|
||||||
['chown', '-R', 'postgres:postgres', pgMountpoint],
|
['chown', '-R', 'postgres:postgres', mountpoint],
|
||||||
{ user: 'root' },
|
{ user: 'root' },
|
||||||
)
|
)
|
||||||
await sub.execFail(
|
await sub.execFail(
|
||||||
@@ -274,18 +285,20 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
|||||||
{ user: 'postgres' },
|
{ user: 'postgres' },
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
await sub.execFail(
|
if (resolvedPassword !== null) {
|
||||||
[
|
await sub.execFail(
|
||||||
'psql',
|
[
|
||||||
'-U',
|
'psql',
|
||||||
user,
|
'-U',
|
||||||
'-d',
|
user,
|
||||||
database,
|
'-d',
|
||||||
'-c',
|
database,
|
||||||
`ALTER USER ${user} WITH PASSWORD '${resolvedPassword}'`,
|
'-c',
|
||||||
],
|
`ALTER USER ${user} WITH PASSWORD '${resolvedPassword}'`,
|
||||||
{ user: 'postgres' },
|
],
|
||||||
)
|
{ user: 'postgres' },
|
||||||
|
)
|
||||||
|
}
|
||||||
await sub.execFail(['pg_ctl', 'stop', '-D', pgdata, '-w'], {
|
await sub.execFail(['pg_ctl', 'stop', '-D', pgdata, '-w'], {
|
||||||
user: 'postgres',
|
user: 'postgres',
|
||||||
})
|
})
|
||||||
@@ -318,6 +331,7 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
|||||||
password,
|
password,
|
||||||
engine,
|
engine,
|
||||||
readyCommand,
|
readyCommand,
|
||||||
|
mysqldOptions = [],
|
||||||
} = config
|
} = config
|
||||||
const dumpFile = `${BACKUP_CONTAINER_MOUNT}/${database}-db.dump`
|
const dumpFile = `${BACKUP_CONTAINER_MOUNT}/${database}-db.dump`
|
||||||
|
|
||||||
@@ -342,6 +356,42 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
|||||||
throw new Error('MySQL/MariaDB failed to become ready within 30 seconds')
|
throw new Error('MySQL/MariaDB failed to become ready within 30 seconds')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function startMysql(sub: {
|
||||||
|
exec(cmd: string[], opts?: any): Promise<{ exitCode: number | null }>
|
||||||
|
execFail(cmd: string[], opts?: any, timeout?: number | null): Promise<any>
|
||||||
|
}) {
|
||||||
|
if (engine === 'mariadb') {
|
||||||
|
// MariaDB doesn't support --daemonize; fire-and-forget the exec
|
||||||
|
sub
|
||||||
|
.exec(
|
||||||
|
[
|
||||||
|
'mysqld',
|
||||||
|
'--user=mysql',
|
||||||
|
`--datadir=${datadir}`,
|
||||||
|
'--bind-address=127.0.0.1',
|
||||||
|
...mysqldOptions,
|
||||||
|
],
|
||||||
|
{ user: 'root' },
|
||||||
|
)
|
||||||
|
.catch((e) =>
|
||||||
|
console.error('[mysql-backup] mysqld exited unexpectedly:', e),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
await sub.execFail(
|
||||||
|
[
|
||||||
|
'mysqld',
|
||||||
|
'--user=mysql',
|
||||||
|
`--datadir=${datadir}`,
|
||||||
|
'--bind-address=127.0.0.1',
|
||||||
|
'--daemonize',
|
||||||
|
...mysqldOptions,
|
||||||
|
],
|
||||||
|
{ user: 'root' },
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new Backups<M>()
|
return new Backups<M>()
|
||||||
.setPreBackup(async (effects) => {
|
.setPreBackup(async (effects) => {
|
||||||
const pw = await resolvePassword(password)
|
const pw = await resolvePassword(password)
|
||||||
@@ -350,7 +400,7 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
|||||||
'ping',
|
'ping',
|
||||||
'-u',
|
'-u',
|
||||||
user,
|
user,
|
||||||
`-p${pw}`,
|
...(pw !== null ? [`-p${pw}`] : []),
|
||||||
'--silent',
|
'--silent',
|
||||||
]
|
]
|
||||||
await SubContainerRc.withTemp<M, void, BackupEffects>(
|
await SubContainerRc.withTemp<M, void, BackupEffects>(
|
||||||
@@ -371,24 +421,14 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
|||||||
user: 'root',
|
user: 'root',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
await sub.execFail(
|
await startMysql(sub)
|
||||||
[
|
|
||||||
'mysqld',
|
|
||||||
'--user=mysql',
|
|
||||||
`--datadir=${datadir}`,
|
|
||||||
'--skip-networking',
|
|
||||||
'--daemonize',
|
|
||||||
],
|
|
||||||
{ user: 'root' },
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
await waitForMysql(sub, readyCmd)
|
await waitForMysql(sub, readyCmd)
|
||||||
await sub.execFail(
|
await sub.execFail(
|
||||||
[
|
[
|
||||||
'mysqldump',
|
'mysqldump',
|
||||||
'-u',
|
'-u',
|
||||||
user,
|
user,
|
||||||
`-p${pw}`,
|
...(pw !== null ? [`-p${pw}`] : []),
|
||||||
'--single-transaction',
|
'--single-transaction',
|
||||||
`--result-file=${dumpFile}`,
|
`--result-file=${dumpFile}`,
|
||||||
database,
|
database,
|
||||||
@@ -396,9 +436,15 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
|||||||
{ user: 'root' },
|
{ user: 'root' },
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
|
// Graceful shutdown via SIGTERM; wait for exit
|
||||||
await sub.execFail(
|
await sub.execFail(
|
||||||
['mysqladmin', '-u', user, `-p${pw}`, 'shutdown'],
|
[
|
||||||
|
'sh',
|
||||||
|
'-c',
|
||||||
|
'PID=$(cat /var/run/mysqld/mysqld.pid) && kill $PID && tail --pid=$PID -f /dev/null',
|
||||||
|
],
|
||||||
{ user: 'root' },
|
{ user: 'root' },
|
||||||
|
null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -435,17 +481,7 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
|||||||
{ user: 'root' },
|
{ user: 'root' },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
await sub.execFail(
|
await startMysql(sub)
|
||||||
[
|
|
||||||
'mysqld',
|
|
||||||
'--user=mysql',
|
|
||||||
`--datadir=${datadir}`,
|
|
||||||
'--skip-networking',
|
|
||||||
'--daemonize',
|
|
||||||
],
|
|
||||||
{ user: 'root' },
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
// After fresh init, root has no password
|
// After fresh init, root has no password
|
||||||
await waitForMysql(sub, [
|
await waitForMysql(sub, [
|
||||||
'mysqladmin',
|
'mysqladmin',
|
||||||
@@ -455,29 +491,32 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
|
|||||||
'--silent',
|
'--silent',
|
||||||
])
|
])
|
||||||
// Create database, user, and set password
|
// Create database, user, and set password
|
||||||
await sub.execFail(
|
const grantSql =
|
||||||
[
|
pw !== null
|
||||||
'mysql',
|
? `CREATE DATABASE IF NOT EXISTS \`${database}\`; CREATE USER IF NOT EXISTS '${user}'@'localhost' IDENTIFIED BY '${pw}'; GRANT ALL ON \`${database}\`.* TO '${user}'@'localhost'; ALTER USER 'root'@'localhost' IDENTIFIED BY '${pw}'; FLUSH PRIVILEGES;`
|
||||||
'-u',
|
: `CREATE DATABASE IF NOT EXISTS \`${database}\`; CREATE USER IF NOT EXISTS '${user}'@'localhost'; GRANT ALL ON \`${database}\`.* TO '${user}'@'localhost'; FLUSH PRIVILEGES;`
|
||||||
'root',
|
await sub.execFail(['mysql', '-u', 'root', '-e', grantSql], {
|
||||||
'-e',
|
user: 'root',
|
||||||
`CREATE DATABASE IF NOT EXISTS \`${database}\`; CREATE USER IF NOT EXISTS '${user}'@'localhost' IDENTIFIED BY '${pw}'; GRANT ALL ON \`${database}\`.* TO '${user}'@'localhost'; ALTER USER 'root'@'localhost' IDENTIFIED BY '${pw}'; FLUSH PRIVILEGES;`,
|
})
|
||||||
],
|
|
||||||
{ user: 'root' },
|
|
||||||
)
|
|
||||||
// Restore from dump
|
// Restore from dump
|
||||||
await sub.execFail(
|
await sub.execFail(
|
||||||
[
|
[
|
||||||
'sh',
|
'sh',
|
||||||
'-c',
|
'-c',
|
||||||
`mysql -u root -p'${pw}' \`${database}\` < ${dumpFile}`,
|
`mysql -u root ${pw !== null ? `-p'${pw}'` : ''} ${database} < ${dumpFile}`,
|
||||||
],
|
],
|
||||||
{ user: 'root' },
|
{ user: 'root' },
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
|
// Graceful shutdown via SIGTERM; wait for exit
|
||||||
await sub.execFail(
|
await sub.execFail(
|
||||||
['mysqladmin', '-u', 'root', `-p${password}`, 'shutdown'],
|
[
|
||||||
|
'sh',
|
||||||
|
'-c',
|
||||||
|
'PID=$(cat /var/run/mysqld/mysqld.pid) && kill $PID && tail --pid=$PID -f /dev/null',
|
||||||
|
],
|
||||||
{ user: 'root' },
|
{ user: 'root' },
|
||||||
|
null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { z } from 'zod'
|
|
||||||
import * as YAML from 'yaml'
|
|
||||||
import * as TOML from '@iarna/toml'
|
import * as TOML from '@iarna/toml'
|
||||||
import * as INI from 'ini'
|
|
||||||
import {
|
import {
|
||||||
XMLParser,
|
|
||||||
XMLBuilder,
|
XMLBuilder,
|
||||||
|
XMLParser,
|
||||||
type X2jOptions,
|
type X2jOptions,
|
||||||
type XmlBuilderOptions,
|
type XmlBuilderOptions,
|
||||||
} from 'fast-xml-parser'
|
} from 'fast-xml-parser'
|
||||||
import * as T from '../../../base/lib/types'
|
import * as INI from 'ini'
|
||||||
import * as fs from 'node:fs/promises'
|
import * as fs from 'node:fs/promises'
|
||||||
|
import * as YAML from 'yaml'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import * as T from '../../../base/lib/types'
|
||||||
import { asError, deepEqual } from '../../../base/lib/util'
|
import { asError, deepEqual } from '../../../base/lib/util'
|
||||||
import { Watchable } from '../../../base/lib/util/Watchable'
|
import { Watchable } from '../../../base/lib/util/Watchable'
|
||||||
import { PathBase } from './Volume'
|
import { PathBase } from './Volume'
|
||||||
@@ -382,7 +382,7 @@ export class FileHelper<A> {
|
|||||||
const mergeData = this.validate(fileMerge({}, fileData, data))
|
const mergeData = this.validate(fileMerge({}, fileData, data))
|
||||||
const toWrite = this.writeData(mergeData)
|
const toWrite = this.writeData(mergeData)
|
||||||
if (toWrite !== fileDataRaw) {
|
if (toWrite !== fileDataRaw) {
|
||||||
this.writeFile(mergeData)
|
await this.writeFile(mergeData)
|
||||||
if (!options.allowWriteAfterConst && effects.constRetry) {
|
if (!options.allowWriteAfterConst && effects.constRetry) {
|
||||||
const records = this.consts.filter(([c]) => c === effects.constRetry)
|
const records = this.consts.filter(([c]) => c === effects.constRetry)
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
|
|||||||
4
sdk/package/package-lock.json
generated
4
sdk/package/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@start9labs/start-sdk",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "0.4.0-beta.65",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@start9labs/start-sdk",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "0.4.0-beta.65",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iarna/toml": "^3.0.0",
|
"@iarna/toml": "^3.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@start9labs/start-sdk",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "0.4.0-beta.65",
|
"version": "1.0.0",
|
||||||
"description": "Software development kit to facilitate packaging services for StartOS",
|
"description": "Software development kit to facilitate packaging services for StartOS",
|
||||||
"main": "./package/lib/index.js",
|
"main": "./package/lib/index.js",
|
||||||
"types": "./package/lib/index.d.ts",
|
"types": "./package/lib/index.d.ts",
|
||||||
|
|||||||
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "startos-ui",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-alpha.22",
|
"version": "0.4.0-beta.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "startos-ui",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-alpha.22",
|
"version": "0.4.0-beta.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^21.2.1",
|
"@angular/cdk": "^21.2.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "startos-ui",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-alpha.22",
|
"version": "0.4.0-beta.0",
|
||||||
"author": "Start9 Labs, Inc",
|
"author": "Start9 Labs, Inc",
|
||||||
"homepage": "https://start9.com/",
|
"homepage": "https://start9.com/",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { FormsModule } from '@angular/forms'
|
|||||||
import { i18nPipe } from '@start9labs/shared'
|
import { i18nPipe } from '@start9labs/shared'
|
||||||
import {
|
import {
|
||||||
TuiButton,
|
TuiButton,
|
||||||
|
TuiCell,
|
||||||
TuiCheckbox,
|
TuiCheckbox,
|
||||||
TuiDialogContext,
|
TuiDialogContext,
|
||||||
TuiNotification,
|
TuiNotification,
|
||||||
@@ -19,6 +20,7 @@ export interface PreserveOverwriteData {
|
|||||||
imports: [
|
imports: [
|
||||||
FormsModule,
|
FormsModule,
|
||||||
TuiButton,
|
TuiButton,
|
||||||
|
TuiCell,
|
||||||
TuiCheckbox,
|
TuiCheckbox,
|
||||||
TuiHeader,
|
TuiHeader,
|
||||||
TuiNotification,
|
TuiNotification,
|
||||||
@@ -49,9 +51,9 @@ export interface PreserveOverwriteData {
|
|||||||
| i18n
|
| i18n
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
<label>
|
<label tuiCell>
|
||||||
<input tuiCheckbox type="checkbox" [(ngModel)]="backupAck" />
|
<input tuiCheckbox type="checkbox" [(ngModel)]="backupAck" />
|
||||||
{{ 'I have a backup of my data' | i18n }}
|
<span tuiTitle>{{ 'I have a backup of my data' | i18n }}</span>
|
||||||
</label>
|
</label>
|
||||||
}
|
}
|
||||||
<footer>
|
<footer>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user