From e59c5f0092add5dec117b720adb78d6b6a9f7bd1 Mon Sep 17 00:00:00 2001 From: Martin Sarsale Date: Wed, 18 Jun 2025 18:35:14 -0300 Subject: [PATCH 1/2] feat: Add support for installing agent dependencies via requirements.sh in Dockerfile generation for Cloud Run deployments --- src/google/adk/cli/cli_deploy.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/google/adk/cli/cli_deploy.py b/src/google/adk/cli/cli_deploy.py index 99c7e9bb1..e42ca650b 100644 --- a/src/google/adk/cli/cli_deploy.py +++ b/src/google/adk/cli/cli_deploy.py @@ -51,6 +51,10 @@ COPY "agents/{app_name}/" "/app/agents/{app_name}/" {install_agent_deps} +USER root +{install_agent_deps_script} +USER myuser + # Copy agent - End EXPOSE {port} @@ -178,6 +182,12 @@ def to_cloud_run( if os.path.exists(requirements_txt_path) else '' ) + requirements_sh_path = os.path.join(agent_src_path, 'requirements.sh') + install_agent_deps_script = ( + f'RUN sh /app/agents/{app_name}/requirements.sh' + if os.path.exists(requirements_sh_path) + else '' + ) click.echo('Copying agent source code complete.') # create Dockerfile @@ -190,6 +200,7 @@ def to_cloud_run( port=port, command='web' if with_ui else 'api_server', install_agent_deps=install_agent_deps, + install_agent_deps_script=install_agent_deps_script, service_option=_get_service_option_by_adk_version( adk_version, session_service_uri, From 7356270962dd4464a519b413f84db4e4572a22a7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:22:16 +0000 Subject: [PATCH 2/2] feat: Add test for requirements.sh in cli_deploy This commit introduces a new test case for the `to_cloud_run` function in `cli_deploy.py`. The new test, `test_to_cloud_run_with_requirements_sh`, specifically verifies that if a `requirements.sh` file is present in my source directory, the generated Dockerfile includes a command to execute this script. The `agent_dir` fixture has been updated to support the creation of a dummy `requirements.sh` file for testing purposes. --- tests/unittests/cli/utils/test_cli_deploy.py | 70 ++++++++++++++++++-- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/tests/unittests/cli/utils/test_cli_deploy.py b/tests/unittests/cli/utils/test_cli_deploy.py index 312844db8..4e7ad2940 100644 --- a/tests/unittests/cli/utils/test_cli_deploy.py +++ b/tests/unittests/cli/utils/test_cli_deploy.py @@ -53,16 +53,24 @@ def _mute_click(monkeypatch: pytest.MonkeyPatch) -> None: @pytest.fixture() -def agent_dir(tmp_path: Path) -> Callable[[bool], Path]: +def agent_dir( + tmp_path: Path, +) -> Callable[[bool, bool], Path]: """Return a factory that creates a dummy agent directory tree.""" - def _factory(include_requirements: bool) -> Path: + def _factory( + include_requirements: bool, include_requirements_sh: bool = False + ) -> Path: base = tmp_path / "agent" base.mkdir() (base / "agent.py").write_text("# dummy agent") (base / "__init__.py").touch() if include_requirements: (base / "requirements.txt").write_text("pytest\n") + if include_requirements_sh: + (base / "requirements.sh").write_text( + 'echo "Hello from requirements.sh"\n' + ) return base return _factory @@ -124,17 +132,19 @@ def test_get_service_option_by_adk_version() -> None: # to_cloud_run @pytest.mark.parametrize("include_requirements", [True, False]) +@pytest.mark.parametrize("include_requirements_sh", [True, False]) def test_to_cloud_run_happy_path( monkeypatch: pytest.MonkeyPatch, - agent_dir: Callable[[bool], Path], + agent_dir: Callable[[bool, bool], Path], include_requirements: bool, + include_requirements_sh: bool, ) -> None: """ End-to-end execution test for `to_cloud_run` covering both presence and absence of *requirements.txt*. """ tmp_dir = Path(tempfile.mkdtemp()) - src_dir = agent_dir(include_requirements) + src_dir = agent_dir(include_requirements, include_requirements_sh) copy_recorder = _Recorder() run_recorder = _Recorder() @@ -179,13 +189,61 @@ def _recording_copytree(*args: Any, **kwargs: Any): shutil.rmtree(tmp_dir, ignore_errors=True) +def test_to_cloud_run_with_requirements_sh( + monkeypatch: pytest.MonkeyPatch, + agent_dir: Callable[[bool, bool], Path], +) -> None: + """Test that requirements.sh is executed when present.""" + tmp_dir = Path(tempfile.mkdtemp()) + # Create agent directory with requirements.sh + src_dir = agent_dir(include_requirements=False, include_requirements_sh=True) + + run_recorder = _Recorder() + + # Cache the ORIGINAL copytree before patching + original_copytree = cli_deploy.shutil.copytree + + def _recording_copytree(*args: Any, **kwargs: Any): + return original_copytree(*args, **kwargs) + + monkeypatch.setattr(cli_deploy.shutil, "copytree", _recording_copytree) + # Skip actual cleanup so that we can inspect generated files later. + monkeypatch.setattr(cli_deploy.shutil, "rmtree", lambda *_a, **_k: None) + monkeypatch.setattr(subprocess, "run", run_recorder) + + cli_deploy.to_cloud_run( + agent_folder=str(src_dir), + project="proj", + region="asia-northeast1", + service_name="svc", + app_name="app", + temp_folder=str(tmp_dir), + port=8080, + trace_to_cloud=False, # Keep it simple for this test + with_ui=False, # Keep it simple for this test + verbosity="info", + adk_version="0.0.5", # adk_version that includes requirements.sh logic + session_service_uri=None, + artifact_service_uri=None, + memory_service_uri=None, + ) + + dockerfile_content = (tmp_dir / "Dockerfile").read_text() + assert "RUN sh /app/agents/app/requirements.sh" in dockerfile_content, ( + "Dockerfile should contain command to run requirements.sh" + ) + + # Manual cleanup + shutil.rmtree(tmp_dir, ignore_errors=True) + + def test_to_cloud_run_cleans_temp_dir( monkeypatch: pytest.MonkeyPatch, - agent_dir: Callable[[bool], Path], + agent_dir: Callable[[bool, bool], Path], ) -> None: """`to_cloud_run` should always delete the temporary folder on exit.""" tmp_dir = Path(tempfile.mkdtemp()) - src_dir = agent_dir(False) + src_dir = agent_dir(False, False) deleted: Dict[str, Path] = {}