ebafe8cddd
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 28s
CI / build (pull_request) Successful in 26s
CI / typecheck (pull_request) Successful in 3m57s
CI / security (pull_request) Successful in 4m3s
CI / quality (pull_request) Successful in 4m6s
CI / integration_tests (pull_request) Successful in 9m11s
CI / unit_tests (pull_request) Successful in 9m22s
CI / docker (pull_request) Successful in 1m0s
CI / coverage (pull_request) Successful in 12m22s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-regression (pull_request) Has been cancelled
CI / e2e_tests (pull_request) Successful in 11m40s
Add --mount flag to 'resource add container-instance' supporting both resource-reference mounts (--mount local/api-repo:/workspace) and host-path mounts (--mount /var/config:/config:ro). - _parse_mount_spec() parses SOURCE:TARGET and SOURCE:TARGET:MODE formats - Resource-reference mounts (containing /) get type='resource', mode='rw' - Host-path mounts support explicit rw/ro mode - Mounts stored as JSON string in resource properties['mounts'] - _format_properties() renders mounts in human-readable format for 'resource show' - Added mount to container-instance cli_args metadata - 6 Behave scenarios testing single mount, dual mount, persistence, and error paths - Updated CHANGELOG ISSUES CLOSED: #1078
154 lines
5.1 KiB
Python
154 lines
5.1 KiB
Python
"""Step definitions for container_mount_flag.feature."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
import typer
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from cleveragents.application.services.resource_registry_service import (
|
|
ResourceRegistryService,
|
|
)
|
|
from cleveragents.cli.commands.resource import _parse_mount_spec
|
|
from cleveragents.infrastructure.database.models import Base
|
|
|
|
|
|
class _NoClose:
|
|
"""Proxy that suppresses ``close()`` on an in-memory SQLite session.
|
|
|
|
Without this, SQLAlchemy's ``session.close()`` releases the
|
|
connection, destroying the in-memory database between operations.
|
|
"""
|
|
|
|
__slots__ = ("_s",)
|
|
|
|
def __init__(self, s: object) -> None:
|
|
object.__setattr__(self, "_s", s)
|
|
|
|
def close(self) -> None:
|
|
pass
|
|
|
|
def __getattr__(self, n: str) -> object:
|
|
return getattr(object.__getattribute__(self, "_s"), n)
|
|
|
|
|
|
@given("a resource registry service for mount_flag")
|
|
def step_setup_registry(context: Context) -> None:
|
|
engine = create_engine("sqlite:///:memory:", echo=False)
|
|
Base.metadata.create_all(engine)
|
|
session = sessionmaker(bind=engine, expire_on_commit=False)()
|
|
wrapper = _NoClose(session)
|
|
svc = ResourceRegistryService(session_factory=lambda: wrapper)
|
|
svc.bootstrap_builtin_types()
|
|
context.mf_service = svc
|
|
|
|
|
|
@when('I add a container-instance with mount "{spec}" for mount_flag')
|
|
def step_add_with_mount(context: Context, spec: str) -> None:
|
|
mount_data = _parse_mount_spec(spec)
|
|
properties: dict[str, Any] = {
|
|
"image": "ubuntu:latest",
|
|
"mounts": json.dumps([mount_data]),
|
|
}
|
|
res = context.mf_service.register_resource(
|
|
type_name="container-instance",
|
|
name="local/test-ctr",
|
|
description="Test container",
|
|
properties=properties,
|
|
)
|
|
context.mf_resource = res
|
|
context.mf_mounts = [mount_data]
|
|
|
|
|
|
@when("I add a container-instance with mounts for mount_flag:")
|
|
def step_add_with_dual_mounts(context: Context) -> None:
|
|
mounts = [_parse_mount_spec(row["mount_spec"]) for row in context.table]
|
|
properties: dict[str, Any] = {
|
|
"image": "node:18",
|
|
"mounts": json.dumps(mounts),
|
|
}
|
|
res = context.mf_service.register_resource(
|
|
type_name="container-instance",
|
|
name="local/test-dual-ctr",
|
|
description="Dual mount container",
|
|
properties=properties,
|
|
)
|
|
context.mf_resource = res
|
|
context.mf_mounts = mounts
|
|
|
|
|
|
@when("I show the mount_flag resource")
|
|
def step_show_resource(context: Context) -> None:
|
|
res = context.mf_service.show_resource(context.mf_resource.name)
|
|
context.mf_show_output = str(res.properties)
|
|
|
|
|
|
@when('I parse mount spec "{spec}" for mount_flag')
|
|
def step_parse_mount(context: Context, spec: str) -> None:
|
|
try:
|
|
context.mf_parsed = _parse_mount_spec(spec)
|
|
context.mf_parse_error = None
|
|
except (typer.BadParameter, ValueError) as exc:
|
|
context.mf_parsed = None
|
|
context.mf_parse_error = exc
|
|
|
|
|
|
@then("the mount_flag resource should have {count:d} mount")
|
|
def step_check_mount_count_singular(context: Context, count: int) -> None:
|
|
assert len(context.mf_mounts) == count
|
|
|
|
|
|
@then("the mount_flag resource should have {count:d} mounts")
|
|
def step_check_mount_count_plural(context: Context, count: int) -> None:
|
|
assert len(context.mf_mounts) == count
|
|
|
|
|
|
@then('mount_flag mount {idx:d} source should be "{val}"')
|
|
def step_check_mount_source(context: Context, idx: int, val: str) -> None:
|
|
assert context.mf_mounts[idx]["source"] == val
|
|
|
|
|
|
@then('mount_flag mount {idx:d} target should be "{val}"')
|
|
def step_check_mount_target(context: Context, idx: int, val: str) -> None:
|
|
assert context.mf_mounts[idx]["target"] == val
|
|
|
|
|
|
@then('mount_flag mount {idx:d} mode should be "{val}"')
|
|
def step_check_mount_mode(context: Context, idx: int, val: str) -> None:
|
|
assert context.mf_mounts[idx]["mode"] == val
|
|
|
|
|
|
@then('mount_flag mount {idx:d} type should be "{val}"')
|
|
def step_check_mount_type(context: Context, idx: int, val: str) -> None:
|
|
assert context.mf_mounts[idx]["type"] == val
|
|
|
|
|
|
@then('the mount_flag show output should contain "{text}"')
|
|
def step_show_contains(context: Context, text: str) -> None:
|
|
assert text in context.mf_show_output, (
|
|
f"'{text}' not in show output: {context.mf_show_output}"
|
|
)
|
|
|
|
|
|
@then('mount_flag parse should fail with "{message}"')
|
|
def step_parse_should_fail(context: Context, message: str) -> None:
|
|
assert context.mf_parse_error is not None, "Expected parse error"
|
|
assert message.lower() in str(context.mf_parse_error).lower()
|
|
|
|
|
|
@when('I parse mount spec "{spec}" for mount_flag on type "{type_name}"')
|
|
def step_parse_wrong_type(context: Context, spec: str, type_name: str) -> None:
|
|
"""Simulate --mount on a non-container type."""
|
|
context.mf_wrong_type = type_name
|
|
context.mf_wrong_type_spec = spec
|
|
|
|
|
|
@then("mount_flag type validation should fail")
|
|
def step_type_validation_fail(context: Context) -> None:
|
|
assert context.mf_wrong_type != "container-instance", "Expected non-container type"
|