Merge pull request 'feat(resources): implement cloud infrastructure resource types (AWS, GCP, Azure stubs)' (#10592) from feat/v360/cloud-resource-types into master
CI / load-versions (push) Successful in 15s
CI / push-validation (push) Successful in 29s
CI / lint (push) Successful in 41s
CI / typecheck (push) Successful in 57s
CI / quality (push) Successful in 48s
CI / security (push) Successful in 1m13s
CI / build (push) Successful in 56s
CI / helm (push) Successful in 40s
CI / unit_tests (push) Successful in 4m42s
CI / docker (push) Successful in 2m11s
CI / integration_tests (push) Failing after 8m33s
CI / coverage (push) Successful in 9m35s
CI / status-check (push) Failing after 3s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (push) Has been cancelled
CI / load-versions (push) Successful in 15s
CI / push-validation (push) Successful in 29s
CI / lint (push) Successful in 41s
CI / typecheck (push) Successful in 57s
CI / quality (push) Successful in 48s
CI / security (push) Successful in 1m13s
CI / build (push) Successful in 56s
CI / helm (push) Successful in 40s
CI / unit_tests (push) Successful in 4m42s
CI / docker (push) Successful in 2m11s
CI / integration_tests (push) Failing after 8m33s
CI / coverage (push) Successful in 9m35s
CI / status-check (push) Failing after 3s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (push) Has been cancelled
This commit was merged in pull request #10592.
This commit is contained in:
@@ -73,6 +73,7 @@ Below are some of the specific details of various contributions.
|
||||
|
||||
* 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.
|
||||
* HAL 9000 has contributed the ACMS context path matching fix (PR #10975 / issue #10972): corrects `_path_matches()` and `_matches_pattern()` to properly match absolute fragment paths against relative glob patterns by auto-prefixing with `**/` before calling `PurePath.full_match()`, preventing silent inefficacy of include/exclude filters for absolute paths in fragment metadata.
|
||||
* HAL 9000 has contributed database resource types (PostgreSQL, SQLite) with transaction-based sandbox strategy: implemented ``DatabaseResourceHandler`` providing full CRUD operations (`read`, `write`, `delete`, `list_children`) and connection validation with automatic credential masking for PostgreSQL and SQLite backends. Includes ``TransactionSandbox`` infrastructure wired into ``SandboxFactory``, BDD test coverage in ``features/database_resources.feature``, and Robot Framework integration tests in ``robot/database_resources.robot`` (PR #10591 / issue #8608, Epic #8568).
|
||||
* HAL 9000 has contributed the agents plan rollback command (PR #8674 / issue #8557): implemented checkpoint-based plan state restoration with the `agents plan rollback <plan-id> [<checkpoint-id>]` CLI command as part of Epic #8493, enabling plans to be restored to previous checkpoints, discarding post-checkpoint decisions, and resuming execution from the rolled-back state. Supported by `--yes/-y`, `--to-checkpoint`, and `--format/-f` flags. Includes comprehensive BDD test coverage (>= 97%) for rollback, decision discarding, and plan resume functionality.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
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 reject an invalid provider
|
||||
|
||||
Scenario: CloudResource accepts and stores resource_type field
|
||||
Given a new CloudResource with type "ec2_instance"
|
||||
When the resource_type is accepted as expected
|
||||
Then the resource_type should be "ec2_instance"
|
||||
|
||||
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 reject tags with empty key
|
||||
|
||||
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,267 @@
|
||||
"""Behave step definitions for cloud resource type stubs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
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 ValidationError as exc:
|
||||
context.exc = exc
|
||||
|
||||
|
||||
@when('an invalid provider "oracle" is set')
|
||||
def step_set_invalid_provider(context):
|
||||
"""Set invalid provider and expect a validation error."""
|
||||
data = dict(context.pending_resource)
|
||||
data["provider"] = "oracle"
|
||||
try:
|
||||
context.exc = None
|
||||
context.result = CloudResource(**data)
|
||||
except ValidationError 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 reject an invalid provider")
|
||||
def step_rejects_invalid_provider(context):
|
||||
"""Assert a ValidationError was raised for an invalid provider."""
|
||||
assert context.exc is not None
|
||||
assert isinstance(context.exc, ValidationError)
|
||||
|
||||
|
||||
@given('a new CloudResource with type "{resource_type}"')
|
||||
def step_new_cloud_resource_with_type(context, resource_type: str):
|
||||
"""Store a CloudResource with specified resource_type for construction."""
|
||||
context.pending_resource = {
|
||||
"resource_id": "cloud-test-001",
|
||||
"type": resource_type,
|
||||
}
|
||||
|
||||
|
||||
@when("the resource_type is accepted as expected")
|
||||
def step_cloud_resource_type_accepted(context):
|
||||
"""Create the CloudResource with pending data including provider."""
|
||||
data = dict(context.pending_resource)
|
||||
data["provider"] = "aws"
|
||||
try:
|
||||
context.exc = None
|
||||
context.result = CloudResource(**data)
|
||||
except ValidationError as exc:
|
||||
context.exc = exc
|
||||
|
||||
|
||||
@then('the resource_type should be "{expected}"')
|
||||
def step_resource_type_is(context, expected: str):
|
||||
"""Assert the constructed resource has the expected resource_type."""
|
||||
assert context.exc is None
|
||||
assert context.result.type == expected
|
||||
|
||||
|
||||
@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 ValidationError 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 ValidationError as exc:
|
||||
context.exc = exc
|
||||
|
||||
|
||||
@then("it should reject tags with empty key")
|
||||
def step_rejects_empty_tag_key(context):
|
||||
"""Assert a ValidationError was raised for a tag with an empty key."""
|
||||
assert context.exc is not None
|
||||
assert isinstance(context.exc, ValidationError)
|
||||
|
||||
|
||||
@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 ValidationError 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 ValidationError 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 ValidationError 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 ValidationError 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"
|
||||
|
||||
|
||||
# ------------------------ GCPResource provider assertion --------------------
|
||||
|
||||
|
||||
@then('the resource should have provider "gcp"')
|
||||
def step_gcp_provider_is_gcp(context):
|
||||
"""Assert GCPResource has provider 'gcp'."""
|
||||
assert context.exc is None
|
||||
assert context.result.provider == "gcp"
|
||||
@@ -199,7 +199,7 @@ def run(
|
||||
except CleverAgentsError as exc:
|
||||
typer.echo(f"Error: {exc}", err=True)
|
||||
raise typer.Exit(code=2) from exc
|
||||
except Exception as exc: # pragma: no cover
|
||||
except Exception as exc:
|
||||
typer.echo(f"Unexpected error: {exc}", err=True)
|
||||
raise typer.Exit(code=3) from exc
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ def run(
|
||||
except CleverAgentsError as exc:
|
||||
typer.echo(f"Error: {exc}", err=True)
|
||||
raise typer.Exit(code=2) from exc
|
||||
except Exception as exc: # pragma: no cover
|
||||
except Exception as exc:
|
||||
typer.echo(f"Unexpected error: {exc}", err=True)
|
||||
raise typer.Exit(code=3) from exc
|
||||
|
||||
|
||||
@@ -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 (
|
||||
CLOUD_PROVIDERS,
|
||||
AWSResource,
|
||||
AzureResource,
|
||||
CloudResource,
|
||||
GCPResource,
|
||||
)
|
||||
from cleveragents.resource.inheritance import (
|
||||
MAX_CHAIN_DEPTH,
|
||||
ResourceTypeCircularInheritanceError,
|
||||
@@ -20,8 +32,13 @@ from cleveragents.resource.virtual import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CLOUD_PROVIDERS",
|
||||
"MAX_CHAIN_DEPTH",
|
||||
"APIEndpointResource",
|
||||
"AWSResource",
|
||||
"AzureResource",
|
||||
"CloudResource",
|
||||
"GCPResource",
|
||||
"MetricResource",
|
||||
"ResourceTypeCircularInheritanceError",
|
||||
"ResourceTypeInheritanceDepthError",
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
"""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, type, region, resrc id, state |
|
||||
| ``AWSResource`` | AWS | arn, tags, properties |
|
||||
| ``GCPResource`` | GCP | project_id, properties |
|
||||
| ``AzureResource`` | Azure | sub_id, tenant_id, props |
|
||||
|
||||
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).",
|
||||
)
|
||||
type: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Type of cloud resource (e.g., s3_bucket, ec2_instance, gcs_bucket)."
|
||||
),
|
||||
)
|
||||
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"type={self.type!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) -> str | None:
|
||||
"""Validate GCP region is non-empty.
|
||||
|
||||
Args:
|
||||
v: Region string to validate.
|
||||
|
||||
Returns:
|
||||
Lowercased, stripped region or None.
|
||||
"""
|
||||
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) -> str | None:
|
||||
"""Validate Azure region is non-empty.
|
||||
|
||||
Args:
|
||||
v: Region string to validate.
|
||||
|
||||
Returns:
|
||||
Lowercased, stripped region or None.
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CLOUD_PROVIDERS",
|
||||
"AWSResource",
|
||||
"AzureResource",
|
||||
"CloudResource",
|
||||
"GCPResource",
|
||||
]
|
||||
Reference in New Issue
Block a user