Files
cleveragents-core/robot/helper_cloud_resources.py
T
freemo c65e8a5285
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 16s
CI / lint (pull_request) Successful in 17s
CI / quality (pull_request) Successful in 29s
CI / typecheck (pull_request) Successful in 59s
CI / security (pull_request) Successful in 59s
CI / unit_tests (pull_request) Successful in 3m9s
CI / integration_tests (pull_request) Successful in 3m34s
CI / e2e_tests (pull_request) Successful in 3m54s
CI / docker (pull_request) Successful in 55s
CI / coverage (pull_request) Successful in 6m1s
CI / build (push) Successful in 15s
CI / lint (push) Successful in 16s
CI / quality (push) Successful in 27s
CI / typecheck (push) Successful in 38s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 48s
CI / unit_tests (push) Successful in 3m14s
CI / integration_tests (push) Successful in 3m30s
CI / docker (push) Successful in 56s
CI / e2e_tests (push) Successful in 4m43s
CI / coverage (push) Successful in 6m1s
CI / benchmark-publish (push) Successful in 19m50s
CI / benchmark-regression (pull_request) Successful in 37m17s
feat(resource): add cloud infrastructure resources
Implement cloud resource types (aws, gcp, azure) with credential
fields, region/tenant metadata, and stubbed sandbox strategies.
Credential resolution uses environment variables and profile names
with no secrets logged.

Key changes:
- Add CloudResourceHandler with aws/gcp/azure type definitions
- Add credential resolution from env vars and profile names
- Add stubbed sandbox strategies (validate config, raise NotImplementedError)
- Register cloud types in bootstrap_builtin_types
- Credential masking via existing redaction patterns
- Add Behave BDD tests, Robot integration tests, ASV benchmarks

ISSUES CLOSED: #343
2026-03-17 13:14:37 -04:00

260 lines
8.0 KiB
Python

"""Helper utilities for cloud resource Robot smoke tests.
Each command prints a single ``<command>-ok`` token on success so the
calling Robot test can assert on ``stdout``.
"""
from __future__ import annotations
import os
import sys
from typing import Any
from unittest.mock import MagicMock
from cleveragents.domain.models.core.resource import (
PhysVirt,
Resource,
ResourceCapabilities,
)
from cleveragents.resource.handlers.cloud import (
CloudResourceHandler,
CloudSandboxStrategy,
resolve_credentials,
validate_credentials,
)
from cleveragents.resource.handlers.protocol import ResourceHandler
_CLOUD_ENV_VARS = [
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"AWS_SESSION_TOKEN",
"AWS_REGION",
"AWS_PROFILE",
"GOOGLE_APPLICATION_CREDENTIALS",
"GCLOUD_PROJECT",
"GCP_REGION",
"AZURE_SUBSCRIPTION_ID",
"AZURE_TENANT_ID",
"AZURE_CLIENT_ID",
"AZURE_CLIENT_SECRET",
"AZURE_REGION",
]
def _clear_cloud_env() -> dict[str, str]:
"""Remove all cloud env vars and return saved values."""
saved: dict[str, str] = {}
for var in _CLOUD_ENV_VARS:
val = os.environ.pop(var, None)
if val is not None:
saved[var] = val
return saved
def _restore_cloud_env(saved: dict[str, str]) -> None:
"""Restore previously saved env vars."""
for var in _CLOUD_ENV_VARS:
if var in saved:
os.environ[var] = saved[var]
else:
os.environ.pop(var, None)
def _make_resource(
provider: str,
properties: dict[str, Any] | None = None,
) -> Resource:
"""Create a test cloud resource."""
return Resource(
resource_id="01KJ5C5TPMP8GGX3QC83E2MAQS",
resource_type_name=provider,
classification=PhysVirt.PHYSICAL,
location=None,
properties=properties or {},
capabilities=ResourceCapabilities(
readable=True,
writable=True,
sandboxable=False,
checkpointable=False,
),
)
def _protocol_check() -> None:
"""Verify CloudResourceHandler satisfies the ResourceHandler protocol."""
handler = CloudResourceHandler()
assert isinstance(handler, ResourceHandler), (
"CloudResourceHandler not a ResourceHandler"
)
print("protocol-check-ok")
def _aws_resolve() -> None:
"""Verify AWS credential resolution from env vars."""
saved = _clear_cloud_env()
try:
os.environ["AWS_ACCESS_KEY_ID"] = "AKIAIOSFODNN7EXAMPLE"
os.environ["AWS_SECRET_ACCESS_KEY"] = "wJalrXUtnFEMI/EXAMPLEKEY"
os.environ["AWS_REGION"] = "us-east-1"
resolved = resolve_credentials("aws", {})
assert resolved["access-key-id"] is not None
assert resolved["secret-access-key"] is not None
assert resolved["region"] == "us-east-1"
errors = validate_credentials("aws", resolved)
assert len(errors) == 0, f"Unexpected errors: {errors}"
print("aws-resolve-ok")
finally:
_restore_cloud_env(saved)
def _gcp_resolve() -> None:
"""Verify GCP credential resolution from env vars."""
saved = _clear_cloud_env()
try:
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "/path/to/sa-key.json"
os.environ["GCLOUD_PROJECT"] = "my-gcp-project"
resolved = resolve_credentials("gcp", {})
assert resolved["service-account-json-path"] is not None
assert resolved["project-id"] == "my-gcp-project"
errors = validate_credentials("gcp", resolved)
assert len(errors) == 0, f"Unexpected errors: {errors}"
print("gcp-resolve-ok")
finally:
_restore_cloud_env(saved)
def _azure_resolve() -> None:
"""Verify Azure credential resolution from env vars."""
saved = _clear_cloud_env()
try:
os.environ["AZURE_SUBSCRIPTION_ID"] = "sub-12345"
os.environ["AZURE_TENANT_ID"] = "tenant-67890"
os.environ["AZURE_CLIENT_ID"] = "client-abcde"
os.environ["AZURE_CLIENT_SECRET"] = "secret-fghij"
resolved = resolve_credentials("azure", {})
assert resolved["subscription-id"] is not None
assert resolved["tenant-id"] is not None
assert resolved["client-id"] is not None
assert resolved["client-secret"] is not None
errors = validate_credentials("azure", resolved)
assert len(errors) == 0, f"Unexpected errors: {errors}"
print("azure-resolve-ok")
finally:
_restore_cloud_env(saved)
def _stub_resolve() -> None:
"""Verify cloud handler raises NotImplementedError for valid config."""
saved = _clear_cloud_env()
try:
os.environ["AWS_ACCESS_KEY_ID"] = "AKIAIOSFODNN7EXAMPLE"
os.environ["AWS_SECRET_ACCESS_KEY"] = "wJalrXUtnFEMI/EXAMPLEKEY"
handler = CloudResourceHandler()
resource = _make_resource("aws")
mock_manager = MagicMock()
try:
handler.resolve(
resource=resource,
plan_id="PLAN_ROBOT",
slot_name="cloud-slot",
sandbox_manager=mock_manager,
)
print("stub-resolve-FAIL: no error raised")
except NotImplementedError:
print("stub-resolve-ok")
finally:
_restore_cloud_env(saved)
def _missing_creds() -> None:
"""Verify cloud handler raises ValueError for missing credentials."""
saved = _clear_cloud_env()
try:
handler = CloudResourceHandler()
resource = _make_resource("aws")
mock_manager = MagicMock()
try:
handler.resolve(
resource=resource,
plan_id="PLAN_ROBOT",
slot_name="cloud-slot",
sandbox_manager=mock_manager,
)
print("missing-creds-FAIL: no error raised")
except ValueError:
print("missing-creds-ok")
except NotImplementedError:
print("missing-creds-FAIL: NotImplementedError instead of ValueError")
finally:
_restore_cloud_env(saved)
def _sandbox_stubs() -> None:
"""Verify sandbox strategy methods raise NotImplementedError."""
for provider in ("aws", "gcp", "azure"):
strategy = CloudSandboxStrategy(provider)
for method_name in ("create", "commit", "rollback"):
method = getattr(strategy, method_name)
try:
method("res-test", "plan-test")
print(f"sandbox-stubs-FAIL: {provider}.{method_name} no error")
return
except NotImplementedError:
pass
print("sandbox-stubs-ok")
def _aws_profile() -> None:
"""Verify AWS profile name satisfies credential requirements."""
saved = _clear_cloud_env()
try:
os.environ["AWS_PROFILE"] = "my-profile"
resolved = resolve_credentials("aws", {})
errors = validate_credentials("aws", resolved)
assert len(errors) == 0, f"Profile should satisfy creds: {errors}"
print("aws-profile-ok")
finally:
_restore_cloud_env(saved)
def _builtin_types() -> None:
"""Verify cloud hierarchy types are in BUILTIN_TYPES."""
from cleveragents.application.services._resource_registry_data import (
BUILTIN_TYPES,
)
names = [t["name"] for t in BUILTIN_TYPES]
# Core cloud types that must be present
for expected in (
"cloud-account",
"cloud-region",
"cloud-network",
"aws-account",
"aws-region",
"aws-vpc",
"gcp",
"azure",
):
assert expected in names, f"'{expected}' not in BUILTIN_TYPES"
print("builtin-types-ok")
_COMMANDS: dict[str, Any] = {
"protocol-check": _protocol_check,
"aws-resolve": _aws_resolve,
"gcp-resolve": _gcp_resolve,
"azure-resolve": _azure_resolve,
"stub-resolve": _stub_resolve,
"missing-creds": _missing_creds,
"sandbox-stubs": _sandbox_stubs,
"aws-profile": _aws_profile,
"builtin-types": _builtin_types,
}
if __name__ == "__main__":
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr)
sys.exit(1)
_COMMANDS[sys.argv[1]]()