fix(cd): verify API executor boundary in production
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m37s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m37s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -2160,14 +2160,13 @@ jobs:
|
||||
echo "✅ Service Registry ConfigMap 已更新"
|
||||
|
||||
# ─── Step 2: 更新 kustomization.yaml image tag ───
|
||||
# host runner 不保證有 root 權限,kustomize 安裝在使用者目錄。
|
||||
# kustomize is an immutable dependency of the internal runner image.
|
||||
# GitHub is frozen, so CD must never download tools from github.com.
|
||||
export PATH="${HOME}/.local/bin:${PATH}"
|
||||
if ! command -v kustomize &>/dev/null; then
|
||||
mkdir -p "${HOME}/.local/bin"
|
||||
curl -sL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv5.3.0/kustomize_v5.3.0_linux_amd64.tar.gz \
|
||||
| tar xz -C "${HOME}/.local/bin"
|
||||
chmod +x "${HOME}/.local/bin/kustomize"
|
||||
fi
|
||||
command -v kustomize >/dev/null 2>&1 || {
|
||||
echo "❌ internal runner image is missing required kustomize"
|
||||
exit 1
|
||||
}
|
||||
|
||||
cd k8s/awoooi-prod
|
||||
# kustomize edit set image 更新 tag
|
||||
@@ -2395,6 +2394,69 @@ jobs:
|
||||
$KUBECTL rollout status deployment/awoooi-worker -n awoooi-prod --timeout=120s
|
||||
echo "✅ 部署完成"
|
||||
|
||||
# AIA-P0-011: production must prove that the public API cannot mutate
|
||||
# workloads or read host executor credentials. Source YAML alone is
|
||||
# insufficient because stale SSA-owned list fields can survive sync.
|
||||
API_SA=$($KUBECTL get deployment/awoooi-api -n awoooi-prod \
|
||||
-o jsonpath='{.spec.template.spec.serviceAccountName}')
|
||||
WORKER_SA=$($KUBECTL get deployment/awoooi-worker -n awoooi-prod \
|
||||
-o jsonpath='{.spec.template.spec.serviceAccountName}')
|
||||
API_MOUNTS=$($KUBECTL get deployment/awoooi-api -n awoooi-prod \
|
||||
-o jsonpath='{range .spec.template.spec.containers[?(@.name=="api")].volumeMounts[*]}{.name}{"\n"}{end}')
|
||||
API_VOLUMES=$($KUBECTL get deployment/awoooi-api -n awoooi-prod \
|
||||
-o jsonpath='{range .spec.template.spec.volumes[*]}{.name}{"\n"}{end}')
|
||||
API_FS_GROUP=$($KUBECTL get deployment/awoooi-api -n awoooi-prod \
|
||||
-o jsonpath='{.spec.template.spec.securityContext.fsGroup}')
|
||||
API_SSH_MCP=$($KUBECTL get deployment/awoooi-api -n awoooi-prod \
|
||||
-o jsonpath='{.spec.template.spec.containers[?(@.name=="api")].env[?(@.name=="SSH_MCP_ENABLED")].value}')
|
||||
API_CHECK_WORKER=$($KUBECTL get deployment/awoooi-api -n awoooi-prod \
|
||||
-o jsonpath='{.spec.template.spec.containers[?(@.name=="api")].env[?(@.name=="ENABLE_AWOOOP_ANSIBLE_CHECK_MODE_WORKER")].value}')
|
||||
API_BACKFILL_WORKER=$($KUBECTL get deployment/awoooi-api -n awoooi-prod \
|
||||
-o jsonpath='{.spec.template.spec.containers[?(@.name=="api")].env[?(@.name=="ENABLE_AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_WORKER")].value}')
|
||||
API_CAN_GET=$($KUBECTL auth can-i \
|
||||
--as=system:serviceaccount:awoooi-prod:awoooi-api \
|
||||
get pods --all-namespaces)
|
||||
API_CAN_PATCH=$($KUBECTL auth can-i \
|
||||
--as=system:serviceaccount:awoooi-prod:awoooi-api \
|
||||
patch deployments.apps --all-namespaces)
|
||||
API_CAN_DELETE=$($KUBECTL auth can-i \
|
||||
--as=system:serviceaccount:awoooi-prod:awoooi-api \
|
||||
delete pods --all-namespaces)
|
||||
|
||||
test "$API_SA" = "awoooi-api" || {
|
||||
echo "❌ API ServiceAccount drift: $API_SA"; exit 1;
|
||||
}
|
||||
test "$WORKER_SA" = "awoooi-executor" || {
|
||||
echo "❌ worker ServiceAccount drift: $WORKER_SA"; exit 1;
|
||||
}
|
||||
if printf '%s\n%s\n' "$API_MOUNTS" "$API_VOLUMES" \
|
||||
| grep -Eq '^(repair-ssh-key|repair-known-hosts|ssh-mcp-key)$'; then
|
||||
echo "❌ API still mounts executor SSH credentials"
|
||||
exit 1
|
||||
fi
|
||||
test -z "$API_FS_GROUP" || {
|
||||
echo "❌ API still carries executor fsGroup: $API_FS_GROUP"; exit 1;
|
||||
}
|
||||
test "$API_SSH_MCP" = "false" || {
|
||||
echo "❌ API SSH MCP is not disabled"; exit 1;
|
||||
}
|
||||
test "$API_CHECK_WORKER" = "false" || {
|
||||
echo "❌ API check-mode executor loop is not disabled"; exit 1;
|
||||
}
|
||||
test "$API_BACKFILL_WORKER" = "false" || {
|
||||
echo "❌ API candidate backfill executor loop is not disabled"; exit 1;
|
||||
}
|
||||
test "$API_CAN_GET" = "yes" || {
|
||||
echo "❌ API read capability missing"; exit 1;
|
||||
}
|
||||
test "$API_CAN_PATCH" = "no" || {
|
||||
echo "❌ API can still patch workloads"; exit 1;
|
||||
}
|
||||
test "$API_CAN_DELETE" = "no" || {
|
||||
echo "❌ API can still delete pods"; exit 1;
|
||||
}
|
||||
echo "✅ AIA-P0-011 API/executor production boundary verified"
|
||||
|
||||
# Health Check
|
||||
HEALTH_PASS=0
|
||||
for i in 1 2 3; do
|
||||
|
||||
@@ -96,6 +96,27 @@ def test_api_reader_rbac_has_no_workload_write_verbs() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_cd_prunes_and_verifies_api_executor_boundary_in_production() -> None:
|
||||
repo_root = Path(__file__).resolve().parents[3]
|
||||
deployment = next(
|
||||
document
|
||||
for document in yaml.safe_load_all(
|
||||
(repo_root / "k8s/awoooi-prod/06-deployment-api.yaml").read_text()
|
||||
)
|
||||
if document and document.get("kind") == "Deployment"
|
||||
)
|
||||
workflow = (repo_root / ".gitea/workflows/cd.yaml").read_text()
|
||||
|
||||
assert deployment["metadata"]["annotations"][
|
||||
"argocd.argoproj.io/sync-options"
|
||||
] == "Replace=true"
|
||||
assert "AIA-P0-011 API/executor production boundary verified" in workflow
|
||||
assert "patch deployments.apps --all-namespaces" in workflow
|
||||
assert "delete pods --all-namespaces" in workflow
|
||||
assert "repair-ssh-key|repair-known-hosts|ssh-mcp-key" in workflow
|
||||
assert "github.com/kubernetes-sigs/kustomize" not in workflow
|
||||
|
||||
|
||||
def test_api_log_does_not_claim_skipped_executor_was_scheduled() -> None:
|
||||
repo_root = Path(__file__).resolve().parents[3]
|
||||
main_source = (repo_root / "apps/api/src/main.py").read_text()
|
||||
|
||||
@@ -9,6 +9,10 @@ kind: Deployment
|
||||
metadata:
|
||||
name: awoooi-api
|
||||
namespace: awoooi-prod
|
||||
annotations:
|
||||
# One controlled replace prunes legacy SSH mounts that were not owned by
|
||||
# Argo CD's server-side apply field manager. Remove after prod verification.
|
||||
argocd.argoproj.io/sync-options: Replace=true
|
||||
labels:
|
||||
app: awoooi-api
|
||||
system: awoooi
|
||||
|
||||
Reference in New Issue
Block a user