Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 7 additions & 12 deletions src/google/adk/cli/cli_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

from ..version import __version__
from .deployers import DeployerFactory
from .deployers._dockerfile_template import _agent_deps_install_layer
from .deployers._dockerfile_template import _DOCKERFILE_TEMPLATE
from .utils import _onboarding

Expand Down Expand Up @@ -801,10 +802,8 @@ def run(
ignore_func = _get_ignore_patterns_func(agent_folder)
shutil.copytree(agent_folder, agent_src_path, ignore=ignore_func)
requirements_txt_path = os.path.join(agent_src_path, 'requirements.txt')
install_agent_deps = (
f'RUN pip install -r "/app/agents/{app_name}/requirements.txt"'
if os.path.exists(requirements_txt_path)
else '# No requirements.txt found.'
install_agent_deps = _agent_deps_install_layer(
app_name, os.path.exists(requirements_txt_path)
)
click.echo('Copying agent source code completed.')

Expand Down Expand Up @@ -1349,10 +1348,8 @@ def to_agent_engine(

def create_dockerfile_for_agent_engine(resource_name: str) -> None:
requirements_txt_path = os.path.join(agent_src_path, 'requirements.txt')
install_agent_deps = (
f'RUN pip install -r "/app/agents/{app_name}/requirements.txt"'
if os.path.exists(requirements_txt_path)
else '# No requirements.txt found.'
install_agent_deps = _agent_deps_install_layer(
app_name, os.path.exists(requirements_txt_path)
)
trigger_sources_option = (
f'--trigger_sources={trigger_sources}' if trigger_sources else ''
Expand Down Expand Up @@ -1566,10 +1563,8 @@ def to_gke(
ignore_func = _get_ignore_patterns_func(agent_folder)
shutil.copytree(agent_folder, agent_src_path, ignore=ignore_func)
requirements_txt_path = os.path.join(agent_src_path, 'requirements.txt')
install_agent_deps = (
f'RUN pip install -r "/app/agents/{app_name}/requirements.txt"'
if os.path.exists(requirements_txt_path)
else ''
install_agent_deps = _agent_deps_install_layer(
app_name, os.path.exists(requirements_txt_path)
)
click.secho('✅ Environment prepared.', fg='green')

Expand Down
24 changes: 20 additions & 4 deletions src/google/adk/cli/deployers/_dockerfile_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,34 @@
RUN python -c "import os, glob, google.adk.cli as cli; d = os.path.dirname(cli.__file__); [os.remove(f) for f in glob.glob(os.path.join(d, 'dev_server*'))]; [os.remove(f) for f in glob.glob(os.path.join(d, '__pycache__', 'dev_server*'))]" || true
# Install ADK - End

# Install Agent Deps - Start
{install_agent_deps}
# Install Agent Deps - End

# Copy agent - Start

# Set permission
COPY --chown=myuser:myuser "agents/{app_name}/" "/app/agents/{app_name}/"
{extra_packages_copy}
# Copy agent - End

# Install Agent Deps - Start
{install_agent_deps}
# Install Agent Deps - End

EXPOSE {port}

CMD adk {command} --port={port} {host_option} {service_option} {trace_to_cloud_option} {otel_to_cloud_option} {allow_origins_option} {a2a_option} {trigger_sources_option} {trigger_oidc_audience_option} {trigger_oidc_service_accounts_option} {gemini_enterprise_option}{express_mode_option} "/app/agents"
"""


def _agent_deps_install_layer(app_name: str, has_requirements: bool) -> str:
"""Returns Dockerfile lines that install agent deps before copying source.

Copying requirements.txt on its own keeps the pip layer cached when only
agent source changes.
"""
if not has_requirements:
return '# No requirements.txt found.'
dest = f'/app/agents/{app_name}/requirements.txt'
return (
f'COPY --chown=myuser:myuser "agents/{app_name}/requirements.txt"'
f' "{dest}"\n'
f'RUN pip install -r "{dest}"'
)
36 changes: 36 additions & 0 deletions tests/unittests/cli/utils/test_cli_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

import click
from click.testing import CliRunner
from google.adk.cli.deployers._dockerfile_template import _agent_deps_install_layer
import pytest

import src.google.adk.cli.cli_deploy as cli_deploy
Expand Down Expand Up @@ -99,6 +100,26 @@ def _factory(include_requirements: bool, include_env: bool) -> Path:
return _factory


def test_agent_deps_install_layer_copies_requirements_before_pip() -> None:
"""Agent requirements are copied as their own layer, then installed."""
layer = _agent_deps_install_layer("my_app", True)
req_copy = (
'COPY --chown=myuser:myuser "agents/my_app/requirements.txt"'
' "/app/agents/my_app/requirements.txt"'
)
req_install = 'RUN pip install -r "/app/agents/my_app/requirements.txt"'
assert req_copy in layer
assert req_install in layer
assert layer.index(req_copy) < layer.index(req_install)


def test_agent_deps_install_layer_without_requirements() -> None:
"""Missing requirements.txt is a no-op comment, not a pip install."""
assert _agent_deps_install_layer("my_app", False) == (
"# No requirements.txt found."
)


# _resolve_project
def test_resolve_project_with_option() -> None:
"""It should return the explicit project value untouched."""
Expand Down Expand Up @@ -403,6 +424,21 @@ def mock_subprocess_run(*args, **kwargs):
dockerfile_content = dockerfile_path.read_text()
assert "CMD adk api_server --with_ui --port=9090" in dockerfile_content
assert 'RUN pip install "google-adk[a2a]==1.2.0"' in dockerfile_content
agent_copy = 'COPY --chown=myuser:myuser "agents/agent/" "/app/agents/agent/"'
if include_requirements:
req_copy = (
'COPY --chown=myuser:myuser "agents/agent/requirements.txt"'
' "/app/agents/agent/requirements.txt"'
)
req_install = 'RUN pip install -r "/app/agents/agent/requirements.txt"'
assert dockerfile_content.index(req_copy) < dockerfile_content.index(
req_install
)
assert dockerfile_content.index(req_install) < dockerfile_content.index(
agent_copy
)
else:
assert "# No requirements.txt found." in dockerfile_content

assert len(run_recorder.calls) == 3, "Expected 3 subprocess calls"

Expand Down
16 changes: 13 additions & 3 deletions tests/unittests/cli/utils/test_cli_deploy_to_cloud_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,20 @@ def test_to_cloud_run_happy_path(
assert "--otel_to_cloud" in dockerfile_content

# Check agent dependencies installation based on include_requirements
agent_copy = 'COPY --chown=myuser:myuser "agents/agent/" "/app/agents/agent/"'
if include_requirements:
assert (
'RUN pip install -r "/app/agents/agent/requirements.txt"'
in dockerfile_content
req_copy = (
'COPY --chown=myuser:myuser "agents/agent/requirements.txt"'
' "/app/agents/agent/requirements.txt"'
)
req_install = 'RUN pip install -r "/app/agents/agent/requirements.txt"'
assert req_copy in dockerfile_content
assert req_install in dockerfile_content
assert dockerfile_content.index(req_copy) < dockerfile_content.index(
req_install
)
assert dockerfile_content.index(req_install) < dockerfile_content.index(
agent_copy
)
else:
assert "# No requirements.txt found." in dockerfile_content
Expand Down
Loading