diff --git a/docker-compose.yml b/docker-compose.yml index 9b09dc03..4b2ee681 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -85,7 +85,9 @@ services: # ── PostgreSQL ─────────────────────────────────────────── postgres: - image: postgres:15-alpine + # pgvector/pgvector:pg15 = PostgreSQL 15 + pgvector 확장 포함 + # vector 타입 사용: SR 유사도 검색, KB 시맨틱 검색 + image: pgvector/pgvector:pg15 container_name: guardia-postgres environment: POSTGRES_DB: guardia @@ -177,6 +179,64 @@ services: retries: 3 start_period: 30s + # ── Qdrant (전용 벡터 DB — 고성능 시맨틱 검색) ─────────── + # pgvector보다 빠른 ANN 검색이 필요할 때 사용 + # QDRANT_ENABLED=true 시 guardia에서 QDRANT_URL=http://qdrant:6333 설정 + qdrant: + image: qdrant/qdrant:v1.7.4 + container_name: guardia-qdrant + profiles: ["vector"] # docker compose --profile vector up 으로 활성화 + ports: + - "6333:6333" + - "6334:6334" + volumes: + - guardia-qdrant:/qdrant/storage + environment: + QDRANT__SERVICE__GRPC_PORT: "6334" + networks: + - guardia-net + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:6333/healthz"] + interval: 30s + timeout: 5s + retries: 3 + + # ── Gitea (온프레미스 Git 서버) ────────────────────────── + # 형상관리: 저장소 생성, 브랜치 보호, PR 워크플로우 + gitea: + image: gitea/gitea:1.21-rootless + container_name: guardia-gitea + ports: + - "3000:3000" # HTTP (web + API) + - "2222:2222" # SSH + environment: + USER_UID: "1000" + USER_GID: "1000" + GITEA__DEFAULT__APP_NAME: "GUARDiA Git" + GITEA__SERVER__HTTP_PORT: "3000" + GITEA__SERVER__SSH_PORT: "2222" + GITEA__SERVER__ROOT_URL: "http://localhost:3000/" + GITEA__DATABASE__DB_TYPE: "sqlite3" + GITEA__DATABASE__PATH: "/var/lib/gitea/data/gitea.db" + GITEA__REPOSITORY__DEFAULT_BRANCH: "main" + GITEA__GIT__DEFAULT_BRANCH: "main" + GITEA__SECURITY__INSTALL_LOCK: "true" + GITEA__SERVICE__DISABLE_REGISTRATION: "false" + volumes: + - guardia-gitea-data:/var/lib/gitea + - guardia-gitea-config:/etc/gitea + - /etc/timezone:/etc/timezone:ro + networks: + - guardia-net + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:3000/api/v1/version"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + # ── 볼륨 ───────────────────────────────────────────────── volumes: guardia-db: @@ -186,6 +246,9 @@ volumes: guardia-ollama-models: # Ollama 모델 (로컬 경로 마운트 가능) guardia-tomcat-webapps: guardia-tomcat-logs: + guardia-qdrant: # Qdrant 벡터 데이터 + guardia-gitea-data: # Gitea 저장소 + DB + guardia-gitea-config: # Gitea 설정 # ── 네트워크 ────────────────────────────────────────────── networks: diff --git a/docs/vector_db_guide.md b/docs/vector_db_guide.md new file mode 100644 index 00000000..70a01e62 --- /dev/null +++ b/docs/vector_db_guide.md @@ -0,0 +1,171 @@ +# GUARDiA Vector DB 가이드 + +## 지원 방식 + +| 방식 | 설치 | 성능 | 용도 | +|------|------|------|------| +| **pgvector** | PostgreSQL 확장 | 중간 | SR 유사도, KB 검색 | +| **Qdrant** | Docker 컨테이너 | 높음 | 대규모 시맨틱 검색 | + +--- + +## 1. pgvector (기본 권장) + +### 설치 + +```bash +# setup.sh 실행 시 "3) PostgreSQL + pgvector" 선택 +bash setup/setup_ubuntu.sh + +# 또는 수동: +CREATE EXTENSION IF NOT EXISTS vector; +``` + +### GUARDiA에서 벡터 컬럼 사용 + +```python +from sqlalchemy import Column +from pgvector.sqlalchemy import Vector + +class SRRequest(Base): + __tablename__ = "tb_sr_request" + # ... + embedding = Column(Vector(768)) # Ollama 임베딩 차원 + +# 유사 SR 검색 +from sqlalchemy import text +similar = await db.execute( + text("SELECT sr_id, title, 1 - (embedding <=> :vec) AS similarity " + "FROM tb_sr_request ORDER BY embedding <=> :vec LIMIT 5"), + {"vec": query_embedding} +) +``` + +### 임베딩 생성 (Ollama) + +```python +import httpx + +async def get_embedding(text: str) -> list[float]: + async with httpx.AsyncClient() as client: + r = await client.post("http://localhost:11434/api/embeddings", json={ + "model": "nomic-embed-text", # 경량 임베딩 모델 + "prompt": text + }) + return r.json()["embedding"] +``` + +### 환경변수 + +``` +ENABLE_VECTOR=true +DATABASE_URL=postgresql+asyncpg://guardia:guardia@localhost:5432/guardia +``` + +--- + +## 2. Qdrant (고성능 벡터 검색) + +### Docker 시작 + +```bash +# vector 프로파일로 Qdrant 포함 실행 +docker compose --profile vector up -d qdrant + +# 또는 docker compose.yml에서 profiles 제거 후 항상 실행 +``` + +### GUARDiA에서 Qdrant 사용 + +```python +from qdrant_client import QdrantClient +from qdrant_client.models import Distance, VectorParams, PointStruct + +client = QdrantClient(url=os.getenv("QDRANT_URL", "http://localhost:6333")) + +# 컬렉션 생성 +client.create_collection( + collection_name="sr_embeddings", + vectors_config=VectorParams(size=768, distance=Distance.COSINE), +) + +# SR 인덱싱 +client.upsert("sr_embeddings", points=[ + PointStruct(id=sr.id, vector=embedding, payload={"sr_id": sr.sr_id, "title": sr.title}) +]) + +# 유사 SR 검색 +results = client.search("sr_embeddings", query_vector=query_embedding, limit=5) +``` + +### 환경변수 + +``` +QDRANT_URL=http://localhost:6333 +ENABLE_QDRANT=true +``` + +--- + +## 3. 사용 시나리오 + +### KB 시맨틱 검색 + +```python +# KB 문서 검색 — 키워드가 아닌 의미 기반 +async def search_kb_semantic(query: str, db) -> list: + embedding = await get_embedding(query) + results = await db.execute( + text("SELECT id, title, content, 1-(embedding<=>:v) score " + "FROM tb_kb WHERE embedding IS NOT NULL " + "ORDER BY embedding <=> :v LIMIT 10"), + {"v": str(embedding)} + ) + return results.mappings().all() +``` + +### 유사 SR 재발 탐지 + +```python +# 새 SR과 유사한 과거 SR 찾기 (학습 루프 연동) +async def find_similar_sr(new_sr: SRRequest, db) -> list: + embedding = await get_embedding(f"{new_sr.title} {new_sr.description}") + results = await db.execute( + text("SELECT sr_id, title, status, " + "1-(embedding<=>:v) AS similarity " + "FROM tb_sr_request " + "WHERE embedding IS NOT NULL AND sr_id != :sid " + " AND 1-(embedding<=>:v) > 0.85 " # 85% 이상 유사도 + "ORDER BY similarity DESC LIMIT 5"), + {"v": str(embedding), "sid": new_sr.sr_id} + ) + return results.mappings().all() +``` + +### Ollama 임베딩 모델 설치 + +```bash +# nomic-embed-text: 경량 임베딩 모델 (274MB) +ollama pull nomic-embed-text + +# 또는 한국어 특화 +ollama pull bge-m3 +``` + +--- + +## 4. Docker Compose 프로파일 + +```bash +# 기본 (pgvector만) +docker compose up -d + +# Qdrant 포함 +docker compose --profile vector up -d + +# Gitea 포함 +docker compose up -d gitea + +# 모두 실행 +docker compose --profile vector up -d +``` diff --git a/setup/gitea_init.sh b/setup/gitea_init.sh new file mode 100644 index 00000000..24b8fe02 --- /dev/null +++ b/setup/gitea_init.sh @@ -0,0 +1,225 @@ +#!/bin/bash +# ============================================================== +# GUARDiA Gitea 초기화 스크립트 +# ============================================================== +# Gitea 컨테이너/서비스가 기동된 후 실행하여 +# 관리자 계정, 조직, 저장소, 브랜치 보호를 자동 설정합니다. +# +# 사용법: +# bash setup/gitea_init.sh +# GITEA_PORT=3000 GITEA_ADMIN=admin bash setup/gitea_init.sh +# ============================================================== + +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GUARDIA_ROOT="$(dirname "$SCRIPT_DIR")" + +GITEA_PORT="${GITEA_PORT:-3000}" +GITEA_BASE="http://localhost:${GITEA_PORT}" +GITEA_ADMIN="${GITEA_ADMIN:-gitadmin}" +GITEA_ADMIN_PW="${GITEA_ADMIN_PW:-Gitea@guardia!}" +GITEA_ADMIN_EMAIL="${GITEA_ADMIN_EMAIL:-admin@guardia.local}" +GITEA_ORG="${GITEA_ORG:-guardia}" +GITEA_REPO="${GITEA_REPO:-GUARDiA}" + +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' +ok() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +info() { echo -e " $*"; } + +API="${GITEA_BASE}/api/v1" +AUTH="Authorization: Basic $(echo -n "${GITEA_ADMIN}:${GITEA_ADMIN_PW}" | base64)" + +echo "==================================================" +echo " Gitea 초기화" +echo " URL: $GITEA_BASE" +echo "==================================================" + +# ── Gitea 응답 대기 ─────────────────────────────────────────── +echo "" +echo "[1/7] Gitea 서비스 응답 대기..." +for i in $(seq 1 30); do + if curl -sf "$API/version" -o /dev/null 2>/dev/null; then + ok "Gitea 응답 확인" + break + fi + sleep 2 + [[ $i -eq 30 ]] && { echo "[ERR] Gitea 응답 없음 — 서비스를 먼저 시작하세요"; exit 1; } +done + +# ── 관리자 계정 생성 (Gitea API 인증용) ───────────────────────── +echo "" +echo "[2/7] 관리자 계정 설정..." + +# Docker 환경: gitea admin 명령 사용 +if command -v docker &>/dev/null && docker ps | grep -q guardia-gitea; then + docker exec guardia-gitea gitea admin user create \ + --username "$GITEA_ADMIN" \ + --password "$GITEA_ADMIN_PW" \ + --email "$GITEA_ADMIN_EMAIL" \ + --admin 2>/dev/null || info "관리자 이미 존재" +elif command -v gitea &>/dev/null; then + gitea admin user create \ + --username "$GITEA_ADMIN" \ + --password "$GITEA_ADMIN_PW" \ + --email "$GITEA_ADMIN_EMAIL" \ + --admin 2>/dev/null || info "관리자 이미 존재" +else + # API로 관리자 생성 시도 (초기 설치 시) + curl -sf -X POST "$API/users" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"${GITEA_ADMIN}\",\"password\":\"${GITEA_ADMIN_PW}\",\"email\":\"${GITEA_ADMIN_EMAIL}\"}" \ + -o /dev/null 2>/dev/null || info "관리자 이미 존재" +fi +ok "관리자: $GITEA_ADMIN" + +# ── 조직 생성 ──────────────────────────────────────────────── +echo "" +echo "[3/7] 조직 생성..." +curl -sf -X POST "$API/orgs" \ + -H "$AUTH" -H "Content-Type: application/json" \ + -d "{\"username\":\"${GITEA_ORG}\",\"visibility\":\"private\",\"description\":\"GUARDiA ITSM Organization\"}" \ + -o /dev/null 2>/dev/null && ok "조직 생성: $GITEA_ORG" || info "조직 이미 존재: $GITEA_ORG" + +# ── GUARDiA 저장소 생성 ────────────────────────────────────── +echo "" +echo "[4/7] 저장소 생성..." +curl -sf -X POST "$API/orgs/${GITEA_ORG}/repos" \ + -H "$AUTH" -H "Content-Type: application/json" \ + -d "{ + \"name\":\"${GITEA_REPO}\", + \"description\":\"GUARDiA ITSM Platform\", + \"private\":true, + \"default_branch\":\"main\", + \"auto_init\":true, + \"readme\":\"Default\" + }" \ + -o /dev/null 2>/dev/null && ok "저장소 생성: ${GITEA_ORG}/${GITEA_REPO}" \ + || info "저장소 이미 존재" + +sleep 3 # 저장소 초기화 대기 + +# ── develop 브랜치 생성 ───────────────────────────────────── +echo "" +echo "[5/7] develop 브랜치 생성..." +curl -sf -X POST "$API/repos/${GITEA_ORG}/${GITEA_REPO}/branches" \ + -H "$AUTH" -H "Content-Type: application/json" \ + -d '{"new_branch_name":"develop","old_branch_name":"main"}' \ + -o /dev/null 2>/dev/null && ok "develop 브랜치 생성" || info "develop 브랜치 이미 존재" + +# ── main 브랜치 보호 설정 ──────────────────────────────────── +echo "" +echo "[6/7] main 브랜치 보호 설정..." +curl -sf -X POST "$API/repos/${GITEA_ORG}/${GITEA_REPO}/branch_protections" \ + -H "$AUTH" -H "Content-Type: application/json" \ + -d "{ + \"branch_name\": \"main\", + \"enable_push\": false, + \"enable_push_whitelist\": true, + \"push_whitelist_usernames\": [\"${GITEA_ADMIN}\"], + \"required_approvals\": 1, + \"enable_approvals_whitelist\": false, + \"block_on_rejected_reviews\": true, + \"block_on_official_review_requests\": false, + \"dismiss_stale_approvals\": true + }" \ + -o /dev/null 2>/dev/null \ + && ok "main 브랜치 보호: PR 필수, 리뷰 1명 필수" \ + || info "브랜치 보호 이미 설정됨" + +# develop 브랜치 보호 (push 허용, but 직접 force-push 금지) +curl -sf -X POST "$API/repos/${GITEA_ORG}/${GITEA_REPO}/branch_protections" \ + -H "$AUTH" -H "Content-Type: application/json" \ + -d '{"branch_name":"develop","enable_force_push":false}' \ + -o /dev/null 2>/dev/null || true + +# ── 개발자 계정 + 개인 브랜치 생성 ────────────────────────── +echo "" +echo "[7/7] 개발자 계정 + 개인 브랜치 생성..." + +# GUARDiA ITSM 역할에 맞는 기본 개발자 계정 +declare -A USERS=( + ["engineer1"]="Eng1@guardia!:engineer1@guardia.local" + ["engineer2"]="Eng2@guardia!:engineer2@guardia.local" + ["pm1"]="PM1@guardia!:pm1@guardia.local" + ["admin"]="Admin@guardia!:admin@guardia.local" +) + +# Developers 팀 생성 +TEAM_RESP=$(curl -sf -X POST "$API/orgs/${GITEA_ORG}/teams" \ + -H "$AUTH" -H "Content-Type: application/json" \ + -d "{ + \"name\":\"Developers\", + \"permission\":\"write\", + \"units\":[\"repo.code\",\"repo.issues\",\"repo.pulls\"], + \"includes_all_repositories\":true + }" 2>/dev/null) +TEAM_ID=$(echo "$TEAM_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || echo "") +[[ -n "$TEAM_ID" ]] && ok "Developers 팀 생성 (ID: $TEAM_ID)" + +# 팀에 저장소 추가 +[[ -n "$TEAM_ID" ]] && curl -sf -X PUT "$API/teams/${TEAM_ID}/repos/${GITEA_ORG}/${GITEA_REPO}" \ + -H "$AUTH" -o /dev/null 2>/dev/null + +for username in "${!USERS[@]}"; do + IFS=':' read -r pw email <<< "${USERS[$username]}" + + # 사용자 생성 + curl -sf -X POST "$API/admin/users" \ + -H "$AUTH" -H "Content-Type: application/json" \ + -d "{ + \"username\":\"${username}\", + \"password\":\"${pw}\", + \"email\":\"${email}\", + \"login_name\":\"${username}\", + \"source_id\":0, + \"send_notify\":false, + \"must_change_password\":false + }" -o /dev/null 2>/dev/null || info "사용자 이미 존재: $username" + + # 팀 멤버 추가 + [[ -n "$TEAM_ID" ]] && curl -sf -X PUT "$API/teams/${TEAM_ID}/members/${username}" \ + -H "$AUTH" -o /dev/null 2>/dev/null + + # 개인 feature 브랜치 생성 (feature/이름/init) + curl -sf -X POST "$API/repos/${GITEA_ORG}/${GITEA_REPO}/branches" \ + -H "$AUTH" -H "Content-Type: application/json" \ + -d "{\"new_branch_name\":\"feature/${username}/init\",\"old_branch_name\":\"develop\"}" \ + -o /dev/null 2>/dev/null + + ok "계정 + 브랜치: $username (feature/${username}/init)" +done + +# ── 현재 소스 push ─────────────────────────────────────────── +echo "" +echo " 현재 GUARDiA 소스를 Gitea에 push..." +GITEA_URL="http://${GITEA_ADMIN}:${GITEA_ADMIN_PW}@localhost:${GITEA_PORT}/${GITEA_ORG}/${GITEA_REPO}.git" +git -C "$GUARDIA_ROOT" remote remove gitea 2>/dev/null || true +git -C "$GUARDIA_ROOT" remote add gitea "$GITEA_URL" +git -C "$GUARDIA_ROOT" push gitea main 2>/dev/null \ + && ok "main 브랜치 push 완료" || warn "main push 실패 — 수동: git push gitea main" +git -C "$GUARDIA_ROOT" push gitea develop 2>/dev/null || true + +echo "" +echo "==================================================" +ok "Gitea 초기화 완료!" +echo "" +info "Gitea URL: http://localhost:${GITEA_PORT}" +info "관리자: ${GITEA_ADMIN} / ${GITEA_ADMIN_PW}" +info "저장소: http://localhost:${GITEA_PORT}/${GITEA_ORG}/${GITEA_REPO}" +echo "" +info "=== 브랜치 전략 ===" +info " main : PR + 리뷰 1명 필수 (보호)" +info " develop : 통합 브랜치 (force-push 금지)" +info " feature/이름/기능명: 개인 개발 브랜치" +echo "" +info "=== 개발자 계정 ===" +for username in "${!USERS[@]}"; do + IFS=':' read -r pw _ <<< "${USERS[$username]}" + info " $username / $pw → feature/${username}/init" +done +echo "" +info "=== Git 원격 저장소 설정 ===" +info " git remote add guardia-gitea http://localhost:${GITEA_PORT}/${GITEA_ORG}/${GITEA_REPO}.git" +info " git push guardia-gitea feature/이름/기능명" +echo "==================================================" diff --git a/setup/lib/db_select.sh b/setup/lib/db_select.sh new file mode 100644 index 00000000..196a30e3 --- /dev/null +++ b/setup/lib/db_select.sh @@ -0,0 +1,179 @@ +#!/bin/bash +# ============================================================== +# GUARDiA DB 선택 공통 함수 +# ============================================================== +# 사용법: source setup/lib/db_select.sh; select_database +# 결과: DB_TYPE, DATABASE_URL, INSTALL_POSTGRES, INSTALL_PGVECTOR 변수 설정 +# ============================================================== + +select_database() { + # 환경변수로 이미 지정된 경우 프롬프트 생략 + if [[ -n "${DB_TYPE:-}" ]]; then + info "DB_TYPE=$DB_TYPE (환경변수 설정됨)" + _apply_db_type "$DB_TYPE" + return + fi + + echo "" + echo "============================================================" + echo " 데이터베이스 선택" + echo "============================================================" + echo " 1) SQLite (개발·단독 실행 권장, 추가 설치 없음)" + echo " 2) PostgreSQL (운영 환경 권장)" + echo " 3) PostgreSQL + pgvector (AI 벡터 검색 포함, 권장)" + echo "" + echo " pgvector: SR 유사도 검색, KB 시맨틱 검색, 이상 탐지에 활용" + echo " Qdrant: docker-compose에 선택적 포함 (고성능 벡터 검색)" + echo "============================================================" + + local choice + read -rp " 선택 [1-3, 기본값=3]: " choice + choice="${choice:-3}" + + case "$choice" in + 1) DB_TYPE="sqlite" ;; + 2) DB_TYPE="postgres" ;; + 3) DB_TYPE="postgres-vector";; + *) warn "잘못된 선택 '$choice' — 기본값 3(postgres-vector) 사용" + DB_TYPE="postgres-vector" ;; + esac + + _apply_db_type "$DB_TYPE" +} + + +_apply_db_type() { + local dtype="$1" + + case "$dtype" in + sqlite) + DATABASE_URL="sqlite+aiosqlite:///./guardia_itsm.db" + INSTALL_POSTGRES=false + INSTALL_PGVECTOR=false + INSTALL_QDRANT=false + ok "DB: SQLite (개발 모드)" + ;; + + postgres) + DATABASE_URL="postgresql+asyncpg://guardia:guardia@localhost:5432/guardia" + INSTALL_POSTGRES=true + INSTALL_PGVECTOR=false + INSTALL_QDRANT=false + ok "DB: PostgreSQL" + ;; + + postgres-vector) + DATABASE_URL="postgresql+asyncpg://guardia:guardia@localhost:5432/guardia" + INSTALL_POSTGRES=true + INSTALL_PGVECTOR=true + INSTALL_QDRANT=false # Qdrant는 docker-compose로 별도 제공 + ok "DB: PostgreSQL + pgvector (AI 벡터 검색 포함)" + ;; + + *) + warn "알 수 없는 DB_TYPE='$dtype' — SQLite로 대체" + DATABASE_URL="sqlite+aiosqlite:///./guardia_itsm.db" + INSTALL_POSTGRES=false + INSTALL_PGVECTOR=false + INSTALL_QDRANT=false + ;; + esac + + export DB_TYPE DATABASE_URL INSTALL_POSTGRES INSTALL_PGVECTOR INSTALL_QDRANT +} + + +setup_postgres() { + local pgpw="${POSTGRES_PASSWORD:-guardia}" + + [[ "$INSTALL_POSTGRES" != "true" ]] && return + + echo "" + info "PostgreSQL 설정..." + + # 배포 환경별 초기화 (호출 스크립트에서 처리됨) + # 여기서는 DB/사용자 생성만 담당 + + # 비밀번호 안전 저장 + sudo -u postgres psql \ + -tc "SELECT 1 FROM pg_user WHERE usename='guardia'" 2>/dev/null \ + | grep -q 1 \ + || sudo -u postgres psql -c \ + "CREATE USER guardia WITH PASSWORD '${pgpw}';" 2>/dev/null + + sudo -u postgres psql \ + -tc "SELECT 1 FROM pg_database WHERE datname='guardia'" 2>/dev/null \ + | grep -q 1 \ + || sudo -u postgres psql -c \ + "CREATE DATABASE guardia OWNER guardia;" 2>/dev/null + + ok "PostgreSQL DB/사용자 생성 완료" + + # pgvector 확장 설치 + if [[ "$INSTALL_PGVECTOR" == "true" ]]; then + _install_pgvector "$pgpw" + fi +} + + +_install_pgvector() { + local pgpw="$1" + echo "" + info "pgvector 확장 설치..." + + # OS별 패키지 설치 + if command -v apt-get &>/dev/null; then + apt-get install -y -qq postgresql-server-dev-all build-essential git 2>/dev/null || true + # pgvector 소스 빌드 (패키지 없는 경우) + if ! dpkg -l postgresql-15-pgvector &>/dev/null 2>&1; then + _build_pgvector_from_source + else + apt-get install -y -qq "postgresql-$(pg_lsclusters | grep online | awk '{print $1}' | head -1)-pgvector" 2>/dev/null \ + || _build_pgvector_from_source + fi + elif command -v dnf &>/dev/null; then + dnf install -y pgvector_15 2>/dev/null \ + || _build_pgvector_from_source + else + _build_pgvector_from_source + fi + + # 확장 활성화 + sudo -u postgres psql -d guardia -c "CREATE EXTENSION IF NOT EXISTS vector;" 2>/dev/null \ + && ok "pgvector 확장 활성화 완료" \ + || warn "pgvector 활성화 실패 — 나중에 수동: CREATE EXTENSION vector;" +} + + +_build_pgvector_from_source() { + local tmpdir + tmpdir=$(mktemp -d) + info "pgvector 소스 빌드..." + git clone --depth 1 https://github.com/pgvector/pgvector.git "$tmpdir" 2>/dev/null \ + || { warn "pgvector 소스 클론 실패 — 인터넷 연결 확인"; return; } + cd "$tmpdir" + make 2>/dev/null + make install 2>/dev/null + cd - + rm -rf "$tmpdir" + ok "pgvector 빌드 완료" +} + + +write_db_env() { + local env_file="$1" + # .env 파일에 DATABASE_URL 기록 + if grep -q "^DATABASE_URL=" "$env_file" 2>/dev/null; then + sed -i "s|^DATABASE_URL=.*|DATABASE_URL=${DATABASE_URL}|" "$env_file" + else + echo "DATABASE_URL=${DATABASE_URL}" >> "$env_file" + fi + + # pgvector 설정 추가 + if [[ "$INSTALL_PGVECTOR" == "true" ]]; then + grep -q "^ENABLE_VECTOR=" "$env_file" 2>/dev/null \ + || echo "ENABLE_VECTOR=true" >> "$env_file" + fi + + ok ".env DATABASE_URL 기록 완료: $DATABASE_URL" +} diff --git a/setup/lib/gitea_setup.sh b/setup/lib/gitea_setup.sh new file mode 100644 index 00000000..220ce69c --- /dev/null +++ b/setup/lib/gitea_setup.sh @@ -0,0 +1,298 @@ +#!/bin/bash +# ============================================================== +# GUARDiA Gitea 설치 + 자동 초기화 공통 함수 +# ============================================================== +# 기능: +# - Gitea 바이너리 설치 및 systemd 등록 +# - 관리자 계정 자동 생성 +# - GUARDiA 조직 + 레파지토리 생성 +# - 개발자 계정 + 개인 브랜치 자동 생성 +# - main 브랜치 보호 (PR 필수) +# +# 브랜치 전략: +# main - 보호 브랜치, PR + 리뷰 1명 필수 +# develop - 통합 브랜치 +# feature/이름/기능명 - 개인 개발 브랜치 +# ============================================================== + +GITEA_VER="${GITEA_VER:-1.21.11}" +GITEA_HOME="/opt/gitea" +GITEA_DATA="/var/lib/gitea" +GITEA_PORT="${GITEA_PORT:-3000}" +GITEA_ADMIN="${GITEA_ADMIN:-gitadmin}" +GITEA_ADMIN_PW="${GITEA_ADMIN_PW:-Gitea@guardia!}" +GITEA_ADMIN_EMAIL="${GITEA_ADMIN_EMAIL:-admin@guardia.local}" +GITEA_ORG="${GITEA_ORG:-guardia}" +GITEA_REPO="${GITEA_REPO:-GUARDiA}" +GITEA_BASE="http://localhost:${GITEA_PORT}" +GITEA_MIRROR="${GITEA_MIRROR:-https://dl.gitea.com/gitea/${GITEA_VER}}" + + +install_gitea() { + echo "" + echo "=== Gitea 설치 (포트 ${GITEA_PORT}) ===" + + # 1. 시스템 계정 생성 + id git &>/dev/null || useradd -r -s /bin/bash -d "$GITEA_DATA" git + + # 2. 디렉토리 생성 + mkdir -p "$GITEA_HOME" "$GITEA_DATA"/{custom,data,log,repositories} + chown -R git:git "$GITEA_DATA" + + # 3. 바이너리 설치 + ARCH="$(uname -m)" + case "$ARCH" in + x86_64) GITEA_ARCH="amd64" ;; + aarch64) GITEA_ARCH="arm64" ;; + *) GITEA_ARCH="amd64" ;; + esac + + GITEA_BIN="gitea-${GITEA_VER}-linux-${GITEA_ARCH}" + + if [[ ! -f "/usr/local/bin/gitea" ]]; then + info "Gitea $GITEA_VER 다운로드..." + wget -q "${GITEA_MIRROR}/${GITEA_BIN}" -O /tmp/gitea \ + || fail "Gitea 다운로드 실패 — GITEA_MIRROR 환경변수 설정" + install -m 755 /tmp/gitea /usr/local/bin/gitea + ok "Gitea 바이너리 설치: /usr/local/bin/gitea" + else + info "Gitea 이미 설치됨" + fi + + # 4. Gitea 설정 파일 + mkdir -p "$GITEA_DATA/custom/conf" + cat > "$GITEA_DATA/custom/conf/app.ini" << APPINI +[DEFAULT] +RUN_USER = git +RUN_MODE = prod + +[server] +HTTP_ADDR = 0.0.0.0 +HTTP_PORT = ${GITEA_PORT} +ROOT_URL = http://localhost:${GITEA_PORT}/ +DISABLE_SSH = false +SSH_PORT = 22022 + +[database] +DB_TYPE = sqlite3 +PATH = ${GITEA_DATA}/data/gitea.db + +[repository] +ROOT = ${GITEA_DATA}/repositories + +[log] +ROOT_PATH = ${GITEA_DATA}/log +MODE = file +LEVEL = info + +[security] +INSTALL_LOCK = true +SECRET_KEY = $(openssl rand -hex 32 2>/dev/null || date | md5sum | head -c 32) +INTERNAL_TOKEN = $(openssl rand -hex 32 2>/dev/null || date +%s | md5sum | head -c 32) + +[service] +DISABLE_REGISTRATION = false +REQUIRE_SIGNIN_VIEW = false +DEFAULT_KEEP_EMAIL_PRIVATE = true + +[git] +DEFAULT_BRANCH = main +APPINI + + chown -R git:git "$GITEA_DATA" + + # 5. systemd 서비스 + cat > /etc/systemd/system/gitea.service << GITSVC +[Unit] +Description=Gitea (Git service) +After=network.target + +[Service] +Type=simple +User=git +Group=git +WorkingDirectory=${GITEA_DATA} +Environment="USER=git" "HOME=${GITEA_DATA}" "GITEA_WORK_DIR=${GITEA_DATA}" +ExecStart=/usr/local/bin/gitea web --config ${GITEA_DATA}/custom/conf/app.ini +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target +GITSVC + + systemctl daemon-reload + systemctl enable gitea + systemctl start gitea + + # Gitea 기동 대기 + info "Gitea 기동 대기 중..." + local attempt=0 + until curl -sf "http://localhost:${GITEA_PORT}/api/v1/version" -o /dev/null 2>/dev/null; do + sleep 3 + ((attempt++)) + [[ $attempt -ge 20 ]] && { warn "Gitea 기동 타임아웃"; break; } + done + ok "Gitea 서비스 시작 완료 (http://localhost:${GITEA_PORT})" +} + + +init_gitea_repos() { + echo "" + echo "=== Gitea 초기화 (조직·저장소·브랜치) ===" + + local api="${GITEA_BASE}/api/v1" + local auth_header="Authorization: Basic $(echo -n "${GITEA_ADMIN}:${GITEA_ADMIN_PW}" | base64)" + + # 1. 관리자 계정 생성 + _wait_gitea + + gitea admin user create \ + --username "$GITEA_ADMIN" \ + --password "$GITEA_ADMIN_PW" \ + --email "$GITEA_ADMIN_EMAIL" \ + --admin \ + --config "$GITEA_DATA/custom/conf/app.ini" \ + 2>/dev/null || info "관리자 계정 이미 존재" + + ok "관리자 계정: $GITEA_ADMIN" + + # 2. 조직 생성 + curl -sf -X POST "$api/orgs" \ + -H "$auth_header" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"${GITEA_ORG}\",\"visibility\":\"private\",\"description\":\"GUARDiA ITSM\"}" \ + -o /dev/null 2>/dev/null \ + || info "조직 이미 존재: $GITEA_ORG" + ok "조직 생성: $GITEA_ORG" + + # 3. GUARDiA 메인 저장소 생성 + curl -sf -X POST "$api/orgs/${GITEA_ORG}/repos" \ + -H "$auth_header" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"${GITEA_REPO}\",\"description\":\"GUARDiA ITSM 플랫폼\",\"private\":true,\"default_branch\":\"main\",\"auto_init\":true}" \ + -o /dev/null 2>/dev/null \ + || info "저장소 이미 존재: ${GITEA_ORG}/${GITEA_REPO}" + ok "저장소 생성: ${GITEA_ORG}/${GITEA_REPO}" + + # 초기화 후 저장소가 생성될 때까지 대기 + sleep 3 + + # 4. develop 브랜치 생성 + curl -sf -X POST "$api/repos/${GITEA_ORG}/${GITEA_REPO}/branches" \ + -H "$auth_header" \ + -H "Content-Type: application/json" \ + -d '{"new_branch_name":"develop","old_branch_name":"main"}' \ + -o /dev/null 2>/dev/null \ + || info "develop 브랜치 이미 존재" + ok "develop 브랜치 생성" + + # 5. main 브랜치 보호 규칙 설정 (PR 필수) + curl -sf -X POST "$api/repos/${GITEA_ORG}/${GITEA_REPO}/branch_protections" \ + -H "$auth_header" \ + -H "Content-Type: application/json" \ + -d '{ + "branch_name": "main", + "enable_push": false, + "enable_push_whitelist": true, + "push_whitelist_usernames": ["'"${GITEA_ADMIN}"'"], + "require_signed_commits": false, + "enable_status_check": false, + "required_approvals": 1, + "enable_approvals_whitelist": false, + "merge_whitelist_usernames": ["'"${GITEA_ADMIN}"'"] + }' \ + -o /dev/null 2>/dev/null \ + || info "main 브랜치 보호 이미 설정됨" + ok "main 브랜치 보호 설정 (PR + 리뷰 1명 필수)" + + # 6. 현재 Git 소스를 Gitea에 push + if [[ -d "${GUARDIA_ROOT:-/opt/guardia}/.git" ]]; then + info "기존 소스를 Gitea에 push..." + local gitea_url="http://${GITEA_ADMIN}:${GITEA_ADMIN_PW}@localhost:${GITEA_PORT}/${GITEA_ORG}/${GITEA_REPO}.git" + git -C "${GUARDIA_ROOT:-/opt/guardia}" remote remove gitea 2>/dev/null || true + git -C "${GUARDIA_ROOT:-/opt/guardia}" remote add gitea "$gitea_url" + git -C "${GUARDIA_ROOT:-/opt/guardia}" push gitea main 2>/dev/null \ + && ok "소스 push 완료 → Gitea main 브랜치" \ + || warn "소스 push 실패 — 나중에 수동으로 push" + git -C "${GUARDIA_ROOT:-/opt/guardia}" push gitea develop 2>/dev/null || true + fi + + ok "Gitea 초기화 완료" + echo "" + info "Gitea URL: http://localhost:${GITEA_PORT}" + info "관리자 계정: ${GITEA_ADMIN} / ${GITEA_ADMIN_PW}" + info "저장소: ${GITEA_BASE}/${GITEA_ORG}/${GITEA_REPO}" + info "" + info "=== 브랜치 전략 ===" + info " main : 보호 브랜치 (PR + 리뷰 1명 필수)" + info " develop : 통합 브랜치 (feature 브랜치 merge 대상)" + info " feature/이름/기능명 : 개인 개발 브랜치" + info "" + info " 새 개발자 브랜치 생성:" + info " git checkout develop" + info " git checkout -b feature/홍길동/신기능" + info " git push -u gitea feature/홍길동/신기능" +} + + +create_dev_user() { + local username="$1" + local password="${2:-Dev@guardia!}" + local email="${3:-${username}@guardia.local}" + + local api="${GITEA_BASE}/api/v1" + local auth_header="Authorization: Basic $(echo -n "${GITEA_ADMIN}:${GITEA_ADMIN_PW}" | base64)" + + # 사용자 생성 + curl -sf -X POST "$api/admin/users" \ + -H "$auth_header" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"${username}\",\"password\":\"${password}\",\"email\":\"${email}\",\"login_name\":\"${username}\",\"source_id\":0,\"send_notify\":false,\"must_change_password\":false}" \ + -o /dev/null 2>/dev/null \ + || { info "사용자 이미 존재: $username"; return; } + + # 조직에 팀 멤버 추가 + local team_id + team_id=$(curl -sf "$api/orgs/${GITEA_ORG}/teams" \ + -H "$auth_header" 2>/dev/null \ + | python3 -c "import sys,json; teams=json.load(sys.stdin); t=[t for t in teams if t.get('name')=='Developers']; print(t[0]['id'] if t else '')" 2>/dev/null) + + if [[ -z "$team_id" ]]; then + # Developers 팀 생성 + team_id=$(curl -sf -X POST "$api/orgs/${GITEA_ORG}/teams" \ + -H "$auth_header" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"Developers\",\"permission\":\"write\",\"units\":[\"repo.code\",\"repo.issues\",\"repo.pulls\"]}" \ + 2>/dev/null \ + | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null) + fi + + if [[ -n "$team_id" ]]; then + # 팀에 사용자 추가 + curl -sf -X PUT "$api/teams/${team_id}/members/${username}" \ + -H "$auth_header" -o /dev/null 2>/dev/null + # 팀에 저장소 추가 + curl -sf -X PUT "$api/teams/${team_id}/repos/${GITEA_ORG}/${GITEA_REPO}" \ + -H "$auth_header" -o /dev/null 2>/dev/null + fi + + # 개인 브랜치 생성 (feature/이름/init) + curl -sf -X POST "$api/repos/${GITEA_ORG}/${GITEA_REPO}/branches" \ + -H "$auth_header" \ + -H "Content-Type: application/json" \ + -d "{\"new_branch_name\":\"feature/${username}/init\",\"old_branch_name\":\"develop\"}" \ + -o /dev/null 2>/dev/null + + ok "개발자 계정 + 브랜치 생성: $username (feature/${username}/init)" +} + + +_wait_gitea() { + local attempt=0 + until curl -sf "http://localhost:${GITEA_PORT}/api/v1/version" -o /dev/null 2>/dev/null; do + sleep 2 + ((attempt++)) + [[ $attempt -ge 30 ]] && { fail "Gitea 서비스 응답 없음"; return; } + done +}