ops: paivana and caddy config refresh
This commit is contained in:
parent
d8dbc6242b
commit
56f814a6de
74 changed files with 4517 additions and 79 deletions
11
scripts/bbb/build.sh
Executable file
11
scripts/bbb/build.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build BBB front stub on Ubuntu (run on koopa).
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
CTX="${ROOT}/configs/bbb"
|
||||
TAG="${IMAGE_TAG:-localhost/koopa-bbb-front:ubuntu}"
|
||||
cd "$CTX"
|
||||
[[ -f Containerfile ]] || { echo "ERROR: missing $CTX/Containerfile" >&2; exit 1; }
|
||||
echo "build $TAG ctx=$CTX"
|
||||
podman build -t "$TAG" -f Containerfile .
|
||||
echo "OK: $TAG"
|
||||
|
|
@ -15,12 +15,52 @@ sudo systemctl reload caddy
|
|||
Caddy **ignores** Apache `.htaccess`. Use:
|
||||
|
||||
```bash
|
||||
sudo bash /home/hernani/koopa-admin-log/scripts/caddy/apply-dossiers-ngi-basicauth.sh
|
||||
# clone koopa-admin-log from Forgejo when needed (no permanent ~/ clone)
|
||||
sudo bash /path/to/koopa-admin-log/scripts/caddy/apply-dossiers-ngi-basicauth.sh
|
||||
```
|
||||
|
||||
Password file (not git): `/home/hernani/koopa-secrets/dossiers-ngi-basicauth.txt`
|
||||
Host runtime: `/etc/caddy/secrets/dossiers-ngi-basicauth.txt` (root-only).
|
||||
SoT: `koopa-admin-secrets-enc` (git-crypt) → `koopa/home-hernani/caddy/`.
|
||||
Hosts: `dossiers.ngi` / `dossiers.2.ngi` / `dossiers.3.ngi`.hacktivism.ch
|
||||
|
||||
## New HTTPS vhost
|
||||
## silverbullet.hacktivism.ch — merge + basic_auth
|
||||
|
||||
Add a site block to the Caddyfile (live + mirror), then validate and reload. Include the host in the shared HTTP site list (ACME + redirect) at the bottom of the file.
|
||||
After podman cutover (`scripts/notes/cutover-to-silverbullet.sh --apply`) and DNS:
|
||||
|
||||
```bash
|
||||
# laptop: SoT → /tmp seed (no secret echo)
|
||||
bash /path/to/koopa-admin-log/scripts/notes/prepare-basicauth-seed.sh
|
||||
scp /tmp/silverbullet-basicauth-seed.txt hernani@koopa:/tmp/
|
||||
|
||||
# koopa root:
|
||||
sudo bash /path/to/koopa-admin-log/scripts/caddy/merge-notes-site.sh
|
||||
sudo NOTES_AUTH_SEED=/tmp/silverbullet-basicauth-seed.txt \
|
||||
bash /path/to/koopa-admin-log/scripts/caddy/apply-notes-basicauth.sh
|
||||
```
|
||||
|
||||
`merge-notes-site.sh` renames leftover `freenotes`/`notes` site blocks → `silverbullet.hacktivism.ch`.
|
||||
Host runtime: `/etc/caddy/secrets/silverbullet-basicauth.txt` (root-only).
|
||||
SoT: `koopa-admin-secrets-enc` (git-crypt) → `koopa/home-hernani/caddy/silverbullet-basicauth.txt`.
|
||||
|
||||
## New HTTPS vhost — `caddy-apply.sh`
|
||||
|
||||
Generic merge (site snippet + ACME `http://` list + ports comment):
|
||||
|
||||
```bash
|
||||
# laptop mirror (no reload):
|
||||
bash scripts/caddy/caddy-apply.sh \
|
||||
--config configs/caddy/Caddyfile \
|
||||
--snippet configs/caddy/galene.hacktivism.ch.caddy \
|
||||
--site galene.hacktivism.ch --port 9029 --label galene \
|
||||
--no-reload
|
||||
|
||||
# koopa live (root):
|
||||
sudo bash …/scripts/caddy/caddy-apply.sh \
|
||||
--snippet …/configs/caddy/SITE.caddy \
|
||||
--site SITE.hacktivism.ch --port NNNN --label NAME
|
||||
```
|
||||
|
||||
Allocated collab vhosts (2026-09-08): `galene` **9029**, `cryptpad` **9030**, `meet` **9031**, `bbb` **9032**.
|
||||
Snippets under `configs/caddy/*.hacktivism.ch.caddy`. Image bases: CachyOS except **bbb=Ubuntu**.
|
||||
|
||||
Still valid: hand-edit live + mirror, then validate/reload.
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@
|
|||
#
|
||||
# Caddy file_server does NOT honor Apache .htaccess — this edits the Caddyfile.
|
||||
#
|
||||
# On koopa (password for sudo):
|
||||
# sudo -n true 2>/dev/null || sudo -v
|
||||
# sudo bash /home/hernani/koopa-admin-log/scripts/caddy/apply-dossiers-ngi-basicauth.sh
|
||||
# On koopa (clone admin-log from Forgejo when needed; no home git dump):
|
||||
# sudo bash /path/to/koopa-admin-log/scripts/caddy/apply-dossiers-ngi-basicauth.sh
|
||||
#
|
||||
# Idempotent. Password file is NOT in git:
|
||||
# /home/hernani/koopa-secrets/dossiers-ngi-basicauth.txt
|
||||
# Host runtime file (root-only, not under ~):
|
||||
# /etc/caddy/secrets/dossiers-ngi-basicauth.txt
|
||||
# SoT (git-crypt): koopa-admin-secrets-enc → koopa/home-hernani/caddy/
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
|
|
@ -19,8 +19,9 @@ fi
|
|||
|
||||
CADDY=/etc/caddy/Caddyfile
|
||||
USER_NAME="${DOSSIERS_AUTH_USER:-hernani}"
|
||||
SECRETS_DIR=/home/hernani/koopa-secrets
|
||||
SECRETS_DIR=/etc/caddy/secrets
|
||||
PASS_FILE="${SECRETS_DIR}/dossiers-ngi-basicauth.txt"
|
||||
OLD_PASS_FILE=/home/hernani/koopa-secrets/dossiers-ngi-basicauth.txt
|
||||
SITES=(
|
||||
dossiers.ngi.hacktivism.ch
|
||||
dossiers.2.ngi.hacktivism.ch
|
||||
|
|
@ -34,9 +35,14 @@ fi
|
|||
command -v caddy >/dev/null || { echo "ERROR: caddy not in PATH" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$SECRETS_DIR"
|
||||
chown hernani:hernani "$SECRETS_DIR"
|
||||
chmod 700 "$SECRETS_DIR"
|
||||
|
||||
if [[ ! -f "$PASS_FILE" && -f "$OLD_PASS_FILE" ]]; then
|
||||
cp -a "$OLD_PASS_FILE" "$PASS_FILE"
|
||||
chmod 600 "$PASS_FILE"
|
||||
echo "OK: migrated $OLD_PASS_FILE → $PASS_FILE"
|
||||
fi
|
||||
|
||||
if [[ -f "$PASS_FILE" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
# file format: USER=... PASS=... HASH=...
|
||||
|
|
|
|||
179
scripts/caddy/apply-notes-basicauth.sh
Executable file
179
scripts/caddy/apply-notes-basicauth.sh
Executable file
|
|
@ -0,0 +1,179 @@
|
|||
#!/usr/bin/env bash
|
||||
# Apply HTTP basic_auth to silverbullet.hacktivism.ch (dossiers pattern).
|
||||
#
|
||||
# On koopa (clone admin-log from Forgejo when needed; no home git dump):
|
||||
# sudo bash /path/to/koopa-admin-log/scripts/caddy/apply-notes-basicauth.sh
|
||||
#
|
||||
# Host runtime file (root-only, not under ~):
|
||||
# /etc/caddy/secrets/silverbullet-basicauth.txt
|
||||
# SoT (git-crypt): koopa-admin-secrets-enc → koopa/home-hernani/caddy/
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
echo "ERROR: run as root: sudo bash $0" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CADDY=/etc/caddy/Caddyfile
|
||||
USER_NAME="${NOTES_AUTH_USER:-hernani}"
|
||||
SECRETS_DIR=/etc/caddy/secrets
|
||||
PASS_FILE="${SECRETS_DIR}/silverbullet-basicauth.txt"
|
||||
# legacy home dumps (migrate once, then delete)
|
||||
OLD_PASS_FILE=/home/hernani/koopa-secrets/silverbullet-basicauth.txt
|
||||
OLD_NOTES_FILE=/home/hernani/koopa-secrets/notes-basicauth.txt
|
||||
OLD_FREENOTES_FILE=/home/hernani/koopa-secrets/freenotes-basicauth.txt
|
||||
SITE=silverbullet.hacktivism.ch
|
||||
|
||||
if [[ ! -f "$CADDY" ]]; then
|
||||
echo "ERROR: missing $CADDY" >&2
|
||||
exit 1
|
||||
fi
|
||||
command -v caddy >/dev/null || { echo "ERROR: caddy not in PATH" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$SECRETS_DIR"
|
||||
chmod 700 "$SECRETS_DIR"
|
||||
|
||||
# Prefer explicit seed (SoT from secrets-enc), then legacy home dumps once.
|
||||
SEED_FILE="${NOTES_AUTH_SEED:-}"
|
||||
if [[ ! -f "$PASS_FILE" && -n "$SEED_FILE" && -f "$SEED_FILE" ]]; then
|
||||
cp -a "$SEED_FILE" "$PASS_FILE"
|
||||
chmod 600 "$PASS_FILE"
|
||||
echo "OK: seeded $SEED_FILE → $PASS_FILE"
|
||||
fi
|
||||
if [[ ! -f "$PASS_FILE" && -f "$OLD_PASS_FILE" ]]; then
|
||||
cp -a "$OLD_PASS_FILE" "$PASS_FILE"
|
||||
chmod 600 "$PASS_FILE"
|
||||
echo "OK: migrated $OLD_PASS_FILE → $PASS_FILE"
|
||||
fi
|
||||
if [[ ! -f "$PASS_FILE" && -f "$OLD_NOTES_FILE" ]]; then
|
||||
cp -a "$OLD_NOTES_FILE" "$PASS_FILE"
|
||||
chmod 600 "$PASS_FILE"
|
||||
echo "OK: migrated $OLD_NOTES_FILE → $PASS_FILE"
|
||||
fi
|
||||
if [[ ! -f "$PASS_FILE" && -f "$OLD_FREENOTES_FILE" ]]; then
|
||||
cp -a "$OLD_FREENOTES_FILE" "$PASS_FILE"
|
||||
chmod 600 "$PASS_FILE"
|
||||
echo "OK: migrated $OLD_FREENOTES_FILE → $PASS_FILE"
|
||||
fi
|
||||
|
||||
if [[ -f "$PASS_FILE" ]]; then
|
||||
USER_NAME="$(awk -F= '/^USER=/{print substr($0,6); exit}' "$PASS_FILE")"
|
||||
PASS="$(awk -F= '/^PASS=/{print substr($0,6); exit}' "$PASS_FILE")"
|
||||
HASH="$(awk -F= '/^HASH=/{print substr($0,6); exit}' "$PASS_FILE")"
|
||||
if [[ -z "$USER_NAME" || -z "$HASH" ]]; then
|
||||
echo "ERROR: $PASS_FILE incomplete — move it aside and re-run" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: reusing credentials in $PASS_FILE"
|
||||
else
|
||||
PASS="$(openssl rand -base64 18 | tr -d '/+=' | head -c 20)"
|
||||
HASH="$(caddy hash-password --plaintext "$PASS")"
|
||||
umask 077
|
||||
cat >"$PASS_FILE" <<EOF
|
||||
USER=${USER_NAME}
|
||||
PASS=${PASS}
|
||||
HASH=${HASH}
|
||||
EOF
|
||||
chmod 600 "$PASS_FILE"
|
||||
echo "OK: wrote new password to $PASS_FILE — mirror into koopa-admin-secrets-enc (git-crypt)"
|
||||
fi
|
||||
|
||||
# Write bcrypt hash with single "$". Doubling to "$$" left literal "$$" in
|
||||
# `caddy adapt` JSON here and made every password 401.
|
||||
HASH_CADDY="$HASH"
|
||||
|
||||
ts=$(date +%Y%m%d-%H%M%S)
|
||||
cp -a "$CADDY" "${CADDY}.bak-silverbullet-auth-${ts}"
|
||||
echo "backup ${CADDY}.bak-silverbullet-auth-${ts}"
|
||||
|
||||
python3 - "$CADDY" "$USER_NAME" "$HASH_CADDY" "$SITE" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
user = sys.argv[2]
|
||||
hash_caddy = sys.argv[3]
|
||||
site = sys.argv[4]
|
||||
text = path.read_text()
|
||||
orig = text
|
||||
|
||||
|
||||
def site_span(src: str, site_name: str) -> tuple[int, int]:
|
||||
marker = f"{site_name} {{"
|
||||
i = src.find(marker)
|
||||
if i < 0:
|
||||
raise SystemExit(
|
||||
f"ERROR: site block not found: {site_name} — merge silverbullet.hacktivism.ch.caddy first"
|
||||
)
|
||||
depth = 0
|
||||
for k in range(i + len(marker) - 1, len(src)):
|
||||
ch = src[k]
|
||||
if ch == "{":
|
||||
depth += 1
|
||||
elif ch == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i, k + 1
|
||||
raise SystemExit(f"ERROR: unclosed block: {site_name}")
|
||||
|
||||
|
||||
def has_basic_auth_directive(block: str) -> bool:
|
||||
# Ignore comments; only real directives count.
|
||||
stripped = "\n".join(
|
||||
ln for ln in block.splitlines() if not ln.lstrip().startswith("#")
|
||||
)
|
||||
return bool(re.search(r"(?m)^\s*basic_?auth\b", stripped))
|
||||
|
||||
|
||||
i, j = site_span(text, site)
|
||||
block = text[i:j]
|
||||
auth_lines = (
|
||||
"\tbasic_auth {\n"
|
||||
f"\t\t{user} {hash_caddy}\n"
|
||||
"\t}\n"
|
||||
)
|
||||
# Replace existing basic_auth block (also fixes legacy "$$" hashes that never matched).
|
||||
block_new, n = re.subn(
|
||||
r"(?m)^[ \t]*basic_auth\s*\{.*?\n[ \t]*\}\n?",
|
||||
auth_lines,
|
||||
block,
|
||||
count=1,
|
||||
flags=re.S,
|
||||
)
|
||||
if n:
|
||||
text = text[:i] + block_new + text[j:]
|
||||
print(f"OK: replaced basic_auth in {site}")
|
||||
elif has_basic_auth_directive(block):
|
||||
print(f"OK: {site} already has basic_auth")
|
||||
else:
|
||||
m = re.search(r'(?m)^[ \t]*header Alt-Svc "clear"[ \t]*\n', block)
|
||||
if m:
|
||||
insert_at = m.end()
|
||||
new_block = block[:insert_at] + auth_lines + block[insert_at:]
|
||||
else:
|
||||
nl = block.find("\n")
|
||||
new_block = block[: nl + 1] + auth_lines + block[nl + 1 :]
|
||||
text = text[:i] + new_block + text[j:]
|
||||
print(f"OK: injected basic_auth in {site}")
|
||||
|
||||
if text != orig:
|
||||
path.write_text(text)
|
||||
print(f"wrote {path}")
|
||||
else:
|
||||
print("no Caddyfile change")
|
||||
PY
|
||||
|
||||
echo "== validate =="
|
||||
caddy validate --config "$CADDY"
|
||||
|
||||
echo "== reload =="
|
||||
if systemctl is-active --quiet caddy; then
|
||||
systemctl reload caddy
|
||||
else
|
||||
echo "WARN: caddy unit not active — start it yourself" >&2
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Done. Browser: https://${SITE}/"
|
||||
echo "User/pass: $PASS_FILE"
|
||||
217
scripts/caddy/caddy-apply.sh
Executable file
217
scripts/caddy/caddy-apply.sh
Executable file
|
|
@ -0,0 +1,217 @@
|
|||
#!/usr/bin/env bash
|
||||
# Generic: merge a site snippet into a Caddyfile + ACME http:// list + ports comment.
|
||||
#
|
||||
# Laptop (mirror only, no reload):
|
||||
# bash scripts/caddy/caddy-apply.sh \
|
||||
# --config configs/caddy/Caddyfile \
|
||||
# --snippet configs/caddy/galene.hacktivism.ch.caddy \
|
||||
# --site galene.hacktivism.ch --port 9029 --label galene \
|
||||
# --no-reload
|
||||
#
|
||||
# Koopa live (root, Freigabe):
|
||||
# sudo bash …/caddy-apply.sh \
|
||||
# --snippet /home/hernani/koopa-admin-log/configs/caddy/galene.hacktivism.ch.caddy \
|
||||
# --site galene.hacktivism.ch --port 9029 --label galene
|
||||
set -euo pipefail
|
||||
|
||||
CADDY=/etc/caddy/Caddyfile
|
||||
SNIPPET=""
|
||||
SITE=""
|
||||
PORT=""
|
||||
LABEL=""
|
||||
INSERT_AFTER="decidim.hacktivism.ch"
|
||||
ACME_ANCHOR="http://decidim.hacktivism.ch"
|
||||
NO_RELOAD=0
|
||||
DRY=0
|
||||
OLD_SITES=()
|
||||
|
||||
usage() {
|
||||
sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
|
||||
echo "Options: --config PATH --snippet PATH --site HOST --port N --label NAME"
|
||||
echo " --insert-after HOST --acme-anchor http://HOST --old-site HOST"
|
||||
echo " --no-reload --dry-run"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--config) CADDY=$2; shift 2 ;;
|
||||
--snippet) SNIPPET=$2; shift 2 ;;
|
||||
--site) SITE=$2; shift 2 ;;
|
||||
--port) PORT=$2; shift 2 ;;
|
||||
--label) LABEL=$2; shift 2 ;;
|
||||
--insert-after) INSERT_AFTER=$2; shift 2 ;;
|
||||
--acme-anchor) ACME_ANCHOR=$2; shift 2 ;;
|
||||
--old-site) OLD_SITES+=("$2"); shift 2 ;;
|
||||
--no-reload) NO_RELOAD=1; shift ;;
|
||||
--dry-run) DRY=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "ERROR: unknown arg: $1" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$SNIPPET" && -n "$SITE" ]] || { echo "ERROR: --snippet and --site required" >&2; exit 2; }
|
||||
[[ -f "$CADDY" ]] || { echo "ERROR: missing $CADDY" >&2; exit 1; }
|
||||
[[ -f "$SNIPPET" ]] || { echo "ERROR: missing $SNIPPET" >&2; exit 1; }
|
||||
LABEL="${LABEL:-$SITE}"
|
||||
|
||||
if [[ "$CADDY" == /etc/caddy/Caddyfile && "$(id -u)" -ne 0 && "$DRY" -eq 0 ]]; then
|
||||
echo "ERROR: live Caddyfile needs root (or --dry-run / --config mirror)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ts=$(date +%Y%m%d-%H%M%S)
|
||||
if [[ "$DRY" -eq 0 ]]; then
|
||||
cp -a "$CADDY" "${CADDY}.bak-caddy-apply-${ts}"
|
||||
echo "backup ${CADDY}.bak-caddy-apply-${ts}"
|
||||
fi
|
||||
|
||||
export CADDY SNIPPET SITE PORT LABEL INSERT_AFTER ACME_ANCHOR DRY
|
||||
export OLD_SITES_CSV
|
||||
OLD_SITES_CSV=$(IFS=,; echo "${OLD_SITES[*]-}")
|
||||
|
||||
python3 - <<'PY'
|
||||
import os, re, sys
|
||||
from pathlib import Path
|
||||
|
||||
caddy = Path(os.environ["CADDY"])
|
||||
snip = Path(os.environ["SNIPPET"]).read_text().rstrip() + "\n\n"
|
||||
site = os.environ["SITE"]
|
||||
port = os.environ.get("PORT") or ""
|
||||
label = os.environ.get("LABEL") or site
|
||||
insert_after = os.environ["INSERT_AFTER"]
|
||||
acme_anchor = os.environ["ACME_ANCHOR"]
|
||||
dry = os.environ.get("DRY") == "1"
|
||||
old_sites = [s for s in os.environ.get("OLD_SITES_CSV", "").split(",") if s]
|
||||
|
||||
text = caddy.read_text()
|
||||
orig = text
|
||||
|
||||
def drop_site_block(text: str, old_site: str) -> str:
|
||||
marker_old = f"{old_site} {{"
|
||||
i = text.find(marker_old)
|
||||
if i < 0:
|
||||
return text
|
||||
j = text.find("\n}", i)
|
||||
if j < 0:
|
||||
return text
|
||||
end = j + 2
|
||||
while end < len(text) and text[end] == "\n":
|
||||
end += 1
|
||||
text = text[:i] + text[end:]
|
||||
text = text.replace(f", http://{old_site}", "").replace(f"http://{old_site}, ", "")
|
||||
text = text.replace(f"http://{old_site}", "")
|
||||
print(f"OK: removed duplicate old block {old_site}")
|
||||
return text
|
||||
|
||||
for old_site in old_sites:
|
||||
if old_site in text and site not in text:
|
||||
text = text.replace(old_site, site)
|
||||
print(f"OK: renamed {old_site} → {site}")
|
||||
elif old_site in text and site in text:
|
||||
text = drop_site_block(text, old_site)
|
||||
|
||||
marker = f"{site} {{"
|
||||
if marker in text:
|
||||
print(f"OK: site block already present: {site}")
|
||||
else:
|
||||
insert_at = None
|
||||
after = text.find(f"{insert_after} {{")
|
||||
if after >= 0:
|
||||
j = text.find("\n}", after)
|
||||
if j >= 0:
|
||||
insert_at = j + 2
|
||||
while insert_at < len(text) and text[insert_at] == "\n":
|
||||
insert_at += 1
|
||||
if insert_at is None:
|
||||
acme = text.find("http://taler.hacktivism.ch")
|
||||
insert_at = acme if acme >= 0 else len(text)
|
||||
text = text[:insert_at] + snip + text[insert_at:]
|
||||
print(f"OK: inserted site block {site}")
|
||||
|
||||
http_tok = f"http://{site}"
|
||||
if http_tok in text:
|
||||
print(f"OK: ACME http list already has {http_tok}")
|
||||
else:
|
||||
# Prefer exact "anchor {" form; else append before " {" of the long http list
|
||||
old = f"{acme_anchor} {{"
|
||||
new = f"{acme_anchor}, {http_tok} {{"
|
||||
if old in text:
|
||||
text = text.replace(old, new, 1)
|
||||
print(f"OK: added {http_tok} to ACME http list (anchor)")
|
||||
else:
|
||||
# Find the shared ACME line (starts with http://taler…)
|
||||
m = re.search(r"(http://taler\.hacktivism\.ch[^\n]*?)(\s*\{)", text)
|
||||
if not m:
|
||||
raise SystemExit("ERROR: ACME http list not found")
|
||||
line = m.group(1)
|
||||
if http_tok in line:
|
||||
print(f"OK: ACME http list already has {http_tok}")
|
||||
else:
|
||||
text = text[: m.start(1)] + line.rstrip() + f", {http_tok}" + text[m.end(1) :]
|
||||
print(f"OK: added {http_tok} to ACME http list (append)")
|
||||
|
||||
# Ports comment header (first matching line starting with "# 9020 castopod")
|
||||
if port:
|
||||
token = f"{port} {label}"
|
||||
def upd_hdr(line: str) -> str:
|
||||
if token in line or f"| {port} " in line or f"| {port}|" in line:
|
||||
return line
|
||||
# insert before " | 9200 forgejo-ssh" if present, else before end
|
||||
if "9200 forgejo-ssh" in line:
|
||||
return line.replace(" | 9200 forgejo-ssh", f" | {token} | 9200 forgejo-ssh", 1)
|
||||
if line.rstrip().endswith("forgejo-ssh"):
|
||||
return line.rstrip() + f" | {token}\n"
|
||||
return line.rstrip() + f" | {token}\n"
|
||||
|
||||
lines = text.splitlines(keepends=True)
|
||||
changed = False
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith("# 9020 castopod"):
|
||||
new_line = upd_hdr(line)
|
||||
if new_line != line:
|
||||
lines[i] = new_line if new_line.endswith("\n") else new_line + "\n"
|
||||
print(f"OK: updated ports comment (+{token})")
|
||||
changed = True
|
||||
else:
|
||||
print(f"OK: ports comment already has {token}")
|
||||
break
|
||||
if changed:
|
||||
text = "".join(lines)
|
||||
|
||||
if text != orig:
|
||||
if dry:
|
||||
print(f"DRY: would write {caddy} ({len(text) - len(orig):+d} bytes)")
|
||||
else:
|
||||
caddy.write_text(text)
|
||||
print(f"wrote {caddy}")
|
||||
else:
|
||||
print("no Caddyfile change")
|
||||
PY
|
||||
|
||||
if [[ "$DRY" -eq 1 ]]; then
|
||||
echo "dry-run done (no validate/reload)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if command -v caddy >/dev/null 2>&1; then
|
||||
echo "== validate =="
|
||||
caddy validate --config "$CADDY"
|
||||
else
|
||||
echo "WARN: caddy binary missing — skip validate" >&2
|
||||
fi
|
||||
|
||||
if [[ "$NO_RELOAD" -eq 1 ]]; then
|
||||
echo "skip reload (--no-reload)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$CADDY" == /etc/caddy/Caddyfile ]] && command -v systemctl >/dev/null 2>&1; then
|
||||
echo "== reload =="
|
||||
if systemctl is-active --quiet caddy; then
|
||||
systemctl reload caddy
|
||||
else
|
||||
echo "WARN: caddy unit not active — start it yourself" >&2
|
||||
fi
|
||||
else
|
||||
echo "skip reload (not live path or no systemctl)"
|
||||
fi
|
||||
133
scripts/caddy/merge-notes-site.sh
Executable file
133
scripts/caddy/merge-notes-site.sh
Executable file
|
|
@ -0,0 +1,133 @@
|
|||
#!/usr/bin/env bash
|
||||
# Merge silverbullet.hacktivism.ch into live /etc/caddy/Caddyfile + ACME http list.
|
||||
# Run on koopa as root AFTER podman is up on :9028:
|
||||
# sudo bash /home/hernani/koopa-admin-log/scripts/caddy/merge-notes-site.sh
|
||||
# Then:
|
||||
# sudo bash /home/hernani/koopa-admin-log/scripts/caddy/apply-notes-basicauth.sh
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
echo "ERROR: run as root: sudo bash $0" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CADDY=/etc/caddy/Caddyfile
|
||||
SNIPPET=/home/hernani/koopa-admin-log/configs/caddy/silverbullet.hacktivism.ch.caddy
|
||||
SITE=silverbullet.hacktivism.ch
|
||||
# Prior hostnames to rename into SITE (order: newest leftovers first)
|
||||
OLD_SITES="freenotes.hacktivism.ch notes.hacktivism.ch"
|
||||
|
||||
[[ -f "$CADDY" ]] || { echo "ERROR: missing $CADDY" >&2; exit 1; }
|
||||
[[ -f "$SNIPPET" ]] || { echo "ERROR: missing $SNIPPET" >&2; exit 1; }
|
||||
command -v caddy >/dev/null || { echo "ERROR: caddy not in PATH" >&2; exit 1; }
|
||||
|
||||
ts=$(date +%Y%m%d-%H%M%S)
|
||||
cp -a "$CADDY" "${CADDY}.bak-silverbullet-merge-${ts}"
|
||||
echo "backup ${CADDY}.bak-silverbullet-merge-${ts}"
|
||||
|
||||
python3 - "$CADDY" "$SNIPPET" "$SITE" $OLD_SITES <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
caddy = Path(sys.argv[1])
|
||||
snippet_path = Path(sys.argv[2])
|
||||
site = sys.argv[3]
|
||||
old_sites = sys.argv[4:]
|
||||
text = caddy.read_text()
|
||||
orig = text
|
||||
snip = snippet_path.read_text().rstrip() + "\n\n"
|
||||
|
||||
def drop_site_block(text: str, old_site: str) -> str:
|
||||
marker_old = f"{old_site} {{"
|
||||
i = text.find(marker_old)
|
||||
if i < 0:
|
||||
return text
|
||||
j = text.find("\n}", i)
|
||||
if j < 0:
|
||||
return text
|
||||
end = j + 2
|
||||
while end < len(text) and text[end] == "\n":
|
||||
end += 1
|
||||
text = text[:i] + text[end:]
|
||||
text = text.replace(f", http://{old_site}", "").replace(f"http://{old_site}, ", "")
|
||||
text = text.replace(f"http://{old_site}", "")
|
||||
print(f"OK: removed duplicate old block {old_site}")
|
||||
return text
|
||||
|
||||
# Rename leftover freenotes/notes.hacktivism.ch → silverbullet.hacktivism.ch
|
||||
for old_site in old_sites:
|
||||
if old_site in text and site not in text:
|
||||
text = text.replace(old_site, site)
|
||||
text = text.replace("9028 freenotes", "9028 silverbullet")
|
||||
text = text.replace("9028 notes", "9028 silverbullet")
|
||||
print(f"OK: renamed {old_site} → {site}")
|
||||
elif old_site in text and site in text:
|
||||
text = drop_site_block(text, old_site)
|
||||
|
||||
marker = f"{site} {{"
|
||||
if marker in text:
|
||||
print(f"OK: site block already present: {site}")
|
||||
else:
|
||||
# Insert after decidim block if present, else before ACME http list
|
||||
insert_at = None
|
||||
dec = text.find("decidim.hacktivism.ch {")
|
||||
if dec >= 0:
|
||||
j = text.find("\n}", dec)
|
||||
if j >= 0:
|
||||
insert_at = j + 2 # after closing brace newline
|
||||
while insert_at < len(text) and text[insert_at] == "\n":
|
||||
insert_at += 1
|
||||
if insert_at is None:
|
||||
acme = text.find("http://taler.hacktivism.ch")
|
||||
insert_at = acme if acme >= 0 else len(text)
|
||||
text = text[:insert_at] + snip + text[insert_at:]
|
||||
print(f"OK: inserted site block {site}")
|
||||
|
||||
http_tok = f"http://{site}"
|
||||
if http_tok in text:
|
||||
print(f"OK: ACME http list already has {http_tok}")
|
||||
else:
|
||||
old = "http://decidim.hacktivism.ch {"
|
||||
new = f"http://decidim.hacktivism.ch, {http_tok} {{"
|
||||
if old not in text:
|
||||
# maybe already on a long list without trailing brace-only form
|
||||
raise SystemExit("ERROR: ACME http list marker not found (decidim)")
|
||||
text = text.replace(old, new, 1)
|
||||
print(f"OK: added {http_tok} to ACME http list")
|
||||
|
||||
# Comment header line near top ports comment
|
||||
for old_hdr, new_hdr in (
|
||||
(
|
||||
"# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9027 decidim | 9200 forgejo-ssh",
|
||||
"# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9027 decidim | 9028 silverbullet | 9200 forgejo-ssh",
|
||||
),
|
||||
(
|
||||
"# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9027 decidim | 9028 notes | 9200 forgejo-ssh",
|
||||
"# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9027 decidim | 9028 silverbullet | 9200 forgejo-ssh",
|
||||
),
|
||||
):
|
||||
if old_hdr in text:
|
||||
text = text.replace(old_hdr, new_hdr, 1)
|
||||
print("OK: updated ports comment header")
|
||||
break
|
||||
|
||||
if text != orig:
|
||||
caddy.write_text(text)
|
||||
print(f"wrote {caddy}")
|
||||
else:
|
||||
print("no Caddyfile change")
|
||||
PY
|
||||
|
||||
echo "== validate =="
|
||||
caddy validate --config "$CADDY"
|
||||
|
||||
echo "== reload =="
|
||||
if systemctl is-active --quiet caddy; then
|
||||
systemctl reload caddy
|
||||
else
|
||||
echo "WARN: caddy unit not active — start it yourself" >&2
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Next: sudo bash /home/hernani/koopa-admin-log/scripts/caddy/apply-notes-basicauth.sh"
|
||||
echo "DNS/VeciGate for ${SITE} still separate."
|
||||
17
scripts/cryptpad/build.sh
Executable file
17
scripts/cryptpad/build.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build CryptPad image on CachyOS base (run on koopa).
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
CTX="${ROOT}/configs/cryptpad"
|
||||
TAG="${IMAGE_TAG:-localhost/koopa-cryptpad:cachyos}"
|
||||
CRYPTPAD_VERSION="${CRYPTPAD_VERSION:-2025.9.0}"
|
||||
cd "$CTX"
|
||||
[[ -f Containerfile ]] || { echo "ERROR: missing $CTX/Containerfile" >&2; exit 1; }
|
||||
echo "build $TAG (CRYPTPAD_VERSION=$CRYPTPAD_VERSION) ctx=$CTX"
|
||||
podman build \
|
||||
--build-arg "CRYPTPAD_VERSION=${CRYPTPAD_VERSION}" \
|
||||
-t "$TAG" \
|
||||
-f Containerfile \
|
||||
.
|
||||
echo "OK: $TAG"
|
||||
podman image inspect "$TAG" --format '{{.Id}} {{.Size}} {{.Architecture}}' 2>/dev/null || true
|
||||
17
scripts/galene/build.sh
Executable file
17
scripts/galene/build.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build Galene image on CachyOS base (run on koopa).
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
CTX="${ROOT}/configs/galene"
|
||||
TAG="${IMAGE_TAG:-localhost/koopa-galene:cachyos}"
|
||||
GALENE_VERSION="${GALENE_VERSION:-0.9.1}"
|
||||
cd "$CTX"
|
||||
[[ -f Containerfile ]] || { echo "ERROR: missing $CTX/Containerfile" >&2; exit 1; }
|
||||
echo "build $TAG (GALENE_VERSION=$GALENE_VERSION) ctx=$CTX"
|
||||
podman build \
|
||||
--build-arg "GALENE_VERSION=${GALENE_VERSION}" \
|
||||
-t "$TAG" \
|
||||
-f Containerfile \
|
||||
.
|
||||
echo "OK: $TAG"
|
||||
podman image inspect "$TAG" --format '{{.Id}} {{.Size}} {{.Architecture}}' 2>/dev/null || true
|
||||
11
scripts/meet/build.sh
Executable file
11
scripts/meet/build.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build Meet web stub on CachyOS (run on koopa).
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
CTX="${ROOT}/configs/meet"
|
||||
TAG="${IMAGE_TAG:-localhost/koopa-meet-web:cachyos}"
|
||||
cd "$CTX"
|
||||
[[ -f Containerfile ]] || { echo "ERROR: missing $CTX/Containerfile" >&2; exit 1; }
|
||||
echo "build $TAG ctx=$CTX"
|
||||
podman build -t "$TAG" -f Containerfile .
|
||||
echo "OK: $TAG"
|
||||
40
scripts/notes/README.md
Normal file
40
scripts/notes/README.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# SilverBullet — scripts (koopa)
|
||||
|
||||
Product **FreeNotes** is separate (native enc). These scripts operate the parked **SilverBullet** web stack on port **9028**.
|
||||
|
||||
| Script | Role |
|
||||
|--------|------|
|
||||
| `status.sh` | Read-only dirs / containers / `:9028` |
|
||||
| `build.sh` | `podman build` → `localhost/koopa-silverbullet:cachyos` |
|
||||
| `up.sh` | `podman-compose up` in `~/koopa-silverbullet` + health |
|
||||
| `cutover-to-silverbullet.sh` | Live freenotes → silverbullet (default **dry-run**; `--apply` mutates) |
|
||||
| `prepare-basicauth-seed.sh` | SoT → `/tmp/silverbullet-basicauth-seed.txt` (no sudo) |
|
||||
|
||||
Caddy (sudo, separate): `scripts/caddy/merge-notes-site.sh`, `apply-notes-basicauth.sh`.
|
||||
|
||||
## Cutover order (needs Freigabe + DNS)
|
||||
|
||||
On koopa as `hernani` (admin-log clone from Forgejo when needed):
|
||||
|
||||
```bash
|
||||
bash …/scripts/notes/status.sh
|
||||
bash …/scripts/notes/cutover-to-silverbullet.sh # dry-run
|
||||
bash …/scripts/notes/cutover-to-silverbullet.sh --apply # Freigabe
|
||||
```
|
||||
|
||||
Then Caddy (root) + seed from laptop SoT:
|
||||
|
||||
```bash
|
||||
# laptop (unlocked secrets-enc):
|
||||
bash …/scripts/notes/prepare-basicauth-seed.sh
|
||||
scp /tmp/silverbullet-basicauth-seed.txt hernani@koopa:/tmp/
|
||||
|
||||
# koopa root:
|
||||
sudo bash …/scripts/caddy/merge-notes-site.sh
|
||||
sudo NOTES_AUTH_SEED=/tmp/silverbullet-basicauth-seed.txt \
|
||||
bash …/scripts/caddy/apply-notes-basicauth.sh
|
||||
```
|
||||
|
||||
DNS: `silverbullet.hacktivism.ch` → koopa (human). Public: `https://silverbullet.hacktivism.ch/`.
|
||||
|
||||
Keeps on cutover: `space/`, `.env`. Refreshes compose/Containerfile/entrypoint/unit from `configs/notes/`.
|
||||
23
scripts/notes/build.sh
Executable file
23
scripts/notes/build.sh
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build SilverBullet image on CachyOS base (run on koopa).
|
||||
# bash ~/koopa-admin-log/scripts/notes/build.sh
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
CTX="${ROOT}/configs/notes"
|
||||
TAG="${IMAGE_TAG:-localhost/koopa-silverbullet:cachyos}"
|
||||
SB_VERSION="${SB_VERSION:-2.10.0}"
|
||||
|
||||
cd "$CTX"
|
||||
[[ -f Containerfile ]] || { echo "ERROR: missing $CTX/Containerfile" >&2; exit 1; }
|
||||
[[ -f entrypoint.sh ]] || { echo "ERROR: missing $CTX/entrypoint.sh" >&2; exit 1; }
|
||||
|
||||
echo "build $TAG (SB_VERSION=$SB_VERSION) ctx=$CTX"
|
||||
podman build \
|
||||
--build-arg "SB_VERSION=${SB_VERSION}" \
|
||||
-t "$TAG" \
|
||||
-f Containerfile \
|
||||
.
|
||||
|
||||
echo "OK: $TAG"
|
||||
podman image inspect "$TAG" --format '{{.Id}} {{.Size}} {{.Architecture}}' 2>/dev/null || true
|
||||
162
scripts/notes/cutover-to-silverbullet.sh
Executable file
162
scripts/notes/cutover-to-silverbullet.sh
Executable file
|
|
@ -0,0 +1,162 @@
|
|||
#!/usr/bin/env bash
|
||||
# Cut over live stack freenotes → silverbullet on koopa (hernani).
|
||||
# Default: dry-run. Mutate only with --apply.
|
||||
#
|
||||
# bash /path/to/koopa-admin-log/scripts/notes/cutover-to-silverbullet.sh
|
||||
# bash /path/to/koopa-admin-log/scripts/notes/cutover-to-silverbullet.sh --apply
|
||||
#
|
||||
# Keeps: space/, .env. Refreshes: compose.yml, Containerfile, entrypoint.sh, systemd unit.
|
||||
# Image: retag localhost/koopa-freenotes:cachyos → localhost/koopa-silverbullet:cachyos (no rebuild).
|
||||
# Does NOT touch Caddy (sudo) — print next steps only.
|
||||
set -euo pipefail
|
||||
|
||||
APPLY=0
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--apply) APPLY=1 ;;
|
||||
-h|--help)
|
||||
sed -n '2,12p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: unknown arg: $a (use --apply)" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
OLD_DIR="${HOME}/koopa-freenotes"
|
||||
NEW_DIR="${HOME}/koopa-silverbullet"
|
||||
OLD_CT=koopa-freenotes
|
||||
NEW_CT=koopa-silverbullet
|
||||
OLD_IMG=localhost/koopa-freenotes:cachyos
|
||||
NEW_IMG=localhost/koopa-silverbullet:cachyos
|
||||
PORT=9028
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
CFG="${ROOT}/configs/notes"
|
||||
|
||||
[[ -d "$CFG" ]] || { echo "ERROR: missing $CFG" >&2; exit 1; }
|
||||
[[ -f "$CFG/compose.yml" ]] || { echo "ERROR: missing $CFG/compose.yml" >&2; exit 1; }
|
||||
|
||||
mode=dry-run
|
||||
[[ "$APPLY" -eq 1 ]] && mode=apply
|
||||
echo "STATUS=start MODE=$mode HOST=$(hostname -s) OLD_DIR=$OLD_DIR NEW_DIR=$NEW_DIR"
|
||||
|
||||
have_old=0
|
||||
have_new=0
|
||||
[[ -d "$OLD_DIR" ]] && have_old=1
|
||||
[[ -d "$NEW_DIR" ]] && have_new=1
|
||||
echo "STATUS=dirs OLD=$have_old NEW=$have_new"
|
||||
|
||||
ct_old=$(podman ps -a --format '{{.Names}}' 2>/dev/null | grep -Fx "$OLD_CT" || true)
|
||||
ct_new=$(podman ps -a --format '{{.Names}}' 2>/dev/null | grep -Fx "$NEW_CT" || true)
|
||||
echo "STATUS=containers OLD=${ct_old:-none} NEW=${ct_new:-none}"
|
||||
|
||||
img_old=$(podman images -q "$OLD_IMG" 2>/dev/null || true)
|
||||
img_new=$(podman images -q "$NEW_IMG" 2>/dev/null || true)
|
||||
echo "STATUS=images OLD=${img_old:-none} NEW=${img_new:-none}"
|
||||
|
||||
dir_err=0
|
||||
if [[ "$have_old" -eq 0 && "$have_new" -eq 1 ]]; then
|
||||
echo "STATUS=already_cutover DIR=$NEW_DIR"
|
||||
elif [[ "$have_old" -eq 0 && "$have_new" -eq 0 ]]; then
|
||||
echo "STATUS=warn neither $OLD_DIR nor $NEW_DIR (ok for laptop dry-run; apply needs koopa)"
|
||||
dir_err=1
|
||||
elif [[ "$have_old" -eq 1 && "$have_new" -eq 1 ]]; then
|
||||
echo "STATUS=warn both dirs exist — resolve manually before --apply"
|
||||
dir_err=1
|
||||
fi
|
||||
|
||||
plan() { echo "PLAN: $*"; }
|
||||
|
||||
plan "stop+rm container $OLD_CT (if present)"
|
||||
plan "mv $OLD_DIR → $NEW_DIR (if old present)"
|
||||
plan "copy compose/Containerfile/entrypoint/unit from $CFG (keep space .env)"
|
||||
plan "podman tag $OLD_IMG $NEW_IMG (if new image missing)"
|
||||
plan "podman-compose up -d in $NEW_DIR"
|
||||
plan "health curl :$PORT"
|
||||
plan "optional: systemctl --user enable container-koopa-silverbullet.service"
|
||||
|
||||
if [[ "$APPLY" -ne 1 ]]; then
|
||||
echo "STATUS=dry-run NEXT=re-run on koopa with --apply (Freigabe)"
|
||||
echo "STATUS=caddy_next sudo bash $ROOT/scripts/caddy/merge-notes-site.sh"
|
||||
echo "STATUS=caddy_next sudo NOTES_AUTH_SEED=/tmp/silverbullet-basicauth-seed.txt bash $ROOT/scripts/caddy/apply-notes-basicauth.sh"
|
||||
echo "STATUS=dns_need silverbullet.hacktivism.ch → koopa"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$dir_err" -eq 1 ]]; then
|
||||
echo "ERROR: refuse --apply with ambiguous/missing live dirs" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$ct_old" ]]; then
|
||||
echo "STATUS=stop $OLD_CT"
|
||||
podman stop -t 20 "$OLD_CT" || true
|
||||
podman rm "$OLD_CT" || true
|
||||
fi
|
||||
|
||||
if [[ "$have_old" -eq 1 ]]; then
|
||||
echo "STATUS=mv $OLD_DIR → $NEW_DIR"
|
||||
mv "$OLD_DIR" "$NEW_DIR"
|
||||
fi
|
||||
|
||||
cd "$NEW_DIR"
|
||||
# Refresh stack files from admin-log; never overwrite space/ or .env
|
||||
cp -a "$CFG/compose.yml" "$NEW_DIR/compose.yml"
|
||||
cp -a "$CFG/Containerfile" "$NEW_DIR/Containerfile"
|
||||
cp -a "$CFG/entrypoint.sh" "$NEW_DIR/entrypoint.sh"
|
||||
chmod +x "$NEW_DIR/entrypoint.sh"
|
||||
cp -a "$CFG/container-koopa-silverbullet.service" "$NEW_DIR/container-koopa-silverbullet.service"
|
||||
cp -a "$CFG/.env.example" "$NEW_DIR/.env.example"
|
||||
# Drop obsolete unit name if present
|
||||
rm -f "$NEW_DIR/container-koopa-freenotes.service"
|
||||
echo "STATUS=refreshed_stack_files DIR=$NEW_DIR"
|
||||
|
||||
if [[ -z "$img_new" ]]; then
|
||||
if [[ -n "$img_old" ]]; then
|
||||
echo "STATUS=tag $OLD_IMG → $NEW_IMG"
|
||||
podman tag "$OLD_IMG" "$NEW_IMG"
|
||||
else
|
||||
echo "STATUS=build missing image via build.sh"
|
||||
bash "$ROOT/scripts/notes/build.sh"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$ct_new" ]]; then
|
||||
echo "STATUS=rm stale $NEW_CT"
|
||||
podman stop -t 10 "$NEW_CT" || true
|
||||
podman rm "$NEW_CT" || true
|
||||
fi
|
||||
|
||||
echo "STATUS=compose_up"
|
||||
podman-compose up -d
|
||||
|
||||
ok=0
|
||||
for i in $(seq 1 40); do
|
||||
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 "http://127.0.0.1:${PORT}/" || true)
|
||||
echo "STATUS=health try=$i code=$code"
|
||||
case "$code" in
|
||||
2*|3*) ok=1; break ;;
|
||||
esac
|
||||
sleep 3
|
||||
done
|
||||
[[ "$ok" -eq 1 ]] || { echo "ERROR: no answer on :${PORT}" >&2; podman-compose ps; exit 1; }
|
||||
|
||||
# User systemd unit (optional; ignore failures)
|
||||
UNIT_SRC="$NEW_DIR/container-koopa-silverbullet.service"
|
||||
UNIT_DST="${HOME}/.config/systemd/user/container-koopa-silverbullet.service"
|
||||
if [[ -f "$UNIT_SRC" ]] && command -v systemctl >/dev/null; then
|
||||
mkdir -p "${HOME}/.config/systemd/user"
|
||||
cp -a "$UNIT_SRC" "$UNIT_DST"
|
||||
systemctl --user daemon-reload || true
|
||||
systemctl --user disable --now container-koopa-freenotes.service 2>/dev/null || true
|
||||
systemctl --user enable --now container-koopa-silverbullet.service 2>/dev/null || true
|
||||
echo "STATUS=systemd_user unit=container-koopa-silverbullet.service"
|
||||
fi
|
||||
|
||||
echo "STATUS=ok CONTAINER=$NEW_CT DIR=$NEW_DIR PORT=$PORT"
|
||||
echo "STATUS=caddy_next sudo bash $ROOT/scripts/caddy/merge-notes-site.sh"
|
||||
echo "STATUS=caddy_next sudo NOTES_AUTH_SEED=/tmp/silverbullet-basicauth-seed.txt bash $ROOT/scripts/caddy/apply-notes-basicauth.sh"
|
||||
echo "STATUS=dns_need silverbullet.hacktivism.ch → koopa"
|
||||
25
scripts/notes/prepare-basicauth-seed.sh
Executable file
25
scripts/notes/prepare-basicauth-seed.sh
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env bash
|
||||
# Copy silverbullet basicauth SoT → /tmp seed for apply-notes-basicauth.sh.
|
||||
# Run on magikoopa (or any host with unlocked koopa-admin-secrets-enc).
|
||||
# Does NOT touch /etc/caddy (no sudo). Does NOT print secret contents.
|
||||
#
|
||||
# bash scripts/notes/prepare-basicauth-seed.sh
|
||||
# # then on koopa (after scp seed or shared path):
|
||||
# sudo NOTES_AUTH_SEED=/tmp/silverbullet-basicauth-seed.txt \
|
||||
# bash …/scripts/caddy/apply-notes-basicauth.sh
|
||||
set -euo pipefail
|
||||
|
||||
OUT="${NOTES_AUTH_SEED_OUT:-/tmp/silverbullet-basicauth-seed.txt}"
|
||||
SOT_DEFAULT="${HOME}/git/koopa-admin-secrets-enc/koopa/home-hernani/caddy/silverbullet-basicauth.txt"
|
||||
SOT="${NOTES_AUTH_SOT:-$SOT_DEFAULT}"
|
||||
|
||||
[[ -f "$SOT" ]] || {
|
||||
echo "ERROR: missing SoT $SOT (unlock git-crypt / set NOTES_AUTH_SOT=)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
umask 077
|
||||
cp -a "$SOT" "$OUT"
|
||||
chmod 600 "$OUT"
|
||||
echo "STATUS=ok OUT=$OUT BYTES=$(wc -c <"$OUT" | tr -d ' ') NEXT=scp_or_use_NOTES_AUTH_SEED"
|
||||
echo "STATUS=hint scp $OUT hernani@koopa:/tmp/ && sudo NOTES_AUTH_SEED=$OUT bash …/apply-notes-basicauth.sh"
|
||||
39
scripts/notes/status.sh
Executable file
39
scripts/notes/status.sh
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env bash
|
||||
# Thin status for SilverBullet stack on koopa (hernani). Safe read-only.
|
||||
# bash scripts/notes/status.sh
|
||||
set -euo pipefail
|
||||
|
||||
OLD_DIR="${HOME}/koopa-freenotes"
|
||||
NEW_DIR="${HOME}/koopa-silverbullet"
|
||||
OLD_CT=koopa-freenotes
|
||||
NEW_CT=koopa-silverbullet
|
||||
OLD_IMG=localhost/koopa-freenotes:cachyos
|
||||
NEW_IMG=localhost/koopa-silverbullet:cachyos
|
||||
PORT=9028
|
||||
|
||||
echo "STATUS=host HOST=$(hostname -s)"
|
||||
echo "STATUS=dirs OLD=$([ -d "$OLD_DIR" ] && echo 1 || echo 0) NEW=$([ -d "$NEW_DIR" ] && echo 1 || echo 0)"
|
||||
|
||||
if command -v podman >/dev/null 2>&1; then
|
||||
ct_old=$(podman ps -a --format '{{.Names}} {{.Status}}' 2>/dev/null | grep -E "^${OLD_CT} " || true)
|
||||
ct_new=$(podman ps -a --format '{{.Names}} {{.Status}}' 2>/dev/null | grep -E "^${NEW_CT} " || true)
|
||||
echo "STATUS=ct_old ${ct_old:-none}"
|
||||
echo "STATUS=ct_new ${ct_new:-none}"
|
||||
img_old=$(podman images -q "$OLD_IMG" 2>/dev/null || true)
|
||||
img_new=$(podman images -q "$NEW_IMG" 2>/dev/null || true)
|
||||
echo "STATUS=img_old ${img_old:-none}"
|
||||
echo "STATUS=img_new ${img_new:-none}"
|
||||
else
|
||||
echo "STATUS=podman missing"
|
||||
fi
|
||||
|
||||
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 "http://127.0.0.1:${PORT}/" 2>/dev/null || echo fail)
|
||||
echo "STATUS=local_http PORT=$PORT CODE=$code"
|
||||
|
||||
if [[ -d "$NEW_DIR" ]]; then
|
||||
echo "STATUS=cutover done DIR=$NEW_DIR"
|
||||
elif [[ -d "$OLD_DIR" ]]; then
|
||||
echo "STATUS=cutover pending DIR=$OLD_DIR NEXT=cutover-to-silverbullet.sh"
|
||||
else
|
||||
echo "STATUS=cutover unknown (no live dir)"
|
||||
fi
|
||||
23
scripts/notes/up.sh
Executable file
23
scripts/notes/up.sh
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env bash
|
||||
# Bring up koopa-silverbullet / SilverBullet.
|
||||
set -euo pipefail
|
||||
DIR="${HOME}/koopa-silverbullet"
|
||||
cd "${DIR}"
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
[[ -f .env ]] && source .env
|
||||
set +a
|
||||
PORT="${HOST_PORT:-9028}"
|
||||
podman-compose up -d
|
||||
echo "== wait for :${PORT} =="
|
||||
ok=0
|
||||
for i in $(seq 1 40); do
|
||||
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 "http://127.0.0.1:${PORT}/" || true)
|
||||
echo "try $i: $code"
|
||||
case "$code" in
|
||||
2*|3*) ok=1; break ;;
|
||||
esac
|
||||
sleep 3
|
||||
done
|
||||
[[ "$ok" -eq 1 ]] || { echo "ERROR: no answer on ${PORT}"; podman-compose ps; exit 1; }
|
||||
echo "local ok. public (after Caddy+DNS): https://silverbullet.hacktivism.ch/"
|
||||
89
scripts/podman-rss.sh
Normal file
89
scripts/podman-rss.sh
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env bash
|
||||
# Human-readable container RSS when rootless podman stats shows 0B / host RAM.
|
||||
# Cause on openSUSE: user@.service Delegate= (20-defaults-SUSE.conf) clears
|
||||
# memory/cpu controllers → no memory.current. CPU% from podman stats still works.
|
||||
# Usage: podman-rss.sh [--json] [NAME_FILTER…]
|
||||
set -euo pipefail
|
||||
|
||||
JSON=0
|
||||
FILTERS=()
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--json|-j) JSON=1 ;;
|
||||
-h|--help)
|
||||
echo "usage: $0 [--json] [NAME_FILTER…]" >&2
|
||||
exit 0
|
||||
;;
|
||||
*) FILTERS+=("$a") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
human() {
|
||||
# stdin: kibibytes integer
|
||||
awk '
|
||||
{
|
||||
k=$1+0
|
||||
if (k >= 1048576) printf "%.1f GiB", k/1048576
|
||||
else if (k >= 1024) printf "%.0f MiB", k/1024
|
||||
else printf "%d KiB", k
|
||||
}'
|
||||
}
|
||||
|
||||
# CPU map: name -> cpu%
|
||||
declare -A CPU=()
|
||||
while IFS=$'\t' read -r name cpu; do
|
||||
[ -n "$name" ] || continue
|
||||
CPU["$name"]="$cpu"
|
||||
done < <(podman stats --no-stream --format '{{.Name}}\t{{.CPUPerc}}' 2>/dev/null || true)
|
||||
|
||||
rows=()
|
||||
total_kib=0
|
||||
while IFS= read -r cid; do
|
||||
[ -n "$cid" ] || continue
|
||||
name=$(podman inspect -f '{{.Name}}' "$cid" 2>/dev/null || true)
|
||||
[ -n "$name" ] || continue
|
||||
if [ ${#FILTERS[@]} -gt 0 ]; then
|
||||
ok=0
|
||||
for f in "${FILTERS[@]}"; do
|
||||
case "$name" in *"$f"*) ok=1; break ;; esac
|
||||
done
|
||||
[ "$ok" -eq 1 ] || continue
|
||||
fi
|
||||
# podman top rss is KiB per process; sum
|
||||
kib=$(podman top "$cid" -o rss 2>/dev/null | awk 'NR>1{s+=$1} END{print s+0}')
|
||||
cpu="${CPU[$name]:-?}"
|
||||
rows+=("${kib}|${name}|${cpu}")
|
||||
total_kib=$((total_kib + kib))
|
||||
done < <(podman ps -q)
|
||||
|
||||
IFS=$'\n' sorted=($(printf '%s\n' "${rows[@]:-}" | sort -t'|' -k1,1nr))
|
||||
unset IFS
|
||||
|
||||
if [ "$JSON" -eq 1 ]; then
|
||||
python3 - "$total_kib" "${sorted[@]:-}" <<'PY'
|
||||
import json, sys
|
||||
total = int(sys.argv[1]) if len(sys.argv) > 1 else 0
|
||||
items = []
|
||||
for row in sys.argv[2:]:
|
||||
if not row.strip():
|
||||
continue
|
||||
kib, name, cpu = row.split("|", 2)
|
||||
items.append({"name": name, "rss_kib": int(kib), "cpu": cpu})
|
||||
print(json.dumps({"ok": True, "total_rss_kib": total, "containers": items}, indent=2))
|
||||
PY
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf '%-44s %10s %8s\n' "NAME" "RSS" "CPU%"
|
||||
printf '%-44s %10s %8s\n' "----" "---" "----"
|
||||
for row in "${sorted[@]:-}"; do
|
||||
[ -n "$row" ] || continue
|
||||
kib="${row%%|*}"
|
||||
rest="${row#*|}"
|
||||
name="${rest%%|*}"
|
||||
cpu="${rest##*|}"
|
||||
hum=$(printf '%s' "$kib" | human)
|
||||
printf '%-44s %10s %8s\n' "$name" "$hum" "$cpu"
|
||||
done
|
||||
printf '%-44s %10s\n' "TOTAL" "$(printf '%s' "$total_kib" | human)"
|
||||
echo "# note: RSS via podman top (no memory cgroup); CPU via podman stats" >&2
|
||||
654
scripts/upgrade-containers.sh
Executable file
654
scripts/upgrade-containers.sh
Executable file
|
|
@ -0,0 +1,654 @@
|
|||
#!/usr/bin/env bash
|
||||
# Upgrade hernani podman containers on koopa: per-target dry-run (prod-safe),
|
||||
# then full backup (compose + mounts/volumes + DB dumps), then apply.
|
||||
#
|
||||
# Run on koopa as hernani:
|
||||
# ~/upgrade-containers.sh # dry-run only (default)
|
||||
# ~/upgrade-containers.sh --apply # dry-run → backup → upgrade per target
|
||||
# ~/upgrade-containers.sh --apply --images-only
|
||||
# ~/upgrade-containers.sh --apply --os-only
|
||||
# ~/upgrade-containers.sh --apply --recreate-only
|
||||
# ~/upgrade-containers.sh --apply --only koopa-forgejo,koopa-notes
|
||||
# ~/upgrade-containers.sh --apply --skip 'taler-hacktivism*,goa-regio-ng'
|
||||
# ~/upgrade-containers.sh --apply --taler-debs
|
||||
# ~/upgrade-containers.sh --apply --rebuild-local
|
||||
# ~/upgrade-containers.sh --apply --include-stopped
|
||||
# ~/upgrade-containers.sh --apply --backup-max-gb 80
|
||||
# ~/upgrade-containers.sh --apply --no-backup # emergency only
|
||||
#
|
||||
# Per-target --apply flow:
|
||||
# 1) dry-run checks that do not recreate/restart or mutate packages
|
||||
# 2) if dry-run fails → skip that target (others continue)
|
||||
# 3) full backup under ~/backups/container-upgrades/<ts>/<name>/
|
||||
# 4) if backup fails → skip apply for that target
|
||||
# 5) pull / recreate / OS upgrade as selected
|
||||
set -euo pipefail
|
||||
|
||||
APPLY=0
|
||||
DO_IMAGES=1
|
||||
DO_RECREATE=1
|
||||
DO_OS=1
|
||||
DO_TALER_DEBS=0
|
||||
REBUILD_LOCAL=0
|
||||
INCLUDE_STOPPED=0
|
||||
DO_BACKUP=1
|
||||
BACKUP_MAX_GB=50
|
||||
ONLY_RAW=""
|
||||
SKIP_RAW=""
|
||||
HOME_DIR="${HOME:-/home/hernani}"
|
||||
ADMIN_LOG="${ADMIN_LOG:-$HOME_DIR/koopa-admin-log}"
|
||||
BACKUP_ROOT="${BACKUP_ROOT:-$HOME_DIR/backups/container-upgrades}"
|
||||
RUN_TS="$(date '+%Y%m%d-%H%M%S')"
|
||||
BACKUP_RUN_DIR="$BACKUP_ROOT/$RUN_TS"
|
||||
|
||||
LOG_TS() { date '+%Y-%m-%dT%H:%M:%S%z'; }
|
||||
log() { printf '[%s] %s\n' "$(LOG_TS)" "$*"; }
|
||||
run() {
|
||||
if [[ "$APPLY" -eq 1 ]]; then
|
||||
log "RUN: $*"
|
||||
"$@"
|
||||
else
|
||||
log "DRY: $*"
|
||||
fi
|
||||
}
|
||||
|
||||
usage() {
|
||||
sed -n '2,28p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
match_glob_list() {
|
||||
local name="$1" raw="$2" g
|
||||
[[ -z "$raw" ]] && return 1
|
||||
raw="${raw//,/ }"
|
||||
for g in $raw; do
|
||||
# shellcheck disable=SC2254
|
||||
case "$name" in
|
||||
$g) return 0 ;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage 0 ;;
|
||||
--apply) APPLY=1; shift ;;
|
||||
--dry-run) APPLY=0; shift ;;
|
||||
--images-only) DO_IMAGES=1; DO_RECREATE=0; DO_OS=0; shift ;;
|
||||
--os-only) DO_IMAGES=0; DO_RECREATE=0; DO_OS=1; shift ;;
|
||||
--recreate-only) DO_IMAGES=0; DO_RECREATE=1; DO_OS=0; shift ;;
|
||||
--taler-debs) DO_TALER_DEBS=1; shift ;;
|
||||
--rebuild-local) REBUILD_LOCAL=1; shift ;;
|
||||
--include-stopped) INCLUDE_STOPPED=1; shift ;;
|
||||
--no-backup) DO_BACKUP=0; shift ;;
|
||||
--backup-max-gb) BACKUP_MAX_GB="${2:-50}"; shift 2 ;;
|
||||
--backup-max-gb=*) BACKUP_MAX_GB="${1#*=}"; shift ;;
|
||||
--only) ONLY_RAW="${2:-}"; shift 2 ;;
|
||||
--skip) SKIP_RAW="${2:-}"; shift 2 ;;
|
||||
--only=*) ONLY_RAW="${1#*=}"; shift ;;
|
||||
--skip=*) SKIP_RAW="${1#*=}"; shift ;;
|
||||
*) log "ERROR: unknown arg: $1"; usage 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$(id -un)" != "hernani" ]] && [[ "${FORCE_USER:-}" != "1" ]]; then
|
||||
log "WARN: expected user hernani (got $(id -un)); set FORCE_USER=1 to override"
|
||||
fi
|
||||
|
||||
declare -a NAMES=()
|
||||
declare -A IMG=()
|
||||
declare -A CDIR=()
|
||||
declare -A STATUS=() # ok|dry_fail|backup_fail|upgraded|skipped
|
||||
declare -A NOTE=()
|
||||
|
||||
list_cmd=(podman ps --format '{{.Names}}|{{.Image}}')
|
||||
if [[ "$INCLUDE_STOPPED" -eq 1 ]]; then
|
||||
list_cmd=(podman ps -a --format '{{.Names}}|{{.Image}}')
|
||||
fi
|
||||
|
||||
while IFS='|' read -r name image; do
|
||||
[[ -z "$name" ]] && continue
|
||||
if [[ ! "$name" =~ ^(koopa-|taler-|goa-|mytops-) ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ -n "$ONLY_RAW" ]] && ! match_glob_list "$name" "$ONLY_RAW"; then
|
||||
continue
|
||||
fi
|
||||
if match_glob_list "$name" "$SKIP_RAW"; then
|
||||
log "SKIP name=$name (matched --skip)"
|
||||
continue
|
||||
fi
|
||||
NAMES+=("$name")
|
||||
IMG["$name"]="$image"
|
||||
dir=$(podman inspect -f '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' "$name" 2>/dev/null || true)
|
||||
CDIR["$name"]="$dir"
|
||||
STATUS["$name"]="pending"
|
||||
done < <("${list_cmd[@]}")
|
||||
|
||||
if [[ ${#NAMES[@]} -eq 0 ]]; then
|
||||
log "No matching containers."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "MODE=$([ "$APPLY" -eq 1 ] && echo apply || echo dry-run) images=$DO_IMAGES recreate=$DO_RECREATE os=$DO_OS backup=$DO_BACKUP backup_max_gb=$BACKUP_MAX_GB"
|
||||
log "Targets (${#NAMES[@]}): ${NAMES[*]}"
|
||||
if [[ "$APPLY" -eq 1 && "$DO_BACKUP" -eq 1 ]]; then
|
||||
mkdir -p "$BACKUP_RUN_DIR"
|
||||
log "BACKUP_DIR=$BACKUP_RUN_DIR"
|
||||
fi
|
||||
|
||||
is_pinned_tag() {
|
||||
local ref="$1" tag="${1##*:}"
|
||||
[[ "$ref" != *:* ]] && return 1
|
||||
case "$tag" in
|
||||
latest|stable|stable-*|main|master|landing|live|setup|rootless|*-rootless|alpine|*-alpine|[0-9]|[0-9][0-9]|[0-9].*-alpine|[0-9][0-9]-alpine) return 1 ;;
|
||||
esac
|
||||
if [[ "$tag" =~ ^[0-9]+\.[0-9]+ ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- prod-safe dry-run for one container (no recreate, no package mutate) ---
|
||||
dry_run_one() {
|
||||
local name="$1" ref="${IMG[$name]}" dir="${CDIR[$name]}"
|
||||
local rc=0
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
|
||||
# running?
|
||||
if ! podman inspect "$name" >/dev/null 2>&1; then
|
||||
log "DRYFAIL $name: inspect failed"
|
||||
NOTE["$name"]="inspect failed"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# image phase checks
|
||||
if [[ "$DO_IMAGES" -eq 1 ]]; then
|
||||
if [[ "$ref" == localhost/* ]]; then
|
||||
if ! podman image exists "$ref" 2>/dev/null; then
|
||||
log "DRYFAIL $name: local image missing $ref"
|
||||
NOTE["$name"]="local image missing"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
log "DRYOK $name: local image present $ref"
|
||||
else
|
||||
# pull --dry-run is not universal; probe registry without replacing running CT
|
||||
if podman pull --help 2>&1 | grep -q -- '--quiet'; then
|
||||
:
|
||||
fi
|
||||
if ! podman image exists "$ref" 2>/dev/null; then
|
||||
log "DRYWARN $name: image not local yet ($ref) — apply will pull"
|
||||
else
|
||||
log "DRYOK $name: image present locally $ref"
|
||||
fi
|
||||
# non-mutating registry probe when possible
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
:
|
||||
fi
|
||||
# Try a no-restart pull into local store is mutating cache but not prod CT.
|
||||
# For dry-run we only validate the ref string / inspect local digest.
|
||||
if is_pinned_tag "$ref"; then
|
||||
log "DRYOK $name: pinned tag $ref (compose bump needed to move)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# compose config validate (no up)
|
||||
if [[ "$DO_RECREATE" -eq 1 ]]; then
|
||||
if [[ -n "$dir" ]]; then
|
||||
if [[ ! -f "$dir/compose.yml" && ! -f "$dir/compose.yaml" && ! -f "$dir/docker-compose.yml" ]]; then
|
||||
log "DRYFAIL $name: compose dir without compose file: $dir"
|
||||
NOTE["$name"]="no compose file"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
if ! (cd "$dir" && podman-compose config >/dev/null 2>"$tmp"); then
|
||||
log "DRYFAIL $name: podman-compose config failed"
|
||||
sed -n '1,20p' "$tmp" | while IFS= read -r line; do log " $line"; done
|
||||
NOTE["$name"]="compose config failed"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
log "DRYOK $name: compose config ok ($dir)"
|
||||
else
|
||||
log "DRYOK $name: no compose label — recreate will be skipped"
|
||||
fi
|
||||
fi
|
||||
|
||||
# OS simulate (no real upgrade)
|
||||
if [[ "$DO_OS" -eq 1 ]]; then
|
||||
if podman ps --format '{{.Names}}' | grep -qx "$name"; then
|
||||
local os_script
|
||||
os_script=$(cat <<'INNER'
|
||||
set -e
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update -qq
|
||||
apt-get -s -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold upgrade >/tmp/os-dry.out
|
||||
echo OS_DRY=apt ok
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
apk update >/dev/null
|
||||
apk upgrade --simulate >/tmp/os-dry.out 2>&1 || apk upgrade -s >/tmp/os-dry.out 2>&1 || true
|
||||
echo OS_DRY=apk ok
|
||||
elif command -v zypper >/dev/null 2>&1; then
|
||||
zypper -n refresh >/dev/null
|
||||
zypper -n update -D >/tmp/os-dry.out 2>&1 || true
|
||||
echo OS_DRY=zypper ok
|
||||
elif command -v microdnf >/dev/null 2>&1; then
|
||||
echo OS_DRY=microdnf none-sim
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf -y check-update >/tmp/os-dry.out 2>&1 || true
|
||||
echo OS_DRY=dnf ok
|
||||
else
|
||||
echo OS_DRY=none
|
||||
fi
|
||||
INNER
|
||||
)
|
||||
if podman exec -u 0 "$name" bash -lc "$os_script" >"$tmp" 2>&1 \
|
||||
|| podman exec -u 0 "$name" sh -lc "$os_script" >"$tmp" 2>&1; then
|
||||
log "DRYOK $name: OS simulate ($(grep -E '^OS_DRY=' "$tmp" | tail -1 || echo ok))"
|
||||
else
|
||||
# many distroless/app images have no pkg manager — treat as ok/skip
|
||||
if grep -q 'OS_DRY=none' "$tmp" 2>/dev/null; then
|
||||
log "DRYOK $name: no package manager (OS skip)"
|
||||
else
|
||||
log "DRYWARN $name: OS simulate soft-fail (will try apply carefully)"
|
||||
sed -n '1,8p' "$tmp" | while IFS= read -r line; do log " $line"; done
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log "DRYOK $name: not running — OS phase will skip"
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$tmp"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
# --- full backup with data ---
|
||||
backup_one() {
|
||||
local name="$1"
|
||||
local dest="$BACKUP_RUN_DIR/$name"
|
||||
local meta="$dest/META.txt"
|
||||
local max_bytes=$((BACKUP_MAX_GB * 1024 * 1024 * 1024))
|
||||
mkdir -p "$dest/binds" "$dest/volumes" "$dest/db" "$dest/compose"
|
||||
|
||||
{
|
||||
echo "name=$name"
|
||||
echo "image=${IMG[$name]}"
|
||||
echo "compose_dir=${CDIR[$name]:-}"
|
||||
echo "ts=$RUN_TS"
|
||||
} >"$meta"
|
||||
|
||||
podman inspect "$name" >"$dest/inspect.json" 2>/dev/null || true
|
||||
|
||||
# compose project files
|
||||
local dir="${CDIR[$name]}"
|
||||
if [[ -n "$dir" && -d "$dir" ]]; then
|
||||
for f in compose.yml compose.yaml docker-compose.yml docker-compose.yaml .env; do
|
||||
if [[ -f "$dir/$f" ]]; then
|
||||
cp -a "$dir/$f" "$dest/compose/$f"
|
||||
fi
|
||||
done
|
||||
# shallow copy of known config sidecars (not whole tree)
|
||||
for f in Caddyfile Containerfile Dockerfile; do
|
||||
[[ -f "$dir/$f" ]] && cp -a "$dir/$f" "$dest/compose/$f"
|
||||
done
|
||||
fi
|
||||
|
||||
# bind mounts + named volumes from inspect
|
||||
local mounts
|
||||
mounts=$(podman inspect -f '{{range .Mounts}}{{.Type}}|{{.Name}}|{{.Source}}|{{.Destination}}|{{.RW}}{{println}}{{end}}' "$name" 2>/dev/null || true)
|
||||
while IFS='|' read -r mtype mname src dst rw; do
|
||||
[[ -z "$mtype" ]] && continue
|
||||
# skip ephemeral / host tz
|
||||
case "$dst" in
|
||||
/etc/localtime|/etc/timezone|/run|/run/*|/tmp|/tmp/*|/dev|/dev/*|/proc|/proc/*|/sys|/sys/*) continue ;;
|
||||
esac
|
||||
case "$src" in
|
||||
/usr/share/zoneinfo/*|/etc/localtime) continue ;;
|
||||
esac
|
||||
|
||||
if [[ "$mtype" == "bind" ]]; then
|
||||
if [[ ! -e "$src" ]]; then
|
||||
log "BACKUPWARN $name: bind missing $src"
|
||||
continue
|
||||
fi
|
||||
local sz
|
||||
sz=$(du -sb "$src" 2>/dev/null | awk '{print $1}')
|
||||
if [[ -n "$sz" && "$sz" -gt "$max_bytes" ]]; then
|
||||
log "BACKUPWARN $name: skip huge bind $src (${sz}B > ${BACKUP_MAX_GB}G); raise --backup-max-gb"
|
||||
echo "SKIP_HUGE_BIND $src size=$sz" >>"$meta"
|
||||
continue
|
||||
fi
|
||||
local base
|
||||
base=$(echo "$dst" | sed 's#^/##; s#/#_#g')
|
||||
[[ -z "$base" ]] && base="root"
|
||||
log "BACKUP $name: bind $src → binds/${base}.tar.gz"
|
||||
if ! tar -C "$(dirname "$src")" -czf "$dest/binds/${base}.tar.gz" "$(basename "$src")" 2>>"$dest/backup.err"; then
|
||||
log "BACKUPFAIL $name: tar bind $src"
|
||||
NOTE["$name"]="backup bind failed: $src"
|
||||
return 1
|
||||
fi
|
||||
echo "BIND $src -> binds/${base}.tar.gz" >>"$meta"
|
||||
elif [[ "$mtype" == "volume" ]]; then
|
||||
local vname="${mname:-}"
|
||||
if [[ -z "$vname" ]]; then
|
||||
# fallback: export via source path
|
||||
if [[ -d "$src" ]]; then
|
||||
local vbase
|
||||
vbase=$(basename "$(dirname "$src")")
|
||||
log "BACKUP $name: volume-path $src → volumes/${vbase}.tar.gz"
|
||||
if ! tar -C "$src" -czf "$dest/volumes/${vbase}.tar.gz" . 2>>"$dest/backup.err"; then
|
||||
log "BACKUPFAIL $name: tar volume path $src"
|
||||
NOTE["$name"]="backup volume failed"
|
||||
return 1
|
||||
fi
|
||||
echo "VOLPATH $src -> volumes/${vbase}.tar.gz" >>"$meta"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
local sz
|
||||
sz=$(du -sb "$src" 2>/dev/null | awk '{print $1}')
|
||||
if [[ -n "$sz" && "$sz" -gt "$max_bytes" ]]; then
|
||||
log "BACKUPWARN $name: skip huge volume $vname (${sz}B > ${BACKUP_MAX_GB}G)"
|
||||
echo "SKIP_HUGE_VOL $vname size=$sz" >>"$meta"
|
||||
continue
|
||||
fi
|
||||
log "BACKUP $name: volume $vname → volumes/${vname}.tar"
|
||||
if podman volume export "$vname" -o "$dest/volumes/${vname}.tar" 2>>"$dest/backup.err"; then
|
||||
gzip -f "$dest/volumes/${vname}.tar" 2>/dev/null || true
|
||||
echo "VOLUME $vname -> volumes/${vname}.tar.gz" >>"$meta"
|
||||
else
|
||||
# fallback tar of mount source
|
||||
if [[ -d "$src" ]] && tar -C "$src" -czf "$dest/volumes/${vname}.tar.gz" . 2>>"$dest/backup.err"; then
|
||||
echo "VOLPATH $src -> volumes/${vname}.tar.gz" >>"$meta"
|
||||
else
|
||||
log "BACKUPFAIL $name: volume $vname"
|
||||
NOTE["$name"]="backup volume failed: $vname"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done <<<"$mounts"
|
||||
|
||||
# DB dumps when obvious
|
||||
if podman ps --format '{{.Names}}' | grep -qx "$name"; then
|
||||
local img_l
|
||||
img_l=$(echo "${IMG[$name]}" | tr '[:upper:]' '[:lower:]')
|
||||
case "$img_l" in
|
||||
*postgres*|*postgis*)
|
||||
local dbuser
|
||||
dbuser=$(podman exec "$name" printenv POSTGRES_USER 2>/dev/null || echo postgres)
|
||||
local dbname
|
||||
dbname=$(podman exec "$name" printenv POSTGRES_DB 2>/dev/null || echo postgres)
|
||||
log "BACKUP $name: pg_dumpall"
|
||||
if podman exec -u 0 "$name" bash -lc "pg_dumpall -U '$dbuser'" >"$dest/db/pg_dumpall.sql" 2>>"$dest/backup.err"; then
|
||||
gzip -f "$dest/db/pg_dumpall.sql"
|
||||
echo "PG_DUMPALL ok db=$dbname user=$dbuser" >>"$meta"
|
||||
else
|
||||
# single-db fallback
|
||||
if podman exec -u 0 "$name" bash -lc "pg_dump -U '$dbuser' '$dbname'" >"$dest/db/pg_dump.sql" 2>>"$dest/backup.err"; then
|
||||
gzip -f "$dest/db/pg_dump.sql"
|
||||
echo "PG_DUMP ok db=$dbname" >>"$meta"
|
||||
else
|
||||
log "BACKUPWARN $name: postgres dump failed (mounts still saved)"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
*mariadb*|*mysql*)
|
||||
local muser mpass mdb
|
||||
muser=$(podman exec "$name" printenv MYSQL_USER 2>/dev/null || podman exec "$name" printenv MARIADB_USER 2>/dev/null || echo root)
|
||||
mpass=$(podman exec "$name" printenv MYSQL_ROOT_PASSWORD 2>/dev/null || podman exec "$name" printenv MARIADB_ROOT_PASSWORD 2>/dev/null || podman exec "$name" printenv MYSQL_PASSWORD 2>/dev/null || true)
|
||||
mdb=$(podman exec "$name" printenv MYSQL_DATABASE 2>/dev/null || podman exec "$name" printenv MARIADB_DATABASE 2>/dev/null || echo "")
|
||||
log "BACKUP $name: mariadb/mysql dump"
|
||||
if [[ -n "$mpass" ]]; then
|
||||
if podman exec -u 0 "$name" bash -lc "MYSQL_PWD='$mpass' mysqldump -u root --all-databases" >"$dest/db/mysql_all.sql" 2>>"$dest/backup.err" \
|
||||
|| podman exec -u 0 "$name" bash -lc "MYSQL_PWD='$mpass' mariadb-dump -u root --all-databases" >"$dest/db/mysql_all.sql" 2>>"$dest/backup.err"; then
|
||||
gzip -f "$dest/db/mysql_all.sql"
|
||||
echo "MYSQL_DUMPALL ok" >>"$meta"
|
||||
else
|
||||
log "BACKUPWARN $name: mysql dump failed (mounts still saved)"
|
||||
fi
|
||||
else
|
||||
log "BACKUPWARN $name: no mysql password in env — skip dump"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
echo "BACKUP_OK" >>"$meta"
|
||||
log "BACKUPOK $name → $dest"
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- apply phases for one container ---
|
||||
apply_images_one() {
|
||||
local name="$1" ref="${IMG[$name]}"
|
||||
[[ "$DO_IMAGES" -eq 1 ]] || return 0
|
||||
if [[ "$ref" == localhost/* ]]; then
|
||||
log "LOCAL image=$ref (container=$name) — pull skipped"
|
||||
return 0
|
||||
fi
|
||||
if is_pinned_tag "$ref"; then
|
||||
log "PINNED image=$ref (container=$name) — pull same tag only"
|
||||
fi
|
||||
run podman pull "$ref"
|
||||
}
|
||||
|
||||
rebuild_one_local() {
|
||||
local name="$1" ref="${IMG[$name]}" dir="${CDIR[$name]}"
|
||||
[[ "$ref" == localhost/* ]] || return 0
|
||||
local cf=""
|
||||
if [[ -n "$dir" && -f "$dir/Containerfile" ]]; then
|
||||
cf="$dir/Containerfile"
|
||||
elif [[ -n "$dir" && -f "$dir/Dockerfile" ]]; then
|
||||
cf="$dir/Dockerfile"
|
||||
fi
|
||||
case "$name" in
|
||||
koopa-nym)
|
||||
[[ -x "$ADMIN_LOG/scripts/nym/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/nym/build.sh"; return; }
|
||||
;;
|
||||
koopa-tor-relay)
|
||||
if [[ -f "$HOME_DIR/koopa-tor-relay/build/Containerfile" ]]; then
|
||||
run podman build -t localhost/koopa-tor-relay:latest -f "$HOME_DIR/koopa-tor-relay/build/Containerfile" "$HOME_DIR/koopa-tor-relay/build"
|
||||
return
|
||||
fi
|
||||
;;
|
||||
koopa-paivana)
|
||||
if [[ -f "$dir/Containerfile" ]]; then
|
||||
run podman build -t localhost/koopa-paivana:latest -f "$dir/Containerfile" "$dir"
|
||||
return
|
||||
fi
|
||||
;;
|
||||
koopa-bbb-front)
|
||||
[[ -x "$ADMIN_LOG/scripts/bbb/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/bbb/build.sh"; return; }
|
||||
;;
|
||||
koopa-meet-web)
|
||||
[[ -x "$ADMIN_LOG/scripts/meet/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/meet/build.sh"; return; }
|
||||
;;
|
||||
koopa-galene|koopa-paivana-galene)
|
||||
[[ -x "$ADMIN_LOG/scripts/galene/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/galene/build.sh"; return; }
|
||||
;;
|
||||
koopa-cryptpad|koopa-cryptpad-edge|koopa-paivana-cryptpad)
|
||||
[[ -x "$ADMIN_LOG/scripts/cryptpad/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/cryptpad/build.sh"; return; }
|
||||
;;
|
||||
koopa-notes|koopa-silverbullet)
|
||||
[[ -x "$ADMIN_LOG/scripts/notes/build.sh" ]] && { run bash "$ADMIN_LOG/scripts/notes/build.sh"; return; }
|
||||
;;
|
||||
esac
|
||||
if [[ -n "$cf" ]]; then
|
||||
run podman build -t "$ref" -f "$cf" "$(dirname "$cf")"
|
||||
else
|
||||
log "NO-REBUILD path for $name ($ref)"
|
||||
fi
|
||||
}
|
||||
|
||||
apply_recreate_one() {
|
||||
local name="$1" dir="${CDIR[$name]}"
|
||||
[[ "$DO_RECREATE" -eq 1 ]] || return 0
|
||||
if [[ -z "$dir" ]]; then
|
||||
log "NO-COMPOSE container=$name — recreate skipped"
|
||||
return 0
|
||||
fi
|
||||
if [[ ! -f "$dir/compose.yml" && ! -f "$dir/compose.yaml" && ! -f "$dir/docker-compose.yml" ]]; then
|
||||
log "WARN: compose dir without compose file: $dir"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$APPLY" -eq 1 ]]; then
|
||||
log "RUN: (cd $dir && podman-compose pull && podman-compose up -d)"
|
||||
(cd "$dir" && podman-compose pull && podman-compose up -d)
|
||||
else
|
||||
log "DRY: (cd $dir && podman-compose pull && podman-compose up -d)"
|
||||
fi
|
||||
}
|
||||
|
||||
os_upgrade_one() {
|
||||
local name="$1"
|
||||
[[ "$DO_OS" -eq 1 ]] || return 0
|
||||
if ! podman ps --format '{{.Names}}' | grep -qx "$name"; then
|
||||
log "SKIP os name=$name (not running)"
|
||||
return 0
|
||||
fi
|
||||
local script
|
||||
script=$(cat <<'INNER'
|
||||
set -e
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update -qq
|
||||
apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold upgrade
|
||||
echo OS=apt ok
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
apk update
|
||||
apk upgrade
|
||||
echo OS=apk ok
|
||||
elif command -v zypper >/dev/null 2>&1; then
|
||||
zypper -n refresh
|
||||
zypper -n update
|
||||
echo OS=zypper ok
|
||||
elif command -v microdnf >/dev/null 2>&1; then
|
||||
microdnf -y upgrade
|
||||
echo OS=microdnf ok
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf -y upgrade
|
||||
echo OS=dnf ok
|
||||
else
|
||||
echo OS=none
|
||||
exit 0
|
||||
fi
|
||||
INNER
|
||||
)
|
||||
if [[ "$APPLY" -eq 1 ]]; then
|
||||
log "RUN: podman exec -u 0 $name OS-upgrade"
|
||||
if ! podman exec -u 0 "$name" bash -lc "$script"; then
|
||||
podman exec -u 0 "$name" sh -lc "$script" || log "WARN: OS upgrade failed for $name"
|
||||
fi
|
||||
else
|
||||
log "DRY: podman exec -u 0 $name OS-upgrade"
|
||||
fi
|
||||
}
|
||||
|
||||
# Deduplicate compose recreate by dir
|
||||
declare -A COMPOSE_DONE=()
|
||||
declare -A IMAGE_DONE=()
|
||||
declare -A BACKUP_DONE=()
|
||||
|
||||
process_one() {
|
||||
local name="$1"
|
||||
log "==== target: $name (${IMG[$name]}) ===="
|
||||
|
||||
# 1) always dry-run first (prod-safe)
|
||||
if ! dry_run_one "$name"; then
|
||||
STATUS["$name"]="dry_fail"
|
||||
log "SKIP apply $name (dry-run failed)"
|
||||
return 0
|
||||
fi
|
||||
STATUS["$name"]="dry_ok"
|
||||
|
||||
# global dry-run mode: stop after checks
|
||||
if [[ "$APPLY" -eq 0 ]]; then
|
||||
log "DRY-RUN only for $name (no backup/apply)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 2) full backup with data BEFORE any mutate (even after dry-run ok)
|
||||
# every container — do not skip siblings in a compose stack (DB/data)
|
||||
if [[ "$DO_BACKUP" -eq 1 ]]; then
|
||||
if [[ -z "${BACKUP_DONE[$name]:-}" ]]; then
|
||||
if ! backup_one "$name"; then
|
||||
STATUS["$name"]="backup_fail"
|
||||
log "SKIP apply $name (backup failed)"
|
||||
return 0
|
||||
fi
|
||||
BACKUP_DONE["$name"]=1
|
||||
else
|
||||
log "BACKUP skip $name (already backed this container)"
|
||||
fi
|
||||
else
|
||||
log "WARN: --no-backup set; proceeding without backup for $name"
|
||||
fi
|
||||
|
||||
# 3) apply
|
||||
if [[ -z "${IMAGE_DONE[${IMG[$name]}]:-}" ]]; then
|
||||
apply_images_one "$name" || { STATUS["$name"]="apply_fail"; NOTE["$name"]="image pull failed"; return 0; }
|
||||
IMAGE_DONE["${IMG[$name]}"]=1
|
||||
fi
|
||||
if [[ "$REBUILD_LOCAL" -eq 1 ]]; then
|
||||
rebuild_one_local "$name" || true
|
||||
fi
|
||||
local dir="${CDIR[$name]}"
|
||||
if [[ -n "$dir" ]]; then
|
||||
if [[ -z "${COMPOSE_DONE[$dir]:-}" ]]; then
|
||||
apply_recreate_one "$name" || { STATUS["$name"]="apply_fail"; NOTE["$name"]="recreate failed"; return 0; }
|
||||
COMPOSE_DONE["$dir"]=1
|
||||
else
|
||||
log "recreate skip $name (compose dir already done)"
|
||||
fi
|
||||
else
|
||||
apply_recreate_one "$name" || true
|
||||
fi
|
||||
os_upgrade_one "$name" || { STATUS["$name"]="apply_fail"; NOTE["$name"]="os upgrade failed"; return 0; }
|
||||
STATUS["$name"]="upgraded"
|
||||
log "DONE $name"
|
||||
}
|
||||
|
||||
# --- main loop: per container ---
|
||||
for name in "${NAMES[@]}"; do
|
||||
process_one "$name"
|
||||
done
|
||||
|
||||
# optional Taler GOA debs (only on --apply, after per-CT work)
|
||||
if [[ "$DO_TALER_DEBS" -eq 1 ]]; then
|
||||
log "==== phase: taler-debs ===="
|
||||
tscript="$ADMIN_LOG/scripts/taler-shared/upgrade-goa-debs.sh"
|
||||
if [[ "$APPLY" -eq 0 ]]; then
|
||||
log "DRY: bash $tscript"
|
||||
elif [[ -x "$tscript" ]]; then
|
||||
run bash "$tscript"
|
||||
else
|
||||
log "ERROR: missing $tscript"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
log "==== summary ===="
|
||||
fail=0
|
||||
for name in "${NAMES[@]}"; do
|
||||
st="${STATUS[$name]:-?}"
|
||||
printf ' %-42s %-12s %s %s\n' "$name" "$st" "${IMG[$name]}" "${NOTE[$name]:-}"
|
||||
case "$st" in
|
||||
dry_fail|backup_fail|apply_fail) fail=1 ;;
|
||||
esac
|
||||
done
|
||||
if [[ "$APPLY" -eq 0 ]]; then
|
||||
log "Dry-run only (default). Re-run with --apply for backup+upgrade per target that passed dry-run."
|
||||
fi
|
||||
if [[ "$fail" -eq 1 ]]; then
|
||||
log "STATUS=partial_fail"
|
||||
exit 1
|
||||
fi
|
||||
log "STATUS=ok"
|
||||
Loading…
Add table
Add a link
Reference in a new issue