fix(security): isolate allowlisted execution broker
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m9s
CD Pipeline / build-and-deploy (push) Successful in 4m35s
CD Pipeline / post-deploy-checks (push) Successful in 1m49s
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m9s
CD Pipeline / build-and-deploy (push) Successful in 4m35s
CD Pipeline / post-deploy-checks (push) Successful in 1m49s
This commit is contained in:
100
apps/api/src/workers/ansible_executor_broker.py
Normal file
100
apps/api/src/workers/ansible_executor_broker.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""Dedicated allowlisted Ansible execution broker process."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import signal
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
|
||||
from src.core.config import settings
|
||||
from src.db.base import close_db
|
||||
from src.jobs.awooop_ansible_check_mode_job import (
|
||||
run_awooop_ansible_check_mode_loop,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
_HEALTH_PATH = Path("/tmp/executor-broker-healthy")
|
||||
_READY_PATH = Path("/tmp/executor-broker-ready")
|
||||
|
||||
|
||||
async def _heartbeat_loop(shutdown_event: asyncio.Event) -> None:
|
||||
while not shutdown_event.is_set():
|
||||
_HEALTH_PATH.touch()
|
||||
try:
|
||||
await asyncio.wait_for(shutdown_event.wait(), timeout=15)
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
|
||||
async def _main() -> None:
|
||||
if not settings.ENABLE_AWOOOP_ANSIBLE_CHECK_MODE_WORKER:
|
||||
raise RuntimeError("ansible_execution_broker_disabled_by_config")
|
||||
|
||||
ssh_key_path = Path(settings.AWOOOP_ANSIBLE_CHECK_MODE_SSH_KEY_PATH)
|
||||
known_hosts_path = Path(settings.AWOOOP_ANSIBLE_CHECK_MODE_KNOWN_HOSTS_PATH)
|
||||
if not ssh_key_path.is_file() or not known_hosts_path.is_file():
|
||||
raise RuntimeError("ansible_execution_broker_transport_not_mounted")
|
||||
|
||||
shutdown_event = asyncio.Event()
|
||||
|
||||
def _shutdown_handler(signum: int, _frame: Any) -> None:
|
||||
logger.info("ansible_execution_broker_shutdown_requested", signal=signum)
|
||||
shutdown_event.set()
|
||||
|
||||
signal.signal(signal.SIGTERM, _shutdown_handler)
|
||||
signal.signal(signal.SIGINT, _shutdown_handler)
|
||||
|
||||
_HEALTH_PATH.touch()
|
||||
_READY_PATH.touch()
|
||||
heartbeat_task = asyncio.create_task(
|
||||
_heartbeat_loop(shutdown_event),
|
||||
name="ansible_execution_broker_heartbeat",
|
||||
)
|
||||
execution_task = asyncio.create_task(
|
||||
run_awooop_ansible_check_mode_loop(),
|
||||
name="run_awooop_ansible_check_mode_loop",
|
||||
)
|
||||
shutdown_wait_task = asyncio.create_task(
|
||||
shutdown_event.wait(),
|
||||
name="ansible_execution_broker_shutdown_wait",
|
||||
)
|
||||
logger.info(
|
||||
"ansible_execution_broker_started",
|
||||
interval_seconds=settings.AWOOOP_ANSIBLE_CHECK_MODE_INTERVAL_SECONDS,
|
||||
batch_limit=settings.AWOOOP_ANSIBLE_CHECK_MODE_BATCH_LIMIT,
|
||||
allowed_risk_levels=(
|
||||
settings.AWOOOP_ANSIBLE_CONTROLLED_APPLY_ALLOWED_RISK_LEVELS
|
||||
),
|
||||
worker_raw_credential_access=False,
|
||||
)
|
||||
|
||||
try:
|
||||
done, _pending = await asyncio.wait(
|
||||
{execution_task, shutdown_wait_task},
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
if execution_task in done and not shutdown_event.is_set():
|
||||
_READY_PATH.unlink(missing_ok=True)
|
||||
await execution_task
|
||||
raise RuntimeError("ansible_execution_broker_loop_exited_unexpectedly")
|
||||
finally:
|
||||
shutdown_wait_task.cancel()
|
||||
execution_task.cancel()
|
||||
heartbeat_task.cancel()
|
||||
for task in (shutdown_wait_task, execution_task, heartbeat_task):
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
await close_db()
|
||||
_HEALTH_PATH.unlink(missing_ok=True)
|
||||
_READY_PATH.unlink(missing_ok=True)
|
||||
logger.info("ansible_execution_broker_stopped")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(_main())
|
||||
@@ -623,22 +623,6 @@ async def _main() -> None:
|
||||
interval_seconds=settings.AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_INTERVAL_SECONDS,
|
||||
batch_limit=settings.AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_BATCH_LIMIT,
|
||||
)
|
||||
ansible_check_mode_task: asyncio.Task[Any] | None = None
|
||||
if settings.ENABLE_AWOOOP_ANSIBLE_CHECK_MODE_WORKER:
|
||||
from src.jobs.awooop_ansible_check_mode_job import (
|
||||
run_awooop_ansible_check_mode_loop,
|
||||
)
|
||||
|
||||
ansible_check_mode_task = asyncio.create_task(
|
||||
run_awooop_ansible_check_mode_loop(),
|
||||
name="run_awooop_ansible_check_mode_loop",
|
||||
)
|
||||
logger.info(
|
||||
"signal_worker_ansible_check_mode_loop_started",
|
||||
interval_seconds=settings.AWOOOP_ANSIBLE_CHECK_MODE_INTERVAL_SECONDS,
|
||||
batch_limit=settings.AWOOOP_ANSIBLE_CHECK_MODE_BATCH_LIMIT,
|
||||
)
|
||||
|
||||
# Setup graceful shutdown
|
||||
shutdown_event = asyncio.Event()
|
||||
|
||||
@@ -670,12 +654,6 @@ async def _main() -> None:
|
||||
await ansible_candidate_backfill_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
if ansible_check_mode_task is not None:
|
||||
ansible_check_mode_task.cancel()
|
||||
try:
|
||||
await ansible_check_mode_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
await worker.stop()
|
||||
await close_worker_redis_pool() # 關閉 Worker 專屬連線
|
||||
await close_redis_pool()
|
||||
|
||||
Reference in New Issue
Block a user