Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 34 additions & 19 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"; \
Expand All @@ -48,17 +48,32 @@ 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..."; \
$(CONTAINER_RUNTIME) rm -f $(LLAMA_STACK_CONTAINER_NAME); \
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..."; \
Comment on lines +60 to +76

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with the principal of this 👍🏽

What do you think about moving this logic to a bash script instead of keeping it in the makefile?

That way the Makefile is clean and easy to read/debug in the future

run-llama-stack:
	@CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) \
	 LLAMA_STACK_CONTAINER_NAME=$(LLAMA_STACK_CONTAINER_NAME) \
	 LLAMA_STACK_IMAGE=$(LLAMA_STACK_IMAGE) \
	 FRESH=$(FRESH) \
	 bash scripts/manage_container.sh || exit 0
	@echo "Starting llama-stack container..."
	$(CONTAINER_RUNTIME) run -d \
		--name $(LLAMA_STACK_CONTAINER_NAME) \
		-p $(LLAMA_STACK_PORT):8321 \

$(CONTAINER_RUNTIME) run -d \
--name $(LLAMA_STACK_CONTAINER_NAME) \
-p $(LLAMA_STACK_PORT):8321 \
Expand Down Expand Up @@ -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"); \
Expand All @@ -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
Expand Down
84 changes: 48 additions & 36 deletions tests/integration/container_lifecycle/test_container_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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",
],
Comment on lines -164 to +170

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using make targets ensures we test what users use - there's a chance in the future that the make target could deviate from what's in here, so if we don't test the make targets directly, we'll miss something.

check=True,
timeout=CONTAINER_BUILD_TIMEOUT,
)
Expand All @@ -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,
)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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",
Expand All @@ -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:
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand All @@ -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
----------
Expand Down Expand Up @@ -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(
Expand Down
Loading