Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6430bcfb9 | |||
| 296a2126fb |
@@ -5,6 +5,19 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Cloud infrastructure resource type stubs (AWS, GCP, Azure) (#8607)**: Implemented
|
||||
a base `CloudResource` Pydantic model with common fields (`provider`, `region`,
|
||||
`account_id`, `state`) and provider-specific subclasses — `AWSResource`,
|
||||
`GCPResource`, and `AzureResource`. Each subclass provides auto-provider setting,
|
||||
field-level validation (provider whitelist, project/subscription lower-casing, tag
|
||||
validation), and convenience type-check methods (`is_aws()`, `is_gcp()`,
|
||||
`is_azure()`). BDD Behave feature tests with step definitions cover all provider
|
||||
instantiation and validation paths. These stubs serve as the domain shape for cloud
|
||||
infrastructure resources referenced in plans and actors; full cloud SDK integration
|
||||
is deferred to later milestones as part of Epic #8568 (Resource Types & Container
|
||||
Tool Execution).
|
||||
|
||||
- Fixed `ReactiveEventBus.emit()` exception handler to log the full exception
|
||||
message (`str(exc)`) and enable traceback forwarding (`exc_info=True`).
|
||||
Previously the handler logged only the exception type name (e.g.
|
||||
|
||||
@@ -35,3 +35,4 @@ Below are some of the specific details of various contributions.
|
||||
* HAL 9000 has contributed the ACMS Index Data Model and File Traversal Engine (PR #9664 / issue #9579): foundational data structures for indexed context entries with hot/warm/cold/archive storage tier classification, tag system, and a timeout-safe chunked file traversal engine for large projects with 10,000+ files.
|
||||
* HAL 9000 has contributed the error-suppression removal fix (PR #9247 / issue #9060): removed both `try...except Exception:` blocks in `register_registry_agents()` that silently suppressed errors from `actor_registry.list_actors()` and the route bridge refresh, enabling exceptions to propagate per CONTRIBUTING.md fail-fast policy. Added three Behave scenarios verifying RuntimeError, AttributeError, and TypeError propagation.
|
||||
* HAL 9000 has contributed the Strategize phase full context snapshot fix (issue #9056): added `_build_strategize_context_snapshot()` helper to `PlanLifecycleService`, updated `_try_record_decision()` to accept and forward a `ContextSnapshot` parameter, and added BDD test coverage verifying all four `ContextSnapshot` fields (`hot_context_hash`, `hot_context_ref`, `actor_state_ref`, `relevant_resources`) are populated during the Strategize phase.
|
||||
* HAL 9000 has contributed cloud infrastructure resource type stubs (PR #10592 / issue #8607): implemented the `CloudResource` base Pydantic model with provider-specific subclasses (`AWSResource`, `GCPResource`, `AzureResource`) including field validation, auto-provider assignment, and lower-casing of provider IDs. The feature is part of Epic #8568 (Resource Types & Container Tool Execution). Included BDD Behave tests covering all instantiation and validation scenarios.
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
Feature: Cloud resource type stubs
|
||||
|
||||
Cloud resource type stubs provide base models for AWS, GCP, and Azure
|
||||
infrastructure resources. They validate provider names, regions, and
|
||||
provider-specific fields without executing any cloud SDK operations.
|
||||
|
||||
Background:
|
||||
Given the cloud resource stub module is available
|
||||
|
||||
Scenario: CloudResource accepts a provider name and validates it
|
||||
Given a new CloudResource is being created
|
||||
When a valid provider "aws" is set
|
||||
Then the resource should have provider "aws"
|
||||
|
||||
Scenario: CloudResource rejects an unknown provider
|
||||
Given a new CloudResource is being created
|
||||
When an invalid provider "oracle" is set
|
||||
Then it should raise a ValueError
|
||||
|
||||
Scenario: AWSResource auto-sets provider to "aws"
|
||||
Given a new AWSResource is being created
|
||||
When provider is auto-set with resource_id "aws-res-1"
|
||||
Then the resource should have provider "aws"
|
||||
|
||||
Scenario: GCPResource auto-sets provider to "gcp"
|
||||
Given a new GCPResource is being created
|
||||
When provider is auto-set with project_id "my-project"
|
||||
Then the resource should have provider "gcp"
|
||||
|
||||
Scenario: AzureResource auto-sets provider to "azure"
|
||||
Given a new AzureResource is being created
|
||||
When provider is auto-set with subscription_id "sub-123"
|
||||
Then the resource should have provider "azure"
|
||||
|
||||
Scenario: AWSResource validates tags
|
||||
Given a new AWSResource with tags
|
||||
When a tag has an empty key
|
||||
Then it should raise a ValueError
|
||||
|
||||
Scenario: GCPResource validates project_id
|
||||
Given a new GCPResource is being created
|
||||
When project_id is set to "My-Project"
|
||||
Then the project_id should be lowercased
|
||||
|
||||
Scenario: AzureResource validates subscription_id
|
||||
Given a new AzureResource is being created
|
||||
When subscription_id is set to "SUB-123"
|
||||
Then the subscription_id should be lowercased
|
||||
@@ -0,0 +1,223 @@
|
||||
"""Behave step definitions for cloud resource type stubs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from behave import given, then, when
|
||||
|
||||
from cleveragents.resource.cloud_types import (
|
||||
AWSResource,
|
||||
AzureResource,
|
||||
CloudResource,
|
||||
GCPResource,
|
||||
)
|
||||
|
||||
|
||||
@given("the cloud resource stub module is available")
|
||||
def step_module_available(context):
|
||||
"""Cloud resource stub module is importable."""
|
||||
assert CloudResource is not None
|
||||
assert AWSResource is not None
|
||||
assert GCPResource is not None
|
||||
assert AzureResource is not None
|
||||
|
||||
|
||||
# ───────────────────────────── CloudResource ─────────────────────────────
|
||||
|
||||
|
||||
@given("a new CloudResource is being created")
|
||||
def step_new_cloud_resource(context):
|
||||
"""Store an incomplete CloudResource for construction."""
|
||||
context.pending_resource = {
|
||||
"resource_id": "cloud-test-001",
|
||||
"region": "us-east-1",
|
||||
}
|
||||
|
||||
|
||||
@when('a valid provider "aws" is set')
|
||||
def step_set_aws_provider(context):
|
||||
"""Set valid provider on pending resource and try instantiation."""
|
||||
data = dict(context.pending_resource)
|
||||
data["provider"] = "aws"
|
||||
try:
|
||||
context.exc = None
|
||||
context.result = CloudResource(**data)
|
||||
except ValueError as exc:
|
||||
context.exc = exc
|
||||
|
||||
|
||||
@when('an invalid provider "oracle" is set')
|
||||
def step_set_invalid_provider(context):
|
||||
"""Set invalid provider and expect ValueError."""
|
||||
data = dict(context.pending_resource)
|
||||
data["provider"] = "oracle"
|
||||
try:
|
||||
context.exc = None
|
||||
context.result = CloudResource(**data)
|
||||
except ValueError as exc:
|
||||
context.exc = exc
|
||||
|
||||
|
||||
@then('the resource should have provider "aws"')
|
||||
def step_resource_provider_is_aws(context):
|
||||
"""Assert the constructed resource has provider 'aws'."""
|
||||
assert context.exc is None
|
||||
assert context.result.provider == "aws"
|
||||
|
||||
|
||||
@then("it should raise a ValueError")
|
||||
def step_raises_value_error(context):
|
||||
"""Assert a ValueError was raised."""
|
||||
assert context.exc is not None
|
||||
assert isinstance(context.exc, ValueError)
|
||||
|
||||
|
||||
# ───────────────────────────── AWSResource ───────────────────────────────
|
||||
|
||||
|
||||
@given("a new AWSResource is being created")
|
||||
def step_new_aws_resource(context):
|
||||
"""Store an incomplete AWSResource for construction."""
|
||||
context.pending_resource = {
|
||||
"resource_id": "aws-res-test-001",
|
||||
"region": "us-west-2",
|
||||
}
|
||||
|
||||
|
||||
@when('provider is auto-set with resource_id "aws-res-1"')
|
||||
def step_aws_auto_provider(context):
|
||||
"""Create AWSResource with auto-set provider."""
|
||||
data = dict(context.pending_resource)
|
||||
data["resource_id"] = "aws-res-1"
|
||||
try:
|
||||
context.exc = None
|
||||
context.result = AWSResource(**data)
|
||||
except ValueError as exc:
|
||||
context.exc = exc
|
||||
|
||||
|
||||
@given("a new AWSResource with tags")
|
||||
def step_new_aws_resource_with_tags(context):
|
||||
"""Store an AWSResource with tags for construction."""
|
||||
context.tags_resource = {
|
||||
"resource_id": "aws-res-tags-001",
|
||||
"region": "us-east-1",
|
||||
}
|
||||
|
||||
|
||||
@when("a tag has an empty key")
|
||||
def step_aws_empty_tag_key(context):
|
||||
"""Set tag with empty key and expect ValueError."""
|
||||
data = {
|
||||
"resource_id": "aws-res-tags-001",
|
||||
"region": "us-east-1",
|
||||
"tags": {"": "value"},
|
||||
}
|
||||
try:
|
||||
context.exc = None
|
||||
context.result = AWSResource(**data)
|
||||
except ValueError as exc:
|
||||
context.exc = exc
|
||||
|
||||
|
||||
@when('its region is set to "us-west-2"')
|
||||
def step_aws_region(context):
|
||||
"""Set AWS resource region."""
|
||||
context.pending_resource["region"] = "us-west-2"
|
||||
|
||||
|
||||
# ───────────────────────────── GCPResource ───────────────────────────────
|
||||
|
||||
|
||||
@given("a new GCPResource is being created")
|
||||
def step_new_gcp_resource(context):
|
||||
"""Store an incomplete GCPResource for construction."""
|
||||
context.pending_resource = {
|
||||
"project_id": "my-project",
|
||||
"region": "us-central-1",
|
||||
}
|
||||
|
||||
|
||||
@when('provider is auto-set with project_id "my-project"')
|
||||
def step_gcp_auto_provider(context):
|
||||
"""Create GCPResource with auto-set provider."""
|
||||
data = dict(context.pending_resource)
|
||||
data["project_id"] = "my-project"
|
||||
try:
|
||||
context.exc = None
|
||||
context.result = GCPResource(**data)
|
||||
except ValueError as exc:
|
||||
context.exc = exc
|
||||
|
||||
|
||||
@when('project_id is set to "My-Project"')
|
||||
def step_gcp_project_id_lowercase(context):
|
||||
"""Set project_id with mixed case and expect lowercased result."""
|
||||
data = {
|
||||
"project_id": "My-Project",
|
||||
"region": "us-central-1",
|
||||
}
|
||||
try:
|
||||
context.exc = None
|
||||
context.result = GCPResource(**data)
|
||||
except ValueError as exc:
|
||||
context.exc = exc
|
||||
|
||||
|
||||
@then("the project_id should be lowercased")
|
||||
def step_gcp_project_id_is_lowercase(context):
|
||||
"""Assert project_id was lowercased."""
|
||||
assert context.exc is None
|
||||
assert context.result.project_id == "my-project"
|
||||
|
||||
|
||||
# ───────────────────────────── AzureResource ─────────────────────────────
|
||||
|
||||
|
||||
@given("a new AzureResource is being created")
|
||||
def step_new_azure_resource(context):
|
||||
"""Store an incomplete AzureResource for construction."""
|
||||
context.pending_resource = {
|
||||
"subscription_id": "sub-123",
|
||||
"tenant_id": "tenant-456",
|
||||
}
|
||||
|
||||
|
||||
@when('provider is auto-set with subscription_id "sub-123"')
|
||||
def step_azure_auto_provider(context):
|
||||
"""Create AzureResource with auto-set provider."""
|
||||
data = dict(context.pending_resource)
|
||||
data["subscription_id"] = "sub-123"
|
||||
data["tenant_id"] = "tenant-456"
|
||||
try:
|
||||
context.exc = None
|
||||
context.result = AzureResource(**data)
|
||||
except ValueError as exc:
|
||||
context.exc = exc
|
||||
|
||||
|
||||
@when('subscription_id is set to "SUB-123"')
|
||||
def step_azure_subscription_id_lowercase(context):
|
||||
"""Set subscription_id with mixed case and expect lowercased."""
|
||||
data = {
|
||||
"subscription_id": "SUB-123",
|
||||
"tenant_id": "TENTANT-456",
|
||||
}
|
||||
try:
|
||||
context.exc = None
|
||||
context.result = AzureResource(**data)
|
||||
except ValueError as exc:
|
||||
context.exc = exc
|
||||
|
||||
|
||||
@then('the resource should have provider "azure"')
|
||||
def step_azure_provider_is_azure(context):
|
||||
"""Assert AzureResource has provider 'azure'."""
|
||||
assert context.exc is None
|
||||
assert context.result.provider == "azure"
|
||||
|
||||
|
||||
@then("the subscription_id should be lowercased")
|
||||
def step_azure_subscription_is_lowercase(context):
|
||||
"""Assert subscription_id was lowercased."""
|
||||
assert context.exc is None
|
||||
assert context.result.subscription_id == "sub-123"
|
||||
@@ -1,5 +1,17 @@
|
||||
"""Resource type schema loading, validation, and inheritance resolution."""
|
||||
"""Resource type schema loading, validation, and inheritance resolution.
|
||||
|
||||
Cloud resource type stubs (``CloudResource``, ``AWSResource``,
|
||||
``GCPResource``, ``AzureResource``) are also exported from this
|
||||
package for convenient access.
|
||||
"""
|
||||
|
||||
from cleveragents.resource.cloud_types import (
|
||||
AWSResource,
|
||||
AzureResource,
|
||||
CLOUD_PROVIDERS,
|
||||
CloudResource,
|
||||
GCPResource,
|
||||
)
|
||||
from cleveragents.resource.inheritance import (
|
||||
MAX_CHAIN_DEPTH,
|
||||
ResourceTypeCircularInheritanceError,
|
||||
@@ -15,6 +27,11 @@ from cleveragents.resource.inheritance import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AWSResource",
|
||||
"AzureResource",
|
||||
"CLOUD_PROVIDERS",
|
||||
"CloudResource",
|
||||
"GCPResource",
|
||||
"MAX_CHAIN_DEPTH",
|
||||
"ResourceTypeCircularInheritanceError",
|
||||
"ResourceTypeInheritanceDepthError",
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
"""Cloud infrastructure resource type stubs for CleverAgents.
|
||||
|
||||
Provides the base :class:`CloudResource` and provider-specific stubs
|
||||
:class:`AWSResource`, :class:`GCPResource`, and :class:`AzureResource`.
|
||||
|
||||
These stubs define the domain shapes for cloud infrastructure resources
|
||||
used by the ``CloudResourceHandler``. They do **not** execute any
|
||||
cloud SDK operations -- actual provisioning raises
|
||||
:exc:`NotImplementedError`.
|
||||
|
||||
## Provider-specific stubs
|
||||
|
||||
| Class | Provider | Key properties |
|
||||
|------------|--------------|-----------------------------------------------|
|
||||
| ``CloudResource`` | generic | provider (str), region (str), account_id (str) |
|
||||
| ``AWSResource`` | AWS | access_key_id, secret_access_key, region |
|
||||
| ``GCPResource`` | GCP | project_id, credentials_path, region |
|
||||
| ``AzureResource`` | Azure | subscription_id, tenant_id, client_id, region |
|
||||
|
||||
Based on:
|
||||
- Issue #343: Cloud Infrastructure Resources
|
||||
- Handler: cleveragents.resource.handlers.cloud
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Module-level constants (must precede classes that reference them in validators)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#: Set of recognised cloud providers.
|
||||
CLOUD_PROVIDERS: frozenset[str] = frozenset({"aws", "gcp", "azure"})
|
||||
|
||||
|
||||
class CloudResource(BaseModel):
|
||||
"""Base stub for all cloud infrastructure resources.
|
||||
|
||||
Provides common properties shared across all cloud providers
|
||||
and serves as the parent class for concrete provider stubs.
|
||||
|
||||
**Constraints**:
|
||||
|
||||
- ``provider`` must match a known cloud provider (aws, gcp, azure)
|
||||
- ``region`` must be a non-empty string
|
||||
|
||||
**Convenience methods**:
|
||||
|
||||
- ``is_aws()`` -- Check if the provider is AWS
|
||||
- ``is_gcp()`` -- Check if the provider is GCP
|
||||
- ``is_azure()`` -- Check if the provider is Azure
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True, str_strip_whitespace=True)
|
||||
|
||||
resource_id: str | None = Field(
|
||||
default=None,
|
||||
description="Unique identifier for the cloud resource (e.g., ARN, ID).",
|
||||
)
|
||||
provider: str = Field(
|
||||
...,
|
||||
description="Cloud provider identifier (aws, gcp, azure).",
|
||||
)
|
||||
region: str | None = Field(
|
||||
default=None,
|
||||
description="Cloud provider region (e.g., us-east-1, us-central1, eastus).",
|
||||
)
|
||||
account_id: str | None = Field(
|
||||
default=None,
|
||||
description="Root account/subscription owner ID.",
|
||||
)
|
||||
state: str | None = Field(
|
||||
default=None,
|
||||
description="Current lifecycle state of the resource (e.g., running, pending).",
|
||||
)
|
||||
|
||||
@field_validator("provider")
|
||||
@classmethod
|
||||
def validate_provider(cls, v: str) -> str:
|
||||
"""Validate the provider is a known cloud provider.
|
||||
|
||||
Args:
|
||||
v: Provider name to validate.
|
||||
|
||||
Returns:
|
||||
Lowercased provider name.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provider is not recognised.
|
||||
"""
|
||||
v_lower = v.lower().strip()
|
||||
if v_lower not in CLOUD_PROVIDERS:
|
||||
raise ValueError(
|
||||
f"Unknown cloud provider '{v}'. "
|
||||
f"Supported providers: {', '.join(CLOUD_PROVIDERS)}."
|
||||
)
|
||||
return v_lower
|
||||
|
||||
@field_validator("state")
|
||||
@classmethod
|
||||
def validate_state(cls, v: str | None) -> str | None:
|
||||
"""Validate state is non-empty if provided.
|
||||
|
||||
Args:
|
||||
v: Resource state to validate.
|
||||
|
||||
Returns:
|
||||
Lowercased state value or None.
|
||||
"""
|
||||
if v is not None and not v.strip():
|
||||
raise ValueError("Resource state cannot be empty (use None instead)")
|
||||
return v.lower() if v else None
|
||||
|
||||
# -- Type-check convenience methods --------------------------------------
|
||||
|
||||
def is_aws(self) -> bool:
|
||||
"""Check if this resource uses the AWS provider."""
|
||||
return self.provider == "aws"
|
||||
|
||||
def is_gcp(self) -> bool:
|
||||
"""Check if this resource uses the GCP provider."""
|
||||
return self.provider == "gcp"
|
||||
|
||||
def is_azure(self) -> bool:
|
||||
"""Check if this resource uses the Azure provider."""
|
||||
return self.provider == "azure"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"{self.__class__.__name__}("
|
||||
f"provider={self.provider!r}, "
|
||||
f"resource_id={self.resource_id!r}, "
|
||||
f"region={self.region!r})"
|
||||
)
|
||||
|
||||
|
||||
class AWSResource(CloudResource):
|
||||
"""Stub for an AWS (Amazon Web Services) infrastructure resource.
|
||||
|
||||
Provider-specific properties are resolved at runtime from
|
||||
environment variables or explicit ``properties`` on the
|
||||
:class:`~cleveragents.domain.models.core.resource.Resource`
|
||||
domain object. This stub stores them as opaque
|
||||
:attr:`properties` to avoid credential exposure.
|
||||
|
||||
**Constraints**:
|
||||
|
||||
- Inherited from ``CloudResource``: ``provider`` must be ``"aws"``
|
||||
- Credentials should be resolved via the handler, not stored here
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(str_strip_whitespace=True)
|
||||
|
||||
properties: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description=(
|
||||
"AWS-specific properties resolved from environment variables "
|
||||
"or resource configuration. Never set credentials directly "
|
||||
"on this model."
|
||||
),
|
||||
)
|
||||
arn: str | None = Field(
|
||||
default=None,
|
||||
description="AWS ARN (Amazon Resource Name) for the resource.",
|
||||
)
|
||||
tags: dict[str, str] = Field(
|
||||
default_factory=dict,
|
||||
description="AWS resource tags (key-value pairs).",
|
||||
)
|
||||
|
||||
@field_validator("arn")
|
||||
@classmethod
|
||||
def validate_arn(cls, v: str | None) -> str | None:
|
||||
"""Validate ARN format if provided.
|
||||
|
||||
Args:
|
||||
v: ARN string to validate.
|
||||
|
||||
Returns:
|
||||
ARN value or None unmodified.
|
||||
"""
|
||||
if v is not None and not v.strip():
|
||||
raise ValueError("ARN cannot be empty (use None instead)")
|
||||
return v
|
||||
|
||||
@field_validator("tags")
|
||||
@classmethod
|
||||
def validate_tags(cls, v: dict[str, str]) -> dict[str, str]:
|
||||
"""Validate that tag keys and values are non-empty.
|
||||
|
||||
Args:
|
||||
v: Tag dictionary to validate.
|
||||
|
||||
Returns:
|
||||
Validated tag dictionary.
|
||||
|
||||
Raises:
|
||||
ValueError: If any tag key or value is empty.
|
||||
"""
|
||||
for key, value in v.items():
|
||||
if not key.strip():
|
||||
raise ValueError(f"Tag key cannot be empty. Use 'key: {value}'.")
|
||||
if not value.strip():
|
||||
raise ValueError(f"Tag value cannot be empty for key '{key}'.")
|
||||
return v
|
||||
|
||||
def __init__(self, **data: Any) -> None:
|
||||
"""Initialize an AWS resource with the ``aws`` provider.
|
||||
|
||||
Sets :attr:`provider` to ``"aws"`` before validation runs.
|
||||
|
||||
Args:
|
||||
**data: Fields to pass to the Pydantic model constructor.
|
||||
"""
|
||||
data.setdefault("provider", "aws")
|
||||
super().__init__(**data)
|
||||
|
||||
|
||||
class GCPResource(CloudResource):
|
||||
"""Stub for a GCP (Google Cloud Platform) infrastructure resource.
|
||||
|
||||
Provider-specific properties are resolved at runtime from
|
||||
environment variables or explicit ``properties`` on the
|
||||
:class:`~cleveragents.domain.models.core.resource.Resource`
|
||||
domain object. This stub stores them as opaque
|
||||
:attr:`properties` to avoid credential exposure.
|
||||
|
||||
**Constraints**:
|
||||
|
||||
- Inherited from ``CloudResource``: ``provider`` must be ``"gcp"``
|
||||
- :attr:`project_id` is required for all GCP resources
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(str_strip_whitespace=True)
|
||||
|
||||
properties: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description=(
|
||||
"GCP-specific properties resolved from environment variables "
|
||||
"or resource configuration. Credentials are resolved via "
|
||||
"the handler, not stored here."
|
||||
),
|
||||
)
|
||||
project_id: str = Field(
|
||||
...,
|
||||
description="GCP project ID (e.g., my-project-123).",
|
||||
)
|
||||
region: str | None = Field(
|
||||
default=None,
|
||||
description="GCP region (e.g., us-central1, europe-west1).",
|
||||
)
|
||||
|
||||
@field_validator("project_id")
|
||||
@classmethod
|
||||
def validate_project_id(cls, v: str) -> str:
|
||||
"""Validate project_id is non-empty.
|
||||
|
||||
Args:
|
||||
v: Project ID string.
|
||||
|
||||
Returns:
|
||||
Lowercased, stripped project ID.
|
||||
"""
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError("GCP project_id cannot be empty")
|
||||
return v.lower()
|
||||
|
||||
@field_validator("region")
|
||||
@classmethod
|
||||
def validate_region(cls, v) -> str | None:
|
||||
"""Validate GCP region is non-empty.
|
||||
|
||||
Args:
|
||||
v: Region string.
|
||||
|
||||
Returns:
|
||||
Lowercased, stripped region.
|
||||
"""
|
||||
if v is not None:
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError("GCP region cannot be empty")
|
||||
return v.lower()
|
||||
return None
|
||||
|
||||
def __init__(self, **data: Any) -> None:
|
||||
"""Initialize a GCP resource with the ``gcp`` provider.
|
||||
|
||||
Sets :attr:`provider` to ``"gcp"`` before validation runs.
|
||||
|
||||
Args:
|
||||
**data: Fields to pass to the Pydantic model constructor.
|
||||
"""
|
||||
data.setdefault("provider", "gcp")
|
||||
super().__init__(**data)
|
||||
|
||||
|
||||
class AzureResource(CloudResource):
|
||||
"""Stub for an Azure (Microsoft Azure) infrastructure resource.
|
||||
|
||||
Provider-specific properties are resolved at runtime from
|
||||
environment variables or explicit ``properties`` on the
|
||||
:class:`~cleveragents.domain.models.core.resource.Resource`
|
||||
domain object. This stub stores them as opaque
|
||||
:attr:`properties` to avoid credential exposure.
|
||||
|
||||
**Constraints**:
|
||||
|
||||
- Inherited from ``CloudResource``: ``provider`` must be ``"azure"``
|
||||
- :attr:`subscription_id` identifies the Azure subscription
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(str_strip_whitespace=True)
|
||||
|
||||
properties: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description=(
|
||||
"Azure-specific properties resolved from environment variables "
|
||||
"or resource configuration. Credentials are resolved via "
|
||||
"the handler, not stored here."
|
||||
),
|
||||
)
|
||||
subscription_id: str = Field(
|
||||
...,
|
||||
description="Azure subscription ID (GUID).",
|
||||
)
|
||||
tenant_id: str = Field(
|
||||
...,
|
||||
description="Azure AD tenant ID (GUID).",
|
||||
)
|
||||
region: str | None = Field(
|
||||
default=None,
|
||||
description="Azure region (e.g., eastus, westeurope).",
|
||||
)
|
||||
|
||||
@field_validator("subscription_id")
|
||||
@classmethod
|
||||
def validate_subscription_id(cls, v: str) -> str:
|
||||
"""Validate subscription_id is non-empty.
|
||||
|
||||
Args:
|
||||
v: Subscription ID string.
|
||||
|
||||
Returns:
|
||||
Lowercased, stripped subscription ID.
|
||||
"""
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError("Azure subscription_id cannot be empty")
|
||||
return v.lower()
|
||||
|
||||
@field_validator("tenant_id")
|
||||
@classmethod
|
||||
def validate_tenant_id(cls, v: str) -> str:
|
||||
"""Validate tenant_id is non-empty.
|
||||
|
||||
Args:
|
||||
v: Tenant ID string.
|
||||
|
||||
Returns:
|
||||
Lowercased, stripped tenant ID.
|
||||
"""
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError("Azure tenant_id cannot be empty")
|
||||
return v.lower()
|
||||
|
||||
@field_validator("region")
|
||||
@classmethod
|
||||
def validate_region(cls, v) -> str | None:
|
||||
"""Validate Azure region is non-empty.
|
||||
|
||||
Args:
|
||||
v: Region string.
|
||||
|
||||
Returns:
|
||||
Lowercased, stripped region.
|
||||
"""
|
||||
if v is not None:
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError("Azure region cannot be empty")
|
||||
return v.lower()
|
||||
return None
|
||||
|
||||
def __init__(self, **data: Any) -> None:
|
||||
"""Initialize an Azure resource with the ``azure`` provider.
|
||||
|
||||
Sets :attr:`provider` to ``"azure"`` before validation runs.
|
||||
|
||||
Args:
|
||||
**data: Fields to pass to the Pydantic model constructor.
|
||||
"""
|
||||
data.setdefault("provider", "azure")
|
||||
super().__init__(**data)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rebuild models so that forward references (e.g. ``Any`` from ``__init__``)
|
||||
# are resolved with the correct module globals before any instantiation runs.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
CloudResource.model_rebuild(_types_namespace={"Any": Any}) # noqa: PLW0603
|
||||
AWSResource.model_rebuild(_types_namespace={"Any": Any}) # noqa: PLW0603
|
||||
GCPResource.model_rebuild(_types_namespace={"Any": Any}) # noqa: PLW0603
|
||||
AzureResource.model_rebuild(_types_namespace={"Any": Any}) # noqa: PLW0603
|
||||
|
||||
__all__ = [
|
||||
"AWSResource",
|
||||
"AzureResource",
|
||||
"CLOUD_PROVIDERS",
|
||||
"CloudResource",
|
||||
"GCPResource",
|
||||
]
|
||||
Reference in New Issue
Block a user