fix(sprint3): 首席架構師 Review C1/C2/C3/M3/m1 修正
C1: _ssh_execute 直接接收 key_path 參數,不反查 LAYER_SSH_CONFIG
C2: PlaybookService.create() proxy,Router 不再穿透呼叫 _repository
C3: CD Step 1b sed 替換 IMAGE_TAG_PLACEHOLDER,消除失敗中斷風險
M3: repair-bot 110/188 regex 統一 [a-z0-9][a-z0-9-]{0,30},禁止底線
m1: defaultMode 0400 加八進位說明注釋
m2: _ssh_execute 用 deadline 計算剩餘 timeout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -267,9 +267,11 @@ jobs:
|
|||||||
|
|
||||||
# Step 1b: Apply Deployment yamls (套用 volumes/resources/probe 等非 image 設定)
|
# Step 1b: Apply Deployment yamls (套用 volumes/resources/probe 等非 image 設定)
|
||||||
# 2026-04-05 Claude Code: 確保 deployment 結構變更(如 SSH key mount)持久化到 K8s
|
# 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
|
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 \
|
ssh -i ~/.ssh/deploy_key wooo@192.168.0.121 \
|
||||||
"export KUBECONFIG=/etc/rancher/k3s/k3s.yaml && sudo kubectl apply -f -"
|
"export KUBECONFIG=/etc/rancher/k3s/k3s.yaml && sudo kubectl apply -f -"
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ async def create_playbook(playbook: Playbook) -> CreatePlaybookResponse:
|
|||||||
"""直接建立 Playbook(管理/seed 用途)"""
|
"""直接建立 Playbook(管理/seed 用途)"""
|
||||||
service = get_playbook_service()
|
service = get_playbook_service()
|
||||||
playbook.source = PlaybookSource.MANUAL
|
playbook.source = PlaybookSource.MANUAL
|
||||||
saved = await service._repository.create(playbook)
|
saved = await service.create(playbook)
|
||||||
return CreatePlaybookResponse(
|
return CreatePlaybookResponse(
|
||||||
success=True,
|
success=True,
|
||||||
playbook_id=saved.playbook_id,
|
playbook_id=saved.playbook_id,
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
src/services/host_repair_agent.py
|
src/services/host_repair_agent.py
|
||||||
Host Repair Agent — 透過 SSH 執行主機層修復
|
Host Repair Agent — 透過 SSH 執行主機層修復
|
||||||
2026-04-05 Claude Code: Sprint 3 Host Auto-Repair
|
2026-04-05 Claude Code: Sprint 3 Host Auto-Repair
|
||||||
|
2026-04-05 Claude Code: C1 修正 — key_path 直接傳入 _ssh_execute,不反查
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@ class HostRepairAgent:
|
|||||||
output = await self._ssh_execute(
|
output = await self._ssh_execute(
|
||||||
host=config["host"],
|
host=config["host"],
|
||||||
user=config["user"],
|
user=config["user"],
|
||||||
|
key_path=config["key_path"],
|
||||||
command=command,
|
command=command,
|
||||||
)
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
@@ -107,13 +109,10 @@ class HostRepairAgent:
|
|||||||
error="" if success else output,
|
error="" if success else output,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _ssh_execute(self, host: str, user: str, command: str) -> str:
|
async def _ssh_execute(self, host: str, user: str, key_path: str, command: str) -> str:
|
||||||
"""執行 SSH 命令,回傳 stdout。"""
|
"""執行 SSH 命令,回傳 stdout。key_path 由呼叫方傳入,不反查。"""
|
||||||
key_path = LAYER_SSH_CONFIG.get(
|
import time
|
||||||
next((k for k, v in LAYER_SSH_CONFIG.items() if v["host"] == host and v["user"] == user), None),
|
deadline = time.monotonic() + SSH_TIMEOUT
|
||||||
{}
|
|
||||||
).get("key_path", "/etc/repair-ssh/id_ed25519")
|
|
||||||
|
|
||||||
proc = await asyncio.wait_for(
|
proc = await asyncio.wait_for(
|
||||||
asyncio.create_subprocess_exec(
|
asyncio.create_subprocess_exec(
|
||||||
"ssh",
|
"ssh",
|
||||||
@@ -128,7 +127,8 @@ class HostRepairAgent:
|
|||||||
),
|
),
|
||||||
timeout=SSH_TIMEOUT,
|
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()
|
output = stdout.decode().strip()
|
||||||
logger.info("SSH repair %s@%s %s → %s", user, host, command, output)
|
logger.info("SSH repair %s@%s %s → %s", user, host, command, output)
|
||||||
return output
|
return output
|
||||||
|
|||||||
@@ -411,6 +411,11 @@ class PlaybookService:
|
|||||||
|
|
||||||
# === CRUD Proxies ===
|
# === 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:
|
async def get_by_id(self, playbook_id: str) -> Playbook | None:
|
||||||
"""取得 Playbook"""
|
"""取得 Playbook"""
|
||||||
return await self._repository.get_by_id(playbook_id)
|
return await self._repository.get_by_id(playbook_id)
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ class TestHostRepairAgent:
|
|||||||
mock_ssh.assert_called_once_with(
|
mock_ssh.assert_called_once_with(
|
||||||
host="192.168.0.110",
|
host="192.168.0.110",
|
||||||
user="wooo",
|
user="wooo",
|
||||||
|
key_path="/etc/repair-ssh/id_ed25519",
|
||||||
command="repair:sentry"
|
command="repair:sentry"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ spec:
|
|||||||
- name: repair-ssh-key
|
- name: repair-ssh-key
|
||||||
secret:
|
secret:
|
||||||
secretName: awoooi-repair-ssh-key
|
secretName: awoooi-repair-ssh-key
|
||||||
defaultMode: 0400
|
defaultMode: 0400 # 八進位 0400 = 十進位 256 = r-------- (owner read-only)
|
||||||
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ declare -A COMPOSE_DIRS=(
|
|||||||
CMD="${SSH_ORIGINAL_COMMAND:-}"
|
CMD="${SSH_ORIGINAL_COMMAND:-}"
|
||||||
log "repair-bot-110 invoked: CMD=$CMD"
|
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]}"
|
COMPONENT="${BASH_REMATCH[1]}"
|
||||||
DIR="${COMPOSE_DIRS[$COMPONENT]}"
|
DIR="${COMPOSE_DIRS[$COMPONENT]}"
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ declare -A SYSTEMD_SERVICES=(
|
|||||||
CMD="${SSH_ORIGINAL_COMMAND:-}"
|
CMD="${SSH_ORIGINAL_COMMAND:-}"
|
||||||
log "repair-bot-188 invoked: CMD=$CMD"
|
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]}"
|
COMPONENT="${BASH_REMATCH[1]}"
|
||||||
|
|
||||||
# Docker Compose 類
|
# Docker Compose 類
|
||||||
|
|||||||
Reference in New Issue
Block a user