mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-20 08:37:59 +02:00
refactor(cli): make the control plane client source-agnostic
This commit is contained in:
@@ -642,18 +642,18 @@ def _create_deployment(
|
||||
step: int,
|
||||
*,
|
||||
name: str,
|
||||
deployment_type: str,
|
||||
source: str,
|
||||
config_rel: str | None = None,
|
||||
secrets: list[dict[str, str]] | None = None,
|
||||
source_config: dict[str, object],
|
||||
source_revision_config: dict[str, object],
|
||||
secrets: list[dict[str, str]],
|
||||
) -> tuple[str, int]:
|
||||
"""Create a deployment and return its ID and next step number."""
|
||||
_log_deploy_step(step, f"Creating deployment '{name}'")
|
||||
created = client.create_deployment(
|
||||
name=name,
|
||||
deployment_type=deployment_type,
|
||||
source=source,
|
||||
config_path=config_rel,
|
||||
source_config=source_config,
|
||||
source_revision_config=source_revision_config,
|
||||
secrets=secrets,
|
||||
)
|
||||
created_id = created.get("id") if isinstance(created, dict) else None
|
||||
@@ -1108,6 +1108,7 @@ def _run_local_build(
|
||||
updated = client.update_deployment(
|
||||
deployment_id,
|
||||
resolved_image,
|
||||
revision_source="internal_docker",
|
||||
secrets=secrets,
|
||||
tracked_packages=tracked_packages,
|
||||
)
|
||||
@@ -1147,9 +1148,10 @@ def _run_external_deploy(
|
||||
)
|
||||
|
||||
_log_deploy_step(step, f"Updating deployment {deployment_id}")
|
||||
updated = client.update_deployment_external(
|
||||
updated = client.update_deployment(
|
||||
deployment_id,
|
||||
resolved_image,
|
||||
revision_source=None,
|
||||
secrets=secrets,
|
||||
tracked_packages=tracked_packages,
|
||||
)
|
||||
@@ -1730,8 +1732,9 @@ def _deploy_cmd(
|
||||
client,
|
||||
step,
|
||||
name=name,
|
||||
deployment_type=deployment_type,
|
||||
source=source,
|
||||
source_config={"deployment_type": deployment_type},
|
||||
source_revision_config={},
|
||||
secrets=secrets,
|
||||
)
|
||||
|
||||
|
||||
@@ -153,21 +153,19 @@ class HostBackendClient:
|
||||
|
||||
def create_deployment(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
deployment_type: str,
|
||||
source: str,
|
||||
config_path: str | None = None,
|
||||
source_config: dict[str, Any],
|
||||
source_revision_config: dict[str, Any],
|
||||
secrets: list[dict[str, str]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create a deployment."""
|
||||
payload: dict[str, Any] = {
|
||||
"name": name,
|
||||
"source": source,
|
||||
"source_config": {"deployment_type": deployment_type},
|
||||
"source_revision_config": {},
|
||||
"source_config": source_config,
|
||||
"source_revision_config": source_revision_config,
|
||||
}
|
||||
if source == "internal_source" and config_path:
|
||||
payload["source_revision_config"]["langgraph_config_path"] = config_path
|
||||
if secrets is not None:
|
||||
payload["secrets"] = secrets
|
||||
return self._request("POST", "/v2/deployments", payload)
|
||||
@@ -202,43 +200,21 @@ class HostBackendClient:
|
||||
self,
|
||||
deployment_id: str,
|
||||
image_uri: str,
|
||||
*,
|
||||
revision_source: str | None,
|
||||
secrets: list[dict[str, str]] | None = None,
|
||||
tracked_packages: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {
|
||||
"revision_source": "internal_docker",
|
||||
"source_revision_config": {"image_uri": image_uri},
|
||||
}
|
||||
if revision_source is not None:
|
||||
payload["revision_source"] = revision_source
|
||||
if tracked_packages:
|
||||
payload["tracked_packages"] = tracked_packages
|
||||
if secrets is not None:
|
||||
payload["secrets"] = secrets
|
||||
return self._request(
|
||||
"PATCH",
|
||||
f"/v2/deployments/{deployment_id}",
|
||||
payload,
|
||||
)
|
||||
|
||||
def update_deployment_external(
|
||||
self,
|
||||
deployment_id: str,
|
||||
image_uri: str,
|
||||
secrets: list[dict[str, str]] | None = None,
|
||||
tracked_packages: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Update a deployment with a pre-built external image."""
|
||||
payload: dict[str, Any] = {
|
||||
"source_revision_config": {"image_uri": image_uri},
|
||||
}
|
||||
if tracked_packages:
|
||||
payload["tracked_packages"] = tracked_packages
|
||||
if secrets is not None:
|
||||
payload["secrets"] = secrets
|
||||
return self._request(
|
||||
"PATCH",
|
||||
f"/v2/deployments/{deployment_id}",
|
||||
payload,
|
||||
)
|
||||
return self._request("PATCH", f"/v2/deployments/{deployment_id}", payload)
|
||||
|
||||
def update_deployment_internal_source(
|
||||
self,
|
||||
|
||||
@@ -94,7 +94,10 @@ def test_request_transport_error_raises():
|
||||
|
||||
def test_create_deployment(client):
|
||||
result = client.create_deployment(
|
||||
name="my-deploy", deployment_type="dev", source="internal_docker"
|
||||
name="my-deploy",
|
||||
source="internal_docker",
|
||||
source_config={"deployment_type": "dev"},
|
||||
source_revision_config={},
|
||||
)
|
||||
assert result == {"ok": True}
|
||||
|
||||
@@ -134,50 +137,19 @@ def test_request_push_token(client):
|
||||
|
||||
def test_update_deployment(client):
|
||||
result = client.update_deployment(
|
||||
"dep-123", "image:latest", secrets=[{"name": "KEY", "value": "val"}]
|
||||
"dep-123",
|
||||
"image:latest",
|
||||
revision_source="internal_docker",
|
||||
secrets=[{"name": "KEY", "value": "val"}],
|
||||
)
|
||||
assert result == {"ok": True}
|
||||
|
||||
|
||||
def test_update_deployment_no_secrets(client):
|
||||
result = client.update_deployment("dep-123", "image:latest")
|
||||
assert result == {"ok": True}
|
||||
|
||||
|
||||
def test_update_deployment_external():
|
||||
captured: dict = {}
|
||||
c = _capturing_client(captured)
|
||||
result = c.update_deployment_external(
|
||||
"dep-123", "registry.example.com/app@sha256:abc123"
|
||||
result = client.update_deployment(
|
||||
"dep-123", "image:latest", revision_source="internal_docker"
|
||||
)
|
||||
assert result == {"ok": True}
|
||||
body = json.loads(captured["body"])
|
||||
assert "revision_source" not in body
|
||||
assert body["source_revision_config"]["image_uri"] == (
|
||||
"registry.example.com/app@sha256:abc123"
|
||||
)
|
||||
|
||||
|
||||
def test_update_deployment_external_forwards_tracked_packages():
|
||||
captured: dict = {}
|
||||
c = _capturing_client(captured)
|
||||
c.update_deployment_external(
|
||||
"dep-123",
|
||||
"registry.example.com/app:latest",
|
||||
tracked_packages=["google-adk:1.0.0"],
|
||||
)
|
||||
body = json.loads(captured["body"])
|
||||
assert body["tracked_packages"] == ["google-adk:1.0.0"]
|
||||
assert "revision_source" not in body
|
||||
|
||||
|
||||
def test_update_deployment_external_omits_tracked_packages_when_absent():
|
||||
captured: dict = {}
|
||||
c = _capturing_client(captured)
|
||||
c.update_deployment_external("dep-123", "registry.example.com/app:latest")
|
||||
body = json.loads(captured["body"])
|
||||
assert "tracked_packages" not in body
|
||||
assert "revision_source" not in body
|
||||
|
||||
|
||||
def _capturing_client(captured: dict) -> HostBackendClient:
|
||||
@@ -197,6 +169,7 @@ def test_update_deployment_forwards_tracked_packages():
|
||||
c.update_deployment(
|
||||
"dep-123",
|
||||
"image:latest",
|
||||
revision_source="internal_docker",
|
||||
tracked_packages=["google-adk:1.0.0"],
|
||||
)
|
||||
body = json.loads(captured["body"])
|
||||
@@ -207,7 +180,7 @@ def test_update_deployment_forwards_tracked_packages():
|
||||
def test_update_deployment_omits_tracked_packages_when_absent():
|
||||
captured: dict = {}
|
||||
c = _capturing_client(captured)
|
||||
c.update_deployment("dep-123", "image:latest")
|
||||
c.update_deployment("dep-123", "image:latest", revision_source="internal_docker")
|
||||
body = json.loads(captured["body"])
|
||||
assert "tracked_packages" not in body
|
||||
|
||||
@@ -296,7 +269,10 @@ def _routing_client(seen: dict) -> HostBackendClient:
|
||||
[
|
||||
pytest.param(
|
||||
lambda c: c.create_deployment(
|
||||
name="my-deploy", deployment_type="dev", source="internal_docker"
|
||||
name="my-deploy",
|
||||
source="internal_docker",
|
||||
source_config={"deployment_type": "dev"},
|
||||
source_revision_config={},
|
||||
),
|
||||
{
|
||||
"name": "my-deploy",
|
||||
@@ -309,8 +285,9 @@ def _routing_client(seen: dict) -> HostBackendClient:
|
||||
pytest.param(
|
||||
lambda c: c.create_deployment(
|
||||
name="my-deploy",
|
||||
deployment_type="prod",
|
||||
source="internal_docker",
|
||||
source_config={"deployment_type": "prod"},
|
||||
source_revision_config={},
|
||||
secrets=[{"name": "KEY", "value": "val"}],
|
||||
),
|
||||
{
|
||||
@@ -325,9 +302,11 @@ def _routing_client(seen: dict) -> HostBackendClient:
|
||||
pytest.param(
|
||||
lambda c: c.create_deployment(
|
||||
name="my-deploy",
|
||||
deployment_type="dev",
|
||||
source="internal_source",
|
||||
config_path="apps/agent/langgraph.json",
|
||||
source_config={"deployment_type": "dev"},
|
||||
source_revision_config={
|
||||
"langgraph_config_path": "apps/agent/langgraph.json"
|
||||
},
|
||||
),
|
||||
{
|
||||
"name": "my-deploy",
|
||||
@@ -343,6 +322,7 @@ def _routing_client(seen: dict) -> HostBackendClient:
|
||||
lambda c: c.update_deployment(
|
||||
"dep-123",
|
||||
"registry.example.com/app@sha256:abc",
|
||||
revision_source="internal_docker",
|
||||
secrets=[{"name": "KEY", "value": "val"}],
|
||||
),
|
||||
{
|
||||
@@ -405,7 +385,10 @@ def test_request_body_matches_control_plane_contract(call, expected_body):
|
||||
[
|
||||
pytest.param(
|
||||
lambda c: c.create_deployment(
|
||||
name="n", deployment_type="dev", source="internal_docker"
|
||||
name="n",
|
||||
source="internal_docker",
|
||||
source_config={"deployment_type": "dev"},
|
||||
source_revision_config={},
|
||||
),
|
||||
"POST",
|
||||
"/v2/deployments",
|
||||
@@ -424,7 +407,7 @@ def test_request_body_matches_control_plane_contract(call, expected_body):
|
||||
id="delete_deployment",
|
||||
),
|
||||
pytest.param(
|
||||
lambda c: c.update_deployment("dep-1", "img"),
|
||||
lambda c: c.update_deployment("dep-1", "img", revision_source=None),
|
||||
"PATCH",
|
||||
"/v2/deployments/dep-1",
|
||||
id="patch_deployment",
|
||||
@@ -603,3 +586,62 @@ def test_control_plane_endpoints_resolve(host_url, langsmith_endpoint, expected)
|
||||
endpoints = ControlPlaneEndpoints.resolve(host_url, langsmith_endpoint)
|
||||
|
||||
assert (endpoints.control_plane_url, endpoints.dashboard_url) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("call", "expected_body"),
|
||||
[
|
||||
pytest.param(
|
||||
lambda c: c.create_deployment(
|
||||
name="agent",
|
||||
source="external_docker",
|
||||
source_config={"resource_spec": {}},
|
||||
source_revision_config={
|
||||
"image_uri": "registry.example.com/agent@sha256:1"
|
||||
},
|
||||
secrets=[],
|
||||
),
|
||||
{
|
||||
"name": "agent",
|
||||
"source": "external_docker",
|
||||
"source_config": {"resource_spec": {}},
|
||||
"source_revision_config": {
|
||||
"image_uri": "registry.example.com/agent@sha256:1"
|
||||
},
|
||||
"secrets": [],
|
||||
},
|
||||
id="create_sends_the_source_configs_as_given",
|
||||
),
|
||||
pytest.param(
|
||||
lambda c: c.update_deployment(
|
||||
"dep-1", "registry.example.com/agent@sha256:2", revision_source=None
|
||||
),
|
||||
{
|
||||
"source_revision_config": {
|
||||
"image_uri": "registry.example.com/agent@sha256:2"
|
||||
}
|
||||
},
|
||||
id="revision_without_source_override_omits_revision_source",
|
||||
),
|
||||
pytest.param(
|
||||
lambda c: c.update_deployment(
|
||||
"dep-1",
|
||||
"registry.example.com/agent@sha256:2",
|
||||
revision_source="internal_docker",
|
||||
tracked_packages=["langgraph:1.0.0"],
|
||||
),
|
||||
{
|
||||
"revision_source": "internal_docker",
|
||||
"source_revision_config": {
|
||||
"image_uri": "registry.example.com/agent@sha256:2"
|
||||
},
|
||||
"tracked_packages": ["langgraph:1.0.0"],
|
||||
},
|
||||
id="revision_with_source_override_names_it",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_source_agnostic_client_calls_match_control_plane_contract(call, expected_body):
|
||||
captured: dict = {}
|
||||
call(_capturing_client(captured))
|
||||
assert json.loads(captured["body"]) == expected_body
|
||||
|
||||
Reference in New Issue
Block a user