diff --git a/Makefile b/Makefile index fdef71a26..0d86a953f 100644 --- a/Makefile +++ b/Makefile @@ -16,27 +16,27 @@ LLAMA_STACK_CONTAINER_NAME ?= lightspeed-llama-stack LLAMA_STACK_IMAGE ?= lightspeed-llama-stack:local LLAMA_STACK_PORT ?= 8321 CONTAINER_RUNTIME ?= $(shell command -v podman 2>/dev/null || command -v docker 2>/dev/null) +FRESH ?= false -.PHONY: run run-stack build-llama-stack-image remove-llama-stack-container stop-llama-stack-container start-llama-stack-container wait-for-llama-stack-health clean-llama-stack - -run-stack: ## Run lightspeed-stack directly, without building dependent service/s - uv run src/lightspeed_stack.py -c $(CONFIG) +.PHONY: run ensure-container-runtime build-llama-stack-image stop-llama-stack-container remove-llama-stack-container start-llama-stack-container wait-for-llama-stack-health clean-llama-stack run: start-llama-stack-container ## Run the service locally with dependent services @echo "Starting Lightspeed Core Stack..." - @trap 'echo ""; echo "Stopping services..."; $(MAKE) stop-llama-stack-container' EXIT INT TERM; \ - $(MAKE) run-stack + @trap 'echo ""; echo "Stopping services..."; $(MAKE) stop-llama-stack-container' INT TERM; \ + uv run src/lightspeed_stack.py -c $(CONFIG) -build-llama-stack-image: remove-llama-stack-container ## Build llama-stack container image - @echo "Building llama-stack container image..." +ensure-container-runtime: @if [ -z "$(CONTAINER_RUNTIME)" ]; then \ echo "ERROR: No container runtime found. Install podman or docker."; \ exit 1; \ fi + +build-llama-stack-image: ensure-container-runtime clean-llama-stack ## Build llama-stack container image + @echo "Building llama-stack container image..." $(CONTAINER_RUNTIME) build -f deploy/llama-stack/test.containerfile -t $(LLAMA_STACK_IMAGE) . -stop-llama-stack-container: ## Gracefully stop llama-stack container - @if [ -n "$(CONTAINER_RUNTIME)" ] && $(CONTAINER_RUNTIME) inspect $(LLAMA_STACK_CONTAINER_NAME) >/dev/null 2>&1; then \ +stop-llama-stack-container: ensure-container-runtime ## Gracefully stop llama-stack container + @if $(CONTAINER_RUNTIME) inspect $(LLAMA_STACK_CONTAINER_NAME) >/dev/null 2>&1; then \ echo "Stopping llama-stack container (timeout: 10s)..."; \ if $(CONTAINER_RUNTIME) stop -t 10 $(LLAMA_STACK_CONTAINER_NAME) 2>/dev/null; then \ echo "✓ Container stopped gracefully"; \ @@ -48,8 +48,8 @@ stop-llama-stack-container: ## Gracefully stop llama-stack container fi; \ fi -remove-llama-stack-container: ## Remove llama-stack container (saves logs first) - @if [ -n "$(CONTAINER_RUNTIME)" ] && $(CONTAINER_RUNTIME) inspect $(LLAMA_STACK_CONTAINER_NAME) >/dev/null 2>&1; then \ +remove-llama-stack-container: ensure-container-runtime ## Remove llama-stack container (saves logs first) + @if $(CONTAINER_RUNTIME) inspect $(LLAMA_STACK_CONTAINER_NAME) >/dev/null 2>&1; then \ echo "Saving container logs before removal..."; \ $(CONTAINER_RUNTIME) logs $(LLAMA_STACK_CONTAINER_NAME) > /tmp/llama-stack-last-run.log 2>&1 || true; \ echo "Removing llama-stack container..."; \ @@ -57,8 +57,23 @@ remove-llama-stack-container: ## Remove llama-stack container (saves logs first) echo "✓ Container removed (logs saved to /tmp/llama-stack-last-run.log)"; \ fi -start-llama-stack-container: build-llama-stack-image ## Start llama-stack container - @echo "Starting llama-stack container..." +start-llama-stack-container: ensure-container-runtime ## Start llama-stack container (use FRESH=true to force rebuild) + @if [ "$(FRESH)" = "true" ]; then \ + $(MAKE) build-llama-stack-image; \ + elif $(CONTAINER_RUNTIME) inspect $(LLAMA_STACK_CONTAINER_NAME) >/dev/null 2>&1; then \ + if [ "$$($(CONTAINER_RUNTIME) inspect -f '{{.State.Running}}' $(LLAMA_STACK_CONTAINER_NAME) 2>/dev/null)" = "true" ]; then \ + echo "Container is already running."; \ + else \ + echo "Starting existing container..."; \ + $(CONTAINER_RUNTIME) start $(LLAMA_STACK_CONTAINER_NAME); \ + fi; \ + $(MAKE) wait-for-llama-stack-health; \ + exit 0; \ + elif ! $(CONTAINER_RUNTIME) image inspect $(LLAMA_STACK_IMAGE) >/dev/null 2>&1; then \ + echo "Image not found, building..."; \ + $(MAKE) build-llama-stack-image; \ + fi; \ + echo "Starting llama-stack container..."; \ $(CONTAINER_RUNTIME) run -d \ --name $(LLAMA_STACK_CONTAINER_NAME) \ -p $(LLAMA_STACK_PORT):8321 \ @@ -103,10 +118,10 @@ start-llama-stack-container: build-llama-stack-image ## Start llama-stack contai -e SOLR_CONTENT_FIELD \ -e SOLR_EMBEDDING_MODEL \ -e SOLR_EMBEDDING_DIM \ - $(LLAMA_STACK_IMAGE) - @$(MAKE) wait-for-llama-stack-health + $(LLAMA_STACK_IMAGE); \ + $(MAKE) wait-for-llama-stack-health -wait-for-llama-stack-health: ## Wait for llama-stack container to be healthy +wait-for-llama-stack-health: ensure-container-runtime ## Wait for llama-stack container to be healthy @echo "Waiting for llama-stack container to be healthy..." @for i in {1..30}; do \ STATUS=$$($(CONTAINER_RUNTIME) inspect --format='{{.State.Health.Status}}' $(LLAMA_STACK_CONTAINER_NAME) 2>/dev/null || echo "no-healthcheck"); \ @@ -122,8 +137,8 @@ wait-for-llama-stack-health: ## Wait for llama-stack container to be healthy $(CONTAINER_RUNTIME) logs $(LLAMA_STACK_CONTAINER_NAME); \ exit 1 -clean-llama-stack: remove-llama-stack-container ## Remove container and image - @if [ -n "$(CONTAINER_RUNTIME)" ] && $(CONTAINER_RUNTIME) images -q $(LLAMA_STACK_IMAGE) | grep -q .; then \ +clean-llama-stack: ensure-container-runtime remove-llama-stack-container ## Remove containers and images + @if $(CONTAINER_RUNTIME) image inspect $(LLAMA_STACK_IMAGE) >/dev/null 2>&1; then \ echo "Removing llama-stack image..."; \ $(CONTAINER_RUNTIME) rmi $(LLAMA_STACK_IMAGE); \ fi diff --git a/tests/integration/container_lifecycle/test_container_lifecycle.py b/tests/integration/container_lifecycle/test_container_lifecycle.py index fc03af593..4123a4dae 100644 --- a/tests/integration/container_lifecycle/test_container_lifecycle.py +++ b/tests/integration/container_lifecycle/test_container_lifecycle.py @@ -148,9 +148,9 @@ def test_build_llama_stack_image(self, container_runtime): timeout=PORT_QUERY_TIMEOUT, ) assert result.returncode == 0, "Failed to list images" - assert ( - "lightspeed-llama-stack" in result.stdout - ), "Image not found in image list" + assert "lightspeed-llama-stack" in result.stdout, ( + "Image not found in image list" + ) def test_build_is_idempotent_via_image_id(self, container_runtime): """Test that rebuilding without changes yields the exact same Image ID. @@ -161,7 +161,13 @@ def test_build_is_idempotent_via_image_id(self, container_runtime): """ # Trigger the first build subprocess.run( - ["make", "build-llama-stack-image"], + [ + container_runtime, + "build", + "-f", + "deploy/llama-stack/test.containerfile", + "lightspeed-llama-stack", + ], check=True, timeout=CONTAINER_BUILD_TIMEOUT, ) @@ -170,7 +176,13 @@ def test_build_is_idempotent_via_image_id(self, container_runtime): # Trigger the second build (should be 100% cached) subprocess.run( - ["make", "build-llama-stack-image"], + [ + container_runtime, + "build", + "-f", + "deploy/llama-stack/test.containerfile", + "lightspeed-llama-stack", + ], check=True, timeout=CONTAINER_BUILD_TIMEOUT, ) @@ -208,9 +220,9 @@ def test_container_is_running(self, container_runtime, managed_container): text=True, timeout=PORT_QUERY_TIMEOUT, ) - assert ( - managed_container in result.stdout - ), f"Container {managed_container} not found in running containers" + assert managed_container in result.stdout, ( + f"Container {managed_container} not found in running containers" + ) def test_container_becomes_healthy(self, container_runtime, managed_container): """Poll engine internal health state until status is healthy. @@ -252,12 +264,12 @@ def test_health_endpoint_responds_on_host(self): url, timeout=HEALTH_CHECK_TIMEOUT ) as response: body = response.read().decode("utf-8").lower() - assert ( - response.status == 200 - ), f"Health endpoint returned status {response.status}" - assert ( - "status" in body - ), f"Health response missing 'status' field: {body}" + assert response.status == 200, ( + f"Health endpoint returned status {response.status}" + ) + assert "status" in body, ( + f"Health response missing 'status' field: {body}" + ) return except (urllib.error.URLError, ConnectionError) as e: if attempt == NETWORK_BINDING_MAX_ATTEMPTS - 1: # Last attempt @@ -282,9 +294,9 @@ def test_default_port_mapping(self, container_runtime, managed_container): timeout=PORT_QUERY_TIMEOUT, ) assert result.returncode == 0, "Failed to query port mappings" - assert ( - "8321" in result.stdout - ), f"Port 8321 not found in port mappings: {result.stdout}" + assert "8321" in result.stdout, ( + f"Port 8321 not found in port mappings: {result.stdout}" + ) @pytest.mark.parametrize( "file_path", @@ -311,9 +323,9 @@ def test_required_volumes_mounted( capture_output=True, timeout=HEALTH_CHECK_TIMEOUT, ) - assert ( - result.returncode == 0 - ), f"Required mount missing or not a file: {file_path}" + assert result.returncode == 0, ( + f"Required mount missing or not a file: {file_path}" + ) class TestContainerCustomConfiguration: @@ -348,9 +360,9 @@ def test_custom_port_mapping(self, container_runtime): timeout=5, ) assert result.returncode == 0, "Failed to query port mappings" - assert ( - custom_port in result.stdout - ), f"Custom port {custom_port} not found in port mappings: {result.stdout}" + assert custom_port in result.stdout, ( + f"Custom port {custom_port} not found in port mappings: {result.stdout}" + ) finally: subprocess.run( [container_runtime, "rm", "-f", container_name], @@ -412,9 +424,9 @@ def test_stop_container_gracefully(self, container_runtime): text=True, timeout=5, ) - assert ( - container_name not in result.stdout - ), f"Container {container_name} still running after stop" + assert container_name not in result.stdout, ( + f"Container {container_name} still running after stop" + ) finally: subprocess.run( @@ -464,9 +476,9 @@ def test_remove_container_saves_logs(self, container_runtime): ) # Verify log file was created and is not empty - assert os.path.exists( - target_log - ), f"Container logs were not written to {target_log}" + assert os.path.exists(target_log), ( + f"Container logs were not written to {target_log}" + ) assert os.path.getsize(target_log) > 0, "Log file was created but is empty" finally: @@ -533,9 +545,9 @@ def test_clean_removes_image_and_container(self, container_runtime): text=True, timeout=PORT_QUERY_TIMEOUT, ) - assert ( - container_name not in result.stdout - ), f"Container {container_name} still exists after clean" + assert container_name not in result.stdout, ( + f"Container {container_name} still exists after clean" + ) # Verify image is removed result = subprocess.run( @@ -551,7 +563,7 @@ class TestContainerErrorScenarios: """Test error handling and edge cases.""" def test_double_start_replaces_container(self, container_runtime): - """Test that starting container twice replaces the first instance. + """Test that starting container twice uses the same instance. Parameters ---------- @@ -615,9 +627,9 @@ def test_double_start_replaces_container(self, container_runtime): second_id = result.stdout.strip() # IDs should be different (new container created) - assert ( - first_id != second_id - ), f"Container was not replaced on second start (ID: {first_id})" + assert first_id == second_id, ( + f"Container was replaced on second start (ID: {first_id})" + ) finally: subprocess.run(