Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

192 lines
6.3 KiB
Python

"""Pre-deploy environment validation for EC2 deployment."""
from __future__ import annotations
import os
import sys
from collections.abc import Callable
from dataclasses import dataclass
import boto3
from config.config import get_configured_llm_provider, get_llm_provider_api_key_env
from config.llm_auth import KEYLESS_PROVIDER_VALUES, SUPPORTED_PROVIDER_VALUES, provider_spec
from config.local_env import bootstrap_opensre_env, get_project_env_path
_DEPLOY_ENV_EXAMPLE = ".env.deploy.example"
# Fixed user-facing labels only — avoid credential-related substrings in print()
# paths (CodeQL clear-text logging).
_MISSING_LABELS: dict[str, str] = {
"aws": "AWS account access for EC2 provisioning",
"telegram_bot": "Telegram gateway bot configuration",
"llm_provider_invalid": "LLM provider setting (unsupported value)",
"llm_api": "LLM provider configuration for the selected provider",
}
_WARNING_LABELS: dict[str, str] = {
"telegram_users": "Telegram allowed-users configuration (recommended)",
"llm_provider_ec2": "LLM provider may not work inside EC2 containers",
}
class DeployEnvValidationError(Exception):
"""Raised after printing the deploy env validation report."""
@dataclass(frozen=True)
class DeployEnvIssue:
"""A deploy env validation issue identified by a stable code."""
code: str
env_vars: tuple[str, ...] = ()
provider: str = ""
def _env_set(name: str) -> bool:
return bool(os.getenv(name, "").strip())
def _aws_credentials_available() -> bool:
if _env_set("AWS_ROLE_ARN"):
return True
if _env_set("AWS_ACCESS_KEY_ID") and _env_set("AWS_SECRET_ACCESS_KEY"):
return True
if _env_set("AWS_PROFILE"):
return True
try:
credentials = boto3.Session().get_credentials()
except Exception: # noqa: BLE001
return False
return credentials is not None
def _collect_deploy_env_issues() -> tuple[list[DeployEnvIssue], list[DeployEnvIssue]]:
"""Return ``(missing_required, warnings)`` for the current process env."""
bootstrap_opensre_env(override=False)
missing: list[DeployEnvIssue] = []
warnings: list[DeployEnvIssue] = []
if not _aws_credentials_available():
missing.append(DeployEnvIssue("aws"))
if not _env_set("TELEGRAM_BOT_TOKEN"):
missing.append(DeployEnvIssue("telegram_bot", env_vars=("TELEGRAM_BOT_TOKEN",)))
if not _env_set("TELEGRAM_ALLOWED_USERS"):
warnings.append(DeployEnvIssue("telegram_users", env_vars=("TELEGRAM_ALLOWED_USERS",)))
provider = get_configured_llm_provider()
if provider not in SUPPORTED_PROVIDER_VALUES:
missing.append(DeployEnvIssue("llm_provider_invalid", env_vars=("LLM_PROVIDER",)))
else:
api_key_env = get_llm_provider_api_key_env(provider)
if api_key_env and not _env_set(api_key_env):
missing.append(DeployEnvIssue("llm_api", env_vars=(api_key_env,), provider=provider))
elif provider in KEYLESS_PROVIDER_VALUES:
spec = provider_spec(provider)
if spec is not None and spec.credential_kind in {"cli", "local"}:
warnings.append(DeployEnvIssue("llm_provider_ec2"))
return missing, warnings
def _supports_color() -> bool:
return sys.stdout.isatty() and os.getenv("NO_COLOR", "").strip() == ""
def _highlight(text: str, *, kind: str) -> str:
if not _supports_color():
return text
if kind == "missing":
return f"\033[31m{text}\033[0m"
if kind == "warn":
return f"\033[33m{text}\033[0m"
if kind == "label":
return f"\033[1m{text}\033[0m"
return text
def _label_for_issue(issue: DeployEnvIssue, *, warning: bool) -> str:
labels = _WARNING_LABELS if warning else _MISSING_LABELS
return labels.get(issue.code, "Deploy environment configuration")
def _format_issue_message(issue: DeployEnvIssue, *, warning: bool) -> str:
label = _label_for_issue(issue, warning=warning)
if issue.code == "telegram_bot" and issue.env_vars:
return f"{issue.env_vars[0]} — API key not set ({label})"
if issue.code == "llm_api" and issue.env_vars:
provider = issue.provider or "selected provider"
return f"{issue.env_vars[0]} — API key not set (required for LLM provider: {provider})"
if issue.code == "aws":
return (
"AWS credentials — not configured "
"(set AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY, AWS_ROLE_ARN, or AWS_PROFILE)"
)
if issue.code == "llm_provider_invalid" and issue.env_vars:
return f"{issue.env_vars[0]} — unsupported or missing value ({label})"
if issue.code == "telegram_users" and issue.env_vars:
return f"{issue.env_vars[0]} — not set ({label})"
return label
def _print_deploy_env_report(missing: list[DeployEnvIssue], warnings: list[DeployEnvIssue]) -> None:
env_path = get_project_env_path()
print("=" * 60)
print(_highlight("Deploy environment validation", kind="label"))
print("=" * 60)
if missing:
print()
print(_highlight("Missing required:", kind="label"))
for issue in missing:
message = _format_issue_message(issue, warning=False)
print(f" {_highlight('MISSING', kind='missing')}: {message}")
if warnings:
print()
print(_highlight("Recommended:", kind="label"))
for issue in warnings:
message = _format_issue_message(issue, warning=True)
print(f" {_highlight('WARN', kind='warn')}: {message}")
if missing or warnings:
print()
print(f"Env file: {env_path}")
print(f"Template: {_DEPLOY_ENV_EXAMPLE}")
print()
if missing:
print(
f"Deploy aborted: {len(missing)} required environment variable(s) missing. "
f"Fix the items above and retry."
)
print()
def validate_deploy_env() -> None:
"""Fail fast when required deploy environment variables are missing."""
missing, warnings = _collect_deploy_env_issues()
if not missing and not warnings:
return
_print_deploy_env_report(missing, warnings)
if missing:
raise DeployEnvValidationError
def run_lifecycle_main(main: Callable[[], None]) -> None:
"""Run a deploy lifecycle CLI entrypoint with clean env-validation exits."""
try:
main()
except DeployEnvValidationError:
raise SystemExit(1) from None