diff --git a/features/container_handler.feature b/features/container_handler.feature new file mode 100644 index 000000000..10fb76385 --- /dev/null +++ b/features/container_handler.feature @@ -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 "" 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 "" 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 "" + 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 "" 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 "" 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 "" 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 "" 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 "" 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 "" 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 "" 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 diff --git a/features/steps/container_handler_steps.py b/features/steps/container_handler_steps.py new file mode 100644 index 000000000..c01a81e43 --- /dev/null +++ b/features/steps/container_handler_steps.py @@ -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 ""') +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 diff --git a/src/cleveragents/resource/handlers/__init__.py b/src/cleveragents/resource/handlers/__init__.py index 1ea044434..ce0e99ff4 100644 --- a/src/cleveragents/resource/handlers/__init__.py +++ b/src/cleveragents/resource/handlers/__init__.py @@ -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", diff --git a/src/cleveragents/resource/handlers/container.py b/src/cleveragents/resource/handlers/container.py new file mode 100644 index 000000000..3fb8b0599 --- /dev/null +++ b/src/cleveragents/resource/handlers/container.py @@ -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"