From c46f7a4c3d4480819c8589bfe6c960e5c97155b1 Mon Sep 17 00:00:00 2001 From: David Asamu Date: Thu, 29 May 2025 21:54:54 +0100 Subject: [PATCH] add support for image_distro in config file --- libs/cli/generate_schema.py | 14 ++++ libs/cli/langgraph_cli/config.py | 24 ++++++- libs/cli/tests/unit_tests/test_config.py | 86 ++++++++++++++++++++++-- 3 files changed, 116 insertions(+), 8 deletions(-) diff --git a/libs/cli/generate_schema.py b/libs/cli/generate_schema.py index 7a76ca5fa..041012c61 100644 --- a/libs/cli/generate_schema.py +++ b/libs/cli/generate_schema.py @@ -165,6 +165,13 @@ def generate_schema(): if "python_version" in python_schema["properties"]: python_schema["properties"]["python_version"]["enum"] = ["3.11", "3.12"] + # Add enum constraint for image_distro + if "image_distro" in python_schema["properties"]: + python_schema["properties"]["image_distro"]["anyOf"] = [ + {"type": "string", "enum": ["debian", "wolfi"]}, + {"type": "null"}, + ] + # Create Node.js schema with node_version node_schema = { "type": "object", @@ -184,6 +191,13 @@ def generate_schema(): {"type": "null"}, ] + # Add enum constraint for image_distro + if "image_distro" in node_schema["properties"]: + node_schema["properties"]["image_distro"]["anyOf"] = [ + {"type": "string", "enum": ["debian", "wolfi"]}, + {"type": "null"}, + ] + # Replace the Config schema with a oneOf constraint config_schema["oneOf"] = [python_schema, node_schema] diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index 5ef67b673..b48ac7779 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -13,6 +13,8 @@ DEFAULT_NODE_VERSION = "20" MIN_PYTHON_VERSION = "3.11" DEFAULT_PYTHON_VERSION = "3.11" +DEFAULT_IMAGE_DISTRO = "debian" + class TTLConfig(TypedDict, total=False): """Configuration for TTL (time-to-live) behavior in the store.""" @@ -367,6 +369,12 @@ class Config(TypedDict, total=False): Defaults to langchain/langgraph-api or langchain/langgraphjs-api.""" + image_distro: Optional[str] + """Optional. Linux distribution for the base image. + + Must be either 'debian' or 'wolfi'. If omitted, defaults to 'debian'. + """ + pip_config_file: Optional[str] """Optional. Path to a pip config file (e.g., "/etc/pip.conf" or "pip.ini") for controlling package installation (custom indices, credentials, etc.). @@ -517,12 +525,15 @@ def validate_config(config: Config) -> Config: "python_version", DEFAULT_PYTHON_VERSION if some_python else None ) + image_distro = config.get("image_distro", DEFAULT_IMAGE_DISTRO) + config = { "node_version": node_version, "python_version": python_version, "pip_config_file": config.get("pip_config_file"), "_INTERNAL_docker_tag": config.get("_INTERNAL_docker_tag"), "base_image": config.get("base_image"), + "image_distro": image_distro, "dependencies": config.get("dependencies", []), "dockerfile_lines": config.get("dockerfile_lines", []), "graphs": config.get("graphs", {}), @@ -576,6 +587,14 @@ def validate_config(config: Config) -> Config: "Add at least one graph to 'graphs' dictionary." ) + # Validate image_distro config + if image_distro := config.get("image_distro"): + if image_distro not in ["debian", "wolfi"]: + raise click.UsageError( + f"Invalid image_distro: '{image_distro}'. " + "Must be either 'debian' or 'wolfi'." + ) + # Validate auth config if auth_conf := config.get("auth"): if "path" in auth_conf: @@ -1306,6 +1325,7 @@ def docker_tag( base_image: Optional[str] = None, ) -> str: base_image = base_image or default_base_image(config) + wolfi_tag = "-wolfi" if config.get("image_distro") == "wolfi" else "" if config.get("_INTERNAL_docker_tag"): return f"{base_image}:{config['_INTERNAL_docker_tag']}" @@ -1313,8 +1333,8 @@ def docker_tag( return f"{base_image}-py{config['python_version']}" if config.get("node_version") and not config.get("python_version"): - return f"{base_image}:{config['node_version']}" - return f"{base_image}:{config['python_version']}" + return f"{base_image}:{config['node_version']}{wolfi_tag}" + return f"{base_image}:{config['python_version']}{wolfi_tag}" def config_to_docker( diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index 5a8ce91da..9d2fc7745 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -34,6 +34,7 @@ def test_validate_config(): "python_version": "3.11", "node_version": None, "pip_config_file": None, + "image_distro": "debian", "dockerfile_lines": [], "env": {}, "store": None, @@ -54,6 +55,7 @@ def test_validate_config(): "python_version": "3.12", "node_version": None, "pip_config_file": "pipconfig.txt", + "image_distro": "debian", "dockerfile_lines": ["ARG meow"], "dependencies": [".", "langchain"], "graphs": { @@ -120,18 +122,90 @@ def test_validate_config(): } ) assert config["python_version"] == "3.12-slim" - with pytest.raises( - ValueError, - match="Invalid http.app format", - ): + with pytest.raises(ValueError, match="Invalid http.app format"): + validate_config({ + "python_version": "3.12", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + "http": {"app": "../../examples/my_app.py"}, + }) + + +def test_validate_config_image_distro(): + """Test validation of image_distro field.""" + # Valid image_distro values should work + config = validate_config( + { + "python_version": "3.11", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + "image_distro": "debian", + } + ) + assert config["image_distro"] == "debian" + + config = validate_config( + { + "python_version": "3.11", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + "image_distro": "wolfi", + } + ) + assert config["image_distro"] == "wolfi" + + # Missing image_distro should default to 'debian' + config = validate_config( + { + "python_version": "3.11", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + } + ) + assert config["image_distro"] == "debian" + + # Invalid image_distro values should raise error + with pytest.raises(click.UsageError) as exc_info: validate_config( { - "python_version": "3.12", + "python_version": "3.11", "dependencies": ["."], "graphs": {"agent": "./agent.py:graph"}, - "http": {"app": "../../examples/my_app.py"}, + "image_distro": "ubuntu", } ) + assert "Invalid image_distro: 'ubuntu'" in str(exc_info.value) + assert "Must be either 'debian' or 'wolfi'" in str(exc_info.value) + + with pytest.raises(click.UsageError) as exc_info: + validate_config( + { + "python_version": "3.11", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + "image_distro": "alpine", + } + ) + assert "Invalid image_distro: 'alpine'" in str(exc_info.value) + + # Test with Node.js config too + config = validate_config( + { + "node_version": "20", + "graphs": {"agent": "./agent.js:graph"}, + "image_distro": "wolfi", + } + ) + assert config["image_distro"] == "wolfi" + + # Test Node.js config with default + config = validate_config( + { + "node_version": "20", + "graphs": {"agent": "./agent.js:graph"}, + } + ) + assert config["image_distro"] == "debian" def test_validate_config_file():