diff --git a/src/google/adk/cli/cli_deploy.py b/src/google/adk/cli/cli_deploy.py index 9d1cbc0d40..b83ee502ab 100644 --- a/src/google/adk/cli/cli_deploy.py +++ b/src/google/adk/cli/cli_deploy.py @@ -63,7 +63,7 @@ def _ensure_agent_engine_dependency(requirements_txt_path: str) -> None: _DOCKERFILE_TEMPLATE: Final[str] = """ -FROM python:3.11-slim +FROM python:{python_version}-slim WORKDIR /app # Create a non-root user @@ -638,6 +638,7 @@ def to_cloud_run( log_level: str, verbosity: str, adk_version: str, + python_version: str = '3.11', allow_origins: Optional[list[str]] = None, session_service_uri: Optional[str] = None, artifact_service_uri: Optional[str] = None, @@ -732,6 +733,7 @@ def to_cloud_run( otel_to_cloud_option='--otel_to_cloud' if otel_to_cloud else '', allow_origins_option=allow_origins_option, adk_version=adk_version, + python_version=python_version, host_option=host_option, a2a_option=a2a_option, ) @@ -1164,6 +1166,7 @@ def to_gke( with_ui: bool, log_level: str, adk_version: str, + python_version: str = '3.11', allow_origins: Optional[list[str]] = None, session_service_uri: Optional[str] = None, artifact_service_uri: Optional[str] = None, @@ -1261,6 +1264,7 @@ def to_gke( otel_to_cloud_option='--otel_to_cloud' if otel_to_cloud else '', allow_origins_option=allow_origins_option, adk_version=adk_version, + python_version=python_version, host_option=host_option, a2a_option='--a2a' if a2a else '', ) diff --git a/src/google/adk/cli/cli_tools_click.py b/src/google/adk/cli/cli_tools_click.py index dcf9415652..8a9d83d95a 100644 --- a/src/google/adk/cli/cli_tools_click.py +++ b/src/google/adk/cli/cli_tools_click.py @@ -1756,6 +1756,16 @@ def cli_api_server( " version in the dev environment)" ), ) +@click.option( + "--python_version", + type=str, + default="3.11", + show_default=True, + help=( + "Optional. The Python version used in the Docker base image." + " (default: 3.11)" + ), +) @click.option( "--a2a", is_flag=True, @@ -1799,6 +1809,7 @@ def cli_deploy_cloud_run( session_db_url: Optional[str] = None, # Deprecated artifact_storage_uri: Optional[str] = None, # Deprecated a2a: bool = False, + python_version: str = "3.11", ): """Deploys an agent to Cloud Run. @@ -1871,6 +1882,7 @@ def cli_deploy_cloud_run( log_level=log_level, verbosity=verbosity, adk_version=adk_version, + python_version=python_version, session_service_uri=session_service_uri, artifact_service_uri=artifact_service_uri, memory_service_uri=memory_service_uri, @@ -2261,6 +2273,16 @@ def cli_deploy_agent_engine( " version in the dev environment)" ), ) +@click.option( + "--python_version", + type=str, + default="3.11", + show_default=True, + help=( + "Optional. The Python version used in the Docker base image." + " (default: 3.11)" + ), +) @adk_services_options(default_use_local_storage=False) @click.argument( "agent", @@ -2286,6 +2308,7 @@ def cli_deploy_gke( artifact_service_uri: Optional[str] = None, memory_service_uri: Optional[str] = None, use_local_storage: bool = False, + python_version: str = "3.11", ): """Deploys an agent to GKE. @@ -2312,6 +2335,7 @@ def cli_deploy_gke( with_ui=with_ui, log_level=log_level, adk_version=adk_version, + python_version=python_version, session_service_uri=session_service_uri, artifact_service_uri=artifact_service_uri, memory_service_uri=memory_service_uri, diff --git a/tests/unittests/cli/utils/test_cli_deploy.py b/tests/unittests/cli/utils/test_cli_deploy.py index e1829d2f98..1ca5683fe9 100644 --- a/tests/unittests/cli/utils/test_cli_deploy.py +++ b/tests/unittests/cli/utils/test_cli_deploy.py @@ -452,6 +452,7 @@ def mock_subprocess_run(*args, **kwargs): allow_origins=["http://localhost:3000", "https://my-app.com"], session_service_uri="sqlite:///", artifact_service_uri="gs://gke-bucket", + python_version="3.12", ) dockerfile_path = tmp_path / "Dockerfile" @@ -459,6 +460,7 @@ def mock_subprocess_run(*args, **kwargs): dockerfile_content = dockerfile_path.read_text() assert "CMD adk web --port=9090" in dockerfile_content assert "RUN pip install google-adk==1.2.0" in dockerfile_content + assert "FROM python:3.12-slim" in dockerfile_content assert len(run_recorder.calls) == 3, "Expected 3 subprocess calls" diff --git a/tests/unittests/cli/utils/test_cli_deploy_to_cloud_run.py b/tests/unittests/cli/utils/test_cli_deploy_to_cloud_run.py index eae87889e6..1429318e7f 100644 --- a/tests/unittests/cli/utils/test_cli_deploy_to_cloud_run.py +++ b/tests/unittests/cli/utils/test_cli_deploy_to_cloud_run.py @@ -130,6 +130,7 @@ def test_to_cloud_run_happy_path( artifact_service_uri="gs://bucket", memory_service_uri="rag://", adk_version="1.3.0", + python_version="3.12", ) agent_dest_path = tmp_path / "agents" / "agent" @@ -145,7 +146,7 @@ def test_to_cloud_run_happy_path( expected_command = "web" if with_ui else "api_server" assert f"CMD adk {expected_command} --port=8080" in dockerfile_content - assert "FROM python:3.11-slim" in dockerfile_content + assert "FROM python:3.12-slim" in dockerfile_content assert ( 'RUN adduser --disabled-password --gecos "" myuser' in dockerfile_content ) @@ -196,6 +197,36 @@ def test_to_cloud_run_happy_path( assert str(rmtree_recorder.get_last_call_args()[0]) == str(tmp_path) +def test_to_cloud_run_default_python_version( + monkeypatch: pytest.MonkeyPatch, + agent_dir: AgentDirFixture, + tmp_path: Path, +) -> None: + """Omitting python_version should default to 3.11 in the Dockerfile.""" + src_dir = agent_dir(include_requirements=False, include_env=False) + monkeypatch.setattr(subprocess, "run", _Recorder()) + monkeypatch.setattr(shutil, "rmtree", _Recorder()) + + cli_deploy.to_cloud_run( + agent_folder=str(src_dir), + project="proj", + region="us-central1", + service_name="svc", + app_name="agent", + temp_folder=str(tmp_path), + port=8080, + trace_to_cloud=False, + otel_to_cloud=False, + with_ui=False, + log_level="info", + verbosity="info", + adk_version="1.0.0", + ) + + dockerfile_content = (tmp_path / "Dockerfile").read_text() + assert "FROM python:3.11-slim" in dockerfile_content + + def test_to_cloud_run_cleans_temp_dir( monkeypatch: pytest.MonkeyPatch, agent_dir: AgentDirFixture, diff --git a/tests/unittests/cli/utils/test_cli_tools_click.py b/tests/unittests/cli/utils/test_cli_tools_click.py index 7c642dbbe9..388b10ce31 100644 --- a/tests/unittests/cli/utils/test_cli_tools_click.py +++ b/tests/unittests/cli/utils/test_cli_tools_click.py @@ -386,6 +386,35 @@ def test_cli_deploy_cloud_run_allows_empty_gcloud_args( assert extra_args == () +def test_cli_deploy_cloud_run_python_version( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """--python_version should be forwarded to cli_deploy.to_cloud_run.""" + rec = _Recorder() + monkeypatch.setattr(cli_tools_click.cli_deploy, "to_cloud_run", rec) + + agent_dir = tmp_path / "agent_cr_pyver" + agent_dir.mkdir() + runner = CliRunner() + result = runner.invoke( + cli_tools_click.main, + [ + "deploy", + "cloud_run", + "--project", + "proj", + "--region", + "us-central1", + "--python_version", + "3.12", + str(agent_dir), + ], + ) + assert result.exit_code == 0 + assert rec.calls, "cli_deploy.to_cloud_run must be invoked" + assert rec.calls[0][1].get("python_version") == "3.12" + + # cli deploy agent_engine def test_cli_deploy_agent_engine_success( tmp_path: Path, monkeypatch: pytest.MonkeyPatch @@ -481,6 +510,37 @@ def test_cli_deploy_gke_success( assert called_kwargs.get("cluster_name") == "my-cluster" +def test_cli_deploy_gke_python_version( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """--python_version should be forwarded to cli_deploy.to_gke.""" + rec = _Recorder() + monkeypatch.setattr(cli_tools_click.cli_deploy, "to_gke", rec) + + agent_dir = tmp_path / "agent_gke_pyver" + agent_dir.mkdir() + runner = CliRunner() + result = runner.invoke( + cli_tools_click.main, + [ + "deploy", + "gke", + "--project", + "test-proj", + "--region", + "us-central1", + "--cluster_name", + "my-cluster", + "--python_version", + "3.13", + str(agent_dir), + ], + ) + assert result.exit_code == 0 + assert rec.calls, "cli_deploy.to_gke must be invoked" + assert rec.calls[0][1].get("python_version") == "3.13" + + # cli eval def test_cli_eval_missing_deps_raises( tmp_path: Path, monkeypatch: pytest.MonkeyPatch