diff --git a/README.md b/README.md index 7ec34bb..c37c2d7 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ provisioners: port: 6379 db: 0 -services: +service-definitions: - name: qwen1.5-vllm type: container description: DeepSeek Qwen 1.5B via vLLM @@ -170,8 +170,8 @@ Configuration reference - `hosts[]`: Named machines and their IPs. - `provisioners[]`: Defines where the provisioner runs and its state cache (Redis). -- `services[]`: Descriptions of services, including hardware `varieties` and - runtime `profiles`. +- `service-definitions[]`: Descriptions of services, including hardware + `varieties` and runtime `profiles`. CLI usage --------- diff --git a/dev/resources/settings.yml b/dev/resources/settings.yml index 47e2087..ccb5093 100644 --- a/dev/resources/settings.yml +++ b/dev/resources/settings.yml @@ -38,7 +38,7 @@ provisioners: port: 6479 db: 0 -services: +service-definitions: - name: simple_test_1 type: container description: Simple test service diff --git a/src/__init__.py b/src/__init__.py index 83f265c..ff2dd98 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,3 +1,3 @@ """the orchestration module coordinates the models and context classes into -one or more services. +one or more service_definitions. """ diff --git a/src/api/provisioner.py b/src/api/provisioner.py index b0b1ff9..fcb92a9 100644 --- a/src/api/provisioner.py +++ b/src/api/provisioner.py @@ -1,7 +1,7 @@ """FastAPI application for the Ozwald Provisioner service. -This API allows an orchestrator to control which services are provisioned -and provides information about available resources. +This API allows an orchestrator to control which service_definitions +are provisioned and provides information about available resources. """ from __future__ import annotations @@ -349,7 +349,7 @@ async def _get_service_runner_logs( provisioner = SystemProvisioner.singleton() cache = provisioner.get_cache() - # Try to resolve container name from active services + # Try to resolve container name from active service_definitions active_cache = ActiveServicesCache(cache) active_services = active_cache.get_services() diff --git a/src/command/ozwald.py b/src/command/ozwald.py index 030679f..b01b1d3 100644 --- a/src/command/ozwald.py +++ b/src/command/ozwald.py @@ -394,7 +394,9 @@ def _parse_services_spec(spec: str) -> list[dict[str, Any]]: for raw in [p.strip() for p in (spec or "").split(",") if p.strip()]: result.append(_parse_services_spec_entry(raw, cfg)) if not result: - raise ValueError("No services parsed from specification string") + raise ValueError( + "No service_definitions parsed from specification string" + ) return result @@ -466,7 +468,9 @@ def _parse_footprint_spec(spec: str) -> list[dict[str, Any]]: for raw in [p.strip() for p in (spec or "").split(",") if p.strip()]: result.append(_parse_footprint_spec_entry(raw, cfg)) if not result: - raise ValueError("No services parsed from footprint specification") + raise ValueError( + "No service_definitions parsed from footprint specification" + ) return result @@ -483,12 +487,13 @@ def action_footprint_services( if all_services: if spec: print( - "Warning: services specification ignored when using --all" + "Warning: service_definitions specification ignored when " + "using --all" ) else: if not spec: print( - "Error: services specification is required when " + "Error: service_definitions specification is required when " "not using --all", ) return 2 @@ -505,7 +510,9 @@ def action_footprint_services( print(f"Unexpected response: {json.dumps(data)}") return 2 except Exception as e: - print(f"Error footprinting services: {type(e).__name__}({e})") + print( + f"Error footprinting service_definitions: {type(e).__name__}({e})" + ) return 2 @@ -516,7 +523,7 @@ def action_update_services(port: int, clear: bool, spec: str | None) -> int: else: if not spec: print( - "Error: services specification is required when " + "Error: service_definitions specification is required when " "not using --clear", ) return 2 @@ -534,7 +541,7 @@ def action_update_services(port: int, clear: bool, spec: str | None) -> int: print(f"Unexpected response: {json.dumps(data)}") return 2 except Exception as e: - print(f"Error updating services: {type(e).__name__}({e})") + print(f"Error updating service_definitions: {type(e).__name__}({e})") return 2 @@ -696,13 +703,14 @@ def build_parser() -> argparse.ArgumentParser: "--clear", action="store_true", help=( - "For update_services: send an empty list to clear active services" + "For update_services: send an empty list to clear active " + "service_definitions" ), ) parser.add_argument( "--all", action="store_true", - help="For footprint_services: footprint all services", + help="For footprint_services: footprint all service_definitions", ) parser.add_argument( "--profile", diff --git a/src/config/reader.py b/src/config/reader.py index abf47ec..596c76a 100644 --- a/src/config/reader.py +++ b/src/config/reader.py @@ -39,7 +39,7 @@ def __init__(self, config_path: str): # Initialize attributes that will be populated self.hosts: List[Host] = [] - self.services: List[ServiceDefinition] = [] + self.service_definitions: List[ServiceDefinition] = [] self.provisioners: List[Provisioner] = [] self.networks: List[Network] = [] # Top-level named volumes (normalized) @@ -69,7 +69,7 @@ def _parse_config(self) -> None: self._parse_hosts() self._parse_networks() self._parse_volumes() - self._parse_services() + self._parse_service_definitions() self._parse_provisioners() # ---------------- Internal helpers ----------------- @@ -202,15 +202,16 @@ def _parse_networks(self) -> None: network = Network(name=network_data["name"]) self.networks.append(network) - def _parse_services(self) -> None: - """Parse services section and create ServiceDefinition models. + def _parse_service_definitions(self) -> None: + """ + Parse service-definitions section and create ServiceDefinition models. Supports service-level profiles and varieties. Varieties behave like alternative definitions (e.g., different container images) that can override docker-compose-like fields; parent-level fields are used as defaults and merged appropriately. """ - services_data = self._raw_config.get("services", []) + services_data = self._raw_config.get("service-definitions", []) for i, service_data in enumerate(services_data): if "name" not in service_data: @@ -346,7 +347,7 @@ def _parse_services(self) -> None: profiles=profiles_dict, varieties=varieties, ) - self.services.append(service_def) + self.service_definitions.append(service_def) def _normalize_service_volumes(self, raw_vols) -> List[str]: """Return a list of docker-ready volume strings. @@ -487,7 +488,7 @@ def get_service_by_name( ) -> Optional[ServiceDefinition]: """Get a service definition by service_name.""" result = None - for service in self.services: + for service in self.service_definitions: if service.service_name == service_name: result = service # logger.info("get_service_by_name %s -> %s", service_name, result) diff --git a/src/orchestration/models.py b/src/orchestration/models.py index 18a6615..cee39fe 100644 --- a/src/orchestration/models.py +++ b/src/orchestration/models.py @@ -131,8 +131,8 @@ class ServiceDefinition(BaseModel): type: str | ServiceType description: str | None = None - # image is required for CONTAINER services, but optional for SOURCE_FILES - # and API services. + # image is required for CONTAINER service_definitions, but optional for + # SOURCE_FILES and API service_definitions. image: str | None = "" depends_on: list[str] | None = Field(default_factory=list) @@ -266,7 +266,10 @@ class ProvisionerState(BaseModel): class OzwaldConfig(BaseModel): hosts: list[Host] = Field(default_factory=list) - services: list[ServiceDefinition] = Field(default_factory=list) + service_definitions: list[ServiceDefinition] = Field( + default_factory=list, + alias="service-definitions", + ) provisioners: list[Provisioner] = Field(default_factory=list) networks: list[Network] = Field(default_factory=list) # Top-level named volume specifications (parsed/normalized by reader) @@ -289,7 +292,7 @@ class ProvisionerProfile(BaseModel): class ResourceConstraints(BaseModel): - """Resource requirements and constraints for services""" + """Resource requirements and constraints for service_definitions""" gpu_memory_required: str | None = None cpu_memory_required: str | None = None @@ -298,7 +301,7 @@ class ResourceConstraints(BaseModel): class HealthCheck(BaseModel): - """Health check configuration for services""" + """Health check configuration for service_definitions""" endpoint: str | None = None interval_seconds: int = 30 @@ -307,7 +310,7 @@ class HealthCheck(BaseModel): class ServiceDependency(BaseModel): - """Defines dependencies between services""" + """Defines dependencies between service_definitions""" service_name: str required: bool = True diff --git a/src/orchestration/provisioner.py b/src/orchestration/provisioner.py index a023562..a09ed38 100644 --- a/src/orchestration/provisioner.py +++ b/src/orchestration/provisioner.py @@ -109,8 +109,8 @@ def singleton(cls, cache: Optional[Cache] = None): return _system_provisioner def get_configured_services(self) -> List[ServiceDefinition]: - """Get all services configured for this provisioner""" - return self.config_reader.services + """Get all service_definitions configured for this provisioner""" + return self.config_reader.service_definitions def get_active_services(self) -> List[ServiceInformation]: """Get all currently active services""" @@ -491,7 +491,7 @@ def _stop_service( updated = True try: - # Some services may not implement stop yet + # Some service_definitions may not implement stop yet stop_fn = getattr( service_instance, "stop", @@ -812,7 +812,7 @@ def target_iterator( targets: List[ConfiguredServiceIdentifier] = [] if request.footprint_all_services: - for svc_def in self.config_reader.services: + for svc_def in self.config_reader.service_definitions: for target in target_iterator(svc_def): targets.append(target) diff --git a/tasks/dev.py b/tasks/dev.py index b1b188a..fa483cb 100644 --- a/tasks/dev.py +++ b/tasks/dev.py @@ -114,7 +114,7 @@ def show_host_resources(c, use_api=False, port=DEFAULT_PROVISIONER_PORT): @task(namespace="dev", name="build-containers") def build_containers(c, name=None): - """Build Docker images by delegating to util.services.""" + """Build Docker images by delegating to util.service_definitions.""" svc.build_containers(name=name) @@ -151,11 +151,6 @@ def _get_installed_gpu_drivers(c): PROVISIONER_NETWORK = "provisioner_network" -def _ensure_provisioner_network(c): - """Deprecated: kept for compatibility. Delegates to util.services.""" - svc.ensure_provisioner_network() - - @task(namespace="dev", name="start-provisioner-network") def start_provisioner_network(c): """Create the shared docker network for provisioner containers.""" @@ -170,13 +165,13 @@ def stop_provisioner_network(c): @task(namespace="dev", name="start-provisioner") def start_provisioner_api(c, port=DEFAULT_PROVISIONER_PORT, restart=True): - """Start the provisioner-api container via util.services.""" + """Start the provisioner-api container via util.service_definitions.""" svc.start_provisioner_api(port=port, restart=restart) @task(namespace="dev", name="stop-provisioner-api") def stop_provisioner_api(c): - """Stop the provisioner-api container via util.services.""" + """Stop the provisioner-api container via util.service_definitions.""" svc.stop_provisioner_api() @@ -276,7 +271,7 @@ def list_configured_services(c, port=DEFAULT_PROVISIONER_PORT): print(f" {key}: {value}") print("\n" + "=" * 80) - print(f"Total services: {len(services_data)}") + print(f"Total service_definitions: {len(services_data)}") print("=" * 80 + "\n") except requests.exceptions.RequestException as e: @@ -441,7 +436,7 @@ def list_active_services(c, port=DEFAULT_PROVISIONER_PORT): print(f" {key}: {value}") print("\n" + "=" * 80) - print(f"Total services: {len(services_data)}") + print(f"Total service_definitions: {len(services_data)}") print("=" * 80 + "\n") except requests.exceptions.RequestException as e: @@ -528,7 +523,9 @@ def update_services(c, service, port=DEFAULT_PROVISIONER_PORT): print("=" * 80) msg = result_data.get("message", "Service update request accepted") print(f"\n{msg}") - print(f"\nRequested services ({len(services_to_update)}):") + print( + f"\nRequested service_definitions ({len(services_to_update)}):" + ) for i, service in enumerate(services_to_update, 1): profile_info = f"@{service.profile}" if service.profile else "" print( @@ -561,7 +558,7 @@ def _docker_group_id(): @task(namespace="dev", name="start-provisioner-backend") def start_provisioner_backend(c, restart=True): - """Start the provisioner-backend container via util.services.""" + """Start the provisioner-backend container via util.service_definitions.""" try: svc.validate_footprint_data_env() except RuntimeError as e: @@ -576,7 +573,9 @@ def start_provisioner_redis( port=DEFAULT_PROVISIONER_REDIS_PORT, restart=True, ): - """Start a Redis container for the provisioner via util.services.""" + """ + Start a Redis container for the provisioner via util.service_definitions. + """ svc.start_provisioner_redis(port=port, restart=restart) @@ -610,7 +609,7 @@ def start_provisioner( svc.ensure_provisioner_network() # Start Redis first so backend and API can connect svc.start_provisioner_redis(port=redis_port, restart=restart) - # Then the backend worker/services + # Then the backend worker/service_definitions svc.start_provisioner_backend( restart=restart, mount_source_dir=mount_source_dir, @@ -626,13 +625,13 @@ def start_provisioner( @task(namespace="dev", name="stop-provisioner-backend") def stop_provisioner_backend(c): - """Stop the provisioner-backend container via util.services.""" + """Stop the provisioner-backend container via util.service_definitions.""" svc.stop_provisioner_backend() @task(namespace="dev", name="stop-provisioner-redis") def stop_provisioner_redis(c): - """Stop the provisioner-redis container via util.services.""" + """Stop the provisioner-redis container via util.service_definitions.""" svc.stop_provisioner_redis() diff --git a/tasks/test.py b/tasks/test.py index 8a454a1..2bd6d40 100644 --- a/tasks/test.py +++ b/tasks/test.py @@ -70,7 +70,7 @@ def _ensure_temp_assets( }, }, ], - "services": [ + "service-definitions": [ { "name": "test_env_and_vols", "type": "container", @@ -120,7 +120,7 @@ def integration( temp_root: str = "", use_dev_settings: bool = False, ): - """Run integration tests against provisioner services.""" + """Run integration tests against provisioner service_definitions.""" # Verify the API health endpoint is responsive (on host) port = int(os.environ.get("OZWALD_PROVISIONER_PORT", 8000)) diff --git a/tests/integration/provisioner/test_container_env_and_vols.py b/tests/integration/provisioner/test_container_env_and_vols.py index d6a1185..7b9029e 100644 --- a/tests/integration/provisioner/test_container_env_and_vols.py +++ b/tests/integration/provisioner/test_container_env_and_vols.py @@ -142,6 +142,7 @@ def env_for_daemon(config_from_env: Path) -> dict: os.environ.get("DEFAULT_OZWALD_PROVISIONER", "jamma"), ), ) + mp.setenv("OZWALD_HOST", os.environ.get("OZWALD_HOST", "localhost")) try: yield os.environ.copy() finally: @@ -188,6 +189,23 @@ def _update_services(service_updates: List[dict]): prov.update_services(infos) +def _start_services_locally(service_updates: List[dict]): + """ + Start service_definitions immediately in-process without relying on a + background daemon. + """ + from orchestration.models import ServiceInformation + from services.container import ContainerService + + # Ensure singletons refer to this process config/cache + _update_services(service_updates) + + infos = [ServiceInformation(**item) for item in service_updates] + for si in infos: + svc = ContainerService(service_info=si) + svc.start() + + # --- The test --- @@ -250,7 +268,7 @@ def test_container_env_and_volumes( "status": ServiceStatus.STARTING, }, ] - _update_services(body) + _start_services_locally(body) # Register instance for teardown started_instances.append(instance_name) diff --git a/tests/integration/provisioner/test_run_backend_daemon.py b/tests/integration/provisioner/test_run_backend_daemon.py index 0cf13e8..d7e0fc3 100644 --- a/tests/integration/provisioner/test_run_backend_daemon.py +++ b/tests/integration/provisioner/test_run_backend_daemon.py @@ -337,7 +337,7 @@ def both_available(): _wait_for( both_available, timeout=45, - description="both services AVAILABLE in cache", + description="both service_definitions AVAILABLE in cache", ) logs_b = _container_logs(container_b, tail=20) @@ -387,5 +387,5 @@ def cache_empty(): _wait_for( cache_empty, timeout=45, - description="active services cache empty", + description="active service_definitions cache empty", ) diff --git a/tests/integration/provisioner/test_varieties_profiles_vols.py b/tests/integration/provisioner/test_varieties_profiles_vols.py index d8c9c49..f2f2564 100644 --- a/tests/integration/provisioner/test_varieties_profiles_vols.py +++ b/tests/integration/provisioner/test_varieties_profiles_vols.py @@ -181,8 +181,8 @@ def _update_services(service_updates: list[dict]): def _start_services_locally(service_updates: list[dict]): - """Start services immediately in-process without relying on a background - daemon. This avoids interference from any externally running + """Start service_definitions immediately in-process without relying on a + background daemon. This avoids interference from any externally running provisioner that may be using a different settings file. """ from orchestration.models import ServiceInformation @@ -230,7 +230,7 @@ def temp_settings_file(tmp_path_factory): cfg = { "hosts": [{"name": "localhost", "ip": "127.0.0.1"}], - "services": [ + "service-definitions": [ { "name": "test_env_and_vols", "type": "container", diff --git a/tests/integration/test_footprint_overrides.py b/tests/integration/test_footprint_overrides.py index 83220c6..0bb2a37 100644 --- a/tests/integration/test_footprint_overrides.py +++ b/tests/integration/test_footprint_overrides.py @@ -9,7 +9,7 @@ def test_complex_footprint_overrides(tmp_path, monkeypatch): """Verify multi-level footprint overrides in a realistic config.""" cfg = { - "services": [ + "service-definitions": [ { "name": "app", "type": "container", diff --git a/tests/integration/test_update_services.py b/tests/integration/test_update_services.py index ee58b39..020a9b5 100644 --- a/tests/integration/test_update_services.py +++ b/tests/integration/test_update_services.py @@ -82,11 +82,13 @@ def _auth_headers() -> dict: def _pick_a_service_from_settings() -> tuple[str, str]: - """Return (service_name, profile_name) from settings services list.""" + """ + Return (service_name, profile_name) from settings service_definitions list. + """ cfg = _load_settings() - services = cfg.get("services") or [] + services = cfg.get("service-definitions") or [] if not services: - raise RuntimeError("No services configured in settings file") + raise RuntimeError("No service-definitions configured in settings file") svc = services[0] service_name = svc["name"] profiles = svc.get("profiles") or [] @@ -97,7 +99,7 @@ def _pick_a_service_from_settings() -> tuple[str, str]: def test_update_services_persists_to_redis(): """POST to update endpoint should store expected payload. - A POST to the active services update endpoint should update the + A POST to the active service_definitions update endpoint should update the active_services key in Redis. """ service_name, profile_name = _pick_a_service_from_settings() @@ -143,7 +145,7 @@ def test_update_services_persists_to_redis(): def test_update_services_rejects_invalid_payload_shape(): """Sending a non-list payload should yield a 422 from FastAPI validation. - A POST to the active services update endpoint should return a 422 + A POST to the active service_definitions update endpoint should return a 422 if the payload is not a list. """ resp = requests.post( @@ -158,7 +160,7 @@ def test_update_services_rejects_invalid_payload_shape(): def test_update_services_rejects_unknown_service(): """Unknown service name should return 400 with a helpful message. - A POST to the active services update endpoint should return a 400 + A POST to the active service_definitions update endpoint should return a 400 if the service name is not found in the settings. """ body = [ diff --git a/tests/unit/api/test_provisioner.py b/tests/unit/api/test_provisioner.py index 1c27462..aa6e05c 100644 --- a/tests/unit/api/test_provisioner.py +++ b/tests/unit/api/test_provisioner.py @@ -186,7 +186,7 @@ def test_update_services_accepts_and_delegates( auth_header: dict[str, str], mocker, ) -> None: - """`/srv/services/update/` responds 202 and calls + """`/srv/services/active/update/` responds 202 and calls `provisioner.update_services(...)` with parsed models. """ # Payload to send (as JSON) @@ -214,7 +214,7 @@ def test_update_services_accepts_and_delegates( ) resp = client.post( - "/srv/services/update/", + "/srv/services/active/update/", json=payload, headers=auth_header, ) diff --git a/tests/unit/config/conftest.py b/tests/unit/config/conftest.py index 3854b1e..426d7cd 100644 --- a/tests/unit/config/conftest.py +++ b/tests/unit/config/conftest.py @@ -94,7 +94,7 @@ def sample_config_dict(): ], }, ], - "services": [ + "service-definitions": [ { "name": "qwen1.5-vllm", "type": "container", @@ -165,13 +165,13 @@ def sample_config_file(sample_config_dict, tmp_path): @pytest.fixture def minimal_config_dict(): """Provides a minimal valid configuration with only required fields.""" - return {"hosts": [], "services": [], "provisioners": []} + return {"hosts": [], "service-definitions": [], "provisioners": []} @pytest.fixture def config_without_cache_dict(): """Provides a configuration without cache to test optional cache field.""" - return {"hosts": [], "services": [], "provisioners": []} + return {"hosts": [], "service-definitions": [], "provisioners": []} @pytest.fixture @@ -188,7 +188,7 @@ def config_with_provisioner_without_cache_dict(): """Provides a configuration with provisioners that don't have cache.""" return { "hosts": [], - "services": [], + "service-definitions": [], "provisioners": [{"name": "test-provisioner", "host": "test-host"}], } @@ -234,7 +234,7 @@ def invalid_yaml_file(tmp_path): @pytest.fixture def missing_orchestrator_config_dict(): """Deprecated: orchestrator section removed from simplified schema.""" - return {"hosts": [], "services": [], "provisioners": []} + return {"hosts": [], "service-definitions": [], "provisioners": []} @pytest.fixture diff --git a/tests/unit/config/test_config_reader.py b/tests/unit/config/test_config_reader.py index 04f266d..416e795 100644 --- a/tests/unit/config/test_config_reader.py +++ b/tests/unit/config/test_config_reader.py @@ -26,17 +26,17 @@ def test_init_with_valid_config_file(self, sample_config_file): assert reader.config_path == sample_config_file assert reader.hosts is not None - assert reader.services is not None + assert reader.service_definitions is not None assert reader.provisioners is not None def test_init_with_minimal_config(self, minimal_config_file): """Verify that ConfigReader can handle minimal valid configuration - with empty lists for hosts, services, and provisioners. + with empty lists for hosts, service_definitions, and provisioners. """ reader = ConfigReader(str(minimal_config_file)) assert len(reader.hosts) == 0 - assert len(reader.services) == 0 + assert len(reader.service_definitions) == 0 assert len(reader.provisioners) == 0 def test_init_with_nonexistent_file(self, tmp_path): @@ -205,7 +205,7 @@ def test_networks_are_parsed(self, tmp_path): cfg = { "networks": [{"name": "layer1"}, {"name": "layer2"}], "hosts": [], - "services": [], + "service-definitions": [], "provisioners": [], } cfg_path = tmp_path / "test_networks.yml" @@ -222,7 +222,7 @@ def test_networks_are_parsed(self, tmp_path): def test_service_networks_are_parsed(self, tmp_path): """Verify that networks in service definitions are parsed.""" cfg = { - "services": [ + "service-definitions": [ { "name": "svc1", "type": "container", @@ -271,9 +271,10 @@ def test_services_are_parsed(self, sample_config_file): """ reader = ConfigReader(str(sample_config_file)) - assert len(reader.services) == 2 + assert len(reader.service_definitions) == 2 assert all( - isinstance(svc, ServiceDefinition) for svc in reader.services + isinstance(svc, ServiceDefinition) + for svc in reader.service_definitions ) def test_service_attributes(self, sample_config_file): @@ -283,7 +284,9 @@ def test_service_attributes(self, sample_config_file): reader = ConfigReader(str(sample_config_file)) qwen_service = next( - s for s in reader.services if s.service_name == "qwen1.5-vllm" + s + for s in reader.service_definitions + if s.service_name == "qwen1.5-vllm" ) assert qwen_service.service_name == "qwen1.5-vllm" # `type` is now a string, not an enum @@ -313,12 +316,14 @@ def test_service_attributes(self, sample_config_file): def test_service_profiles_are_parsed(self, sample_config_file): """Verify that service profiles with their environment are - correctly parsed and associated with services. + correctly parsed and associated with service_definitions. """ reader = ConfigReader(str(sample_config_file)) qwen_service = next( - s for s in reader.services if s.service_name == "qwen1.5-vllm" + s + for s in reader.service_definitions + if s.service_name == "qwen1.5-vllm" ) assert len(qwen_service.profiles) == 2 @@ -338,7 +343,9 @@ def test_profile_inherits_and_overrides_base_environment( """ cfg = sample_config_dict # Ensure base has MODEL_NAME - svc = next(s for s in cfg["services"] if s["name"] == "qwen1.5-vllm") + svc = next( + s for s in cfg["service-definitions"] if s["name"] == "qwen1.5-vllm" + ) svc["environment"]["MODEL_NAME"] = "base-model/name" # Make profiles a list case and override MODEL_NAME inside 'embed' for p in svc["profiles"]: @@ -371,7 +378,9 @@ def test_variety_overrides_base_but_profile_overrides_variety( profile via get_effective_service_definition. """ cfg = sample_config_dict - svc = next(s for s in cfg["services"] if s["name"] == "qwen1.5-vllm") + svc = next( + s for s in cfg["service-definitions"] if s["name"] == "qwen1.5-vllm" + ) # Base value svc["environment"]["FOO"] = "base" # Ensure varieties exist and set FOO at variety level @@ -407,11 +416,14 @@ def test_variety_overrides_base_but_profile_overrides_variety( assert eff_p.environment["FOO"] == "profile" def test_service_without_profiles(self, sample_config_file): - """Verify that services without profiles have an empty profiles list.""" + """ + Verify that service_definitions without profiles have an empty + profiles list. + """ reader = ConfigReader(str(sample_config_file)) chunker_service = next( - s for s in reader.services if s.service_name == "chunker" + s for s in reader.service_definitions if s.service_name == "chunker" ) assert len(chunker_service.profiles) == 0 @@ -506,7 +518,7 @@ def test_full_configuration_parsing(self, sample_config_file): # Verify sections are populated per simplified schema assert len(reader.hosts) > 0 - assert len(reader.services) > 0 + assert len(reader.service_definitions) > 0 assert len(reader.provisioners) > 0 def test_pathlib_path_initialization(self, sample_config_file): @@ -533,7 +545,7 @@ def test_volumes_parsed_and_normalized(self, tmp_path): host1.mkdir() cfg = { "hosts": [], - "services": [ + "service-definitions": [ { "name": "svc", "type": "container", @@ -596,7 +608,7 @@ class TestFootprintParsing: def test_footprint_parsing_simple(self, tmp_path): """Verify that footprint is parsed from base service definition.""" cfg = { - "services": [ + "service-definitions": [ { "name": "svc", "type": "container", @@ -622,7 +634,7 @@ def test_footprint_parsing_profile_merge(self, tmp_path): Verify that footprint is merged via get_effective_service_definition. """ cfg = { - "services": [ + "service-definitions": [ { "name": "svc", "type": "container", @@ -662,7 +674,7 @@ def test_footprint_parsing_profile_merge(self, tmp_path): def test_footprint_parsing_variety(self, tmp_path): """Verify that footprint is extracted for varieties.""" cfg = { - "services": [ + "service-definitions": [ { "name": "svc", "type": "container", @@ -694,7 +706,7 @@ class TestEffectiveServiceDefinition: def test_merge_precedence(self, tmp_path): """Verify merge precedence: Profile > Variety > Base.""" cfg = { - "services": [ + "service-definitions": [ { "name": "svc", "type": "container", @@ -734,7 +746,7 @@ def test_merge_precedence(self, tmp_path): def test_property_merging(self, tmp_path): """Verify property merging precedence: Profile > Variety > Base.""" cfg = { - "services": [ + "service-definitions": [ { "name": "svc", "type": "container", @@ -769,7 +781,7 @@ def test_property_merging(self, tmp_path): def test_volume_merging(self, tmp_path): """Verify volume merging by target precedence.""" cfg = { - "services": [ + "service-definitions": [ { "name": "svc", "type": "container", @@ -807,7 +819,7 @@ def test_volume_merging(self, tmp_path): def test_network_merging(self, tmp_path): """Verify network merging precedence: Profile > Variety > Base.""" cfg = { - "services": [ + "service-definitions": [ { "name": "svc", "type": "container", diff --git a/tests/unit/orchestration/test_provisioner.py b/tests/unit/orchestration/test_provisioner.py index e8e14c8..17483d5 100644 --- a/tests/unit/orchestration/test_provisioner.py +++ b/tests/unit/orchestration/test_provisioner.py @@ -249,8 +249,8 @@ def stop(self): assert stop_called["count"] == 1 assert fake_cache.set_calls, "expected cache write" - # With current semantics, stopped services are removed from cache. - # The final persisted list should no longer include the service. + # With current semantics, stopped service_definitions are removed from + # cache. The final persisted list should no longer include the service. persisted_list = fake_cache.set_calls[-1] assert isinstance(persisted_list, list) assert persisted_list == [] @@ -330,7 +330,7 @@ def test_empty_list_marks_all_active_as_stopping_and_persists( ): _prov_mod, prov, fake_cache = provisioner_env - # Seed two AVAILABLE services + # Seed two AVAILABLE service_definitions a = _svc_info("svc-a", ServiceStatus.AVAILABLE) b = _svc_info("svc-b", ServiceStatus.AVAILABLE) fake_cache._services = [a, b] diff --git a/tests/unit/orchestration/test_service.py b/tests/unit/orchestration/test_service.py index 9b6f070..ae2842c 100644 --- a/tests/unit/orchestration/test_service.py +++ b/tests/unit/orchestration/test_service.py @@ -242,7 +242,7 @@ def test_build_service_registry_discovers_services_package( # Build the registry registry = BaseProvisionableService._build_service_registry() - # SimpleTestOneService is defined in services.testing with + # SimpleTestOneService is defined in service_definitions.testing with # service_type 'simple_test_one' assert "simple_test_one" in registry cls = registry["simple_test_one"] diff --git a/tests/unit/util/test_active_services_cache.py b/tests/unit/util/test_active_services_cache.py index 99aa327..605c4ad 100644 --- a/tests/unit/util/test_active_services_cache.py +++ b/tests/unit/util/test_active_services_cache.py @@ -2,8 +2,10 @@ These tests validate that `ActiveServicesCache`: - initializes a Redis client with expected parameters, -- stores services atomically using a lock and handles lock contention/errors, -- and retrieves services as `ServiceInformation` objects from cached JSON. +- stores service_definitions atomically using a lock and handles lock\ + contention/errors, +- and retrieves service_definitions as `ServiceInformation` objects from cached + JSON. The tests use pytest fixtures and the `mocker` fixture (no `patch` decorator). """ @@ -114,8 +116,8 @@ def test_sets_services_when_lock_acquired( active_cache_default: ActiveServicesCache, redis_mock, ): - """When the lock is acquired, services are JSON-encoded and stored, - and the lock is released. + """When the lock is acquired, service_definitions are JSON-encoded and + stored, and the lock is released. """ redis_client = redis_mock.return_value lock = redis_client.lock.return_value