235 lines
7.1 KiB
JavaScript
235 lines
7.1 KiB
JavaScript
import {
|
|
DEFAULT_SSH_HOST,
|
|
NGINX_MARKER,
|
|
NGINX_SITE_LINK,
|
|
NGINX_SNIPPET,
|
|
SERVER_PUBLIC_ROOT,
|
|
SERVER_RELEASES_ROOT,
|
|
SERVER_STAGING_ROOT,
|
|
parseOptions,
|
|
renderNoReleasePage,
|
|
runSsh,
|
|
usageError,
|
|
validateSshHost
|
|
} from './lib.mjs'
|
|
|
|
const usage = `Usage:
|
|
node scripts/updates/provision-server.mjs [--ssh-host local001]
|
|
|
|
Creates /srv/ycode, /srv/ycode/releases/versions and /srv/ycode-staging, backs up the
|
|
nginx site configuration, installs a YCode-only snippet, validates nginx, then reloads.
|
|
It never serves an index and never creates a fake latest.json.
|
|
`
|
|
|
|
const snippet = `${NGINX_MARKER}
|
|
location = /ycode/releases/latest.json {
|
|
alias ${SERVER_RELEASES_ROOT}/latest.json;
|
|
default_type application/json;
|
|
add_header Cache-Control "no-store" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
limit_except GET { deny all; }
|
|
}
|
|
|
|
location = /ycode/releases/index.html {
|
|
alias ${SERVER_RELEASES_ROOT}/index.html;
|
|
default_type text/html;
|
|
charset utf-8;
|
|
add_header Cache-Control "no-store" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
limit_except GET { deny all; }
|
|
}
|
|
|
|
location /ycode/ {
|
|
alias ${SERVER_PUBLIC_ROOT}/;
|
|
autoindex off;
|
|
index index.html;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
limit_except GET { deny all; }
|
|
}
|
|
`
|
|
|
|
async function main() {
|
|
const options = parseOptions(process.argv.slice(2), { values: ['ssh-host'] })
|
|
const host = validateSshHost(options['ssh-host'] || DEFAULT_SSH_HOST)
|
|
const snippetBase64 = Buffer.from(snippet, 'utf8').toString('base64')
|
|
const noReleasePageBase64 = Buffer.from(renderNoReleasePage(), 'utf8').toString('base64')
|
|
const script = `set -euo pipefail
|
|
export LC_ALL=C
|
|
|
|
site_link='${NGINX_SITE_LINK}'
|
|
snippet='${NGINX_SNIPPET}'
|
|
site="$(readlink -f "$site_link")"
|
|
test -f "$site"
|
|
test -f /etc/nginx/nginx.conf
|
|
nginx -t >/dev/null 2>&1
|
|
|
|
install -d -o root -g www-data -m 0755 '${SERVER_PUBLIC_ROOT}'
|
|
install -d -o root -g www-data -m 0755 '${SERVER_RELEASES_ROOT}'
|
|
install -d -o root -g www-data -m 0755 '${SERVER_RELEASES_ROOT}/versions'
|
|
install -d -o root -g root -m 0700 '${SERVER_STAGING_ROOT}'
|
|
|
|
page_tmp="$(mktemp '${SERVER_RELEASES_ROOT}/.index.html.XXXXXX')"
|
|
printf '%s' '${noReleasePageBase64}' | base64 -d > "$page_tmp"
|
|
chmod 0644 "$page_tmp"
|
|
if [ -f '${SERVER_RELEASES_ROOT}/latest.json' ]; then
|
|
if [ ! -f '${SERVER_RELEASES_ROOT}/index.html' ]; then
|
|
rm -f "$page_tmp"
|
|
echo "latest.json exists without a generated download page" >&2
|
|
exit 1
|
|
fi
|
|
if grep -q 'data-ycode-state="no-release"' '${SERVER_RELEASES_ROOT}/index.html'; then
|
|
rm -f "$page_tmp"
|
|
echo "latest.json exists while the download page still says no release" >&2
|
|
exit 1
|
|
fi
|
|
rm -f "$page_tmp"
|
|
elif [ ! -f '${SERVER_RELEASES_ROOT}/index.html' ]; then
|
|
mv -f "$page_tmp" '${SERVER_RELEASES_ROOT}/index.html'
|
|
else
|
|
if ! grep -q 'data-ycode-state="no-release"' '${SERVER_RELEASES_ROOT}/index.html'; then
|
|
rm -f "$page_tmp"
|
|
echo "download page exists without latest.json" >&2
|
|
exit 1
|
|
fi
|
|
rm -f "$page_tmp"
|
|
fi
|
|
|
|
backup_dir=/var/backups/nginx
|
|
install -d -o root -g root -m 0700 "$backup_dir"
|
|
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
config_backup="$backup_dir/ycode-before-${'$'}{stamp}-$(basename "$site")"
|
|
snippet_backup="$backup_dir/ycode-before-${'$'}{stamp}-ycode-releases.conf"
|
|
test ! -e "$config_backup"
|
|
cp -a "$site" "$config_backup"
|
|
|
|
snippet_existed=0
|
|
if [ -f "$snippet" ]; then
|
|
cp -a "$snippet" "$snippet_backup"
|
|
snippet_existed=1
|
|
fi
|
|
|
|
restore() {
|
|
changed=0
|
|
cp -a "$config_backup" "$site"
|
|
if [ "$snippet_existed" -eq 1 ]; then
|
|
cp -a "$snippet_backup" "$snippet"
|
|
else
|
|
rm -f "$snippet"
|
|
fi
|
|
}
|
|
|
|
changed=0
|
|
snippet_tmp=
|
|
cleanup() {
|
|
rc=$?
|
|
if [ -n "$snippet_tmp" ]; then
|
|
rm -f "$snippet_tmp"
|
|
fi
|
|
if [ "$rc" -ne 0 ] && [ "$changed" -eq 1 ]; then
|
|
restore
|
|
nginx -t >/dev/null 2>&1 && systemctl reload nginx >/dev/null 2>&1 || true
|
|
fi
|
|
exit "$rc"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
install -d -o root -g root -m 0755 /etc/nginx/snippets
|
|
snippet_tmp="$(mktemp /etc/nginx/snippets/.ycode-releases.XXXXXX)"
|
|
printf '%s' '${snippetBase64}' | base64 -d > "$snippet_tmp"
|
|
chmod 0644 "$snippet_tmp"
|
|
mv -f "$snippet_tmp" "$snippet"
|
|
changed=1
|
|
|
|
python3 - "$site" <<'PY'
|
|
import os
|
|
import re
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
|
|
site = sys.argv[1]
|
|
marker = "${NGINX_MARKER}"
|
|
include = "\tinclude ${NGINX_SNIPPET};"
|
|
with open(site, "r", encoding="utf-8") as stream:
|
|
original = stream.read()
|
|
|
|
for required in ("location /baocode/ {", "location /baocode/releases/ {"):
|
|
if original.count(required) != 1:
|
|
raise SystemExit(f"refusing to change nginx: expected one {required}")
|
|
|
|
if marker in original:
|
|
if include not in original:
|
|
raise SystemExit("refusing to change nginx: YCode marker exists without include")
|
|
updated = original
|
|
else:
|
|
match = re.search(r"(?m)^([ \\t]*)location /baocode/ \\{", original)
|
|
if not match:
|
|
raise SystemExit("refusing to change nginx: baocode anchor not found")
|
|
indent = match.group(1)
|
|
insertion = f"{indent}{marker}\\n{indent}include ${NGINX_SNIPPET};\\n\\n"
|
|
updated = original[:match.start()] + insertion + original[match.start():]
|
|
|
|
if updated != original:
|
|
directory = os.path.dirname(site)
|
|
descriptor, temporary = tempfile.mkstemp(prefix=".ycode-nginx-", dir=directory)
|
|
try:
|
|
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
|
|
stream.write(updated)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
shutil.copystat(site, temporary)
|
|
os.replace(temporary, site)
|
|
finally:
|
|
if os.path.exists(temporary):
|
|
os.unlink(temporary)
|
|
|
|
with open(site, "r", encoding="utf-8") as stream:
|
|
final = stream.read()
|
|
for required in ("location /baocode/ {", "location /baocode/releases/ {"):
|
|
if final.count(required) != 1:
|
|
raise SystemExit("refusing to change nginx: baocode locations changed")
|
|
if marker not in final or include not in final:
|
|
raise SystemExit("nginx include was not installed")
|
|
PY
|
|
|
|
if ! nginx -t; then
|
|
echo "nginx validation failed; configuration restored" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! systemctl reload nginx; then
|
|
echo "nginx reload failed; configuration restored" >&2
|
|
exit 1
|
|
fi
|
|
|
|
status="$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1/ycode/releases/latest.json || true)"
|
|
if [ -f '${SERVER_RELEASES_ROOT}/latest.json' ]; then
|
|
expected_status=200
|
|
else
|
|
expected_status=404
|
|
fi
|
|
if [ "$status" != "$expected_status" ]; then
|
|
echo "unexpected /ycode/releases/latest.json status: $status (expected $expected_status)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
page_status="$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1/ycode/releases/ || true)"
|
|
if [ "$page_status" != "200" ]; then
|
|
echo "unexpected /ycode/releases/ status: $page_status (expected 200)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'site=%s\\n' "$site"
|
|
printf 'backup=%s\\n' "$config_backup"
|
|
printf 'snippet=%s\\n' "$snippet"
|
|
printf 'latest_status=%s\\n' "$status"
|
|
printf 'page_status=%s\\n' "$page_status"
|
|
`
|
|
|
|
const output = runSsh(host, script)
|
|
process.stdout.write(output)
|
|
console.log(`Provisioned ${host}; no release manifest was created.`)
|
|
}
|
|
|
|
main().catch((error) => usageError(usage, error))
|