fix(resource): implement missing container handler module for container infrastructure resource types #3245
@@ -0,0 +1,277 @@
|
||||
@container-handler
|
||||
Feature: Container infrastructure resource handler module
|
||||
Verifies that the container handler module exists and implements all
|
||||
five handler classes referenced in _resource_registry_container.py:
|
||||
ContainerRuntimeHandler, ContainerImageHandler, ContainerChildHandler,
|
||||
ContainerVolumeHandler, ContainerNetworkHandler.
|
||||
|
||||
All handlers must conform to the ResourceHandler protocol and be
|
||||
importable from cleveragents.resource.handlers.container.
|
||||
|
||||
# ── Module importability ─────────────────────────────────
|
||||
|
||||
Scenario: Container handler module is importable
|
||||
When I import the container handler module
|
||||
Then the container handler module import should succeed without errors
|
||||
|
||||
Scenario Outline: Handler class is importable from container module
|
||||
When I import "<class_name>" from the container handler module
|
||||
Then the class should be importable
|
||||
|
||||
Examples:
|
||||
| class_name |
|
||||
| ContainerRuntimeHandler |
|
||||
| ContainerImageHandler |
|
||||
| ContainerChildHandler |
|
||||
| ContainerVolumeHandler |
|
||||
| ContainerNetworkHandler |
|
||||
|
||||
# ── Protocol conformance ─────────────────────────────────
|
||||
|
||||
Scenario Outline: Handler class conforms to ResourceHandler protocol
|
||||
Given a "<class_name>" instance
|
||||
Then the instance should satisfy the ResourceHandler protocol
|
||||
|
||||
Examples:
|
||||
| class_name |
|
||||
| ContainerRuntimeHandler |
|
||||
| ContainerImageHandler |
|
||||
| ContainerChildHandler |
|
||||
| ContainerVolumeHandler |
|
||||
| ContainerNetworkHandler |
|
||||
|
||||
# ── Handler resolution via resolver ─────────────────────
|
||||
|
||||
Scenario Outline: Handler resolves via resolve_handler for container types
|
||||
When I resolve the handler "<handler_ref>"
|
||||
Then the handler should resolve without HandlerResolutionError
|
||||
|
||||
Examples:
|
||||
| handler_ref |
|
||||
| cleveragents.resource.handlers.container:ContainerRuntimeHandler |
|
||||
| cleveragents.resource.handlers.container:ContainerImageHandler |
|
||||
| cleveragents.resource.handlers.container:ContainerChildHandler |
|
||||
| cleveragents.resource.handlers.container:ContainerVolumeHandler |
|
||||
| cleveragents.resource.handlers.container:ContainerNetworkHandler |
|
||||
|
||||
# ── ContainerRuntimeHandler ──────────────────────────────
|
||||
|
||||
Scenario: ContainerRuntimeHandler has correct type label
|
||||
Given a "ContainerRuntimeHandler" instance
|
||||
Then the handler type_label should be "container-runtime"
|
||||
|
||||
Scenario: ContainerRuntimeHandler resolve raises NotImplementedError
|
||||
Given a "ContainerRuntimeHandler" instance
|
||||
And a container resource of type "container-runtime" with location "/var/run/docker.sock"
|
||||
When I call resolve on the container handler
|
||||
Then a container handler NotImplementedError should be raised
|
||||
|
||||
Scenario: ContainerRuntimeHandler read raises NotImplementedError
|
||||
Given a "ContainerRuntimeHandler" instance
|
||||
And a container resource of type "container-runtime" with location "/var/run/docker.sock"
|
||||
When I call read on the container handler
|
||||
Then a container handler NotImplementedError should be raised
|
||||
|
||||
Scenario: ContainerRuntimeHandler content_hash returns EMPTY_CONTENT_HASH for no location
|
||||
Given a "ContainerRuntimeHandler" instance
|
||||
And a container resource of type "container-runtime" with no location
|
||||
When I call content_hash on the container handler
|
||||
Then the content_hash result should be the EMPTY_CONTENT_HASH sentinel
|
||||
|
||||
Scenario: ContainerRuntimeHandler content_hash returns identity hash for location
|
||||
Given a "ContainerRuntimeHandler" instance
|
||||
And a container resource of type "container-runtime" with location "/var/run/docker.sock"
|
||||
When I call content_hash on the container handler
|
||||
Then the content_hash result should be a non-empty hex string
|
||||
|
||||
# ── ContainerImageHandler ────────────────────────────────
|
||||
|
||||
Scenario: ContainerImageHandler has correct type label
|
||||
Given a "ContainerImageHandler" instance
|
||||
Then the handler type_label should be "container-image"
|
||||
|
||||
Scenario: ContainerImageHandler resolve raises NotImplementedError
|
||||
Given a "ContainerImageHandler" instance
|
||||
And a container resource of type "container-image" with location "ubuntu:22.04"
|
||||
When I call resolve on the container handler
|
||||
Then a container handler NotImplementedError should be raised
|
||||
|
||||
Scenario: ContainerImageHandler content_hash returns identity hash for image ref
|
||||
Given a "ContainerImageHandler" instance
|
||||
And a container resource of type "container-image" with location "ubuntu:22.04"
|
||||
When I call content_hash on the container handler
|
||||
Then the content_hash result should be a non-empty hex string
|
||||
|
||||
# ── ContainerChildHandler ────────────────────────────────
|
||||
|
||||
Scenario: ContainerChildHandler has correct type label
|
||||
Given a "ContainerChildHandler" instance
|
||||
Then the handler type_label should be "container-child"
|
||||
|
||||
Scenario: ContainerChildHandler resolve raises NotImplementedError
|
||||
Given a "ContainerChildHandler" instance
|
||||
And a container resource of type "container-mount" with location "/mnt/data"
|
||||
When I call resolve on the container handler
|
||||
Then a container handler NotImplementedError should be raised
|
||||
|
||||
Scenario: ContainerChildHandler read raises NotImplementedError
|
||||
Given a "ContainerChildHandler" instance
|
||||
And a container resource of type "container-mount" with location "/mnt/data"
|
||||
When I call read on the container handler
|
||||
Then a container handler NotImplementedError should be raised
|
||||
|
||||
# ── ContainerVolumeHandler ───────────────────────────────
|
||||
|
||||
Scenario: ContainerVolumeHandler has correct type label
|
||||
Given a "ContainerVolumeHandler" instance
|
||||
Then the handler type_label should be "container-volume"
|
||||
|
||||
Scenario: ContainerVolumeHandler resolve raises NotImplementedError
|
||||
Given a "ContainerVolumeHandler" instance
|
||||
And a container resource of type "container-volume" with location "my-volume"
|
||||
When I call resolve on the container handler
|
||||
Then a container handler NotImplementedError should be raised
|
||||
|
||||
Scenario: ContainerVolumeHandler content_hash returns identity hash for volume name
|
||||
Given a "ContainerVolumeHandler" instance
|
||||
And a container resource of type "container-volume" with location "my-volume"
|
||||
When I call content_hash on the container handler
|
||||
Then the content_hash result should be a non-empty hex string
|
||||
|
||||
# ── ContainerNetworkHandler ──────────────────────────────
|
||||
|
||||
Scenario: ContainerNetworkHandler has correct type label
|
||||
Given a "ContainerNetworkHandler" instance
|
||||
Then the handler type_label should be "container-network"
|
||||
|
||||
Scenario: ContainerNetworkHandler resolve raises NotImplementedError
|
||||
Given a "ContainerNetworkHandler" instance
|
||||
And a container resource of type "container-network" with location "bridge"
|
||||
When I call resolve on the container handler
|
||||
Then a container handler NotImplementedError should be raised
|
||||
|
||||
Scenario: ContainerNetworkHandler content_hash returns identity hash for network name
|
||||
Given a "ContainerNetworkHandler" instance
|
||||
And a container resource of type "container-network" with location "bridge"
|
||||
When I call content_hash on the container handler
|
||||
Then the content_hash result should be a non-empty hex string
|
||||
|
||||
# ── CRUD stubs ───────────────────────────────────────────
|
||||
|
||||
Scenario Outline: Handler write raises NotImplementedError
|
||||
Given a "<class_name>" instance
|
||||
And a container resource of type "container-runtime" with location "/var/run/docker.sock"
|
||||
When I call write on the container handler with data b"test"
|
||||
Then a container handler NotImplementedError should be raised
|
||||
|
||||
Examples:
|
||||
| class_name |
|
||||
| ContainerRuntimeHandler |
|
||||
| ContainerImageHandler |
|
||||
| ContainerChildHandler |
|
||||
| ContainerVolumeHandler |
|
||||
| ContainerNetworkHandler |
|
||||
|
||||
Scenario Outline: Handler delete raises NotImplementedError
|
||||
Given a "<class_name>" instance
|
||||
And a container resource of type "container-runtime" with location "/var/run/docker.sock"
|
||||
When I call delete on the container handler
|
||||
Then a container handler NotImplementedError should be raised
|
||||
|
||||
Examples:
|
||||
| class_name |
|
||||
| ContainerRuntimeHandler |
|
||||
| ContainerImageHandler |
|
||||
| ContainerChildHandler |
|
||||
| ContainerVolumeHandler |
|
||||
| ContainerNetworkHandler |
|
||||
|
||||
Scenario Outline: Handler list_children raises NotImplementedError
|
||||
Given a "<class_name>" instance
|
||||
And a container resource of type "container-runtime" with location "/var/run/docker.sock"
|
||||
When I call list_children on the container handler
|
||||
Then a container handler NotImplementedError should be raised
|
||||
|
||||
Examples:
|
||||
| class_name |
|
||||
| ContainerRuntimeHandler |
|
||||
| ContainerImageHandler |
|
||||
| ContainerChildHandler |
|
||||
| ContainerVolumeHandler |
|
||||
| ContainerNetworkHandler |
|
||||
|
||||
Scenario Outline: Handler diff raises NotImplementedError
|
||||
Given a "<class_name>" instance
|
||||
And a container resource of type "container-runtime" with location "/var/run/docker.sock"
|
||||
When I call diff on the container handler with other_location "/other"
|
||||
Then a container handler NotImplementedError should be raised
|
||||
|
||||
Examples:
|
||||
| class_name |
|
||||
| ContainerRuntimeHandler |
|
||||
| ContainerImageHandler |
|
||||
| ContainerChildHandler |
|
||||
| ContainerVolumeHandler |
|
||||
| ContainerNetworkHandler |
|
||||
|
||||
Scenario Outline: Handler discover_children raises NotImplementedError
|
||||
Given a "<class_name>" instance
|
||||
And a container resource of type "container-runtime" with location "/var/run/docker.sock"
|
||||
When I call discover_children on the container handler
|
||||
Then a container handler NotImplementedError should be raised
|
||||
|
||||
Examples:
|
||||
| class_name |
|
||||
| ContainerRuntimeHandler |
|
||||
| ContainerImageHandler |
|
||||
| ContainerChildHandler |
|
||||
| ContainerVolumeHandler |
|
||||
| ContainerNetworkHandler |
|
||||
|
||||
# ── Lifecycle stubs ──────────────────────────────────────
|
||||
|
||||
Scenario Outline: Handler create_checkpoint raises NotImplementedError
|
||||
Given a "<class_name>" instance
|
||||
And a container resource of type "container-runtime" with location "/var/run/docker.sock"
|
||||
When I call create_checkpoint on the container handler
|
||||
Then a container handler NotImplementedError should be raised
|
||||
|
||||
Examples:
|
||||
| class_name |
|
||||
| ContainerRuntimeHandler |
|
||||
| ContainerImageHandler |
|
||||
| ContainerChildHandler |
|
||||
| ContainerVolumeHandler |
|
||||
| ContainerNetworkHandler |
|
||||
|
||||
Scenario Outline: Handler rollback_to raises NotImplementedError
|
||||
Given a "<class_name>" instance
|
||||
And a container resource of type "container-runtime" with location "/var/run/docker.sock"
|
||||
When I call rollback_to on the container handler
|
||||
Then a container handler NotImplementedError should be raised
|
||||
|
||||
Examples:
|
||||
| class_name |
|
||||
| ContainerRuntimeHandler |
|
||||
| ContainerImageHandler |
|
||||
| ContainerChildHandler |
|
||||
| ContainerVolumeHandler |
|
||||
| ContainerNetworkHandler |
|
||||
|
||||
# ── Registry integration ─────────────────────────────────
|
||||
|
||||
Scenario: container-runtime handler resolves without error after module creation
|
||||
When I resolve the handler for resource type "container-runtime"
|
||||
Then the handler should resolve without HandlerResolutionError
|
||||
|
||||
Scenario: container-image handler resolves without error after module creation
|
||||
When I resolve the handler for resource type "container-image"
|
||||
Then the handler should resolve without HandlerResolutionError
|
||||
|
||||
Scenario: container-volume handler resolves without error after module creation
|
||||
When I resolve the handler for resource type "container-volume"
|
||||
Then the handler should resolve without HandlerResolutionError
|
||||
|
||||
Scenario: container-network handler resolves without error after module creation
|
||||
When I resolve the handler for resource type "container-network"
|
||||
Then the handler should resolve without HandlerResolutionError
|
||||
@@ -0,0 +1,389 @@
|
||||
"""Step definitions for container_handler.feature.
|
||||
|
||||
Tests for the container infrastructure resource handler module
|
||||
(cleveragents.resource.handlers.container).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from behave import given, then, when # type: ignore[attr-defined]
|
||||
|
||||
from cleveragents.domain.models.core.resource import (
|
||||
PhysVirt,
|
||||
Resource,
|
||||
ResourceCapabilities,
|
||||
)
|
||||
from cleveragents.resource.handlers._base import EMPTY_CONTENT_HASH
|
||||
from cleveragents.resource.handlers.protocol import ResourceHandler
|
||||
|
||||
# ── Handler class registry ───────────────────────────────────
|
||||
|
||||
|
||||
def _get_handler_class(class_name: str) -> type:
|
||||
"""Import and return a handler class from the container module."""
|
||||
from cleveragents.resource.handlers import container as _container_mod
|
||||
|
||||
cls = getattr(_container_mod, class_name, None)
|
||||
if cls is None:
|
||||
raise AssertionError(
|
||||
f"Class '{class_name}' not found in "
|
||||
"cleveragents.resource.handlers.container"
|
||||
)
|
||||
return cls
|
||||
|
||||
|
||||
def _make_container_resource(
|
||||
resource_type: str,
|
||||
location: str | None = None,
|
||||
) -> Resource:
|
||||
"""Create a minimal container resource for testing."""
|
||||
return Resource(
|
||||
resource_id="01KJ5C5TPMP8GGX3QC83E2MAQS",
|
||||
resource_type_name=resource_type,
|
||||
classification=PhysVirt.PHYSICAL,
|
||||
location=location,
|
||||
properties={},
|
||||
capabilities=ResourceCapabilities(
|
||||
readable=True,
|
||||
writable=False,
|
||||
sandboxable=False,
|
||||
checkpointable=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ── When steps ───────────────────────────────────────────────
|
||||
|
||||
|
||||
@when("I import the container handler module")
|
||||
def step_import_container_module(context: Any) -> None:
|
||||
"""Attempt to import the container handler module."""
|
||||
try:
|
||||
import cleveragents.resource.handlers.container as _mod
|
||||
|
||||
context.import_error = None # type: ignore[attr-defined]
|
||||
context.container_module = _mod # type: ignore[attr-defined]
|
||||
except Exception as exc:
|
||||
context.import_error = exc # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when('I import "{class_name}" from the container handler module')
|
||||
def step_import_class_from_container_module(context: Any, class_name: str) -> None:
|
||||
"""Attempt to import a specific class from the container handler module."""
|
||||
try:
|
||||
cls = _get_handler_class(class_name)
|
||||
context.imported_class = cls # type: ignore[attr-defined]
|
||||
context.import_error = None # type: ignore[attr-defined]
|
||||
except Exception as exc:
|
||||
context.import_error = exc # type: ignore[attr-defined]
|
||||
context.imported_class = None # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when('I resolve the handler "<handler_ref>"')
|
||||
def step_resolve_handler_ref_template(context: Any, handler_ref: str) -> None:
|
||||
"""Resolve a handler by reference string (template step)."""
|
||||
step_resolve_handler_ref(context, handler_ref)
|
||||
|
||||
|
||||
@when('I resolve the handler "{handler_ref}"')
|
||||
def step_resolve_handler_ref(context: Any, handler_ref: str) -> None:
|
||||
"""Resolve a handler by reference string."""
|
||||
from cleveragents.resource.handlers.resolver import (
|
||||
HandlerResolutionError,
|
||||
clear_handler_cache,
|
||||
resolve_handler,
|
||||
)
|
||||
|
||||
clear_handler_cache()
|
||||
try:
|
||||
context.resolved_handler = resolve_handler(handler_ref) # type: ignore[attr-defined]
|
||||
context.resolution_error = None # type: ignore[attr-defined]
|
||||
except HandlerResolutionError as exc:
|
||||
context.resolution_error = exc # type: ignore[attr-defined]
|
||||
context.resolved_handler = None # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when('I resolve the handler for resource type "{resource_type}"')
|
||||
def step_resolve_handler_for_type(context: Any, resource_type: str) -> None:
|
||||
"""Resolve a handler for a resource type via the registry."""
|
||||
from cleveragents.application.services._resource_registry_data import BUILTIN_TYPES
|
||||
from cleveragents.resource.handlers.resolver import (
|
||||
HandlerResolutionError,
|
||||
clear_handler_cache,
|
||||
resolve_handler,
|
||||
)
|
||||
|
||||
clear_handler_cache()
|
||||
handler_ref = None
|
||||
for type_def in BUILTIN_TYPES:
|
||||
if type_def["name"] == resource_type:
|
||||
handler_ref = type_def.get("handler")
|
||||
break
|
||||
|
||||
if handler_ref is None:
|
||||
context.resolution_error = AssertionError( # type: ignore[attr-defined]
|
||||
f"Resource type '{resource_type}' not found in BUILTIN_TYPES"
|
||||
)
|
||||
context.resolved_handler = None # type: ignore[attr-defined]
|
||||
return
|
||||
|
||||
try:
|
||||
context.resolved_handler = resolve_handler(handler_ref) # type: ignore[attr-defined]
|
||||
context.resolution_error = None # type: ignore[attr-defined]
|
||||
except HandlerResolutionError as exc:
|
||||
context.resolution_error = exc # type: ignore[attr-defined]
|
||||
context.resolved_handler = None # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when("I call resolve on the container handler")
|
||||
def step_call_resolve(context: Any) -> None:
|
||||
"""Call resolve() on the handler and capture any exception."""
|
||||
mock_sandbox = MagicMock()
|
||||
try:
|
||||
context.handler.resolve( # type: ignore[attr-defined]
|
||||
resource=context.container_resource, # type: ignore[attr-defined]
|
||||
plan_id="test-plan-01",
|
||||
slot_name="test-slot",
|
||||
sandbox_manager=mock_sandbox,
|
||||
access="read_only",
|
||||
)
|
||||
context.raised_exception = None # type: ignore[attr-defined]
|
||||
except Exception as exc:
|
||||
context.raised_exception = exc # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when("I call read on the container handler")
|
||||
def step_call_read(context: Any) -> None:
|
||||
"""Call read() on the handler and capture any exception."""
|
||||
try:
|
||||
context.handler.read( # type: ignore[attr-defined]
|
||||
resource=context.container_resource, # type: ignore[attr-defined]
|
||||
)
|
||||
context.raised_exception = None # type: ignore[attr-defined]
|
||||
except Exception as exc:
|
||||
context.raised_exception = exc # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when('I call write on the container handler with data b"test"')
|
||||
def step_call_write(context: Any) -> None:
|
||||
"""Call write() on the handler and capture any exception."""
|
||||
try:
|
||||
context.handler.write( # type: ignore[attr-defined]
|
||||
resource=context.container_resource, # type: ignore[attr-defined]
|
||||
path="",
|
||||
data=b"test",
|
||||
)
|
||||
context.raised_exception = None # type: ignore[attr-defined]
|
||||
except Exception as exc:
|
||||
context.raised_exception = exc # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when("I call delete on the container handler")
|
||||
def step_call_delete(context: Any) -> None:
|
||||
"""Call delete() on the handler and capture any exception."""
|
||||
try:
|
||||
context.handler.delete( # type: ignore[attr-defined]
|
||||
resource=context.container_resource, # type: ignore[attr-defined]
|
||||
)
|
||||
context.raised_exception = None # type: ignore[attr-defined]
|
||||
except Exception as exc:
|
||||
context.raised_exception = exc # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when("I call list_children on the container handler")
|
||||
def step_call_list_children(context: Any) -> None:
|
||||
"""Call list_children() on the handler and capture any exception."""
|
||||
try:
|
||||
context.handler.list_children( # type: ignore[attr-defined]
|
||||
resource=context.container_resource, # type: ignore[attr-defined]
|
||||
)
|
||||
context.raised_exception = None # type: ignore[attr-defined]
|
||||
except Exception as exc:
|
||||
context.raised_exception = exc # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when('I call diff on the container handler with other_location "/other"')
|
||||
def step_call_diff(context: Any) -> None:
|
||||
"""Call diff() on the handler and capture any exception."""
|
||||
try:
|
||||
context.handler.diff( # type: ignore[attr-defined]
|
||||
resource=context.container_resource, # type: ignore[attr-defined]
|
||||
other_location="/other",
|
||||
)
|
||||
context.raised_exception = None # type: ignore[attr-defined]
|
||||
except Exception as exc:
|
||||
context.raised_exception = exc # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when("I call discover_children on the container handler")
|
||||
def step_call_discover_children(context: Any) -> None:
|
||||
"""Call discover_children() on the handler and capture any exception."""
|
||||
try:
|
||||
context.handler.discover_children( # type: ignore[attr-defined]
|
||||
resource=context.container_resource, # type: ignore[attr-defined]
|
||||
)
|
||||
context.raised_exception = None # type: ignore[attr-defined]
|
||||
except Exception as exc:
|
||||
context.raised_exception = exc # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when("I call create_checkpoint on the container handler")
|
||||
def step_call_create_checkpoint(context: Any) -> None:
|
||||
"""Call create_checkpoint() on the handler and capture any exception."""
|
||||
mock_sandbox = MagicMock()
|
||||
try:
|
||||
context.handler.create_checkpoint( # type: ignore[attr-defined]
|
||||
resource=context.container_resource, # type: ignore[attr-defined]
|
||||
plan_id="test-plan-01",
|
||||
sandbox_manager=mock_sandbox,
|
||||
)
|
||||
context.raised_exception = None # type: ignore[attr-defined]
|
||||
except Exception as exc:
|
||||
context.raised_exception = exc # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when("I call rollback_to on the container handler")
|
||||
def step_call_rollback_to(context: Any) -> None:
|
||||
"""Call rollback_to() on the handler and capture any exception."""
|
||||
mock_sandbox = MagicMock()
|
||||
try:
|
||||
context.handler.rollback_to( # type: ignore[attr-defined]
|
||||
resource=context.container_resource, # type: ignore[attr-defined]
|
||||
plan_id="test-plan-01",
|
||||
checkpoint_id="ckpt-001",
|
||||
sandbox_manager=mock_sandbox,
|
||||
)
|
||||
context.raised_exception = None # type: ignore[attr-defined]
|
||||
except Exception as exc:
|
||||
context.raised_exception = exc # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when("I call content_hash on the container handler")
|
||||
def step_call_content_hash(context: Any) -> None:
|
||||
"""Call content_hash() on the handler and capture the result."""
|
||||
try:
|
||||
context.content_hash_result = context.handler.content_hash( # type: ignore[attr-defined]
|
||||
context.container_resource # type: ignore[attr-defined]
|
||||
)
|
||||
context.raised_exception = None # type: ignore[attr-defined]
|
||||
except Exception as exc:
|
||||
context.raised_exception = exc # type: ignore[attr-defined]
|
||||
context.content_hash_result = None # type: ignore[attr-defined]
|
||||
|
||||
|
||||
# ── Given steps ──────────────────────────────────────────────
|
||||
|
||||
|
||||
@given('a "{class_name}" instance')
|
||||
def step_handler_instance(context: Any, class_name: str) -> None:
|
||||
"""Create a handler instance by class name."""
|
||||
cls = _get_handler_class(class_name)
|
||||
context.handler = cls() # type: ignore[attr-defined]
|
||||
context.handler_class_name = class_name # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@given('a container resource of type "{resource_type}" with location "{location}"')
|
||||
def step_container_resource_with_location(
|
||||
context: Any, resource_type: str, location: str
|
||||
) -> None:
|
||||
"""Create a container resource with a specific location."""
|
||||
context.container_resource = _make_container_resource( # type: ignore[attr-defined]
|
||||
resource_type=resource_type,
|
||||
location=location,
|
||||
)
|
||||
|
||||
|
||||
@given('a container resource of type "{resource_type}" with no location')
|
||||
def step_container_resource_no_location(context: Any, resource_type: str) -> None:
|
||||
"""Create a container resource with no location."""
|
||||
context.container_resource = _make_container_resource( # type: ignore[attr-defined]
|
||||
resource_type=resource_type,
|
||||
location=None,
|
||||
)
|
||||
|
||||
|
||||
# ── Then steps ───────────────────────────────────────────────
|
||||
|
||||
|
||||
@then("the container handler module import should succeed without errors")
|
||||
def step_import_succeeded(context: Any) -> None:
|
||||
"""Assert that the import succeeded."""
|
||||
assert context.import_error is None, ( # type: ignore[attr-defined]
|
||||
f"Import failed with: {context.import_error}" # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
|
||||
@then("the class should be importable")
|
||||
def step_class_importable(context: Any) -> None:
|
||||
"""Assert that the class was imported successfully."""
|
||||
assert context.import_error is None, ( # type: ignore[attr-defined]
|
||||
f"Class import failed with: {context.import_error}" # type: ignore[attr-defined]
|
||||
)
|
||||
assert context.imported_class is not None # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@then("the instance should satisfy the ResourceHandler protocol")
|
||||
def step_satisfies_protocol(context: Any) -> None:
|
||||
"""Assert that the handler instance satisfies ResourceHandler protocol."""
|
||||
assert isinstance(context.handler, ResourceHandler), ( # type: ignore[attr-defined]
|
||||
f"{context.handler_class_name} does not satisfy ResourceHandler protocol" # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
|
||||
@then("the handler should resolve without HandlerResolutionError")
|
||||
def step_handler_resolved(context: Any) -> None:
|
||||
"""Assert that the handler resolved without error."""
|
||||
assert context.resolution_error is None, ( # type: ignore[attr-defined]
|
||||
f"Handler resolution failed: {context.resolution_error}" # type: ignore[attr-defined]
|
||||
)
|
||||
assert context.resolved_handler is not None # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@then('the handler type_label should be "{expected_label}"')
|
||||
def step_handler_type_label(context: Any, expected_label: str) -> None:
|
||||
"""Assert the handler's _type_label class variable."""
|
||||
actual = getattr(context.handler, "_type_label", None) # type: ignore[attr-defined]
|
||||
assert actual == expected_label, (
|
||||
f"Expected _type_label='{expected_label}', got '{actual}'"
|
||||
)
|
||||
|
||||
|
||||
@then("a container handler NotImplementedError should be raised")
|
||||
def step_not_implemented_raised(context: Any) -> None:
|
||||
"""Assert that a NotImplementedError was raised."""
|
||||
exc = context.raised_exception # type: ignore[attr-defined]
|
||||
assert exc is not None, "Expected NotImplementedError but no exception was raised"
|
||||
assert isinstance(exc, NotImplementedError), (
|
||||
f"Expected NotImplementedError but got {type(exc).__name__}: {exc}"
|
||||
)
|
||||
|
||||
|
||||
@then("the content_hash result should be the EMPTY_CONTENT_HASH sentinel")
|
||||
def step_content_hash_is_empty(context: Any) -> None:
|
||||
"""Assert that content_hash returned the EMPTY_CONTENT_HASH sentinel."""
|
||||
result = context.content_hash_result # type: ignore[attr-defined]
|
||||
assert result == EMPTY_CONTENT_HASH, (
|
||||
f"Expected EMPTY_CONTENT_HASH but got: {result!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the content_hash result should be a non-empty hex string")
|
||||
def step_content_hash_is_hex(context: Any) -> None:
|
||||
"""Assert that content_hash returned a non-empty hex string."""
|
||||
result = context.content_hash_result # type: ignore[attr-defined]
|
||||
assert result is not None, "content_hash returned None"
|
||||
assert isinstance(result, str), f"Expected str, got {type(result)}"
|
||||
assert len(result) > 0, "content_hash returned empty string"
|
||||
assert result != EMPTY_CONTENT_HASH, (
|
||||
"content_hash returned EMPTY_CONTENT_HASH but expected identity hash"
|
||||
)
|
||||
# Verify it's a valid hex string
|
||||
try:
|
||||
int(result, 16)
|
||||
except ValueError as exc:
|
||||
raise AssertionError(
|
||||
f"content_hash result is not a valid hex string: {result!r}"
|
||||
) from exc
|
||||
@@ -14,13 +14,17 @@ which defines a single ``resolve`` method returning a
|
||||
|
||||
## Built-in Handlers
|
||||
|
||||
| Handler | Resource Type(s) | Strategy |
|
||||
|--------------------------|---------------------------|----------------------|
|
||||
| ``GitCheckoutHandler`` | ``git-checkout`` | ``git_worktree`` |
|
||||
| ``FsDirectoryHandler`` | ``fs-directory`` | ``copy_on_write`` |
|
||||
| ``DevcontainerHandler`` | ``devcontainer-instance`` | ``snapshot`` |
|
||||
| ``CloudResourceHandler`` | ``cloud-*``, ``aws-*`` | ``none`` |
|
||||
| ``DatabaseHandler`` | ``postgres``, ``mysql`` | ``transaction`` |
|
||||
- ``GitCheckoutHandler`` — ``git-checkout`` (git_worktree)
|
||||
- ``FsDirectoryHandler`` — ``fs-directory`` (copy_on_write)
|
||||
- ``DevcontainerHandler`` — ``devcontainer-instance`` (snapshot)
|
||||
- ``CloudResourceHandler`` — ``cloud-*``, ``aws-*`` (none)
|
||||
- ``DatabaseHandler`` — ``postgres``, ``mysql`` (transaction)
|
||||
- ``ContainerRuntimeHandler`` — ``container-runtime`` (none)
|
||||
- ``ContainerImageHandler`` — ``container-image`` (none)
|
||||
- ``ContainerChildHandler`` — ``container-mount``, ``container-exec-env``,
|
||||
``container-port`` (snapshot)
|
||||
- ``ContainerVolumeHandler`` — ``container-volume`` (snapshot)
|
||||
- ``ContainerNetworkHandler`` — ``container-network`` (none)
|
||||
|
||||
## Handler Resolution
|
||||
|
||||
@@ -30,6 +34,13 @@ dynamically imports the module and returns an instance.
|
||||
"""
|
||||
|
||||
from cleveragents.resource.handlers.cloud import CloudResourceHandler
|
||||
from cleveragents.resource.handlers.container import (
|
||||
ContainerChildHandler,
|
||||
ContainerImageHandler,
|
||||
ContainerNetworkHandler,
|
||||
ContainerRuntimeHandler,
|
||||
ContainerVolumeHandler,
|
||||
)
|
||||
from cleveragents.resource.handlers.database import DatabaseResourceHandler
|
||||
from cleveragents.resource.handlers.devcontainer import DevcontainerHandler
|
||||
from cleveragents.resource.handlers.fs_directory import FsDirectoryHandler
|
||||
@@ -54,6 +65,11 @@ __all__ = [
|
||||
"AccessResult",
|
||||
"CheckpointResult",
|
||||
"CloudResourceHandler",
|
||||
"ContainerChildHandler",
|
||||
"ContainerImageHandler",
|
||||
"ContainerNetworkHandler",
|
||||
"ContainerRuntimeHandler",
|
||||
"ContainerVolumeHandler",
|
||||
"Content",
|
||||
"DatabaseResourceHandler",
|
||||
"DeleteResult",
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
"""Container infrastructure resource handlers for CleverAgents.
|
||||
|
||||
Implements the five handler classes referenced in
|
||||
``_resource_registry_container.py`` for the seven built-in container
|
||||
infrastructure resource types:
|
||||
|
||||
- ``ContainerRuntimeHandler`` -- for ``container-runtime``
|
||||
- ``ContainerImageHandler`` -- for ``container-image``
|
||||
- ``ContainerChildHandler`` -- for ``container-mount``, ``container-exec-env``,
|
||||
``container-port`` (shared child handler)
|
||||
- ``ContainerVolumeHandler`` -- for ``container-volume``
|
||||
- ``ContainerNetworkHandler`` -- for ``container-network``
|
||||
|
||||
All handlers extend BaseResourceHandler and satisfy the ResourceHandler protocol.
|
||||
|
||||
Container resources use ``sandbox_strategy = "none"`` (or ``"snapshot"``
|
||||
for child/volume types) because the container runtime itself provides
|
||||
isolation. Actual container SDK operations are NOT implemented --
|
||||
calling :meth:`resolve` raises :exc:`NotImplementedError` after basic
|
||||
validation. This mirrors the pattern used by CloudResourceHandler.
|
||||
|
||||
Based on:
|
||||
- src/cleveragents/application/services/_resource_registry_container.py
|
||||
- docs/specification.md ~line 25096-25114 (container handler table)
|
||||
- ADR-039: Container Resource Types
|
||||
- Issue #2907: Container infrastructure resource types missing handler module
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from cleveragents.domain.models.core.resource import Resource, SandboxStrategy
|
||||
from cleveragents.infrastructure.sandbox.manager import SandboxManager
|
||||
from cleveragents.resource.handlers._base import EMPTY_CONTENT_HASH, BaseResourceHandler
|
||||
from cleveragents.resource.handlers.protocol import (
|
||||
Content,
|
||||
DeleteResult,
|
||||
DiffResult,
|
||||
WriteResult,
|
||||
)
|
||||
from cleveragents.tool.context import BoundResource
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
"ContainerChildHandler",
|
||||
"ContainerImageHandler",
|
||||
"ContainerNetworkHandler",
|
||||
"ContainerRuntimeHandler",
|
||||
"ContainerVolumeHandler",
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _identity_hash(resource: Resource, algorithm: str = "sha256") -> str:
|
||||
"""Compute an identity hash for a container resource.
|
||||
|
||||
The hash is based on the resource type name and location (e.g. socket
|
||||
path, image reference, volume name, network name). Returns
|
||||
:data:`EMPTY_CONTENT_HASH` if no location is set.
|
||||
|
||||
Args:
|
||||
resource: The container resource.
|
||||
algorithm: Hash algorithm name (default ``sha256``).
|
||||
|
||||
Returns:
|
||||
Hex-encoded hash digest, or :data:`EMPTY_CONTENT_HASH`.
|
||||
"""
|
||||
if not resource.location:
|
||||
return EMPTY_CONTENT_HASH
|
||||
h = hashlib.new(algorithm)
|
||||
h.update(resource.resource_type_name.encode("utf-8"))
|
||||
h.update(b"\0")
|
||||
h.update(resource.location.encode("utf-8"))
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared base for all container handlers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _ContainerBaseHandler(BaseResourceHandler):
|
||||
"""Shared base for all container infrastructure handlers.
|
||||
|
||||
Provides:
|
||||
- ``resolve`` -- raises NotImplementedError (container sandbox
|
||||
provisioning is not yet implemented).
|
||||
- ``content_hash`` -- identity hash based on resource type + location.
|
||||
- All CRUD and lifecycle stubs -- raise NotImplementedError.
|
||||
|
||||
Subclasses set ``_default_strategy`` and ``_type_label``.
|
||||
"""
|
||||
|
||||
def resolve(
|
||||
self,
|
||||
*,
|
||||
resource: Resource,
|
||||
plan_id: str,
|
||||
slot_name: str,
|
||||
sandbox_manager: SandboxManager,
|
||||
access: str = "read_only",
|
||||
) -> BoundResource:
|
||||
"""Raise -- container sandbox provisioning is not yet implemented.
|
||||
|
||||
Args:
|
||||
resource: A container resource instance.
|
||||
plan_id: The plan requesting the sandbox.
|
||||
slot_name: Name of the tool resource slot being filled.
|
||||
sandbox_manager: The sandbox lifecycle manager (unused).
|
||||
access: Access mode (unused).
|
||||
|
||||
Raises:
|
||||
NotImplementedError: Always.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
f"Container resource sandbox provisioning is not yet implemented. "
|
||||
f"Resource '{resource.resource_id}' "
|
||||
f"(type={resource.resource_type_name}, "
|
||||
f"plan={plan_id}, slot={slot_name})."
|
||||
)
|
||||
|
||||
# -- CRUD stubs --------------------------------------------------------
|
||||
|
||||
def read(self, *, resource: Resource, path: str = "") -> Content:
|
||||
"""Not supported for container resources."""
|
||||
raise NotImplementedError(f"{self._type_label} handler does not support read()")
|
||||
|
||||
def write(self, *, resource: Resource, path: str, data: bytes) -> WriteResult:
|
||||
"""Not supported for container resources."""
|
||||
raise NotImplementedError(
|
||||
f"{self._type_label} handler does not support write()"
|
||||
)
|
||||
|
||||
def delete(self, *, resource: Resource, path: str = "") -> DeleteResult:
|
||||
"""Not supported for container resources."""
|
||||
raise NotImplementedError(
|
||||
f"{self._type_label} handler does not support delete()"
|
||||
)
|
||||
|
||||
def list_children(self, *, resource: Resource) -> list[str]:
|
||||
"""Not supported for container resources."""
|
||||
raise NotImplementedError(
|
||||
f"{self._type_label} handler does not support list_children()"
|
||||
)
|
||||
|
||||
def diff(self, *, resource: Resource, other_location: str) -> DiffResult:
|
||||
"""Not supported for container resources."""
|
||||
raise NotImplementedError(f"{self._type_label} handler does not support diff()")
|
||||
|
||||
def discover_children(self, *, resource: Resource) -> list[Resource]:
|
||||
"""Not supported for container resources."""
|
||||
raise NotImplementedError(
|
||||
f"{self._type_label} handler does not support discover_children()"
|
||||
)
|
||||
|
||||
# -- Lifecycle stubs ---------------------------------------------------
|
||||
|
||||
def create_sandbox(
|
||||
self,
|
||||
*,
|
||||
resource: Resource,
|
||||
plan_id: str,
|
||||
sandbox_manager: SandboxManager,
|
||||
) -> Any:
|
||||
"""Not supported for container resources."""
|
||||
raise NotImplementedError(
|
||||
f"{self._type_label} handler does not support create_sandbox()"
|
||||
)
|
||||
|
||||
def create_checkpoint(
|
||||
self,
|
||||
*,
|
||||
resource: Resource,
|
||||
plan_id: str,
|
||||
sandbox_manager: SandboxManager,
|
||||
phase: str = "execution",
|
||||
) -> Any:
|
||||
"""Not supported for container resources."""
|
||||
raise NotImplementedError(
|
||||
f"{self._type_label} handler does not support create_checkpoint()"
|
||||
)
|
||||
|
||||
def rollback_to(
|
||||
self,
|
||||
*,
|
||||
resource: Resource,
|
||||
plan_id: str,
|
||||
checkpoint_id: str,
|
||||
sandbox_manager: SandboxManager,
|
||||
) -> Any:
|
||||
"""Not supported for container resources."""
|
||||
raise NotImplementedError(
|
||||
f"{self._type_label} handler does not support rollback_to()"
|
||||
)
|
||||
|
||||
def project_access(
|
||||
self,
|
||||
*,
|
||||
resource: Resource,
|
||||
principal: str,
|
||||
action: str = "read",
|
||||
project_id: str = "",
|
||||
) -> Any:
|
||||
"""Not supported for container resources."""
|
||||
raise NotImplementedError(
|
||||
f"{self._type_label} handler does not support project_access()"
|
||||
)
|
||||
|
||||
# -- Content hashing ---------------------------------------------------
|
||||
|
||||
def content_hash(
|
||||
self,
|
||||
resource: Resource,
|
||||
*,
|
||||
algorithm: str = "sha256",
|
||||
) -> str:
|
||||
"""Compute an identity hash for a container resource.
|
||||
|
||||
The hash is based on the resource type name and location.
|
||||
Returns :data:`EMPTY_CONTENT_HASH` if no location is set.
|
||||
|
||||
Args:
|
||||
resource: The container resource.
|
||||
algorithm: Hash algorithm name (default ``sha256``).
|
||||
|
||||
Returns:
|
||||
Hex-encoded hash digest, or :data:`EMPTY_CONTENT_HASH`.
|
||||
"""
|
||||
return _identity_hash(resource, algorithm)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Concrete handler classes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ContainerRuntimeHandler(_ContainerBaseHandler):
|
||||
"""Handler for ``container-runtime`` resource types.
|
||||
|
||||
A container runtime resource represents a container engine
|
||||
(Docker, Podman, containerd, etc.) identified by its socket path
|
||||
or remote endpoint URL.
|
||||
|
||||
Sandbox provisioning is NOT implemented -- calling :meth:`resolve`
|
||||
raises :exc:`NotImplementedError`.
|
||||
|
||||
This handler satisfies the ResourceHandler protocol.
|
||||
"""
|
||||
|
||||
_default_strategy = SandboxStrategy.NONE
|
||||
_type_label = "container-runtime"
|
||||
|
||||
|
||||
class ContainerImageHandler(_ContainerBaseHandler):
|
||||
"""Handler for ``container-image`` resource types.
|
||||
|
||||
A container image resource represents an immutable container image
|
||||
identified by tag and/or digest (e.g. ``ubuntu:22.04``,
|
||||
``ghcr.io/org/app@sha256:...``).
|
||||
|
||||
Sandbox provisioning is NOT implemented -- calling :meth:`resolve`
|
||||
raises :exc:`NotImplementedError`.
|
||||
|
||||
This handler satisfies the ResourceHandler protocol.
|
||||
"""
|
||||
|
||||
_default_strategy = SandboxStrategy.NONE
|
||||
_type_label = "container-image"
|
||||
|
||||
|
||||
class ContainerChildHandler(_ContainerBaseHandler):
|
||||
"""Shared handler for container child resource types.
|
||||
|
||||
Used by three resource types that represent child components of a
|
||||
running container instance:
|
||||
|
||||
- ``container-mount`` -- bind mount or volume mount inside a container
|
||||
- ``container-exec-env`` -- execution environment variables and config
|
||||
- ``container-port`` -- port mapping between host and container
|
||||
|
||||
These resources are not user-addable (``user_addable = False``) and
|
||||
are discovered automatically from a running container instance.
|
||||
|
||||
Sandbox provisioning is NOT implemented -- calling :meth:`resolve`
|
||||
raises :exc:`NotImplementedError`.
|
||||
|
||||
This handler satisfies the ResourceHandler protocol.
|
||||
"""
|
||||
|
||||
_default_strategy = SandboxStrategy.SNAPSHOT
|
||||
_type_label = "container-child"
|
||||
|
||||
|
||||
class ContainerVolumeHandler(_ContainerBaseHandler):
|
||||
"""Handler for ``container-volume`` resource types.
|
||||
|
||||
A container volume resource represents a named container volume
|
||||
managed by the runtime engine (Docker, Podman, etc.).
|
||||
|
||||
Sandbox provisioning is NOT implemented -- calling :meth:`resolve`
|
||||
raises :exc:`NotImplementedError`.
|
||||
|
||||
This handler satisfies the ResourceHandler protocol.
|
||||
"""
|
||||
|
||||
_default_strategy = SandboxStrategy.SNAPSHOT
|
||||
_type_label = "container-volume"
|
||||
|
||||
|
||||
class ContainerNetworkHandler(_ContainerBaseHandler):
|
||||
"""Handler for ``container-network`` resource types.
|
||||
|
||||
A container network resource represents a container network
|
||||
(bridge, host, overlay, macvlan, or none).
|
||||
|
||||
Sandbox provisioning is NOT implemented -- calling :meth:`resolve`
|
||||
raises :exc:`NotImplementedError`.
|
||||
|
||||
This handler satisfies the ResourceHandler protocol.
|
||||
"""
|
||||
|
||||
_default_strategy = SandboxStrategy.NONE
|
||||
_type_label = "container-network"
|
||||
Reference in New Issue
Block a user