diff --git a/EXAMPLES.md b/EXAMPLES.md index 9377b08bb..0d3ff62cc 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -7,9 +7,40 @@ Runnable examples live in [`examples/`](./examples). ## Table of Contents +- [Blueprint with Build Context](#blueprint-with-build-context) - [Devbox From Blueprint (Run Command, Shutdown)](#devbox-from-blueprint-lifecycle) - [MCP Hub + Claude Code + GitHub](#mcp-github-tools) + +## Blueprint with Build Context + +**Use case:** Create a blueprint using the object store to provide docker build context files, then verify files are copied into the image. Uses the async SDK. + +**Tags:** `blueprint`, `object-store`, `build-context`, `devbox`, `cleanup`, `async` + +### Workflow +- Create a temporary directory with sample application files +- Upload the directory to object storage as build context +- Create a blueprint with a Dockerfile that copies the context files +- Create a devbox from the blueprint +- Verify the files were copied into the image +- Shutdown devbox and delete blueprint and storage object + +### Prerequisites +- `RUNLOOP_API_KEY` + +### Run +```sh +uv run python -m examples.blueprint_with_build_context +``` + +### Test +```sh +uv run pytest -m smoketest tests/smoketests/examples/ +``` + +**Source:** [`examples/blueprint_with_build_context.py`](./examples/blueprint_with_build_context.py) + ## Devbox From Blueprint (Run Command, Shutdown) diff --git a/examples/blueprint_with_build_context.py b/examples/blueprint_with_build_context.py new file mode 100644 index 000000000..3bd9395d3 --- /dev/null +++ b/examples/blueprint_with_build_context.py @@ -0,0 +1,130 @@ +#!/usr/bin/env -S uv run python +""" +--- +title: Blueprint with Build Context +slug: blueprint-with-build-context +use_case: Create a blueprint using the object store to provide docker build context files, then verify files are copied into the image. Uses the async SDK. +workflow: + - Create a temporary directory with sample application files + - Upload the directory to object storage as build context + - Create a blueprint with a Dockerfile that copies the context files + - Create a devbox from the blueprint + - Verify the files were copied into the image + - Shutdown devbox and delete blueprint and storage object +tags: + - blueprint + - object-store + - build-context + - devbox + - cleanup + - async +prerequisites: + - RUNLOOP_API_KEY +run: uv run python -m examples.blueprint_with_build_context +test: uv run pytest -m smoketest tests/smoketests/examples/ +--- +""" + +from __future__ import annotations + +import tempfile +from pathlib import Path +from datetime import timedelta + +from runloop_api_client import AsyncRunloopSDK +from runloop_api_client.lib.polling import PollingConfig + +from ._harness import run_as_cli, unique_name, wrap_recipe +from .example_types import ExampleCheck, RecipeOutput, RecipeContext + +# building can take time: make sure to set a long blueprint build timeout +BLUEPRINT_POLL_TIMEOUT_S = 10 * 60 +BLUEPRINT_POLL_MAX_ATTEMPTS = 600 + +# configure object storage ttl for the build context +BUILD_CONTEXT_TTL = timedelta(hours=1) + + +async def recipe(ctx: RecipeContext) -> RecipeOutput: + """Create a blueprint with build context from object storage, then verify files in a devbox.""" + cleanup = ctx.cleanup + + sdk = AsyncRunloopSDK() + + # setup: create a temporary directory with sample application files to use as build context + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_path = Path(tmp_dir) + (tmp_path / "app.py").write_text('print("Hello from app")') + (tmp_path / "config.txt").write_text("key=value") + + # upload the build context to object storage + storage_obj = await sdk.storage_object.upload_from_dir( + tmp_path, + name=unique_name("example-build-context"), + ttl=BUILD_CONTEXT_TTL, + ) + cleanup.add(f"storage_object:{storage_obj.id}", storage_obj.delete) + + # create a blueprint with the build context + blueprint = await sdk.blueprint.create( + name=unique_name("example-blueprint-context"), + dockerfile="FROM ubuntu:22.04\nWORKDIR /app\nCOPY . .", + build_context=storage_obj.as_build_context(), + polling_config=PollingConfig( + timeout_seconds=BLUEPRINT_POLL_TIMEOUT_S, + max_attempts=BLUEPRINT_POLL_MAX_ATTEMPTS, + ), + ) + cleanup.add(f"blueprint:{blueprint.id}", blueprint.delete) + + devbox = await blueprint.create_devbox( + name=unique_name("example-devbox"), + launch_parameters={ + "resource_size_request": "X_SMALL", + "keep_alive_time_seconds": 60 * 5, + }, + ) + cleanup.add(f"devbox:{devbox.id}", devbox.shutdown) + + app_result = await devbox.cmd.exec("cat /app/app.py") + app_stdout = await app_result.stdout() + + config_result = await devbox.cmd.exec("cat /app/config.txt") + config_stdout = await config_result.stdout() + + return RecipeOutput( + resources_created=[ + f"storage_object:{storage_obj.id}", + f"blueprint:{blueprint.id}", + f"devbox:{devbox.id}", + ], + checks=[ + ExampleCheck( + name="app.py exists and readable", + passed=app_result.exit_code == 0, + details=f"exitCode={app_result.exit_code}", + ), + ExampleCheck( + name="app.py contains expected content", + passed='print("Hello from app")' in app_stdout, + details=app_stdout.strip(), + ), + ExampleCheck( + name="config.txt exists and readable", + passed=config_result.exit_code == 0, + details=f"exitCode={config_result.exit_code}", + ), + ExampleCheck( + name="config.txt contains expected content", + passed="key=value" in config_stdout, + details=config_stdout.strip(), + ), + ], + ) + + +run_blueprint_with_build_context_example = wrap_recipe(recipe) + + +if __name__ == "__main__": + run_as_cli(run_blueprint_with_build_context_example) diff --git a/examples/registry.py b/examples/registry.py index 41a4b4b51..cb6b780a9 100644 --- a/examples/registry.py +++ b/examples/registry.py @@ -9,11 +9,19 @@ from .example_types import ExampleResult from .mcp_github_tools import run_mcp_github_tools_example +from .blueprint_with_build_context import run_blueprint_with_build_context_example from .devbox_from_blueprint_lifecycle import run_devbox_from_blueprint_lifecycle_example ExampleRegistryEntry = dict[str, Any] example_registry: list[ExampleRegistryEntry] = [ + { + "slug": "blueprint-with-build-context", + "title": "Blueprint with Build Context", + "file_name": "blueprint_with_build_context.py", + "required_env": ["RUNLOOP_API_KEY"], + "run": run_blueprint_with_build_context_example, + }, { "slug": "devbox-from-blueprint-lifecycle", "title": "Devbox From Blueprint (Run Command, Shutdown)",