diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index 1f04dac9..488f6b23 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -267,9 +267,11 @@ jobs: # Step 1b: Apply Deployment yamls (套用 volumes/resources/probe 等非 image 設定) # 2026-04-05 Claude Code: 確保 deployment 結構變更(如 SSH key mount)持久化到 K8s - # 注意: IMAGE_TAG_PLACEHOLDER 會在 Step 2 的 kubectl set image 立即覆蓋 + # C3 修正 2026-04-05: 先 sed 替換 IMAGE_TAG_PLACEHOLDER 為正確 sha, + # 避免 Step 1b 和 Step 2 之間若中斷導致 K8s desired state 含無效 image tag + IMAGE_TAG="${{ github.sha }}" for f in k8s/awoooi-prod/06-deployment-api.yaml k8s/awoooi-prod/05-deployment-web.yaml k8s/awoooi-prod/08-deployment-worker.yaml; do - cat "$f" | \ + sed "s/IMAGE_TAG_PLACEHOLDER/${IMAGE_TAG}/g" "$f" | \ ssh -i ~/.ssh/deploy_key wooo@192.168.0.121 \ "export KUBECONFIG=/etc/rancher/k3s/k3s.yaml && sudo kubectl apply -f -" done diff --git a/apps/api/src/api/v1/playbooks.py b/apps/api/src/api/v1/playbooks.py index 446b21bd..8a0c2891 100644 --- a/apps/api/src/api/v1/playbooks.py +++ b/apps/api/src/api/v1/playbooks.py @@ -71,7 +71,7 @@ async def create_playbook(playbook: Playbook) -> CreatePlaybookResponse: """直接建立 Playbook(管理/seed 用途)""" service = get_playbook_service() playbook.source = PlaybookSource.MANUAL - saved = await service._repository.create(playbook) + saved = await service.create(playbook) return CreatePlaybookResponse( success=True, playbook_id=saved.playbook_id, diff --git a/apps/api/src/services/host_repair_agent.py b/apps/api/src/services/host_repair_agent.py index 66108909..1e7a7730 100644 --- a/apps/api/src/services/host_repair_agent.py +++ b/apps/api/src/services/host_repair_agent.py @@ -2,11 +2,12 @@ src/services/host_repair_agent.py Host Repair Agent — 透過 SSH 執行主機層修復 2026-04-05 Claude Code: Sprint 3 Host Auto-Repair +2026-04-05 Claude Code: C1 修正 — key_path 直接傳入 _ssh_execute,不反查 """ import asyncio import re import logging -from dataclasses import dataclass, field +from dataclasses import dataclass logger = logging.getLogger(__name__) @@ -81,6 +82,7 @@ class HostRepairAgent: output = await self._ssh_execute( host=config["host"], user=config["user"], + key_path=config["key_path"], command=command, ) except asyncio.TimeoutError: @@ -107,13 +109,10 @@ class HostRepairAgent: error="" if success else output, ) - async def _ssh_execute(self, host: str, user: str, command: str) -> str: - """執行 SSH 命令,回傳 stdout。""" - key_path = LAYER_SSH_CONFIG.get( - next((k for k, v in LAYER_SSH_CONFIG.items() if v["host"] == host and v["user"] == user), None), - {} - ).get("key_path", "/etc/repair-ssh/id_ed25519") - + async def _ssh_execute(self, host: str, user: str, key_path: str, command: str) -> str: + """執行 SSH 命令,回傳 stdout。key_path 由呼叫方傳入,不反查。""" + import time + deadline = time.monotonic() + SSH_TIMEOUT proc = await asyncio.wait_for( asyncio.create_subprocess_exec( "ssh", @@ -128,7 +127,8 @@ class HostRepairAgent: ), timeout=SSH_TIMEOUT, ) - stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=SSH_TIMEOUT) + remaining = max(1.0, deadline - time.monotonic()) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=remaining) output = stdout.decode().strip() logger.info("SSH repair %s@%s %s → %s", user, host, command, output) return output diff --git a/apps/api/src/services/playbook_service.py b/apps/api/src/services/playbook_service.py index d51a40a5..23f5783c 100644 --- a/apps/api/src/services/playbook_service.py +++ b/apps/api/src/services/playbook_service.py @@ -411,6 +411,11 @@ class PlaybookService: # === CRUD Proxies === + # 2026-04-05 Claude Code: C2 修正 — 提供 create() proxy,Router 不再直接呼叫 _repository + async def create(self, playbook: Playbook) -> Playbook: + """直接建立 Playbook(管理/seed 用途)""" + return await self._repository.create(playbook) + async def get_by_id(self, playbook_id: str) -> Playbook | None: """取得 Playbook""" return await self._repository.get_by_id(playbook_id) diff --git a/apps/api/tests/test_host_repair_agent.py b/apps/api/tests/test_host_repair_agent.py index 3b4feb99..4ab3eca5 100644 --- a/apps/api/tests/test_host_repair_agent.py +++ b/apps/api/tests/test_host_repair_agent.py @@ -88,6 +88,7 @@ class TestHostRepairAgent: mock_ssh.assert_called_once_with( host="192.168.0.110", user="wooo", + key_path="/etc/repair-ssh/id_ed25519", command="repair:sentry" ) diff --git a/k8s/awoooi-prod/06-deployment-api.yaml b/k8s/awoooi-prod/06-deployment-api.yaml index 2ab002cf..8da3a315 100644 --- a/k8s/awoooi-prod/06-deployment-api.yaml +++ b/k8s/awoooi-prod/06-deployment-api.yaml @@ -102,7 +102,7 @@ spec: - name: repair-ssh-key secret: secretName: awoooi-repair-ssh-key - defaultMode: 0400 + defaultMode: 0400 # 八進位 0400 = 十進位 256 = r-------- (owner read-only) --- apiVersion: v1 diff --git a/scripts/repair-bot/repair-bot-110.sh b/scripts/repair-bot/repair-bot-110.sh index 5870e065..c14b8dab 100755 --- a/scripts/repair-bot/repair-bot-110.sh +++ b/scripts/repair-bot/repair-bot-110.sh @@ -29,7 +29,7 @@ declare -A COMPOSE_DIRS=( CMD="${SSH_ORIGINAL_COMMAND:-}" log "repair-bot-110 invoked: CMD=$CMD" -if [[ "$CMD" =~ ^repair:([a-z0-9_-]+)$ ]]; then +if [[ "$CMD" =~ ^repair:([a-z0-9][a-z0-9-]{0,30})$ ]]; then # M3: 統一 Python 端 regex,禁止底線 COMPONENT="${BASH_REMATCH[1]}" DIR="${COMPOSE_DIRS[$COMPONENT]}" diff --git a/scripts/repair-bot/repair-bot-188.sh b/scripts/repair-bot/repair-bot-188.sh index f4590834..a76cb6c0 100755 --- a/scripts/repair-bot/repair-bot-188.sh +++ b/scripts/repair-bot/repair-bot-188.sh @@ -30,7 +30,7 @@ declare -A SYSTEMD_SERVICES=( CMD="${SSH_ORIGINAL_COMMAND:-}" log "repair-bot-188 invoked: CMD=$CMD" -if [[ "$CMD" =~ ^repair:([a-z0-9_-]+)$ ]]; then +if [[ "$CMD" =~ ^repair:([a-z0-9][a-z0-9-]{0,30})$ ]]; then # M3: 統一 Python 端 regex,禁止底線 COMPONENT="${BASH_REMATCH[1]}" # Docker Compose 類