diff --git a/README.md b/README.md index 80eef89..bc50138 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,11 @@ One command sets up a host. Run it as root; it asks what the host should be `/opt/runix`, and wires up systemd. ```sh -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 ``` -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 -``` +Binaries are served from object storage, so this needs no account and no +token. To pin a version, add `--version v0.5.0`. ### Roles @@ -56,16 +52,15 @@ 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://runix-releases.s3-alpha-web.astra-dev.online/install.sh | sudo sh -s -- -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 +object storage for the latest release and updates the control plane and each agent with one click. Point a control plane at this repository 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 ``` ## Releases @@ -78,11 +73,13 @@ Each release publishes, for `linux/amd64` and `linux/arm64`: - `SHA256SUMS` — checksums; the installer verifies every download against it Binaries are built automatically from source on tag by the CI runner and -published here and to the source repository. +published to object storage as well as to this repository. `latest.json` +beside them names the newest version and every asset's checksum — that is +what the installer and the in-app updater read. ## 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 +curl -fsSLO https://runix-releases.s3-alpha-web.astra-dev.online/v0.5.0/runix-server_linux_amd64 +curl -fsSL https://runix-releases.s3-alpha-web.astra-dev.online/v0.5.0/SHA256SUMS | sha256sum -c --ignore-missing ``` diff --git a/install.sh b/install.sh index c8eeeb1..4efcc15 100755 --- a/install.sh +++ b/install.sh @@ -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 +# +# /latest.json names the newest version +# // 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 -- \\"