#!/bin/bash # install-lian_node.sh — taskserv installer for a persistent BuildKit node. # Runs on the target Debian/Ubuntu host as root. Idempotent. # # Order on the server: fleet_base → lian_node → fleet_agent (per servers.ncl). # TLS material is delivered by fleet-agent install_tls after the agent connects # (adr-009: cert-manager source, no acme_certd dependency). # # Inputs (env file, rendered by Tera from workspace NCL): # LIAN_NODE_LOGICAL_ID — fleet logical id (rejected if "UNSET") # LIAN_NODE_PRIVATE_IP — pinned private IP (informational) # BUILDKIT_VERSION — moby/buildkit release tag (e.g. v0.29.0) # SCCACHE_VERSION — mozilla/sccache release tag (e.g. v0.9.1) # NUSHELL_VERSION — nushell release tag (e.g. 0.112.2) # JUST_VERSION — casey/just release version (e.g. 1.51.0) # ORAS_VERSION — oras-project/oras release version (e.g. 1.3.2) # BUILDKIT_ROOT — buildkit data dir # SCCACHE_DISK_PATH — local sccache cache dir # BUILDKIT_TLS_DOMAIN — FQDN the cert covers (e.g. fleet.librecloud.online) # BUILDKIT_TLS_CERT_PATH — leaf cert path (default /etc/buildkit/tls/server.crt) # BUILDKIT_TLS_CA_PATH — CA cert path for mTLS (default /etc/buildkit/tls/ca.crt) # BUILDKIT_TLS_KEY_PATH — private key path (default /etc/buildkit/tls/server.key) # BUILDKIT_TCP_LISTEN_PORT — TLS TCP listen port (default 1234) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" for f in env-lian_node _credentials.env; do if [ -f "${SCRIPT_DIR}/${f}" ]; then # shellcheck source=/dev/null source "${SCRIPT_DIR}/${f}" fi done : "${LIAN_NODE_LOGICAL_ID:?LIAN_NODE_LOGICAL_ID must be set}" : "${BUILDKIT_VERSION:?BUILDKIT_VERSION must be set}" : "${SCCACHE_VERSION:?SCCACHE_VERSION must be set}" : "${NUSHELL_VERSION:?NUSHELL_VERSION must be set}" : "${JUST_VERSION:?JUST_VERSION must be set}" : "${ORAS_VERSION:?ORAS_VERSION must be set}" : "${BUILDKIT_TLS_DOMAIN:?BUILDKIT_TLS_DOMAIN must be set}" BUILDKIT_TLS_CERT_PATH="${BUILDKIT_TLS_CERT_PATH:-/etc/buildkit/tls/server.crt}" BUILDKIT_TLS_CA_PATH="${BUILDKIT_TLS_CA_PATH:-/etc/buildkit/tls/ca.crt}" BUILDKIT_TLS_KEY_PATH="${BUILDKIT_TLS_KEY_PATH:-/etc/buildkit/tls/server.key}" BUILDKIT_ROOT="${BUILDKIT_ROOT:-/var/lib/buildkit}" SCCACHE_DISK_PATH="${SCCACHE_DISK_PATH:-/var/cache/sccache}" BUILDKIT_TCP_LISTEN_PORT="${BUILDKIT_TCP_LISTEN_PORT:-1234}" # Swap policy (see NCL `swap` block in lian_node component contract). SWAP_MODE="${SWAP_MODE:-preserve}" SWAP_SIZE_GB="${SWAP_SIZE_GB:-8}" SWAP_PATH="${SWAP_PATH:-/swapfile}" SWAP_SWAPPINESS="${SWAP_SWAPPINESS:-10}" SWAP_VFS_CACHE_PRESSURE="${SWAP_VFS_CACHE_PRESSURE:-50}" bold() { printf "\033[1m%s\033[0m\n" "$*"; } fatal() { printf "\033[31mFATAL:\033[0m %s\n" "$*" >&2; exit 1; } # ── Swap management ────────────────────────────────────────────────── # Three modes, declared per-node in NCL: # 'file — create $SWAP_PATH, mkswap, swapon, persist via /etc/fstab # 'disabled — swapoff -a, remove /etc/fstab entries (cluster-worker prep) # 'preserve — no-op (default — leave whatever host state exists) # Idempotent: re-runs converge to the declared state, never duplicate fstab. _setup_swap_file() { local size_gb="$1" path="$2" # If existing file has wrong size, recreate. if [ -f "$path" ]; then local actual_bytes actual_bytes=$(stat -c %s "$path" 2>/dev/null || stat -f %z "$path" 2>/dev/null || echo 0) local want_bytes=$((size_gb * 1024 * 1024 * 1024)) if [ "$actual_bytes" -eq "$want_bytes" ] && swapon --show=NAME --noheadings 2>/dev/null | grep -qx "$path"; then echo " swap: $path already active at ${size_gb}G — no-op" return 0 fi echo " swap: recreating $path (was ${actual_bytes}B, want ${want_bytes}B or not active)" swapoff "$path" 2>/dev/null || true rm -f "$path" fi # Prefer fallocate (instant, no zero-fill); fall back to dd if fs lacks support. if ! fallocate -l "${size_gb}G" "$path" 2>/dev/null; then echo " swap: fallocate failed (xfs/ext4 without extent allocation?) — falling back to dd" dd if=/dev/zero of="$path" bs=1M count=$((size_gb * 1024)) status=progress fi chmod 0600 "$path" mkswap "$path" >/dev/null swapon "$path" # Persist via /etc/fstab. Idempotent. if ! grep -q "^${path}\b" /etc/fstab 2>/dev/null; then printf '%s\tnone\tswap\tsw\t0 0\n' "$path" >> /etc/fstab fi echo " swap: ${path} active at ${size_gb}G + persisted in /etc/fstab" } _setup_swap_sysctl() { local swappiness="$1" cache_pressure="$2" local conf="/etc/sysctl.d/99-lian-node-swap.conf" cat > "${conf}.new" </dev/null echo " swap: vm.swappiness=${swappiness} vm.vfs_cache_pressure=${cache_pressure} applied" } _disable_swap() { swapoff -a 2>/dev/null || true if grep -qE '^[^#].*\bswap\b' /etc/fstab 2>/dev/null; then # Comment out any swap line (don't delete — keeps history for postmortem). sed -i.bak '/[[:space:]]swap[[:space:]]/s/^/# disabled-by-lian_node /' /etc/fstab echo " swap: disabled + /etc/fstab swap entries commented out" else echo " swap: already off" fi } apply_swap_policy() { bold "==> swap policy: ${SWAP_MODE}" case "$SWAP_MODE" in file) _setup_swap_file "$SWAP_SIZE_GB" "$SWAP_PATH" _setup_swap_sysctl "$SWAP_SWAPPINESS" "$SWAP_VFS_CACHE_PRESSURE" ;; disabled) _disable_swap ;; preserve) echo " swap: preserve — no changes to host state" ;; *) fatal "unknown SWAP_MODE='$SWAP_MODE' (expected file|disabled|preserve)" ;; esac } [ "$(id -u)" -eq 0 ] || fatal "must run as root" [ "$LIAN_NODE_LOGICAL_ID" != "UNSET" ] || fatal "LIAN_NODE_LOGICAL_ID is sentinel 'UNSET' — caller must override" ARCH=$(dpkg --print-architecture) case "$ARCH" in arm64|amd64) ;; *) fatal "unsupported arch: $ARCH" ;; esac # arch translations RUST_TRIPLE_ARM64="aarch64-unknown-linux-musl" RUST_TRIPLE_AMD64="x86_64-unknown-linux-musl" NU_TRIPLE_ARM64="aarch64-unknown-linux-gnu" NU_TRIPLE_AMD64="x86_64-unknown-linux-gnu" ORAS_ARCH="$( [ "$ARCH" = "arm64" ] && echo arm64 || echo amd64 )" ACTION="${1:-install}" # Download + install a single binary from a github tarball. # Usage: _fetch_bin _fetch_bin() { local url="$1" member="$2" dest="$3" local tmp; tmp=$(mktemp -d) wget -q -O "${tmp}/pkg.tgz" "${url}" tar -xzf "${tmp}/pkg.tgz" -C "${tmp}" if [ ! -f "${tmp}/${member}" ]; then # Some tarballs nest under /; find it. local found found=$(find "${tmp}" -name "$(basename "${member}")" -type f | head -1 || true) [ -n "${found}" ] || { rm -rf "${tmp}"; fatal "binary ${member} not found in ${url}"; } install -m 0755 "${found}" "${dest}" else install -m 0755 "${tmp}/${member}" "${dest}" fi rm -rf "${tmp}" } cmd_install() { # Swap policy first — buildkit + linker workloads benefit from the overflow # buffer being in place BEFORE any heavy install (cargo, oras pulls). apply_swap_policy bold "==> [1/8] Ensure system packages" export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq \ uidmap fuse-overlayfs slirp4netns \ wget curl ca-certificates bold "==> [2/8] Install just ${JUST_VERSION}" if just --version 2>/dev/null | grep -q "${JUST_VERSION}"; then echo " just ${JUST_VERSION} already installed" else local triple="${RUST_TRIPLE_ARM64}" [ "$ARCH" = "amd64" ] && triple="${RUST_TRIPLE_AMD64}" _fetch_bin "https://github.com/casey/just/releases/download/${JUST_VERSION}/just-${JUST_VERSION}-${triple}.tar.gz" "just" /usr/local/bin/just echo " installed $(just --version)" fi bold "==> [3/8] Install oras ${ORAS_VERSION}" if oras version 2>/dev/null | grep -q "${ORAS_VERSION}"; then echo " oras ${ORAS_VERSION} already installed" else _fetch_bin "https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}/oras_${ORAS_VERSION}_linux_${ORAS_ARCH}.tar.gz" "oras" /usr/local/bin/oras echo " installed $(oras version | head -1)" fi bold "==> [4/8] Install buildkit ${BUILDKIT_VERSION}" if buildctl --version 2>/dev/null | grep -q "${BUILDKIT_VERSION#v}" && [ -x /usr/local/bin/buildkit-runc ]; then echo " buildkit ${BUILDKIT_VERSION} already installed (with worker)" else local tmp; tmp=$(mktemp -d) wget -q -O "${tmp}/buildkit.tar.gz" \ "https://github.com/moby/buildkit/releases/download/${BUILDKIT_VERSION}/buildkit-${BUILDKIT_VERSION}.linux-${ARCH}.tar.gz" # Extract ALL binaries (buildkitd, buildctl, buildkit-runc, buildkit-cni-*, qemu-*). # buildkit-runc REQUIRED — without it buildkitd fails with "no worker found". tar -xzf "${tmp}/buildkit.tar.gz" -C /usr/local/ rm -rf "${tmp}" echo " installed $(buildctl --version)" fi bold "==> [5/8] Install sccache ${SCCACHE_VERSION}" if sccache --version 2>/dev/null | grep -q "${SCCACHE_VERSION#v}"; then echo " sccache ${SCCACHE_VERSION} already installed" else local triple="${RUST_TRIPLE_ARM64}" [ "$ARCH" = "amd64" ] && triple="${RUST_TRIPLE_AMD64}" local member="sccache-${SCCACHE_VERSION}-${triple}/sccache" _fetch_bin "https://github.com/mozilla/sccache/releases/download/${SCCACHE_VERSION}/sccache-${SCCACHE_VERSION}-${triple}.tar.gz" "${member}" /usr/local/bin/sccache echo " installed $(sccache --version)" fi bold "==> [6/8] Install nushell ${NUSHELL_VERSION}" if nu --version 2>/dev/null | grep -q "${NUSHELL_VERSION}"; then echo " nushell ${NUSHELL_VERSION} already installed" else local triple="${NU_TRIPLE_ARM64}" [ "$ARCH" = "amd64" ] && triple="${NU_TRIPLE_AMD64}" local member="nu-${NUSHELL_VERSION}-${triple}/nu" _fetch_bin "https://github.com/nushell/nushell/releases/download/${NUSHELL_VERSION}/nu-${NUSHELL_VERSION}-${triple}.tar.gz" "${member}" /usr/local/bin/nu echo " installed $(nu --version)" fi bold "==> [7/8] Configure buildkitd systemd unit with TLS" install -d -m 0755 "${BUILDKIT_ROOT}" "${SCCACHE_DISK_PATH}" # Pre-create the TLS dir fleet-agent's install_tls will write into. install -d -m 0750 /etc/buildkit/tls groupadd --system buildkit 2>/dev/null || true chown root:buildkit /etc/buildkit/tls cat > /etc/systemd/system/buildkitd.service </dev/null || echo " buildkitd: start deferred (certs pending install_tls from fleet-agent)" bold "==> [8/9] Fleet operational wrappers" # Docker-compat credential bridge: oras and other OCI tools read from # $DOCKER_CONFIG/config.json. fleet_base writes /etc/containers/auth.json; # expose it under /etc/fleet/docker/config.json so DOCKER_CONFIG works. install -d -m 0755 /etc/fleet/docker ln -sfn /etc/containers/auth.json /etc/fleet/docker/config.json cat > /usr/local/bin/fleet-oras <<'WRAPPER' #!/bin/sh DOCKER_CONFIG=/etc/fleet/docker exec oras "$@" WRAPPER chmod 0755 /usr/local/bin/fleet-oras cat > /usr/local/bin/fleet-buildctl <<'WRAPPER' #!/bin/sh exec buildctl --addr unix:///run/buildkit/buildkitd.sock "$@" WRAPPER chmod 0755 /usr/local/bin/fleet-buildctl echo " fleet-oras : DOCKER_CONFIG=/etc/fleet/docker → /etc/containers/auth.json" echo " fleet-buildctl : addr=unix:///run/buildkit/buildkitd.sock" bold "==> [9/9] sshd hardening" install -d -m 0755 /etc/ssh/sshd_config.d cat > /etc/ssh/sshd_config.d/10-lian-node-hardening.conf <<'EOF' PasswordAuthentication no PubkeyAuthentication yes PermitRootLogin prohibit-password ChallengeResponseAuthentication no KbdInteractiveAuthentication no EOF systemctl reload ssh || systemctl reload sshd || true bold "==> Done" systemctl is-active buildkitd >/dev/null && echo " buildkitd : active (tls on :${BUILDKIT_TCP_LISTEN_PORT})" || echo " buildkitd : pending TLS (install_tls from fleet-agent will start it)" echo " logical_id : ${LIAN_NODE_LOGICAL_ID}" echo " private_ip : ${LIAN_NODE_PRIVATE_IP}" echo " tls domain : ${BUILDKIT_TLS_DOMAIN}" echo " buildctl : $(buildctl --version)" echo " just : $(just --version)" echo " oras : $(oras version | head -1)" echo " sccache : $(sccache --version)" echo " nushell : $(nu --version)" } cmd_status() { systemctl status buildkitd --no-pager echo "=== wrappers ===" echo " fleet-oras : $([ -x /usr/local/bin/fleet-oras ] && echo ok || echo MISSING)" echo " fleet-buildctl : $([ -x /usr/local/bin/fleet-buildctl ] && echo ok || echo MISSING)" echo " docker config : $([ -L /etc/fleet/docker/config.json ] && readlink /etc/fleet/docker/config.json || echo MISSING)" echo "=== swap ===" local active active=$(swapon --show=NAME,SIZE,USED --noheadings 2>/dev/null || true) if [ -n "$active" ]; then printf ' %s\n' "$active" else echo " (no swap active)" fi echo " swappiness : $(sysctl -n vm.swappiness 2>/dev/null || echo '?')" echo " vfs_cache_pressure : $(sysctl -n vm.vfs_cache_pressure 2>/dev/null || echo '?')" echo " fstab entries : $(grep -E '[[:space:]]swap[[:space:]]' /etc/fstab 2>/dev/null | grep -v '^#' | wc -l | tr -d ' ')" } cmd_uninstall() { systemctl disable --now buildkitd 2>/dev/null || true rm -f /etc/systemd/system/buildkitd.service systemctl daemon-reload rm -f /usr/local/bin/buildkitd /usr/local/bin/buildctl /usr/local/bin/buildkit-runc rm -f /usr/local/bin/just /usr/local/bin/oras rm -f /usr/local/bin/fleet-oras /usr/local/bin/fleet-buildctl rm -f /etc/fleet/docker/config.json rmdir --ignore-fail-on-non-empty /etc/fleet/docker 2>/dev/null || true rm -f /etc/ssh/sshd_config.d/10-lian-node-hardening.conf rm -f /etc/acme-certd/reload.d/buildkitd.sh echo "uninstalled (data dirs ${BUILDKIT_ROOT} and ${SCCACHE_DISK_PATH} preserved)" } case "$ACTION" in install) cmd_install ;; status) cmd_status ;; uninstall) cmd_uninstall ;; *) fatal "unknown action: $ACTION (install|status|uninstall)" ;; esac