Files
AriWeb-Suite/deploy/module-as-user.sh
T

60 lines
6.5 KiB
Bash
Executable File

#!/bin/bash
# deploy/module-as-user.sh — 도입 모듈을 "그 모듈의 실행 계정" 으로 설치하기 위한 최소 파일 작업 도우미(§32).
#
# 왜 — Node Agent 는 master 계정으로 돌지만 도입 모듈(AriMail 등)은 서버에 이미 다른 계정(arihakdang 등)으로 설치되어 있고
# 그 디렉터리(/srv/<모듈>/releases, data)는 그 계정만 쓸 수 있다. Agent 가 직접 쓰면 EACCES 로 끝나므로(실제 환경 검증에서 발견),
# sudoers(deploy/sudoers/ariweb-agent)에 "이 스크립트만 root 가 아닌 임의 계정으로" 허용하고 Agent 가 `sudo -n -u <계정> bash <이 파일> <하위 명령>` 으로 부른다.
# 하위 명령은 모듈 배치에 필요한 것만 두고, 모든 경로 인자는 --app-dir 아래(또는 명시한 artifact)로 제한한다. 비밀값은 다루지 않고 출력하지 않는다.
#
# 사용: module-as-user.sh <하위 명령> --app-dir <모듈 디렉터리> [인자…]
# extract --tgz <artifact.tar.gz> --dest <appDir/releases/<ver>> 릴리스 디렉터리를 비우고 artifact 를 푼다
# write --file <appDir 아래 파일> [--mode 600] 표준 입력을 파일로 쓴다(REVISION.json·.previous.json · 0.4.0 hal-managed.json 은 --mode 600)
# switch --link <appDir/current> --target <릴리스 디렉터리> 심링크 원자 교체(ln → mv -T)
# rename --dir <releases 아래 디렉터리> --target <releases 아래 새 이름> 디렉터리 이름 바꾸기(같은 버전 재설치 때 실행 중인 디렉터리 보존)
# remove --dir <appDir/releases/<old>> 오래된 릴리스 삭제(releases 아래만)
# backup-db --db <sqlite 파일> --out <백업 파일> [--node <node>] SQLite 온라인 백업(VACUUM INTO)
# migrate --cwd <릴리스 디렉터리> --script <상대 경로> [--env-file <f>] [--node <node>] EnvironmentFile 을 읽어 마이그레이션 실행
set -euo pipefail
SUB="${1:-}"; shift || true
MODE=""; APP_DIR=""; TGZ=""; DEST=""; FILE=""; LINK=""; TARGET=""; DIR=""; DB=""; OUT=""; NODE_BIN=""; CWD=""; SCRIPT=""; ENV_FILE=""
while [ $# -gt 0 ]; do case "$1" in
--app-dir) APP_DIR="$2"; shift 2;; --mode) MODE="$2"; shift 2;; --tgz) TGZ="$2"; shift 2;; --dest) DEST="$2"; shift 2;; --file) FILE="$2"; shift 2;; --link) LINK="$2"; shift 2;; --target) TARGET="$2"; shift 2;;
--dir) DIR="$2"; shift 2;; --db) DB="$2"; shift 2;; --out) OUT="$2"; shift 2;; --node) NODE_BIN="$2"; shift 2;; --cwd) CWD="$2"; shift 2;; --script) SCRIPT="$2"; shift 2;; --env-file) ENV_FILE="$2"; shift 2;;
*) echo "알 수 없는 인자 $1" >&2; exit 2;; esac; done
[ -n "$APP_DIR" ] && [ -d "$APP_DIR" ] || { echo "--app-dir <존재하는 디렉터리> 가 필요합니다" >&2; exit 2; }
APP_DIR="$(cd "$APP_DIR" && pwd -P)"
# 경로가 appDir 아래인지(심링크를 따라간 실제 경로 기준, 아직 없는 경로는 부모 기준) 확인한다 — 다른 디렉터리를 건드리지 못하게.
inside() { local p="$1" d real; d="$(dirname "$p")"; while [ ! -d "$d" ] && [ "$d" != "/" ]; do d="$(dirname "$d")"; done; real="$(cd "$d" && pwd -P)"; case "$real" in "$APP_DIR"|"$APP_DIR"/*) return 0;; esac; echo "경로가 모듈 디렉터리($APP_DIR) 밖입니다: $p" >&2; exit 3; }
[ -n "$NODE_BIN" ] || NODE_BIN="$(command -v node || true)"
case "$SUB" in
extract)
[ -f "$TGZ" ] || { echo "artifact 가 없습니다: $TGZ" >&2; exit 2; }; inside "$DEST"; case "$DEST" in "$APP_DIR"/releases/*) ;; *) echo "--dest 는 releases 아래여야 합니다" >&2; exit 3;; esac
rm -rf "$DEST"; mkdir -p "$DEST"; tar -xzf "$TGZ" -C "$DEST"; echo "extracted $DEST";;
write)
# --mode 600(0.4.0): 비밀값이 든 파일(hal-managed.json)은 처음부터 그 계정만 읽게 만든다(umask 077 → chmod 600 → 원자 교체)
inside "$FILE"; mkdir -p "$(dirname "$FILE")"; tmp="$FILE.tmp-$$"
if [ "$MODE" = "600" ]; then ( umask 077; cat > "$tmp" ); chmod 600 "$tmp"; elif [ -n "$MODE" ]; then echo "--mode 는 600 만 허용합니다" >&2; exit 2; else cat > "$tmp"; fi
mv -f "$tmp" "$FILE"; echo "written $FILE";;
switch)
inside "$LINK"; inside "$TARGET"; [ -d "$TARGET" ] || { echo "대상 디렉터리가 없습니다: $TARGET" >&2; exit 2; }
ln -sfn "$TARGET" "$LINK.tmp-$$"; mv -Tf "$LINK.tmp-$$" "$LINK"; echo "switched $LINK -> $TARGET";;
rename)
inside "$DIR"; inside "$TARGET"; case "$DIR" in "$APP_DIR"/releases/*) ;; *) echo "--dir 는 releases 아래여야 합니다" >&2; exit 3;; esac; case "$TARGET" in "$APP_DIR"/releases/*) ;; *) echo "--target 은 releases 아래여야 합니다" >&2; exit 3;; esac
mv -T "$DIR" "$TARGET"; echo "renamed $DIR -> $TARGET";;
remove)
inside "$DIR"; case "$DIR" in "$APP_DIR"/releases/*) ;; *) echo "--dir 는 releases 아래여야 합니다" >&2; exit 3;; esac; rm -rf "$DIR"; echo "removed $DIR";;
backup-db)
inside "$OUT"; [ -f "$DB" ] || { echo "skipped no_db"; exit 0; }; mkdir -p "$(dirname "$OUT")"
# node:sqlite 의 VACUUM INTO 로 WAL 포함 일관된 사본을 만든다(원본 deploy.sh 의 python backup 과 같은 목적).
"$NODE_BIN" --no-warnings -e "const {DatabaseSync}=require('node:sqlite');const d=new DatabaseSync(process.argv[1],{readOnly:true});try{d.exec(\"VACUUM INTO '\"+process.argv[2].replace(/'/g,\"''\")+\"'\")}finally{d.close()}" "$DB" "$OUT"; echo "backup $OUT";;
migrate)
inside "$CWD"; [ -f "$CWD/$SCRIPT" ] || { echo "skipped no_migrate"; exit 0; }
# systemd 유닛의 EnvironmentFile(DB_FILE·DATA_DIR 등)을 같은 계정으로 읽어 환경에 싣는다 — 없으면 모듈 기본 경로(릴리스 디렉터리 안)를 쓰게 되어 운영 DB 가 아닌 빈 DB 를 만드는 문제를 막는다.
# systemd EnvironmentFile 문법(KEY=값, 값에 공백·따옴표 가능)은 bash 소스가 아니다 — 줄 단위로 읽어 따옴표를 벗기고 export 한다(실제 환경: MAIL_FROM_NAME=Ari Hak dang 에서 소스 실패).
if [ -n "$ENV_FILE" ]; then [ -r "$ENV_FILE" ] || { echo "EnvironmentFile 을 읽을 수 없습니다: $ENV_FILE" >&2; exit 2; }
while IFS= read -r line || [ -n "$line" ]; do line="${line%$'\r'}"; case "$line" in ''|'#'*) continue;; esac; key="${line%%=*}"; val="${line#*=}"; key="${key#export }"; key="${key// /}"
case "$key" in *[!A-Za-z0-9_]*|'') continue;; esac; case "$val" in \"*\") val="${val#\"}"; val="${val%\"}";; \'*\') val="${val#\'}"; val="${val%\'}";; esac; export "$key=$val"; done < "$ENV_FILE"; fi
cd "$CWD"; exec "$NODE_BIN" --no-warnings "$SCRIPT";;
*) echo "알 수 없는 하위 명령: $SUB" >&2; exit 2;;
esac