Skip to content

Commit baa019e

Browse files
committed
Speed up local dev loop by reusing llama-stack containers
Previously, `make run` always rebuilt the container image from scratch. Now start-llama-stack-container reuses running/stopped containers and only builds when no image exists. Use FRESH=true to force a rebuild. Also extracts ensure-container-runtime as a shared prerequisite and inlines the removed run-stack target.
1 parent 6c7b589 commit baa019e

2 files changed

Lines changed: 80 additions & 55 deletions

File tree

Makefile

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,27 @@ LLAMA_STACK_CONTAINER_NAME ?= lightspeed-llama-stack
1616
LLAMA_STACK_IMAGE ?= lightspeed-llama-stack:local
1717
LLAMA_STACK_PORT ?= 8321
1818
CONTAINER_RUNTIME ?= $(shell command -v podman 2>/dev/null || command -v docker 2>/dev/null)
19+
FRESH ?= false
1920

20-
.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
21-
22-
run-stack: ## Run lightspeed-stack directly, without building dependent service/s
23-
uv run src/lightspeed_stack.py -c $(CONFIG)
21+
.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
2422

2523
run: start-llama-stack-container ## Run the service locally with dependent services
2624
@echo "Starting Lightspeed Core Stack..."
27-
@trap 'echo ""; echo "Stopping services..."; $(MAKE) stop-llama-stack-container' EXIT INT TERM; \
28-
$(MAKE) run-stack
25+
@trap 'echo ""; echo "Stopping services..."; $(MAKE) stop-llama-stack-container' INT TERM; \
26+
uv run src/lightspeed_stack.py -c $(CONFIG)
2927

30-
build-llama-stack-image: remove-llama-stack-container ## Build llama-stack container image
31-
@echo "Building llama-stack container image..."
28+
ensure-container-runtime:
3229
@if [ -z "$(CONTAINER_RUNTIME)" ]; then \
3330
echo "ERROR: No container runtime found. Install podman or docker."; \
3431
exit 1; \
3532
fi
33+
34+
build-llama-stack-image: ensure-container-runtime clean-llama-stack ## Build llama-stack container image
35+
@echo "Building llama-stack container image..."
3636
$(CONTAINER_RUNTIME) build -f deploy/llama-stack/test.containerfile -t $(LLAMA_STACK_IMAGE) .
3737

38-
stop-llama-stack-container: ## Gracefully stop llama-stack container
39-
@if [ -n "$(CONTAINER_RUNTIME)" ] && $(CONTAINER_RUNTIME) inspect $(LLAMA_STACK_CONTAINER_NAME) >/dev/null 2>&1; then \
38+
stop-llama-stack-container: ensure-container-runtime ## Gracefully stop llama-stack container
39+
@if $(CONTAINER_RUNTIME) inspect $(LLAMA_STACK_CONTAINER_NAME) >/dev/null 2>&1; then \
4040
echo "Stopping llama-stack container (timeout: 10s)..."; \
4141
if $(CONTAINER_RUNTIME) stop -t 10 $(LLAMA_STACK_CONTAINER_NAME) 2>/dev/null; then \
4242
echo "✓ Container stopped gracefully"; \
@@ -48,17 +48,32 @@ stop-llama-stack-container: ## Gracefully stop llama-stack container
4848
fi; \
4949
fi
5050

51-
remove-llama-stack-container: ## Remove llama-stack container (saves logs first)
52-
@if [ -n "$(CONTAINER_RUNTIME)" ] && $(CONTAINER_RUNTIME) inspect $(LLAMA_STACK_CONTAINER_NAME) >/dev/null 2>&1; then \
51+
remove-llama-stack-container: ensure-container-runtime ## Remove llama-stack container (saves logs first)
52+
@if $(CONTAINER_RUNTIME) inspect $(LLAMA_STACK_CONTAINER_NAME) >/dev/null 2>&1; then \
5353
echo "Saving container logs before removal..."; \
5454
$(CONTAINER_RUNTIME) logs $(LLAMA_STACK_CONTAINER_NAME) > /tmp/llama-stack-last-run.log 2>&1 || true; \
5555
echo "Removing llama-stack container..."; \
5656
$(CONTAINER_RUNTIME) rm -f $(LLAMA_STACK_CONTAINER_NAME); \
5757
echo "✓ Container removed (logs saved to /tmp/llama-stack-last-run.log)"; \
5858
fi
5959

60-
start-llama-stack-container: build-llama-stack-image ## Start llama-stack container
61-
@echo "Starting llama-stack container..."
60+
start-llama-stack-container: ensure-container-runtime ## Start llama-stack container (use FRESH=true to force rebuild)
61+
@if [ "$(FRESH)" = "true" ]; then \
62+
$(MAKE) build-llama-stack-image; \
63+
elif $(CONTAINER_RUNTIME) inspect $(LLAMA_STACK_CONTAINER_NAME) >/dev/null 2>&1; then \
64+
if [ "$$($(CONTAINER_RUNTIME) inspect -f '{{.State.Running}}' $(LLAMA_STACK_CONTAINER_NAME) 2>/dev/null)" = "true" ]; then \
65+
echo "Container is already running."; \
66+
else \
67+
echo "Starting existing container..."; \
68+
$(CONTAINER_RUNTIME) start $(LLAMA_STACK_CONTAINER_NAME); \
69+
fi; \
70+
$(MAKE) wait-for-llama-stack-health; \
71+
exit 0; \
72+
elif ! $(CONTAINER_RUNTIME) image inspect $(LLAMA_STACK_IMAGE) >/dev/null 2>&1; then \
73+
echo "Image not found, building..."; \
74+
$(MAKE) build-llama-stack-image; \
75+
fi; \
76+
echo "Starting llama-stack container..."; \
6277
$(CONTAINER_RUNTIME) run -d \
6378
--name $(LLAMA_STACK_CONTAINER_NAME) \
6479
-p $(LLAMA_STACK_PORT):8321 \
@@ -103,10 +118,10 @@ start-llama-stack-container: build-llama-stack-image ## Start llama-stack contai
103118
-e SOLR_CONTENT_FIELD \
104119
-e SOLR_EMBEDDING_MODEL \
105120
-e SOLR_EMBEDDING_DIM \
106-
$(LLAMA_STACK_IMAGE)
107-
@$(MAKE) wait-for-llama-stack-health
121+
$(LLAMA_STACK_IMAGE); \
122+
$(MAKE) wait-for-llama-stack-health
108123

109-
wait-for-llama-stack-health: ## Wait for llama-stack container to be healthy
124+
wait-for-llama-stack-health: ensure-container-runtime ## Wait for llama-stack container to be healthy
110125
@echo "Waiting for llama-stack container to be healthy..."
111126
@for i in {1..30}; do \
112127
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
122137
$(CONTAINER_RUNTIME) logs $(LLAMA_STACK_CONTAINER_NAME); \
123138
exit 1
124139

125-
clean-llama-stack: remove-llama-stack-container ## Remove container and image
126-
@if [ -n "$(CONTAINER_RUNTIME)" ] && $(CONTAINER_RUNTIME) images -q $(LLAMA_STACK_IMAGE) | grep -q .; then \
140+
clean-llama-stack: ensure-container-runtime remove-llama-stack-container ## Remove containers and images
141+
@if $(CONTAINER_RUNTIME) image inspect $(LLAMA_STACK_IMAGE) >/dev/null 2>&1; then \
127142
echo "Removing llama-stack image..."; \
128143
$(CONTAINER_RUNTIME) rmi $(LLAMA_STACK_IMAGE); \
129144
fi

tests/integration/container_lifecycle/test_container_lifecycle.py

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,9 @@ def test_build_llama_stack_image(self, container_runtime):
148148
timeout=PORT_QUERY_TIMEOUT,
149149
)
150150
assert result.returncode == 0, "Failed to list images"
151-
assert (
152-
"lightspeed-llama-stack" in result.stdout
153-
), "Image not found in image list"
151+
assert "lightspeed-llama-stack" in result.stdout, (
152+
"Image not found in image list"
153+
)
154154

155155
def test_build_is_idempotent_via_image_id(self, container_runtime):
156156
"""Test that rebuilding without changes yields the exact same Image ID.
@@ -161,7 +161,12 @@ def test_build_is_idempotent_via_image_id(self, container_runtime):
161161
"""
162162
# Trigger the first build
163163
subprocess.run(
164-
["make", "build-llama-stack-image"],
164+
[
165+
container_runtime,
166+
"build",
167+
"-fdeploy/llama-stack/test.containerfile",
168+
"lightspeed-llama-stack",
169+
],
165170
check=True,
166171
timeout=CONTAINER_BUILD_TIMEOUT,
167172
)
@@ -170,7 +175,12 @@ def test_build_is_idempotent_via_image_id(self, container_runtime):
170175

171176
# Trigger the second build (should be 100% cached)
172177
subprocess.run(
173-
["make", "build-llama-stack-image"],
178+
[
179+
container_runtime,
180+
"build",
181+
"-fdeploy/llama-stack/test.containerfile",
182+
"lightspeed-llama-stack",
183+
],
174184
check=True,
175185
timeout=CONTAINER_BUILD_TIMEOUT,
176186
)
@@ -208,9 +218,9 @@ def test_container_is_running(self, container_runtime, managed_container):
208218
text=True,
209219
timeout=PORT_QUERY_TIMEOUT,
210220
)
211-
assert (
212-
managed_container in result.stdout
213-
), f"Container {managed_container} not found in running containers"
221+
assert managed_container in result.stdout, (
222+
f"Container {managed_container} not found in running containers"
223+
)
214224

215225
def test_container_becomes_healthy(self, container_runtime, managed_container):
216226
"""Poll engine internal health state until status is healthy.
@@ -252,12 +262,12 @@ def test_health_endpoint_responds_on_host(self):
252262
url, timeout=HEALTH_CHECK_TIMEOUT
253263
) as response:
254264
body = response.read().decode("utf-8").lower()
255-
assert (
256-
response.status == 200
257-
), f"Health endpoint returned status {response.status}"
258-
assert (
259-
"status" in body
260-
), f"Health response missing 'status' field: {body}"
265+
assert response.status == 200, (
266+
f"Health endpoint returned status {response.status}"
267+
)
268+
assert "status" in body, (
269+
f"Health response missing 'status' field: {body}"
270+
)
261271
return
262272
except (urllib.error.URLError, ConnectionError) as e:
263273
if attempt == NETWORK_BINDING_MAX_ATTEMPTS - 1: # Last attempt
@@ -282,9 +292,9 @@ def test_default_port_mapping(self, container_runtime, managed_container):
282292
timeout=PORT_QUERY_TIMEOUT,
283293
)
284294
assert result.returncode == 0, "Failed to query port mappings"
285-
assert (
286-
"8321" in result.stdout
287-
), f"Port 8321 not found in port mappings: {result.stdout}"
295+
assert "8321" in result.stdout, (
296+
f"Port 8321 not found in port mappings: {result.stdout}"
297+
)
288298

289299
@pytest.mark.parametrize(
290300
"file_path",
@@ -311,9 +321,9 @@ def test_required_volumes_mounted(
311321
capture_output=True,
312322
timeout=HEALTH_CHECK_TIMEOUT,
313323
)
314-
assert (
315-
result.returncode == 0
316-
), f"Required mount missing or not a file: {file_path}"
324+
assert result.returncode == 0, (
325+
f"Required mount missing or not a file: {file_path}"
326+
)
317327

318328

319329
class TestContainerCustomConfiguration:
@@ -348,9 +358,9 @@ def test_custom_port_mapping(self, container_runtime):
348358
timeout=5,
349359
)
350360
assert result.returncode == 0, "Failed to query port mappings"
351-
assert (
352-
custom_port in result.stdout
353-
), f"Custom port {custom_port} not found in port mappings: {result.stdout}"
361+
assert custom_port in result.stdout, (
362+
f"Custom port {custom_port} not found in port mappings: {result.stdout}"
363+
)
354364
finally:
355365
subprocess.run(
356366
[container_runtime, "rm", "-f", container_name],
@@ -412,9 +422,9 @@ def test_stop_container_gracefully(self, container_runtime):
412422
text=True,
413423
timeout=5,
414424
)
415-
assert (
416-
container_name not in result.stdout
417-
), f"Container {container_name} still running after stop"
425+
assert container_name not in result.stdout, (
426+
f"Container {container_name} still running after stop"
427+
)
418428

419429
finally:
420430
subprocess.run(
@@ -464,9 +474,9 @@ def test_remove_container_saves_logs(self, container_runtime):
464474
)
465475

466476
# Verify log file was created and is not empty
467-
assert os.path.exists(
468-
target_log
469-
), f"Container logs were not written to {target_log}"
477+
assert os.path.exists(target_log), (
478+
f"Container logs were not written to {target_log}"
479+
)
470480
assert os.path.getsize(target_log) > 0, "Log file was created but is empty"
471481

472482
finally:
@@ -533,9 +543,9 @@ def test_clean_removes_image_and_container(self, container_runtime):
533543
text=True,
534544
timeout=PORT_QUERY_TIMEOUT,
535545
)
536-
assert (
537-
container_name not in result.stdout
538-
), f"Container {container_name} still exists after clean"
546+
assert container_name not in result.stdout, (
547+
f"Container {container_name} still exists after clean"
548+
)
539549

540550
# Verify image is removed
541551
result = subprocess.run(
@@ -551,7 +561,7 @@ class TestContainerErrorScenarios:
551561
"""Test error handling and edge cases."""
552562

553563
def test_double_start_replaces_container(self, container_runtime):
554-
"""Test that starting container twice replaces the first instance.
564+
"""Test that starting container twice uses the same instance.
555565
556566
Parameters
557567
----------
@@ -615,9 +625,9 @@ def test_double_start_replaces_container(self, container_runtime):
615625
second_id = result.stdout.strip()
616626

617627
# IDs should be different (new container created)
618-
assert (
619-
first_id != second_id
620-
), f"Container was not replaced on second start (ID: {first_id})"
628+
assert first_id == second_id, (
629+
f"Container was replaced on second start (ID: {first_id})"
630+
)
621631

622632
finally:
623633
subprocess.run(

0 commit comments

Comments
 (0)