Merge pull request 'fix(launcher): run on a bare Alpine host (POSIX prologue, self-installs bash)' (#6) from fix/alpine-posix-launcher into main
Reviewed-on: #6
This commit is contained in:
@@ -18,7 +18,7 @@ with automatic Let's Encrypt TLS, orchestrated with Docker Compose.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.anomalous.dev/57_Wolve/automations/raw/branch/main/automations.sh \
|
||||
| REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git bash
|
||||
| REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git sh
|
||||
```
|
||||
|
||||
Or, from a clone:
|
||||
@@ -27,6 +27,12 @@ Or, from a clone:
|
||||
./automations.sh
|
||||
```
|
||||
|
||||
Pipe it to `sh`, not `bash` — a stock Alpine box has busybox `ash` and no bash
|
||||
at all. The launcher's prologue is POSIX sh: it installs `git`/`bash` for the
|
||||
distro it finds itself on (apk / apt-get / dnf / yum), then re-execs itself
|
||||
under bash. `./automations.sh` from a clone does the same, so a bare Alpine
|
||||
install needs nothing prepared by hand.
|
||||
|
||||
[`automations.sh`](automations.sh) opens a **Gum** wizard (auto-installed) that
|
||||
lets you:
|
||||
|
||||
@@ -55,11 +61,14 @@ pipe, so download first):
|
||||
|
||||
```bash
|
||||
curl -fsSLO https://your-host/automations-bundle.sh
|
||||
bash automations-bundle.sh # launcher wizard
|
||||
bash automations-bundle.sh bash scripts/setup-host.sh # run one script
|
||||
SSH_PORT=2222 bash automations-bundle.sh bash scripts/harden-jumphost.sh
|
||||
sh automations-bundle.sh # launcher wizard
|
||||
sh automations-bundle.sh bash scripts/setup-host.sh # run one script
|
||||
SSH_PORT=2222 sh automations-bundle.sh bash scripts/harden-jumphost.sh
|
||||
```
|
||||
|
||||
Like the launcher, the bundle's stub is POSIX sh and installs `bash` if the
|
||||
host lacks it, so it runs on a bare Alpine box.
|
||||
|
||||
It extracts to `INSTALL_DIR` (default `/opt/automations`) and runs the launcher
|
||||
or the command you pass. The payload excludes ignored files, so no secrets are
|
||||
embedded.
|
||||
|
||||
+83
-28
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/sh
|
||||
#
|
||||
# automations.sh -- one command to run or deploy anything in this repo.
|
||||
#
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
# 1. One-liner on a fresh target host (clones the repo, then launches):
|
||||
# curl -fsSL https://git.anomalous.dev/57_Wolve/automations/raw/branch/main/automations.sh \
|
||||
# | REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git bash
|
||||
# | REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git sh
|
||||
#
|
||||
# 2. From a clone:
|
||||
# ./automations.sh
|
||||
@@ -22,43 +22,98 @@
|
||||
# choices in, or just call the underlying deployments/<name>/deploy.sh
|
||||
# directly -- they all honor SKIP_PROMPTS=1.
|
||||
|
||||
set -euo pipefail
|
||||
# ============================================================================
|
||||
# PROLOGUE -- POSIX sh only. Everything below the "exec bash" handoff is bash.
|
||||
#
|
||||
# The shebang is /bin/sh, not bash, on purpose: a stock Alpine box has busybox
|
||||
# ash and NO bash at all, so a `#!/usr/bin/env bash` launcher dies before it
|
||||
# can install anything ("env: 'bash': No such file or directory"). This part
|
||||
# therefore has to parse and run under ash: no [[ ]], no arrays, no
|
||||
# BASH_SOURCE, no printf -v. It locates (or clones) the repo, makes sure bash
|
||||
# exists, and re-execs this same file under bash -- which then skips the
|
||||
# prologue via BASH_VERSION and runs the real launcher.
|
||||
# ============================================================================
|
||||
set -eu
|
||||
|
||||
_boot_log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
|
||||
_boot_die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# Install packages with whichever manager this distro has -- apk (Alpine),
|
||||
# apt-get (Debian/Ubuntu), dnf/yum (Alma/RHEL). oslib.sh's pkg_install can't
|
||||
# help here: it's bash, and on the piped path it isn't even on disk yet.
|
||||
_boot_install() {
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
apk add -q "$@" 2>/dev/null && return 0
|
||||
apk update -q >/dev/null 2>&1 || true # stale/absent index on a fresh box
|
||||
apk add -q "$@"
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "$@"
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y -q "$@"
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y -q "$@"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
_boot_need() { # _boot_need <command> [package] -> 0 if it's available afterwards
|
||||
if command -v "$1" >/dev/null 2>&1; then return 0; fi
|
||||
_boot_log "$1 not found; installing it..."
|
||||
_boot_install "${2:-$1}" || true
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Self-locate, or bootstrap by cloning the repo (one-liner / piped form).
|
||||
# ----------------------------------------------------------------------------
|
||||
_self="${BASH_SOURCE[0]:-}"
|
||||
if [[ -n "$_self" && -f "$(cd "$(dirname "$_self")" 2>/dev/null && pwd)/scripts/lib.sh" ]]; then
|
||||
ROOT="$(cd "$(dirname "$_self")" && pwd)"
|
||||
ROOT=""
|
||||
# Strip the last path component ourselves rather than calling dirname: busybox
|
||||
# dirname takes its first argument literally, so `dirname -- "$0"` would answer
|
||||
# "." on Alpine. `$0` is "sh"/"bash" (no slash) when we're piped from curl.
|
||||
case "$0" in
|
||||
*/*) _dir="${0%/*}" ;;
|
||||
*) _dir="." ;;
|
||||
esac
|
||||
_dir="$(CDPATH= cd "$_dir" 2>/dev/null && pwd)" || _dir=""
|
||||
if [ -n "$_dir" ] && [ -f "$_dir/scripts/lib.sh" ]; then
|
||||
ROOT="$_dir"
|
||||
else
|
||||
# Piped via curl: we don't have the repo on disk. Clone it, then re-exec.
|
||||
# Piped via curl: we don't have the repo on disk. Clone it, then hand off.
|
||||
: "${REPO_URL:=}"
|
||||
: "${REPO_BRANCH:=main}"
|
||||
[[ -n "$REPO_URL" ]] || {
|
||||
echo "[x] Running standalone (piped). Set REPO_URL=... so I can clone the repo." >&2
|
||||
exit 1
|
||||
}
|
||||
# We need git to clone, but oslib.sh's pkg_install isn't on disk yet (that's
|
||||
# what we're cloning). Install git inline across the supported package
|
||||
# managers -- apk (Alpine), apt-get (Debian/Ubuntu), dnf/yum (Alma/RHEL).
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo "[+] git not found; installing it..." >&2
|
||||
if command -v apk >/dev/null 2>&1; then apk add -q git || true
|
||||
elif command -v apt-get >/dev/null 2>&1; then { apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq git; } || true
|
||||
elif command -v dnf >/dev/null 2>&1; then dnf install -y -q git || true
|
||||
elif command -v yum >/dev/null 2>&1; then yum install -y -q git || true
|
||||
fi
|
||||
fi
|
||||
command -v git >/dev/null 2>&1 || {
|
||||
echo "[x] git is required to clone the repo, but it isn't installed and I couldn't install it automatically (need root + a supported package manager). Install git, then re-run." >&2
|
||||
exit 1
|
||||
}
|
||||
[ -n "$REPO_URL" ] || _boot_die "Running standalone (piped). Set REPO_URL=... so I can clone the repo."
|
||||
_boot_need git || _boot_die "git is required to clone the repo, but it isn't installed and I couldn't install it automatically (need root + a supported package manager). Install git, then re-run."
|
||||
_tmp="$(mktemp -d -t automations.XXXXXX)"
|
||||
echo "[+] Cloning $REPO_URL ($REPO_BRANCH)..."
|
||||
_boot_log "Cloning $REPO_URL ($REPO_BRANCH)..."
|
||||
git clone --depth 1 --branch "$REPO_BRANCH" "$REPO_URL" "$_tmp"
|
||||
exec bash "$_tmp/automations.sh" "$@"
|
||||
ROOT="$_tmp"
|
||||
fi
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Hand off to bash. Needed when we're running under ash/dash, and when the body
|
||||
# we want is the freshly cloned copy rather than the piped stdin we came from.
|
||||
# ----------------------------------------------------------------------------
|
||||
if [ -z "${BASH_VERSION:-}" ] || [ ! -f "$0" ] || [ "$ROOT" != "$_dir" ]; then
|
||||
if [ "${_AUTOMATIONS_REEXEC:-0}" = 1 ]; then
|
||||
# Already handed off once. If we're in bash the handoff worked and only
|
||||
# the path comparison differs (symlinked checkout) -- just continue.
|
||||
[ -n "${BASH_VERSION:-}" ] || _boot_die "Re-exec under bash did not take effect. Run it explicitly: bash $ROOT/automations.sh"
|
||||
else
|
||||
# The launcher, everything it sources (scripts/lib.sh, scripts/oslib.sh),
|
||||
# and every deploy.sh it invokes are bash. Alpine images routinely ship
|
||||
# without it, so install it before going any further.
|
||||
_boot_need bash || _boot_die "bash is required, but it isn't installed and I couldn't install it automatically (need root + a supported package manager). Install bash, then re-run."
|
||||
_AUTOMATIONS_REEXEC=1; export _AUTOMATIONS_REEXEC
|
||||
exec bash "$ROOT/automations.sh" "$@"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# Running under bash from here down.
|
||||
# ============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=scripts/lib.sh
|
||||
. "$ROOT/scripts/lib.sh"
|
||||
load_globals
|
||||
|
||||
+28
-10
@@ -49,34 +49,52 @@ PAYLOAD="$(make_tar | base64 | tr -d '\n')"
|
||||
# ---------------------------------------------------------------------------
|
||||
{
|
||||
cat <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/sh
|
||||
#
|
||||
# automations-bundle.sh -- self-extracting bundle of the automations repo.
|
||||
# Generated by build-bundle.sh. Download, then run (it can't extract from a
|
||||
# pipe -- it needs to read itself as a file):
|
||||
#
|
||||
# curl -fsSLO https://your-host/automations-bundle.sh
|
||||
# bash automations-bundle.sh # launcher wizard
|
||||
# bash automations-bundle.sh bash scripts/setup-host.sh # run a script
|
||||
# SSH_PORT=2222 bash automations-bundle.sh bash scripts/harden-jumphost.sh
|
||||
# sh automations-bundle.sh # launcher wizard
|
||||
# sh automations-bundle.sh bash scripts/setup-host.sh # run a script
|
||||
# SSH_PORT=2222 sh automations-bundle.sh bash scripts/harden-jumphost.sh
|
||||
#
|
||||
# POSIX sh, not bash: a stock Alpine host has busybox ash and no bash, so a
|
||||
# bash stub would fail before it could install anything. It installs bash
|
||||
# itself (everything inside the bundle is bash) and then runs the launcher.
|
||||
#
|
||||
# Env:
|
||||
# INSTALL_DIR where to extract (default /opt/automations)
|
||||
# BUNDLE_KEEP 1 to keep the extracted repo (default), 0 to use a temp dir
|
||||
# and remove it after the command finishes
|
||||
|
||||
set -euo pipefail
|
||||
set -eu
|
||||
: "${INSTALL_DIR:=/opt/automations}"
|
||||
: "${BUNDLE_KEEP:=1}"
|
||||
|
||||
SELF="${BASH_SOURCE[0]:-$0}"
|
||||
if [[ ! -f "$SELF" ]]; then
|
||||
SELF="$0"
|
||||
if [ ! -f "$SELF" ]; then
|
||||
echo "[x] Run me as a downloaded file, not via a pipe:" >&2
|
||||
echo " curl -fsSLO <url>/automations-bundle.sh && bash automations-bundle.sh" >&2
|
||||
echo " curl -fsSLO <url>/automations-bundle.sh && sh automations-bundle.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$BUNDLE_KEEP" != "1" ]]; then
|
||||
# The repo contents -- launcher, scripts/, deployments/ -- are all bash.
|
||||
if ! command -v bash >/dev/null 2>&1; then
|
||||
echo "[+] bash not found; installing it..." >&2
|
||||
if command -v apk >/dev/null 2>&1; then { apk add -q bash 2>/dev/null || { apk update -q && apk add -q bash; }; } || true
|
||||
elif command -v apt-get >/dev/null 2>&1; then { apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq bash; } || true
|
||||
elif command -v dnf >/dev/null 2>&1; then dnf install -y -q bash || true
|
||||
elif command -v yum >/dev/null 2>&1; then yum install -y -q bash || true
|
||||
fi
|
||||
command -v bash >/dev/null 2>&1 || {
|
||||
echo "[x] bash is required, but it isn't installed and I couldn't install it automatically (need root + a supported package manager). Install bash, then re-run." >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
if [ "$BUNDLE_KEEP" != "1" ]; then
|
||||
INSTALL_DIR="$(mktemp -d -t automations.XXXXXX)"
|
||||
trap 'rm -rf "$INSTALL_DIR"' EXIT
|
||||
fi
|
||||
@@ -88,7 +106,7 @@ sed -e '1,/^__ARCHIVE_BELOW__$/d' "$SELF" | base64 -d | tar -xz -C "$INSTALL_DIR
|
||||
cd "$INSTALL_DIR"
|
||||
chmod +x automations.sh build-bundle.sh scripts/*.sh deployments/*/*.sh 2>/dev/null || true
|
||||
|
||||
if [[ "$#" -gt 0 ]]; then
|
||||
if [ "$#" -gt 0 ]; then
|
||||
exec "$@"
|
||||
else
|
||||
exec bash ./automations.sh
|
||||
|
||||
Reference in New Issue
Block a user