Skip to content
7 changes: 6 additions & 1 deletion src/google/adk/cli/cli_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -590,6 +590,7 @@ def _get_service_option_by_adk_version(
artifact_uri: Optional[str],
memory_uri: Optional[str],
use_local_storage: Optional[bool] = None,
python_version: str = '3.11',
) -> str:
"""Returns service option string based on adk_version."""
parsed_version = parse(adk_version)
Expand Down Expand Up @@ -638,6 +639,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,
Expand Down Expand Up @@ -732,6 +734,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,
)
Expand Down Expand Up @@ -1164,6 +1167,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,
Expand Down Expand Up @@ -1261,6 +1265,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 '',
)
Expand Down
24 changes: 24 additions & 0 deletions src/google/adk/cli/cli_tools_click.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
),
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This @click.option for --python_version is duplicated for the cli_deploy_gke command as well. To improve maintainability and avoid duplication, consider creating a reusable decorator for this option, similar to how adk_services_options is implemented.

For example:

def python_version_option():
  return 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)"
      ),
  )

@deploy.command(...)
@python_version_option()
def cli_deploy_cloud_run(...):
  ...

@deploy.command(...)
@python_version_option()
def cli_deploy_gke(...):
  ...

This change would make the code more DRY (Don't Repeat Yourself).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I should implement this or stick to the existing pattern used for the other flag parameters

@click.option(
"--a2a",
is_flag=True,
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand All @@ -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.

Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions tests/unittests/cli/utils/test_cli_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,13 +452,15 @@ 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"
assert dockerfile_path.is_file()
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"

Expand Down
33 changes: 32 additions & 1 deletion tests/unittests/cli/utils/test_cli_deploy_to_cloud_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
)
Expand Down Expand Up @@ -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,
Expand Down
31 changes: 31 additions & 0 deletions tests/unittests/cli/utils/test_cli_tools_click.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,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
Expand Down
Loading