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
192 lines
6.3 KiB
Python
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
|