feat(dev): 로컬 데이터소스 9종 파리티(minio·rabbitmq·solr·oracle) — 0.21.0
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
129
.yakcloud/dev.py
129
.yakcloud/dev.py
@ -48,13 +48,21 @@ TAG = os.environ.get("TAG", "dev")
|
||||
# 로컬 데이터 소스 = kind '밖'의 **공유** 도커/포드만 컨테이너(타입별 1개) + 프로젝트별 격리 DB.
|
||||
# 관리형 shared 모델과 동일 계약 — 이미 있는 공유 서버에 <project>_<src> 격리 DB/유저만 민팅(소스 생성비 최소).
|
||||
# 클러스터(kind)는 앱(비즈니스)만. 앱은 no-selector Service+Endpoints(host=소스명)로 공유 컨테이너의 격리 DB 접속.
|
||||
_SVC_PORT = {"POSTGRESQL": 5432, "MYSQL": 3306, "MARIADB": 3306, "MONGODB": 27017, "REDIS": 6379}
|
||||
_SVC_PORT = {"POSTGRESQL": 5432, "MYSQL": 3306, "MARIADB": 3306, "MONGODB": 27017, "REDIS": 6379,
|
||||
"MINIO": 9000, "RABBITMQ": 5672, "SOLR": 8983, "ORACLE": 1521}
|
||||
_IMG = {"POSTGRESQL": "postgres:16-alpine", "MYSQL": "mysql:8", "MARIADB": "mariadb:11",
|
||||
"MONGODB": "mongo:7", "REDIS": "redis:7-alpine"}
|
||||
"MONGODB": "mongo:7", "REDIS": "redis:7-alpine",
|
||||
"MINIO": "minio/minio", "RABBITMQ": "rabbitmq:3-management", "SOLR": "solr:9",
|
||||
"ORACLE": os.environ.get("YAK_DEV_ORACLE_IMG", "gvenzl/oracle-free:slim")}
|
||||
_SHARED_CT = {"POSTGRESQL": "yak-dev-postgres", "MYSQL": "yak-dev-mysql", "MARIADB": "yak-dev-mariadb",
|
||||
"MONGODB": "yak-dev-mongo", "REDIS": "yak-dev-redis"}
|
||||
"MONGODB": "yak-dev-mongo", "REDIS": "yak-dev-redis",
|
||||
"MINIO": "yak-dev-minio", "RABBITMQ": "yak-dev-rabbitmq", "SOLR": "yak-dev-solr",
|
||||
"ORACLE": "yak-dev-oracle"}
|
||||
SHARED_NET = os.environ.get("YAK_DEV_NET", "kind") # kind 가 쓰는 도커 네트워크
|
||||
SHARED_ADMIN = "yakdevadmin" # dev 전용 공유 컨테이너 관리자 비밀번호(로컬 한정)
|
||||
MINIO_ROOT_USER = "yakdevadmin" # MinIO 루트 유저(로컬 dev, ≥3자)
|
||||
ORACLE_PDB = os.environ.get("YAK_DEV_ORACLE_PDB", "FREEPDB1") # gvenzl/oracle-free 기본 PDB
|
||||
MC_IMG = os.environ.get("YAK_DEV_MC_IMG", "minio/mc") # 버킷 민팅용 MinIO 클라이언트
|
||||
SUPPORTED = set(_SVC_PORT)
|
||||
MANAGED = "app.kubernetes.io/managed-by=yakcloud-dev"
|
||||
|
||||
@ -141,6 +149,14 @@ def _dbname(project: str, source: str) -> str:
|
||||
return (out or "app")[:40]
|
||||
|
||||
|
||||
def _minio_bucket(unit: str) -> str:
|
||||
"""S3/MinIO 버킷명 규칙 — 소문자·숫자·하이픈·점만(언더스코어 불가), 3~63자, 시작/끝은 영숫자."""
|
||||
b = "".join(c if (c.isalnum() or c in "-.") else "-" for c in unit.replace("_", "-").lower()).strip("-.")
|
||||
if len(b) < 3:
|
||||
b = (b + "-bkt")
|
||||
return b[:63].strip("-.")
|
||||
|
||||
|
||||
def _container_ip(cname: str) -> str:
|
||||
r = _rt(["inspect", "-f",
|
||||
'{{(index .NetworkSettings.Networks "' + SHARED_NET + '").IPAddress}}', cname], check=False)
|
||||
@ -151,10 +167,13 @@ def _ensure_shared(stype: str) -> tuple[str, str]:
|
||||
"""타입별 '공유' 컨테이너를 kind 네트워크에 보장(idempotent, 관리자 자격) → (이름, IP)."""
|
||||
cname = _SHARED_CT[stype]
|
||||
if not _rt(["ps", "-q", "-f", f"name=^{cname}$"], check=False).stdout.strip():
|
||||
_rt(["rm", "-f", cname], check=False) # 죽은 동명 컨테이너 정리
|
||||
# 죽은 동명 컨테이너 + 익명 볼륨(-v)까지 정리 — 재생성 시 항상 깨끗한 볼륨으로 시작(고아 볼륨 누적·재사용 방지).
|
||||
# (특히 rabbitmq/erlang 은 옛 uid 소유 볼륨을 재사용하면 .erlang.cookie eacces 로 재크래시)
|
||||
_rt(["rm", "-f", "-v", cname], check=False)
|
||||
img = _IMG[stype]
|
||||
run = ["run", "-d", "--name", cname, "--network", SHARED_NET,
|
||||
"--label", "yakcloud.dev/shared=1", "--label", f"yakcloud.dev/type={stype}"]
|
||||
tries = 90
|
||||
if stype == "POSTGRESQL":
|
||||
run += ["-e", f"POSTGRES_PASSWORD={SHARED_ADMIN}", img]
|
||||
ready = "pg_isready -U postgres >/dev/null 2>&1"
|
||||
@ -168,12 +187,30 @@ def _ensure_shared(stype: str) -> tuple[str, str]:
|
||||
run += ["-e", "MONGO_INITDB_ROOT_USERNAME=root", "-e", f"MONGO_INITDB_ROOT_PASSWORD={SHARED_ADMIN}", img]
|
||||
ready = (f'mongosh "mongodb://root:{SHARED_ADMIN}@127.0.0.1:27017/admin" --quiet '
|
||||
'--eval "db.runCommand({ping:1}).ok" | grep -q 1')
|
||||
else: # REDIS
|
||||
elif stype == "REDIS":
|
||||
run += [img, "redis-server", "--requirepass", SHARED_ADMIN]
|
||||
ready = f'redis-cli -a {SHARED_ADMIN} ping 2>/dev/null | grep -q PONG'
|
||||
elif stype == "MINIO":
|
||||
run += ["-e", f"MINIO_ROOT_USER={MINIO_ROOT_USER}", "-e", f"MINIO_ROOT_PASSWORD={SHARED_ADMIN}",
|
||||
img, "server", "/data", "--console-address", ":9001"]
|
||||
ready = None # 준비 대기·버킷 생성은 민팅(mc ready)에서 — minio 서버 이미지엔 헬스툴 없음
|
||||
elif stype == "RABBITMQ":
|
||||
# --user 0(root) 로 실행하면 엔트리포인트가 gosu 로 rabbitmq(999) 유저로 내려가 부팅한다.
|
||||
# ⚠ 준비체크/민팅을 root 로 exec 하면 erlang 이 root 소유 .erlang.cookie 를 만들어 메인(999)과 충돌 → eacces 크래시.
|
||||
# 반드시 'gosu rabbitmq' 로 diagnostics/ctl 을 999 로 실행해야 함(쿠키 소유자 일치). check_running=앱 기동 확인.
|
||||
run += ["--user", "0", "-e", "RABBITMQ_DEFAULT_USER=root", "-e", f"RABBITMQ_DEFAULT_PASS={SHARED_ADMIN}", img]
|
||||
ready = "gosu rabbitmq rabbitmq-diagnostics -q check_running >/dev/null 2>&1"
|
||||
elif stype == "SOLR":
|
||||
run += [img]
|
||||
ready = "wait-for-solr.sh --max-attempts 2 --wait-seconds 1 >/dev/null 2>&1"
|
||||
else: # ORACLE
|
||||
run += ["-e", f"ORACLE_PASSWORD={SHARED_ADMIN}", img]
|
||||
ready = "healthcheck.sh >/dev/null 2>&1"
|
||||
tries = 240 # 첫 DB 생성이 느림(최대 ~8분)
|
||||
log(f"공유 소스 컨테이너 '{cname}' ({stype}) 기동 ({RUNTIME}, net={SHARED_NET})")
|
||||
_rt(run, capture=False)
|
||||
for _ in range(90):
|
||||
if ready:
|
||||
for _ in range(tries):
|
||||
if _dexec(cname, ready, check=False).returncode == 0:
|
||||
break
|
||||
time.sleep(2)
|
||||
@ -204,6 +241,44 @@ def _mint(stype: str, cname: str, unit: str, pw: str) -> None:
|
||||
_dexec(cname, f'mongosh "mongodb://root:{SHARED_ADMIN}@127.0.0.1:27017/admin" --quiet --eval \'{js}\'')
|
||||
|
||||
|
||||
def _mint_minio(cname: str, ip: str, bucket: str) -> None:
|
||||
"""별도 mc 컨테이너로 준비 대기(mc ready) + 프로젝트별 버킷 생성(idempotent). minio 서버 이미지엔 mc 없음.
|
||||
별칭은 MC_HOST env 로 주입(막 뜬 서버에 'mc alias set' 검증 핑이 너무 일찍 실패하는 경쟁 회피) → mc ready 가 대기."""
|
||||
host_env = f"MC_HOST_h=http://{MINIO_ROOT_USER}:{SHARED_ADMIN}@{ip}:9000"
|
||||
cmd = f"mc ready h && mc mb --ignore-existing h/{bucket}"
|
||||
_rt(["run", "--rm", "--network", SHARED_NET, "-e", host_env, "--entrypoint", "sh", MC_IMG, "-c", cmd], capture=False)
|
||||
|
||||
|
||||
def _mint_rabbitmq(cname: str, vhost: str, user: str, pw: str) -> None:
|
||||
"""프로젝트별 vhost + 유저 + 권한(idempotent). rabbitmqctl 은 'gosu rabbitmq'(999)로 —
|
||||
root 로 실행하면 erlang 쿠키 소유자 불일치로 노드 접속 실패/충돌(부팅 크래시와 동일 원인)."""
|
||||
g = "gosu rabbitmq rabbitmqctl -q"
|
||||
cmd = (f"{g} list_vhosts | grep -qx {vhost} || {g} add_vhost {vhost}; "
|
||||
f"{g} list_users | awk '{{print $1}}' | grep -qx {user} "
|
||||
f"&& {g} change_password {user} {pw} || {g} add_user {user} {pw}; "
|
||||
f'{g} set_permissions -p {vhost} {user} ".*" ".*" ".*"')
|
||||
_dexec(cname, cmd)
|
||||
|
||||
|
||||
def _mint_solr(cname: str, core: str) -> None:
|
||||
"""프로젝트별 core 생성(이미 있으면 무시)."""
|
||||
_dexec(cname, f"solr create -c {core} 2>/dev/null || true", check=False)
|
||||
|
||||
|
||||
def _mint_oracle(cname: str, user: str, pw: str) -> None:
|
||||
"""프로젝트별 스키마/유저 생성(PDB, idempotent — ORA-01920=이미 존재 무시)."""
|
||||
plsql = (
|
||||
"SET FEEDBACK OFF VERIFY OFF;\n"
|
||||
f"BEGIN EXECUTE IMMEDIATE 'CREATE USER {user} IDENTIFIED BY \"{pw}\"'; "
|
||||
"EXCEPTION WHEN OTHERS THEN IF SQLCODE!=-1920 THEN RAISE; END IF; END;\n/\n"
|
||||
f"GRANT CONNECT, RESOURCE TO {user};\n"
|
||||
f"ALTER USER {user} QUOTA UNLIMITED ON USERS;\n"
|
||||
"EXIT;\n"
|
||||
)
|
||||
_rt(["exec", "-i", cname, "bash", "-c",
|
||||
f"sqlplus -S -L system/{SHARED_ADMIN}@localhost:1521/{ORACLE_PDB}"], input=plsql)
|
||||
|
||||
|
||||
def provision_source(ns: str, project: str, req: dict) -> dict | None:
|
||||
"""requires 소스 → 공유 도커 컨테이너의 프로젝트별 격리 DB + no-selector Service/Endpoints. 접속정보 반환."""
|
||||
name = req["name"]
|
||||
@ -221,6 +296,23 @@ def provision_source(ns: str, project: str, req: dict) -> dict | None:
|
||||
dbnum = str(int(hashlib.sha1(unit.encode()).hexdigest(), 16) % 16) # DB 번호 격리(0~15)
|
||||
conn = {"stype": stype, "host": svc, "port": port, "user": "", "password": SHARED_ADMIN, "db": dbnum}
|
||||
log(f"source '{name}' (REDIS) → 공유 '{cname}' · DB#{dbnum} · 서비스 {svc}:{port}")
|
||||
elif stype == "MINIO":
|
||||
bucket = _minio_bucket(unit) # S3 버킷명(언더스코어→하이픈)
|
||||
_mint_minio(cname, ip, bucket) # 프로젝트별 버킷
|
||||
conn = {"stype": stype, "host": svc, "port": port, "user": MINIO_ROOT_USER, "password": SHARED_ADMIN, "bucket": bucket}
|
||||
log(f"source '{name}' (MINIO) → 공유 '{cname}' · 버킷 '{bucket}' · 서비스 {svc}:{port}")
|
||||
elif stype == "RABBITMQ":
|
||||
_mint_rabbitmq(cname, unit, unit, pw) # vhost=유저=unit
|
||||
conn = {"stype": stype, "host": svc, "port": port, "user": unit, "password": pw, "vhost": unit}
|
||||
log(f"source '{name}' (RABBITMQ) → 공유 '{cname}' · vhost/유저 '{unit}' · 서비스 {svc}:{port}")
|
||||
elif stype == "SOLR":
|
||||
_mint_solr(cname, unit) # 프로젝트별 core
|
||||
conn = {"stype": stype, "host": svc, "port": port, "user": "", "password": "", "core": unit}
|
||||
log(f"source '{name}' (SOLR) → 공유 '{cname}' · core '{unit}' · 서비스 {svc}:{port}")
|
||||
elif stype == "ORACLE":
|
||||
_mint_oracle(cname, unit, pw) # 프로젝트별 스키마/유저
|
||||
conn = {"stype": stype, "host": svc, "port": port, "user": unit, "password": pw, "service": ORACLE_PDB}
|
||||
log(f"source '{name}' (ORACLE) → 공유 '{cname}' · 스키마 '{unit}' · {svc}:{port}/{ORACLE_PDB}")
|
||||
else:
|
||||
_mint(stype, cname, unit, pw)
|
||||
conn = {"stype": stype, "host": svc, "port": port, "user": unit, "password": pw, "db": unit}
|
||||
@ -245,7 +337,7 @@ def provision_source(ns: str, project: str, req: dict) -> dict | None:
|
||||
def bind_env(alias: str, conn: dict) -> dict:
|
||||
p = alias.upper().replace("-", "_") # 백엔드와 동일 — 하이픈 alias 에서도 env 키 일치
|
||||
host, port = conn["host"], str(conn["port"])
|
||||
user, pw, db = conn["user"], conn["password"], conn["db"]
|
||||
user, pw, db = conn.get("user", ""), conn.get("password", ""), conn.get("db", "")
|
||||
qu, qp = quote(user, safe=""), quote(pw, safe="")
|
||||
out = {f"{p}_HOST": host, f"{p}_PORT": port}
|
||||
st = conn["stype"]
|
||||
@ -261,6 +353,29 @@ def bind_env(alias: str, conn: dict) -> dict:
|
||||
elif st == "MARIADB":
|
||||
out.update({f"{p}_USERNAME": user, f"{p}_PASSWORD": pw, f"{p}_DB": db,
|
||||
f"{p}_URL": f"mysql://{qu}:{qp}@{host}:{port}/{db}"})
|
||||
elif st == "MINIO":
|
||||
endpoint = f"http://{host}:{port}" # 로컬 kind = http(비 TLS)
|
||||
out.update({f"{p}_USERNAME": user, f"{p}_PASSWORD": pw,
|
||||
f"{p}_URL": endpoint, f"{p}_ENDPOINT": endpoint,
|
||||
f"{p}_ACCESS_KEY": user, f"{p}_SECRET_KEY": pw,
|
||||
f"{p}_BUCKET": conn.get("bucket", ""), f"{p}_REGION": "us-east-1", f"{p}_USE_SSL": "false"})
|
||||
elif st == "RABBITMQ":
|
||||
vhost = conn.get("vhost", "")
|
||||
out.update({f"{p}_USERNAME": user, f"{p}_PASSWORD": pw, f"{p}_VHOST": vhost,
|
||||
f"{p}_URL": f"amqp://{qu}:{qp}@{host}:{port}/{quote(vhost, safe='')}",
|
||||
f"{p}_MGMT_URL": f"http://{host}:15672"})
|
||||
elif st == "SOLR":
|
||||
core = conn.get("core", "")
|
||||
endpoint = f"http://{host}:{port}"
|
||||
out.update({f"{p}_USERNAME": "", f"{p}_PASSWORD": "",
|
||||
f"{p}_CORE": core, f"{p}_ENDPOINT": endpoint,
|
||||
f"{p}_URL": f"{endpoint}/solr/{core}"})
|
||||
elif st == "ORACLE":
|
||||
service = conn.get("service", "")
|
||||
out.update({f"{p}_USERNAME": user, f"{p}_PASSWORD": pw, f"{p}_SERVICE": service,
|
||||
f"{p}_URL": f"oracle://{qu}:{qp}@{host}:{port}/{service}",
|
||||
f"{p}_JDBC_URL": f"jdbc:oracle:thin:@//{host}:{port}/{service}",
|
||||
f"{p}_DSN": f"{host}:{port}/{service}"})
|
||||
else: # POSTGRESQL
|
||||
out.update({f"{p}_USERNAME": user, f"{p}_PASSWORD": pw, f"{p}_DB": db,
|
||||
f"{p}_URL": f"postgresql://{qu}:{qp}@{host}:{port}/{db}"})
|
||||
|
||||
@ -16,7 +16,7 @@ set -uo pipefail # -e 미사용: 'test && action' 관용구가 값 없을 때
|
||||
|
||||
REPO="${YAKCLOUD_STARTER_REPO:-https://gitea.yakenator.io/yakenator/yakcloud-starter}"
|
||||
BRANCH="${YAKCLOUD_STARTER_BRANCH:-main}"
|
||||
VERSION="0.20.0"
|
||||
VERSION="0.21.0"
|
||||
# 로컬 개발 클러스터(kind) — 개발 후 운영까지 '직선 배포'의 dev 구간.
|
||||
DEV_CTX="${YAK_DEV_CONTEXT:-kind-yak-dev}"
|
||||
DEV_CLUSTER="${YAK_DEV_CLUSTER:-yak-dev}"
|
||||
|
||||
Reference in New Issue
Block a user