feat(dev): 로컬 dev 소스를 공유 도커 컨테이너+격리 DB로 재작성(클러스터=비즈니스만)
로컬 kind 개발에서 데이터 소스를 타입별 공유 도커 컨테이너(yak-dev-*)로 올리고 프로젝트-소스별 격리 DB·계정(<project>_<source>)을 민팅한다. 클러스터에는 no-selector Service + 수동 Endpoints 로 kind 밖 컨테이너를 가리켜 앱만 배포한다. 관리형 shared 모드와 동일한 바인딩 env 파리티 유지. dev down 시 공유 컨테이너 정리. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
241
.yakcloud/dev.py
241
.yakcloud/dev.py
@ -24,8 +24,10 @@ import base64
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import hashlib
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from urllib.parse import quote
|
||||
|
||||
@ -40,14 +42,16 @@ RUNTIME = os.environ.get("YAK_DEV_RUNTIME", "docker")
|
||||
HOST_SUFFIX = os.environ.get("YAK_DEV_HOST_SUFFIX", "dev.localhost")
|
||||
TAG = os.environ.get("TAG", "dev")
|
||||
|
||||
# 소스 타입별 로컬 컨테이너 스펙 — 관리형과 동일한 인클러스터 시크릿 키(_bind_env_for 입력)를 세팅.
|
||||
# port = 백엔드 _SVC_PORT 와 동일. img = 공개 오피셜 이미지(로컬 dev 는 파리티보다 기동성 우선).
|
||||
# 유저명은 백엔드와 동일하게 고정 'appuser'(소스명 파생 금지 — 'root' 등 예약어 충돌·환경 간 값 발산 방지).
|
||||
APP_USER = "appuser"
|
||||
_SVC_PORT = {"POSTGRESQL": 5432, "MYSQL": 3306, "MARIADB": 3306,
|
||||
"MONGODB": 27017, "REDIS": 6379}
|
||||
# 로컬 데이터 소스 = 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}
|
||||
_IMG = {"POSTGRESQL": "postgres:16-alpine", "MYSQL": "mysql:8", "MARIADB": "mariadb:11",
|
||||
"MONGODB": "mongo:7", "REDIS": "redis:7-alpine"}
|
||||
_SHARED_CT = {"POSTGRESQL": "yak-dev-postgres", "MYSQL": "yak-dev-mysql", "MARIADB": "yak-dev-mariadb",
|
||||
"MONGODB": "yak-dev-mongo", "REDIS": "yak-dev-redis"}
|
||||
SHARED_NET = os.environ.get("YAK_DEV_NET", "kind") # kind 가 쓰는 도커 네트워크
|
||||
SHARED_ADMIN = "yakdevadmin" # dev 전용 공유 컨테이너 관리자 비밀번호(로컬 한정)
|
||||
SUPPORTED = set(_SVC_PORT)
|
||||
MANAGED = "app.kubernetes.io/managed-by=yakcloud-dev"
|
||||
|
||||
@ -109,128 +113,129 @@ def ensure_ns(ns: str) -> None:
|
||||
|
||||
# ── 소스 자격: 최초 생성 시 랜덤(유저=고정 appuser, db=소스별), 이후 재사용(idempotent) ──────
|
||||
def get_or_make_creds(ns: str, name: str) -> dict:
|
||||
"""프로젝트-소스별 비밀번호 — 기존 Secret 이 있으면 재사용(멱등), 없으면 랜덤 생성."""
|
||||
sec = f"yak-dev-src-{sanitize(name)}"
|
||||
r = kubectl(["get", "secret", sec, "-n", ns, "-o", "json"], check=False)
|
||||
if r.returncode == 0:
|
||||
data = json.loads(r.stdout).get("data", {})
|
||||
c = {k: base64.b64decode(v).decode() for k, v in data.items()}
|
||||
c["secret"] = sec # 재사용 경로도 secret 이름을 포함(멱등 재배포 시 KeyError 방지)
|
||||
return c
|
||||
return {"user": APP_USER, "password": secrets.token_hex(16),
|
||||
"db": sanitize(name)[:24] or "app", "secret": sec}
|
||||
pw = (json.loads(r.stdout).get("data", {}) or {}).get("password")
|
||||
if pw:
|
||||
return {"password": base64.b64decode(pw).decode(), "secret": sec}
|
||||
return {"password": secrets.token_hex(16), "secret": sec}
|
||||
|
||||
|
||||
def _source_container(stype: str, c: dict) -> dict:
|
||||
"""소스 타입별 컨테이너 정의 + 인클러스터 시크릿 키(관리형과 동일) + '앱-유저 인증' readiness.
|
||||
def _rt(args: list[str], **kw) -> subprocess.CompletedProcess:
|
||||
return sh([RUNTIME, *args], **kw)
|
||||
|
||||
readiness 를 tcp 가 아니라 **앱 유저 로그인 exec** 으로 게이트한다 — 초기화(유저/DB 생성) 중
|
||||
포트만 열린 상태를 'Ready' 로 오판해 workload 가 먼저 붙어 인증 실패로 크래시하는 것을 막는다."""
|
||||
port = _SVC_PORT[stype]
|
||||
rootpw = c["password"] + "root"
|
||||
env: list[dict] = []
|
||||
args: list[str] | None = None
|
||||
init_cm: dict | None = None
|
||||
vol_mounts: list[dict] = []
|
||||
|
||||
def _dexec(cname: str, shell_cmd: str, check: bool = True) -> subprocess.CompletedProcess:
|
||||
return _rt(["exec", cname, "sh", "-c", shell_cmd], check=check)
|
||||
|
||||
|
||||
def _dbname(project: str, source: str) -> str:
|
||||
"""프로젝트별 격리 단위(DB·유저)명 — DB 안전(소문자·영숫자·언더스코어)."""
|
||||
raw = f"{project}_{source}".lower()
|
||||
out = "".join(c if (c.isalnum() or c == "_") else "_" for c in raw).strip("_")
|
||||
return (out or "app")[:40]
|
||||
|
||||
|
||||
def _container_ip(cname: str) -> str:
|
||||
r = _rt(["inspect", "-f",
|
||||
'{{(index .NetworkSettings.Networks "' + SHARED_NET + '").IPAddress}}', cname], check=False)
|
||||
return (r.stdout or "").strip()
|
||||
|
||||
|
||||
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) # 죽은 동명 컨테이너 정리
|
||||
img = _IMG[stype]
|
||||
run = ["run", "-d", "--name", cname, "--network", SHARED_NET,
|
||||
"--label", "yakcloud.dev/shared=1", "--label", f"yakcloud.dev/type={stype}"]
|
||||
if stype == "POSTGRESQL":
|
||||
run += ["-e", f"POSTGRES_PASSWORD={SHARED_ADMIN}", img]
|
||||
ready = "pg_isready -U postgres >/dev/null 2>&1"
|
||||
elif stype == "MYSQL":
|
||||
run += ["-e", f"MYSQL_ROOT_PASSWORD={SHARED_ADMIN}", img]
|
||||
ready = f'mysql -uroot -p{SHARED_ADMIN} -e "SELECT 1" >/dev/null 2>&1'
|
||||
elif stype == "MARIADB":
|
||||
run += ["-e", f"MARIADB_ROOT_PASSWORD={SHARED_ADMIN}", img]
|
||||
ready = f'mariadb -uroot -p{SHARED_ADMIN} -e "SELECT 1" >/dev/null 2>&1'
|
||||
elif stype == "MONGODB":
|
||||
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
|
||||
run += [img, "redis-server", "--requirepass", SHARED_ADMIN]
|
||||
ready = f'redis-cli -a {SHARED_ADMIN} ping 2>/dev/null | grep -q PONG'
|
||||
log(f"공유 소스 컨테이너 '{cname}' ({stype}) 기동 ({RUNTIME}, net={SHARED_NET})")
|
||||
_rt(run, capture=False)
|
||||
for _ in range(90):
|
||||
if _dexec(cname, ready, check=False).returncode == 0:
|
||||
break
|
||||
time.sleep(2)
|
||||
else:
|
||||
die(f"공유 컨테이너 '{cname}' 준비 대기 초과 — '{RUNTIME} logs {cname}' 확인")
|
||||
ip = _container_ip(cname)
|
||||
if not ip:
|
||||
die(f"공유 컨테이너 '{cname}' IP 조회 실패 — 도커 네트워크 '{SHARED_NET}' 확인")
|
||||
return cname, ip
|
||||
|
||||
|
||||
def _mint(stype: str, cname: str, unit: str, pw: str) -> None:
|
||||
"""공유 컨테이너에 프로젝트별 격리 DB/유저 생성(idempotent). unit=DB·유저명(영숫자·언더스코어)."""
|
||||
if stype == "POSTGRESQL":
|
||||
env = [{"name": "POSTGRES_USER", "value": c["user"]},
|
||||
{"name": "POSTGRES_PASSWORD", "value": c["password"]},
|
||||
{"name": "POSTGRES_DB", "value": c["db"]},
|
||||
{"name": "PGDATA", "value": "/var/lib/postgresql/data/pgdata"}]
|
||||
probe = ["sh", "-c", 'PGPASSWORD="$POSTGRES_PASSWORD" psql -h 127.0.0.1 -U "$POSTGRES_USER" '
|
||||
'-d "$POSTGRES_DB" -c "SELECT 1" >/dev/null 2>&1']
|
||||
elif stype == "MYSQL":
|
||||
env = [{"name": "MYSQL_USER", "value": c["user"]},
|
||||
{"name": "MYSQL_PASSWORD", "value": c["password"]},
|
||||
{"name": "MYSQL_DATABASE", "value": c["db"]},
|
||||
{"name": "MYSQL_ROOT_PASSWORD", "value": rootpw}]
|
||||
probe = ["sh", "-c", 'mysql -h127.0.0.1 -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" '
|
||||
'"$MYSQL_DATABASE" -e "SELECT 1" >/dev/null 2>&1']
|
||||
elif stype == "MARIADB":
|
||||
env = [{"name": "MARIADB_USER", "value": c["user"]},
|
||||
{"name": "MARIADB_PASSWORD", "value": c["password"]},
|
||||
{"name": "MARIADB_DATABASE", "value": c["db"]},
|
||||
{"name": "MARIADB_ROOT_PASSWORD", "value": rootpw}]
|
||||
probe = ["sh", "-c", 'mariadb -h127.0.0.1 -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" '
|
||||
'"$MARIADB_DATABASE" -e "SELECT 1" >/dev/null 2>&1']
|
||||
elif stype == "REDIS":
|
||||
args = ["redis-server", "--requirepass", c["password"]]
|
||||
env = [{"name": "REDISCLI_AUTH", "value": c["password"]}]
|
||||
probe = ["sh", "-c", 'redis-cli ping | grep -q PONG']
|
||||
_dexec(cname, f'psql -U postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname=\'{unit}\'" | grep -q 1 '
|
||||
f'|| psql -U postgres -c "CREATE ROLE {unit} LOGIN PASSWORD \'{pw}\'"')
|
||||
_dexec(cname, f'psql -U postgres -tAc "SELECT 1 FROM pg_database WHERE datname=\'{unit}\'" | grep -q 1 '
|
||||
f'|| psql -U postgres -c "CREATE DATABASE {unit} OWNER {unit}"')
|
||||
elif stype in ("MYSQL", "MARIADB"):
|
||||
client = "mysql" if stype == "MYSQL" else "mariadb"
|
||||
sql = (f"CREATE DATABASE IF NOT EXISTS {unit}; "
|
||||
f"CREATE USER IF NOT EXISTS '{unit}'@'%' IDENTIFIED BY '{pw}'; "
|
||||
f"GRANT ALL ON {unit}.* TO '{unit}'@'%'; FLUSH PRIVILEGES;")
|
||||
_dexec(cname, f'{client} -uroot -p{SHARED_ADMIN} -e "{sql}"')
|
||||
elif stype == "MONGODB":
|
||||
# 루트 유저(root) + 앱 db 에 앱 유저(appuser) 생성(authSource=db 파리티) via init.js
|
||||
env = [{"name": "MONGO_INITDB_ROOT_USERNAME", "value": "root"},
|
||||
{"name": "MONGO_INITDB_ROOT_PASSWORD", "value": rootpw},
|
||||
{"name": "MONGO_INITDB_DATABASE", "value": c["db"]},
|
||||
{"name": "MONGO_APP_USER", "value": c["user"]},
|
||||
{"name": "MONGO_APP_PASSWORD", "value": c["password"]},
|
||||
{"name": "MONGO_APP_DB", "value": c["db"]}]
|
||||
js = (f'db.getSiblingDB("{c["db"]}").createUser({{user:"{c["user"]}",'
|
||||
f'pwd:"{c["password"]}",roles:[{{role:"readWrite",db:"{c["db"]}"}}]}});')
|
||||
init_cm = {"js": js}
|
||||
vol_mounts = [{"name": "initdb", "mountPath": "/docker-entrypoint-initdb.d"}]
|
||||
probe = ["sh", "-c", 'mongosh "mongodb://$MONGO_APP_USER:$MONGO_APP_PASSWORD@127.0.0.1:'
|
||||
f'{port}/$MONGO_APP_DB?authSource=$MONGO_APP_DB" --quiet '
|
||||
'--eval "db.runCommand({ping:1}).ok" | grep -q 1']
|
||||
else: # pragma: no cover
|
||||
die(f"내부오류: 미지원 타입 {stype}")
|
||||
cont = {"name": "db", "image": _IMG[stype], "imagePullPolicy": "IfNotPresent",
|
||||
"ports": [{"containerPort": port}], "env": env,
|
||||
"readinessProbe": {"exec": {"command": probe}, "initialDelaySeconds": 5,
|
||||
"periodSeconds": 5, "failureThreshold": 40, "timeoutSeconds": 5},
|
||||
"volumeMounts": vol_mounts}
|
||||
if args:
|
||||
cont["args"] = args
|
||||
return {"container": cont, "port": port, "init_cm": init_cm, "vol_mounts": vol_mounts}
|
||||
js = (f'try{{db.getSiblingDB("{unit}").createUser({{user:"{unit}",pwd:"{pw}",'
|
||||
f'roles:[{{role:"readWrite",db:"{unit}"}}]}})}}catch(e){{if(!/already exists/i.test(e.message))throw e}}')
|
||||
_dexec(cname, f'mongosh "mongodb://root:{SHARED_ADMIN}@127.0.0.1:27017/admin" --quiet --eval \'{js}\'')
|
||||
|
||||
|
||||
def provision_source(ns: str, req: dict) -> dict | None:
|
||||
"""requires 소스를 로컬 컨테이너로 프로비저닝(idempotent) → 접속정보 반환."""
|
||||
def provision_source(ns: str, project: str, req: dict) -> dict | None:
|
||||
"""requires 소스 → 공유 도커 컨테이너의 프로젝트별 격리 DB + no-selector Service/Endpoints. 접속정보 반환."""
|
||||
name = req["name"]
|
||||
stype = req["type"].upper()
|
||||
if req.get("mode") == "shared":
|
||||
warn(f"source '{name}': mode=shared 는 로컬 dev 에서 일반 로컬 컨테이너로 대체(격리 DB 민팅 없음).")
|
||||
if req.get("mode") == "local":
|
||||
warn(f"source '{name}': mode=local(인클러스터)는 로컬 dev 에서 공유 컨테이너로 대체됩니다.")
|
||||
if stype not in SUPPORTED:
|
||||
die(f"source '{name}' 타입 {stype} — 로컬 dev 미지원(지원: {', '.join(sorted(SUPPORTED))}). "
|
||||
f"관리형(deploy/promote)에서는 지원됩니다.")
|
||||
dep = sanitize(f"src-{name}")
|
||||
die(f"source '{name}' 타입 {stype} — 로컬 dev 미지원(지원: {', '.join(sorted(SUPPORTED))}).")
|
||||
svc = sanitize(name)
|
||||
creds = get_or_make_creds(ns, name)
|
||||
spec = _source_container(stype, creds)
|
||||
labels = {"app": dep, "app.kubernetes.io/managed-by": "yakcloud-dev",
|
||||
"yakcloud.dev/role": "source", "yakcloud.dev/source": svc}
|
||||
objs: list[dict] = [{
|
||||
"apiVersion": "v1", "kind": "Secret",
|
||||
"metadata": {"name": creds["secret"], "namespace": ns, "labels": labels},
|
||||
"stringData": {"user": creds["user"], "password": creds["password"], "db": creds["db"]},
|
||||
}]
|
||||
volumes: list[dict] = []
|
||||
if spec["init_cm"]:
|
||||
cm = f"{dep}-init"
|
||||
objs.append({"apiVersion": "v1", "kind": "ConfigMap",
|
||||
"metadata": {"name": cm, "namespace": ns, "labels": labels},
|
||||
"data": {"init.js": spec["init_cm"]["js"]}})
|
||||
volumes.append({"name": "initdb", "configMap": {"name": cm}})
|
||||
objs.append({
|
||||
"apiVersion": "apps/v1", "kind": "Deployment",
|
||||
"metadata": {"name": dep, "namespace": ns, "labels": labels},
|
||||
"spec": {"replicas": 1, "selector": {"matchLabels": {"app": dep}},
|
||||
"strategy": {"type": "Recreate"},
|
||||
"template": {"metadata": {"labels": labels},
|
||||
"spec": {"containers": [spec["container"]], "volumes": volumes}}},
|
||||
})
|
||||
objs.append({
|
||||
"apiVersion": "v1", "kind": "Service",
|
||||
"metadata": {"name": svc, "namespace": ns, "labels": labels},
|
||||
"spec": {"selector": {"app": dep},
|
||||
"ports": [{"port": spec["port"], "targetPort": spec["port"]}]},
|
||||
})
|
||||
log(f"source '{name}' ({stype}) 프로비저닝 → 서비스 {svc}:{spec['port']}")
|
||||
apply(objs)
|
||||
r = kubectl(["rollout", "status", f"deployment/{dep}", "-n", ns, "--timeout=300s"], check=False)
|
||||
if r.returncode != 0:
|
||||
die(f"source '{name}' 기동/인증 준비 대기 실패 — 'kubectl --context {CTX} -n {ns} get pods' 확인")
|
||||
return {"stype": stype, "host": svc, "port": spec["port"],
|
||||
"user": creds["user"], "password": creds["password"], "db": creds["db"]}
|
||||
unit = _dbname(project, name)
|
||||
port = _SVC_PORT[stype]
|
||||
pw = get_or_make_creds(ns, name)["password"] # 프로젝트-소스별 비밀번호(Secret 재사용=멱등)
|
||||
cname, ip = _ensure_shared(stype)
|
||||
if stype == "REDIS":
|
||||
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}")
|
||||
else:
|
||||
_mint(stype, cname, unit, pw)
|
||||
conn = {"stype": stype, "host": svc, "port": port, "user": unit, "password": pw, "db": unit}
|
||||
log(f"source '{name}' ({stype}) → 공유 '{cname}' · 격리 DB '{unit}' · 서비스 {svc}:{port}")
|
||||
labels = {"app.kubernetes.io/managed-by": "yakcloud-dev", "yakcloud.dev/role": "source", "yakcloud.dev/source": svc}
|
||||
# no-selector Service + 수동 Endpoints → kind 밖 공유 컨테이너 IP 로 라우팅.
|
||||
apply([
|
||||
{"apiVersion": "v1", "kind": "Secret",
|
||||
"metadata": {"name": f"yak-dev-src-{svc}", "namespace": ns, "labels": labels},
|
||||
"stringData": {"password": pw, "unit": unit}},
|
||||
{"apiVersion": "v1", "kind": "Service",
|
||||
"metadata": {"name": svc, "namespace": ns, "labels": labels},
|
||||
"spec": {"ports": [{"port": port, "targetPort": port}]}},
|
||||
{"apiVersion": "v1", "kind": "Endpoints",
|
||||
"metadata": {"name": svc, "namespace": ns, "labels": labels},
|
||||
"subsets": [{"addresses": [{"ip": ip}], "ports": [{"port": port}]}]},
|
||||
])
|
||||
return conn
|
||||
|
||||
|
||||
# ── 바인딩 env: 백엔드 _bind_env_for 와 동일(하이픈 alias → 언더스코어 프리픽스 포함) ────────
|
||||
@ -245,8 +250,8 @@ def bind_env(alias: str, conn: dict) -> dict:
|
||||
out.update({f"{p}_USERNAME": user, f"{p}_PASSWORD": pw, f"{p}_DB": db,
|
||||
f"{p}_URL": f"mongodb://{qu}:{qp}@{host}:{port}/{db}?authSource={db}"})
|
||||
elif st == "REDIS":
|
||||
out.update({f"{p}_USERNAME": "", f"{p}_PASSWORD": pw, f"{p}_DB": "0",
|
||||
f"{p}_URL": f"redis://:{qp}@{host}:{port}/0"})
|
||||
out.update({f"{p}_USERNAME": "", f"{p}_PASSWORD": pw, f"{p}_DB": db,
|
||||
f"{p}_URL": f"redis://:{qp}@{host}:{port}/{db}"})
|
||||
elif st == "MYSQL":
|
||||
out.update({f"{p}_USERNAME": user, f"{p}_PASSWORD": pw, f"{p}_DB": db,
|
||||
f"{p}_URL": f"mysql://{qu}:{qp}@{host}:{port}/{db}"})
|
||||
@ -338,7 +343,7 @@ def deploy_workload(project: str, ns: str, w: dict, src_conns: dict) -> tuple[st
|
||||
|
||||
# ── 고아 정리: 매니페스트에 없는(라벨된) 소스/워크로드 리소스 삭제(직선 리컨실) ──────────────
|
||||
def _prune(ns: str, role: str, label_key: str, desired: set) -> None:
|
||||
r = kubectl(["get", "deployment,service,secret,configmap,ingress", "-n", ns,
|
||||
r = kubectl(["get", "deployment,service,endpoints,secret,configmap,ingress", "-n", ns,
|
||||
"-l", f"{MANAGED},yakcloud.dev/role={role}", "-o", "json"], check=False)
|
||||
if r.returncode != 0:
|
||||
return
|
||||
@ -405,7 +410,7 @@ def cmd_deploy(m: dict) -> None:
|
||||
ensure_ns(ns)
|
||||
src_conns: dict[str, dict] = {}
|
||||
for req in reqs:
|
||||
conn = provision_source(ns, req)
|
||||
conn = provision_source(ns, project, req)
|
||||
if conn:
|
||||
src_conns[req["name"]] = conn
|
||||
results = [deploy_workload(project, ns, w, src_conns) for w in wls]
|
||||
|
||||
@ -492,6 +492,9 @@ cmd_dev() {
|
||||
local rt kenv=""; rt="$(_dev_runtime)"; [ "$rt" = podman ] && kenv="KIND_EXPERIMENTAL_PROVIDER=podman"
|
||||
info "kind 클러스터 '$DEV_CLUSTER' 삭제…"
|
||||
env $kenv kind delete cluster --name "$DEV_CLUSTER" && echo "✓ 삭제 완료"
|
||||
# 공유 데이터 소스 컨테이너(yak-dev-*)도 정리(머신 레벨 전체 teardown).
|
||||
local scs; scs="$("${rt:-docker}" ps -aq -f label=yakcloud.dev/shared=1 2>/dev/null)"
|
||||
[ -n "$scs" ] && { info "공유 소스 컨테이너 정리…"; echo "$scs" | xargs "${rt:-docker}" rm -f >/dev/null 2>&1; }
|
||||
_dev_deregister
|
||||
;;
|
||||
*) echo "yakcloud dev <up|deploy [tag]|status|clean|down>"; exit 1 ;;
|
||||
|
||||
Reference in New Issue
Block a user