BAT-006 (REQ-016/017/018): - client token 발급 경로 제거 (issue_client_token, POST /auth/tokens, 관련 스키마/테스트) - service-token rotate unique index revoked_at IS NULL 조건 추가 (Alembic CONCURRENTLY 마이그레이션) - POST /auth/service-tokens body daemon_id forward 누락 보정 (1-line fix) BAT-007 (REQ-022/023): - daemon deactivate 시 바인딩 service token 자동 revoke (CLI --force 옵션 + revoke_tokens_by_daemon_id) - NULL daemon_id legacy token 진단 CLI (migrate-null-daemon --dry-run) 거버넌스 문서: dcy-003/004 Discovery, REQ-016~023/025~032 신규, BAT-006/007 전체 산출물 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
93 lines
3.7 KiB
Python
93 lines
3.7 KiB
Python
"""Daemon registry CLI commands."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
|
|
import typer
|
|
|
|
from app.cli.core.utils import render_output
|
|
from app.services.auth_service import AuthService
|
|
|
|
daemon_app = typer.Typer(help="Daemon registry management commands.")
|
|
|
|
|
|
def _with_service(ctx: typer.Context, callback):
|
|
app_context = ctx.obj
|
|
repository = app_context.repository_factory(app_context.settings)
|
|
service = AuthService(repository, app_context.settings)
|
|
try:
|
|
from app.cli.core.utils import execute_command
|
|
return execute_command(app_context.console, callback=lambda: callback(repository, app_context))
|
|
finally:
|
|
if callable(getattr(type(repository), "close", None)):
|
|
repository.close()
|
|
|
|
|
|
@daemon_app.command("create")
|
|
def create_daemon(
|
|
ctx: typer.Context,
|
|
daemon_slug: str = typer.Argument(..., help="Unique daemon slug (scoped to the owner user)."),
|
|
user: str = typer.Option(..., "--user", help="Username of the daemon owner."),
|
|
display_name: Optional[str] = typer.Option(None, "--display-name", help="Human-readable daemon name."),
|
|
) -> None:
|
|
"""Create a new daemon registry entry."""
|
|
def _create(repo, app_context):
|
|
owner = repo.get_user_by_username(user)
|
|
if owner is None:
|
|
raise ValueError(f"사용자를 찾을 수 없습니다: {user}")
|
|
return repo.create_daemon(owner.id, daemon_slug=daemon_slug, display_name=display_name)
|
|
|
|
result = _with_service(ctx, _create)
|
|
from app.domain.models import Daemon
|
|
render_output(ctx.obj.console, ctx.obj.output_format, result)
|
|
|
|
|
|
@daemon_app.command("list")
|
|
def list_daemons(
|
|
ctx: typer.Context,
|
|
user: str = typer.Option(..., "--user", help="Username to list daemons for."),
|
|
) -> None:
|
|
"""List all daemon registry entries for a user."""
|
|
def _list(repo, app_context):
|
|
owner = repo.get_user_by_username(user)
|
|
if owner is None:
|
|
raise ValueError(f"사용자를 찾을 수 없습니다: {user}")
|
|
return repo.list_daemons_for_user(owner.id)
|
|
|
|
result = _with_service(ctx, _list)
|
|
render_output(ctx.obj.console, ctx.obj.output_format, result, empty_message="No daemons found.")
|
|
|
|
|
|
@daemon_app.command("update")
|
|
def update_daemon(
|
|
ctx: typer.Context,
|
|
daemon_id: str = typer.Argument(..., help="Daemon ID to update."),
|
|
display_name: Optional[str] = typer.Option(None, "--display-name", help="New display name."),
|
|
deactivate: bool = typer.Option(False, "--deactivate", help="Mark daemon as inactive."),
|
|
activate: bool = typer.Option(False, "--activate", help="Mark daemon as active."),
|
|
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation when deactivating."),
|
|
) -> None:
|
|
"""Update a daemon registry entry."""
|
|
if deactivate and not force:
|
|
confirmed = typer.confirm(f"Daemon {daemon_id}을(를) 비활성화하면 바인딩된 service token이 즉시 revoke됩니다. 계속하시겠습니까?")
|
|
if not confirmed:
|
|
raise typer.Exit(code=0)
|
|
|
|
def _update(repo, app_context):
|
|
is_active: bool | None = None
|
|
if deactivate and activate:
|
|
raise ValueError("--activate와 --deactivate를 동시에 사용할 수 없습니다")
|
|
if deactivate:
|
|
is_active = False
|
|
elif activate:
|
|
is_active = True
|
|
result = repo.update_daemon(daemon_id, display_name=display_name, is_active=is_active)
|
|
if deactivate:
|
|
revoked_count = repo.revoke_tokens_by_daemon_id(daemon_id)
|
|
app_context.console.print(f"Revoked {revoked_count} service token(s) bound to daemon {daemon_id}.")
|
|
return result
|
|
|
|
result = _with_service(ctx, _update)
|
|
render_output(ctx.obj.console, ctx.obj.output_format, result)
|