Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad9c632a9d | ||
|
|
3ad4854189 | ||
|
|
45a012a6e4 | ||
|
|
8ae2364cc0 | ||
|
|
0ae771786a |
@@ -1,44 +1,114 @@
|
||||
<div align="center">
|
||||
|
||||
# Runix
|
||||
|
||||
Runix is an infrastructure management platform: one control plane manages a
|
||||
fleet of hosts through lightweight agents that dial out over WebSocket
|
||||
(NAT-friendly — nothing connects *in* to your servers). Manage Docker,
|
||||
Compose, systemd units and native daemons; browse and edit files; open
|
||||
terminals and consoles; schedule tasks; and update the whole fleet from the
|
||||
UI.
|
||||
**One console for your whole fleet.**
|
||||
Containers, system services and custom daemons — managed the same way, on every host.
|
||||
|
||||
This repository holds the **release binaries and the installer**. The source
|
||||
lives in a separate repository.
|
||||
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Frunix-releases.s3-alpha-web.astra-dev.online%2Flatest.json&query=%24.version&label=release&color=3fb950" alt="Latest release">
|
||||
<img src="https://img.shields.io/badge/platforms-linux%20amd64%20%7C%20arm64-555" alt="Platforms">
|
||||
<img src="https://img.shields.io/badge/install-one%20command-3fb950" alt="One-command install">
|
||||
<img src="https://img.shields.io/badge/dependencies-none-blue" alt="No runtime dependencies">
|
||||
|
||||
<br>
|
||||
|
||||
<img src="docs/images/dashboard.png" alt="Runix dashboard" width="100%">
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
This repository publishes the **release binaries and the installer**. The source
|
||||
lives in a separate, private repository.
|
||||
|
||||
## What it is
|
||||
|
||||
Runix manages servers the way you'd want to: you say *restart that thing*, and
|
||||
it doesn't matter whether "that thing" is a Docker container, a Compose
|
||||
project, a systemd unit or a plain process you wrote yourself.
|
||||
|
||||
Everything is a **Runtime** with the same lifecycle — start, stop, restart,
|
||||
logs, metrics, a shell — so the interface stays the same as your stack changes.
|
||||
|
||||
It ships as **two static binaries with no runtime dependencies**. The control
|
||||
plane serves its own web UI from inside the executable; agents dial *out* to it
|
||||
over WebSocket, so managed hosts need no inbound ports, no VPN and no public IP.
|
||||
|
||||
## Install
|
||||
|
||||
One command sets up a host. Run it as root; it asks what the host should be
|
||||
(control plane, agent, or both), checks prerequisites, installs under
|
||||
`/opt/runix`, and wires up systemd.
|
||||
One command. It asks what the host should be, then does the rest:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://vcs.astra-dev.online/svesnav/runix/raw/branch/main/install.sh | sudo sh
|
||||
curl -fsSL https://vcs.astra-dev.online/svesnav/runix/releases/download/latest/install.sh -o install.sh
|
||||
chmod +x install.sh
|
||||
./install.sh
|
||||
```
|
||||
|
||||
Prefer to pin a version, or your instance still requires sign-in to read raw
|
||||
files? Use the release asset instead — it downloads without a login:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://vcs.astra-dev.online/svesnav/runix/releases/download/v0.5.0/install.sh | sudo sh -s -- --role all-in-one
|
||||
```
|
||||
What should this host run?
|
||||
1) Control plane + agent (single-host install)
|
||||
2) Control plane only
|
||||
3) Agent only — join a control plane running elsewhere
|
||||
```
|
||||
|
||||
Pick **1** and you get a working system from an empty machine: it provisions
|
||||
PostgreSQL in Docker, starts the control plane, registers the host with it, and
|
||||
installs the agent with a token it mints itself. Then open the URL it prints
|
||||
and log in.
|
||||
|
||||
Downloads come from object storage and need no account, no token and no login.
|
||||
Every binary is checked against the release's `SHA256SUMS` before it is
|
||||
installed — a mismatch aborts.
|
||||
|
||||
### Roles
|
||||
|
||||
| Command | What it installs |
|
||||
|---|---|
|
||||
| `sudo sh install.sh --role all-in-one` | Control plane **and** an agent on this host (single-host setup) |
|
||||
| `sudo sh install.sh --role server` | Control plane only |
|
||||
| `sudo sh install.sh --role agent --url https://runix.example.com --token <token>` | An agent that joins an existing control plane |
|
||||
| `install.sh --role all-in-one` | Control plane **and** an agent on this host |
|
||||
| `install.sh --role server` | Control plane only |
|
||||
| `install.sh --role agent --url https://runix.example.com --token <token>` | An agent joining an existing control plane |
|
||||
|
||||
The all-in-one and server roles can provision PostgreSQL in Docker for you
|
||||
(`--db docker`) or use one you already run (`--dsn postgres://…`). Add `-y`
|
||||
for an unattended install that takes sensible defaults. `sh install.sh
|
||||
--help` lists every flag.
|
||||
<details>
|
||||
<summary><b>Unattended installs and options</b></summary>
|
||||
|
||||
<br>
|
||||
|
||||
Every question is also a flag, so the same script works from Ansible or CI:
|
||||
|
||||
```sh
|
||||
# Single-host install, no questions asked
|
||||
curl -fsSL https://vcs.astra-dev.online/svesnav/runix/releases/download/latest/install.sh -o install.sh
|
||||
chmod +x install.sh
|
||||
./install.sh --role all-in-one --yes
|
||||
|
||||
# Agent joining an existing control plane
|
||||
curl -fsSL https://vcs.astra-dev.online/svesnav/runix/releases/download/latest/install.sh -o install.sh
|
||||
chmod +x install.sh
|
||||
./install.sh --role agent --url https://runix.example.com --token rnx_agt_...
|
||||
|
||||
# Behind a reverse proxy: listen on loopback only
|
||||
curl -fsSL https://vcs.astra-dev.online/svesnav/runix/releases/download/latest/install.sh -o install.sh
|
||||
chmod +x install.sh
|
||||
./install.sh --role server --bind 127.0.0.1 --public-url https://runix.example.com -y
|
||||
```
|
||||
|
||||
`-y` takes the recommended default for anything you didn't pass. Values with no
|
||||
safe default — the role, an agent's URL and token — fail loudly instead of
|
||||
being guessed.
|
||||
|
||||
| Flag | Purpose |
|
||||
|---|---|
|
||||
| `--db docker` / `--dsn` | Provision PostgreSQL in Docker, or use one you already run |
|
||||
| `--bind`, `--port` | The IP and port the control plane listens on |
|
||||
| `--public-url` | Where browsers reach the UI (sets the CORS allow-list) |
|
||||
| `--version` | Install a specific release instead of the latest |
|
||||
| `--s3`, `--gitea` | Where to download from |
|
||||
| `--prefix` | Install root (default `/opt/runix`) |
|
||||
| `--no-start` | Configure everything, start nothing |
|
||||
|
||||
`install.sh --help` lists them all.
|
||||
|
||||
</details>
|
||||
|
||||
Everything lives under one directory:
|
||||
|
||||
@@ -49,40 +119,220 @@ Everything lives under one directory:
|
||||
/opt/runix/agent/ supervised daemon state (agent)
|
||||
```
|
||||
|
||||
## Update
|
||||
## Updating
|
||||
|
||||
Re-running the installer upgrades in place — binaries are replaced and
|
||||
services restarted, while your configuration (above all the JWT and
|
||||
encryption secrets) is preserved:
|
||||
Re-running the installer upgrades in place. Binaries are replaced and services
|
||||
restarted, while your configuration — above all the JWT and encryption secrets
|
||||
— is preserved:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://vcs.astra-dev.online/svesnav/runix/raw/branch/main/install.sh | sudo sh -- -y
|
||||
curl -fsSL https://vcs.astra-dev.online/svesnav/runix/releases/download/latest/install.sh -o install.sh
|
||||
chmod +x install.sh
|
||||
./install.sh -y
|
||||
```
|
||||
|
||||
Or update from the UI: an administrator sees an **Updates** page that checks
|
||||
this repository for the latest release and updates the control plane and each
|
||||
agent with one click. Point a control plane at this repository with:
|
||||
Configuration files are edited a key at a time rather than rewritten, so
|
||||
anything you added by hand survives, and an upgrade with no flags leaves them
|
||||
byte for byte identical.
|
||||
|
||||
Or update from the UI: an administrator gets an **Updates** page that checks
|
||||
for the latest release and moves the control plane and each agent to it with
|
||||
one click. Point a control plane at this release channel with:
|
||||
|
||||
```
|
||||
RUNIX_UPDATE_API_BASE=https://vcs.astra-dev.online/api/v1
|
||||
RUNIX_UPDATE_REPO=svesnav/runix
|
||||
RUNIX_UPDATE_MANIFEST=https://runix-releases.s3-alpha-web.astra-dev.online/latest.json
|
||||
```
|
||||
|
||||
## What you get
|
||||
|
||||
### Every workload, one interface
|
||||
|
||||
Native daemons, Docker containers, Compose projects and systemd units, each in
|
||||
its own tab, all with the same actions. Runix supervises native daemons itself
|
||||
— restart policies, backoff, log capture — so a plain binary gets the same
|
||||
treatment as a container.
|
||||
|
||||
<img src="docs/images/runtimes.png" alt="Runtimes" width="100%">
|
||||
|
||||
### A console that actually takes input
|
||||
|
||||
Not a log tail, and not a shell *next to* your process — the process's own
|
||||
stdin. Type `stop` into a game server and the game server sees it. Copy and
|
||||
paste work like they do in a terminal.
|
||||
|
||||
<img src="docs/images/console.png" alt="Interactive runtime console" width="100%">
|
||||
|
||||
### Per-host metrics and inventory
|
||||
|
||||
CPU, memory, disk and load, live and historical, plus the hardware and OS
|
||||
details the agent reports on every heartbeat.
|
||||
|
||||
<img src="docs/images/server-overview.png" alt="Server overview" width="100%">
|
||||
|
||||
### A real file manager
|
||||
|
||||
Browse, edit with syntax highlighting, upload by drag-and-drop, download whole
|
||||
directories as `tar.gz`, create archives, change permissions — with tabs, a
|
||||
right-click context menu, sorting, search and multi-select. Transfers stream
|
||||
frame by frame, so a large file is never buffered in the control plane's
|
||||
memory.
|
||||
|
||||
<img src="docs/images/files.png" alt="File manager" width="100%">
|
||||
|
||||
### Terminals, into the host or into a container
|
||||
|
||||
A real PTY on the host, or a shell inside a running container, over the same
|
||||
outbound connection the agent already holds open. Sessions survive a page
|
||||
reload.
|
||||
|
||||
<img src="docs/images/terminal.png" alt="Host terminal" width="100%">
|
||||
|
||||
### Docker, beyond containers
|
||||
|
||||
Images, volumes and networks with disk usage and pruning — because they aren't
|
||||
runtimes and pretending otherwise would have bent the model out of shape.
|
||||
|
||||
<img src="docs/images/docker.png" alt="Docker resources" width="100%">
|
||||
|
||||
### Scheduling, with a cron engine of its own
|
||||
|
||||
Restart something nightly, run a command weekly. Multiple control-plane
|
||||
instances can share a database without ever double-running a task.
|
||||
|
||||
<img src="docs/images/schedule.png" alt="Scheduled tasks" width="100%">
|
||||
|
||||
### Access control down to a single container
|
||||
|
||||
Grant a user, a role or a group access to the whole fleet, one server, or one
|
||||
specific runtime on one specific host — including file access scoped to just
|
||||
that runtime's directory. Everything is a dropdown; nothing asks you to paste
|
||||
a UUID.
|
||||
|
||||
<img src="docs/images/grants.png" alt="Permission grants" width="100%">
|
||||
|
||||
### Audit log
|
||||
|
||||
Who did what, to which resource, from which IP — recorded for every mutating
|
||||
action.
|
||||
|
||||
<img src="docs/images/audit.png" alt="Audit log" width="100%">
|
||||
|
||||
### And the rest
|
||||
|
||||
- **Auth** — argon2id, JWT with rotating refresh tokens and reuse detection,
|
||||
TOTP MFA with recovery codes, personal access tokens
|
||||
- **Backup/restore** of configuration, deliberately without secrets
|
||||
- **Self-update** for the control plane and agents, refusing any download
|
||||
without a matching checksum
|
||||
- **Plugins** — external processes speaking line-delimited JSON on stdio can
|
||||
register entirely new runtime types
|
||||
- **English and Russian** UI
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
Browser ──────► runix-server ──────► PostgreSQL
|
||||
(control plane) Redis (optional, multi-instance)
|
||||
▲
|
||||
│ agents dial OUT over WebSocket
|
||||
│ (one connection: RPC + byte streams)
|
||||
┌─────────────┼─────────────┐
|
||||
│ │ │
|
||||
runix-agent runix-agent runix-agent
|
||||
docker systemd daemons
|
||||
```
|
||||
|
||||
- **Agents connect outbound.** Managed hosts need no open ports. One connection
|
||||
multiplexes correlated RPC calls and byte streams (logs, terminals, file
|
||||
transfers) at once.
|
||||
- **The runtime abstraction is small on purpose.** A core interface every
|
||||
provider implements, plus optional capability interfaces it opts into. The
|
||||
capability set travels to the UI, which renders only the actions that exist —
|
||||
so no button is ever a "not supported" error waiting to happen.
|
||||
|
||||
## Requirements
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **OS** | Linux, `amd64` or `arm64` |
|
||||
| **Install** | root (the installer writes to `/opt` and `/etc/systemd/system`) |
|
||||
| **Service manager** | systemd (optional — without it the installer prints how to run the binaries) |
|
||||
| **Database** | PostgreSQL 17, provisioned in Docker for you or one you already run (control plane only) |
|
||||
| **Docker** | Optional. Needed to provision PostgreSQL and to manage Docker runtimes |
|
||||
|
||||
Agents have no dependencies at all: one static binary.
|
||||
|
||||
## Configuration
|
||||
|
||||
Both binaries are configured by environment, read from `/opt/runix/etc/*.env`.
|
||||
|
||||
<details>
|
||||
<summary><b><code>runix-server</code> — the essentials</b></summary>
|
||||
|
||||
<br>
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `RUNIX_HTTP_ADDR` | `:8080` | Listen address; `127.0.0.1:8080` to sit behind a proxy |
|
||||
| `RUNIX_DATABASE_DSN` | — | PostgreSQL connection string (required) |
|
||||
| `RUNIX_JWT_SECRET` | — | Signing key for sessions (required; min 32 chars) |
|
||||
| `RUNIX_ENCRYPTION_KEY` | — | Encrypts stored secrets such as TOTP (required) |
|
||||
| `RUNIX_CORS_ORIGINS` | — | Browser origins allowed for API + WebSocket |
|
||||
| `RUNIX_UPDATE_MANIFEST` | this release channel | Where the Updates page looks for new versions |
|
||||
| `RUNIX_REDIS_ADDR` | — | Optional; shares events across control-plane instances |
|
||||
|
||||
The installer generates the secrets and preserves them across upgrades —
|
||||
rotating them would invalidate every session and make stored MFA secrets
|
||||
unreadable.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b><code>runix-agent</code></b></summary>
|
||||
|
||||
<br>
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `RUNIX_AGENT_SERVER_URL` | — | Control-plane URL to dial (required) |
|
||||
| `RUNIX_AGENT_TOKEN` | — | Enrollment token, minted by the control plane (required) |
|
||||
| `RUNIX_AGENT_DATA_DIR` | `/opt/runix/agent` | Where supervised daemon state lives |
|
||||
| `RUNIX_AGENT_SHELL` | login shell | Shell used for host terminals |
|
||||
|
||||
</details>
|
||||
|
||||
## Releases
|
||||
|
||||
Each release publishes, for `linux/amd64` and `linux/arm64`:
|
||||
|
||||
- `runix-server_linux_<arch>` — the control plane
|
||||
- `runix-agent_linux_<arch>` — the agent
|
||||
- `install.sh` — this installer
|
||||
- `SHA256SUMS` — checksums; the installer verifies every download against it
|
||||
| File | |
|
||||
|---|---|
|
||||
| `runix-server_linux_<arch>` | The control plane, with the web UI embedded |
|
||||
| `runix-agent_linux_<arch>` | The agent |
|
||||
| `install.sh` | The installer |
|
||||
| `SHA256SUMS` | Checksums for everything above |
|
||||
|
||||
Binaries are built automatically from source on tag by the CI runner and
|
||||
published here and to the source repository.
|
||||
Built automatically from source by CI on every version tag, and published here
|
||||
and to object storage. `latest.json` names the newest version and each asset's
|
||||
checksum — it is what the installer and the in-app updater read, and it is
|
||||
written last so a half-finished upload is never advertised.
|
||||
|
||||
## Verify a download by hand
|
||||
### Verify a download by hand
|
||||
|
||||
```sh
|
||||
curl -fsSLO https://vcs.astra-dev.online/svesnav/runix/releases/download/v0.5.0/runix-server_linux_amd64
|
||||
curl -fsSL https://vcs.astra-dev.online/svesnav/runix/releases/download/v0.5.0/SHA256SUMS | sha256sum -c --ignore-missing
|
||||
BASE=https://runix-releases.s3-alpha-web.astra-dev.online
|
||||
curl -fsSLO $BASE/v0.5.2/runix-server_linux_amd64
|
||||
curl -fsSL $BASE/v0.5.2/SHA256SUMS | sha256sum -c --ignore-missing
|
||||
```
|
||||
|
||||
## Security notes
|
||||
|
||||
- Agents dial **out**; nothing connects in to a managed host.
|
||||
- Every downloaded binary is verified against a published SHA-256 before it
|
||||
replaces anything. An unverified download is refused, not warned about.
|
||||
- `/opt/runix/etc/*.env` holds the secrets and is written `0600`.
|
||||
- Access is deny-by-default: a permission can be granted globally, per server
|
||||
group, per server, or on a single runtime, and the UI only renders controls
|
||||
the account can actually use.
|
||||
- Runix runs workloads you define, as the user you configure. Treat access to
|
||||
the control plane as equivalent to shell on every managed host.
|
||||
|
||||
|
After Width: | Height: | Size: 393 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 130 KiB |
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
# Runix installer — the one command that sets a host up.
|
||||
#
|
||||
# curl -fsSL https://vcs.astra-dev.online/svesnav/runix/raw/branch/main/install.sh | sudo sh
|
||||
# curl -fsSL https://runix-releases.s3-alpha-web.astra-dev.online/install.sh | sudo sh
|
||||
#
|
||||
# Asks what this host should be (control plane, agent, or both), checks
|
||||
# the prerequisites, installs under /opt/runix and wires up systemd.
|
||||
@@ -19,9 +19,15 @@
|
||||
set -eu
|
||||
|
||||
PREFIX=${RUNIX_PREFIX:-/opt/runix}
|
||||
# Releases are served from a Gitea instance by default. Point GITEA at a
|
||||
# GitHub-style host by clearing it (RUNIX_GITEA=) and setting a download
|
||||
# base, if you mirror the release elsewhere.
|
||||
# Releases are served from object storage by default: it needs no account,
|
||||
# so a plain `curl … | sh` works on a fresh box. The layout is
|
||||
#
|
||||
# <base>/latest.json names the newest version
|
||||
# <base>/<version>/<asset> the binaries, install.sh and SHA256SUMS
|
||||
#
|
||||
# Clear it (RUNIX_S3=) to fall back to the git forge below.
|
||||
S3_BASE=${RUNIX_S3:-https://runix-releases.s3-alpha-web.astra-dev.online}
|
||||
# The forge is the fallback source, used when S3_BASE is empty.
|
||||
GITEA=${RUNIX_GITEA:-https://vcs.astra-dev.online}
|
||||
REPO=${RUNIX_REPO:-svesnav/runix}
|
||||
VERSION=${RUNIX_VERSION:-latest}
|
||||
@@ -86,8 +92,9 @@ Options:
|
||||
--server-binary PATH Install a local control-plane build
|
||||
--agent-binary PATH Install a local agent build
|
||||
--version VERSION Release to install (default: latest)
|
||||
--repo OWNER/NAME Release repository (default: $REPO)
|
||||
--gitea URL Gitea instance serving releases (default: $GITEA)
|
||||
--s3 URL Object store serving releases (default: $S3_BASE)
|
||||
--repo OWNER/NAME Release repository, when not using --s3
|
||||
--gitea URL Gitea instance serving releases (fallback source)
|
||||
--repo-token TOK Read token, if the instance requires sign-in
|
||||
--prefix PATH Install root (default: $PREFIX)
|
||||
--no-start Install and configure, but do not start services
|
||||
@@ -95,7 +102,7 @@ Options:
|
||||
-h, --help Show this help
|
||||
|
||||
Environment equivalents: RUNIX_PREFIX, RUNIX_VERSION, RUNIX_REPO,
|
||||
RUNIX_GITEA, RUNIX_TOKEN, RUNIX_DATABASE_DSN, RUNIX_ADMIN_PASSWORD,
|
||||
RUNIX_S3, RUNIX_GITEA, RUNIX_TOKEN, RUNIX_DATABASE_DSN, RUNIX_ADMIN_PASSWORD,
|
||||
RUNIX_HTTP_HOST, RUNIX_AGENT_SERVER_URL, RUNIX_AGENT_TOKEN,
|
||||
RUNIX_POSTGRES_PORT.
|
||||
|
||||
@@ -125,7 +132,8 @@ while [ $# -gt 0 ]; do
|
||||
--agent-binary) AGENT_BIN="$2"; shift 2 ;;
|
||||
--version) VERSION="$2"; shift 2 ;;
|
||||
--repo) REPO="$2"; shift 2 ;;
|
||||
--gitea) GITEA="$2"; shift 2 ;;
|
||||
--s3) S3_BASE="$2"; shift 2 ;;
|
||||
--gitea) GITEA="$2"; S3_BASE=""; shift 2 ;;
|
||||
--repo-token|--github-token) DL_TOKEN="$2"; shift 2 ;;
|
||||
--prefix) PREFIX="$2"; shift 2 ;;
|
||||
--no-start) NO_START=1; shift ;;
|
||||
@@ -136,7 +144,9 @@ while [ $# -gt 0 ]; do
|
||||
done
|
||||
|
||||
if [ -z "$DOWNLOAD_BASE" ]; then
|
||||
if [ -n "$GITEA" ]; then
|
||||
if [ -n "$S3_BASE" ]; then
|
||||
DOWNLOAD_BASE="$S3_BASE"
|
||||
elif [ -n "$GITEA" ]; then
|
||||
DOWNLOAD_BASE="$GITEA/$REPO/releases"
|
||||
else
|
||||
DOWNLOAD_BASE="https://github.com/$REPO/releases"
|
||||
@@ -802,6 +812,16 @@ RESOLVED_TAG=""
|
||||
resolve_tag() {
|
||||
if [ "$VERSION" != latest ]; then RESOLVED_TAG="$VERSION"; return 0; fi
|
||||
[ -n "$RESOLVED_TAG" ] && return 0
|
||||
# Object storage serves no API, so the newest version is named in a
|
||||
# small manifest published beside the binaries.
|
||||
if [ -n "$S3_BASE" ]; then
|
||||
_man=$(mktemp)
|
||||
if fetch "$S3_BASE/latest.json" "$_man" "application/json"; then
|
||||
RESOLVED_TAG=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_man" | head -n1)
|
||||
fi
|
||||
rm -f "$_man"
|
||||
[ -n "$RESOLVED_TAG" ] && return 0
|
||||
fi
|
||||
if [ -n "$GITEA" ]; then
|
||||
_loc=$(redirect_of "$DOWNLOAD_BASE/latest" 2>/dev/null || true)
|
||||
RESOLVED_TAG=$(printf '%s' "$_loc" | sed -n 's#.*/releases/tag/##p')
|
||||
@@ -843,19 +863,26 @@ asset_id() {
|
||||
|
||||
# try_download NAME DEST — quiet, returns non-zero if the asset is absent.
|
||||
try_download() {
|
||||
_name=$1; _dest=$2
|
||||
if [ -n "$GITEA" ]; then
|
||||
# Distinct variable names: POSIX sh has no locals, and resolve_tag
|
||||
# below calls fetch, which would otherwise overwrite the _dest we were
|
||||
# handed — sending the binary to the manifest's temp file instead of
|
||||
# where the caller expects it.
|
||||
_tdname=$1; _tddest=$2
|
||||
if [ -n "$S3_BASE" ]; then
|
||||
resolve_tag || return 1
|
||||
fetch "$S3_BASE/$RESOLVED_TAG/$_tdname" "$_tddest" || return 1
|
||||
elif [ -n "$GITEA" ]; then
|
||||
# Gitea assets download by tag; the token (if any) rides in fetch.
|
||||
resolve_tag || return 1
|
||||
fetch "$DOWNLOAD_BASE/download/$RESOLVED_TAG/$_name" "$_dest" || return 1
|
||||
fetch "$DOWNLOAD_BASE/download/$RESOLVED_TAG/$_tdname" "$_tddest" || return 1
|
||||
elif [ -n "$DL_TOKEN" ]; then
|
||||
_id=$(asset_id "$_name") || return 1
|
||||
_id=$(asset_id "$_tdname") || return 1
|
||||
fetch "https://api.github.com/repos/$REPO/releases/assets/$_id" \
|
||||
"$_dest" "application/octet-stream" || return 1
|
||||
"$_tddest" "application/octet-stream" || return 1
|
||||
elif [ "$VERSION" = latest ]; then
|
||||
fetch "$DOWNLOAD_BASE/latest/download/$_name" "$_dest" || return 1
|
||||
fetch "$DOWNLOAD_BASE/latest/download/$_tdname" "$_tddest" || return 1
|
||||
else
|
||||
fetch "$DOWNLOAD_BASE/download/$VERSION/$_name" "$_dest" || return 1
|
||||
fetch "$DOWNLOAD_BASE/download/$VERSION/$_tdname" "$_tddest" || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1016,6 +1043,14 @@ install_server() {
|
||||
mkdir -p "$PREFIX"
|
||||
install_binary runix-server "$SERVER_BIN"
|
||||
|
||||
# Applying an update from the UI replaces the binary in place, which
|
||||
# means writing a new file into this directory and renaming it over the
|
||||
# old one — so the service user has to own the directory, not just the
|
||||
# file. Without this the update fails with a bare permission error long
|
||||
# after the download has succeeded.
|
||||
chown "$SERVER_USER" "$BIN_DIR" 2>/dev/null || true
|
||||
chown "$SERVER_USER" "$BIN_DIR/runix-server" 2>/dev/null || true
|
||||
|
||||
[ "$DB_MODE" = docker ] && provision_postgres
|
||||
[ -n "$DSN" ] || fail "no database configured"
|
||||
|
||||
@@ -1094,7 +1129,11 @@ NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ReadWritePaths=$CONFIG_DIR
|
||||
# The binary directory is writable so the control plane can replace its own
|
||||
# binary when an administrator applies an update from the UI. That is the
|
||||
# price of in-app updates: the service can rewrite the file it runs from.
|
||||
# Drop \$BIN_DIR here to forbid it, and upgrade by re-running this script.
|
||||
ReadWritePaths=$CONFIG_DIR $BIN_DIR
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1376,7 +1415,9 @@ fi
|
||||
if [ "$ROLE" = server ]; then
|
||||
echo
|
||||
echo " Add hosts from the UI (Servers → Add server), then run on each:"
|
||||
if [ -n "$GITEA" ]; then
|
||||
if [ -n "$S3_BASE" ]; then
|
||||
say " curl -fsSL $S3_BASE/install.sh | sudo sh -s -- \\"
|
||||
elif [ -n "$GITEA" ]; then
|
||||
say " curl -fsSL $GITEA/$REPO/raw/branch/main/install.sh | sudo sh -s -- \\"
|
||||
else
|
||||
say " curl -fsSL $DOWNLOAD_BASE/latest/download/install.sh | sudo sh -s -- \\"
|
||||
|
||||