feat(cli): add internal docker tag support (#4333)

This commit is contained in:
David Duong
2025-04-17 22:32:31 +02:00
committed by GitHub
5 changed files with 105 additions and 7 deletions
+25 -6
View File
@@ -326,6 +326,10 @@ class Config(TypedDict, total=False):
Must be >= 20 if provided.
"""
_INTERNAL_docker_tag: Optional[str]
"""Optional. Internal use only.
"""
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.).
@@ -480,6 +484,7 @@ def validate_config(config: Config) -> 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"),
"dependencies": config.get("dependencies", []),
"dockerfile_lines": config.get("dockerfile_lines", []),
"graphs": config.get("graphs", {}),
@@ -1025,7 +1030,9 @@ def _get_node_pm_install_cmd(config_path: pathlib.Path, config: Config) -> str:
def python_config_to_docker(
config_path: pathlib.Path, config: Config, base_image: str
config_path: pathlib.Path,
config: Config,
base_image: str,
) -> tuple[str, dict[str, str]]:
"""Generate a Dockerfile from the configuration."""
# configure pip
@@ -1040,6 +1047,8 @@ def python_config_to_docker(
else ""
)
docker_tag = config.get("_INTERNAL_docker_tag") or config["python_version"]
# collect dependencies
pypi_deps = [dep for dep in config["dependencies"] if not dep.startswith(".")]
local_deps = _assemble_local_deps(config_path, config)
@@ -1160,7 +1169,7 @@ ADD {relpath} /deps/{name}
)
docker_file_contents = [
f"FROM {base_image}:{config['python_version']}",
f"FROM {base_image}:{docker_tag}",
"",
os.linesep.join(config["dockerfile_lines"]),
"",
@@ -1192,10 +1201,13 @@ ADD {relpath} /deps/{name}
def node_config_to_docker(
config_path: pathlib.Path, config: Config, base_image: str
config_path: pathlib.Path,
config: Config,
base_image: str,
) -> tuple[str, dict[str, str]]:
faux_path = f"/deps/{config_path.parent.name}"
install_cmd = _get_node_pm_install_cmd(config_path, config)
docker_tag = config.get("_INTERNAL_docker_tag") or config["node_version"]
env_vars: list[str] = []
@@ -1222,7 +1234,7 @@ def node_config_to_docker(
env_vars.append(f"ENV LANGSERVE_GRAPHS='{json.dumps(config['graphs'])}'")
docker_file_contents = [
f"FROM {base_image}:{config['node_version']}",
f"FROM {base_image}:{docker_tag}",
"",
os.linesep.join(config["dockerfile_lines"]),
"",
@@ -1246,8 +1258,13 @@ def default_base_image(config: Config) -> str:
return "langchain/langgraph-api"
def docker_tag(config: Config, base_image: Optional[str] = None) -> str:
def docker_tag(
config: Config,
base_image: Optional[str] = None,
) -> str:
base_image = base_image or default_base_image(config)
if config.get("_INTERNAL_docker_tag"):
return f"{base_image}:{config['_INTERNAL_docker_tag']}"
if config.get("node_version") and not config.get("python_version"):
return f"{base_image}:{config['node_version']}"
@@ -1255,7 +1272,9 @@ def docker_tag(config: Config, base_image: Optional[str] = None) -> str:
def config_to_docker(
config_path: pathlib.Path, config: Config, base_image: Optional[str] = None
config_path: pathlib.Path,
config: Config,
base_image: Optional[str] = None,
) -> tuple[str, dict[str, str]]:
base_image = base_image or default_base_image(config)
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
version = "0.2.4"
version = "0.2.5"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
+22
View File
@@ -29,6 +29,17 @@
],
"description": "Optional. Path to a pip config file (e.g., \"/etc/pip.conf\" or \"pip.ini\") for controlling\npackage installation (custom indices, credentials, etc.).\n\nOnly relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.\n"
},
"_INTERNAL_docker_tag": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional. Internal use only.\n"
},
"auth": {
"anyOf": [
{
@@ -145,6 +156,17 @@
],
"description": "Optional. Node.js version as a major version (e.g. '20'), if your deployment needs Node.\nMust be >= 20 if provided.\n"
},
"_INTERNAL_docker_tag": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional. Internal use only.\n"
},
"auth": {
"anyOf": [
{
+22
View File
@@ -29,6 +29,17 @@
],
"description": "Optional. Path to a pip config file (e.g., \"/etc/pip.conf\" or \"pip.ini\") for controlling\npackage installation (custom indices, credentials, etc.).\n\nOnly relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.\n"
},
"_INTERNAL_docker_tag": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional. Internal use only.\n"
},
"auth": {
"anyOf": [
{
@@ -145,6 +156,17 @@
],
"description": "Optional. Node.js version as a major version (e.g. '20'), if your deployment needs Node.\nMust be >= 20 if provided.\n"
},
"_INTERNAL_docker_tag": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional. Internal use only.\n"
},
"auth": {
"anyOf": [
{
+35
View File
@@ -29,6 +29,7 @@ def test_validate_config():
}
actual_config = validate_config(expected_config)
expected_config = {
"_INTERNAL_docker_tag": None,
"python_version": "3.11",
"node_version": None,
"pip_config_file": None,
@@ -47,6 +48,7 @@ def test_validate_config():
# full config
env = ".env"
expected_config = {
"_INTERNAL_docker_tag": None,
"python_version": "3.12",
"node_version": None,
"pip_config_file": "pipconfig.txt",
@@ -567,6 +569,39 @@ RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not foun
assert additional_contexts == {}
def test_config_to_docker_nodejs_internal_docker_tag():
graphs = {"agent": "./graphs/agent.js:graph"}
actual_docker_stdin, additional_contexts = config_to_docker(
PATH_TO_CONFIG,
validate_config(
{
"node_version": "20",
"graphs": graphs,
"dockerfile_lines": ["ARG meow", "ARG foo"],
"auth": {"path": "./graphs/auth.mts:auth"},
"ui": {"agent": "./graphs/agent.ui.jsx"},
"ui_config": {"shared": ["nuqs"]},
"_INTERNAL_docker_tag": "my-tag",
}
),
"langchain/langgraphjs-api",
)
expected_docker_stdin = """FROM langchain/langgraphjs-api:my-tag
ARG meow
ARG foo
ADD . /deps/unit_tests
RUN cd /deps/unit_tests && npm i
ENV LANGGRAPH_AUTH='{"path": "./graphs/auth.mts:auth"}'
ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}'
ENV LANGGRAPH_UI_CONFIG='{"shared": ["nuqs"]}'
ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
WORKDIR /deps/unit_tests
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
assert additional_contexts == {}
def test_config_to_docker_gen_ui_python():
graphs = {"agent": "./agent.py:graph"}
actual_docker_stdin, additional_contexts = config_to_docker(