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
68 changes: 68 additions & 0 deletions src/google/adk/cli/cli_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@

from ..version import __version__
from .deployers import DeployerFactory
from .deployers._dockerfile_template import _DEFAULT_PYTHON_VERSION
from .deployers._dockerfile_template import _render_dockerfile
from .deployers._dockerfile_template import _render_install_agent_deps
from .deployers._dockerfile_template import _validate_app_name
from .deployers._dockerfile_template import _validate_python_version
from .utils import _onboarding

logger = logging.getLogger('google_adk.' + __name__)
Expand Down Expand Up @@ -254,6 +256,50 @@ def _apply_service_account_to_agent_config(
agent_config['service_account'] = _validate_service_account(existing)


def _resolve_python_version_from_agent_config(
agent_config: dict[str, Any],
python_version: Optional[str],
) -> str:
"""Resolves the container Python version for an Agent Engine deploy.

Supports three sources, in increasing precedence:

1. The default, ``3.11``.
2. Top-level ``python_version`` in ``.agent_engine_config.json`` (popped so
it is not forwarded to the Agent Engine API, which ignores it when the
image is built from a Dockerfile).
3. Explicit ``python_version`` argument (CLI flag), which overrides both.

Args:
agent_config: The parsed agent platform config; ``python_version`` is
removed from it.
python_version: The CLI value, or None when the flag was omitted.

Returns:
The validated Python version to render into the Dockerfile.

Raises:
click.ClickException: If either value is not a valid Python 3.11+ version
string.
"""
config_python_version = agent_config.pop('python_version', None)
if config_python_version is not None:
if not isinstance(config_python_version, str):
raise click.ClickException(
'python_version in agent platform config must be a string.'
)
config_python_version = _validate_python_version(config_python_version)
if python_version is not None:
python_version = _validate_python_version(python_version)
if config_python_version and config_python_version != python_version:
click.echo(
'Overriding python_version in agent platform config with'
f' {python_version}'
)
return python_version
return config_python_version or _DEFAULT_PYTHON_VERSION


def _on_rm_error(func: Callable[..., Any], path: str, exc_info: Any) -> None:
"""Error handler for shutil.rmtree to handle read-only files on Windows."""
os.chmod(path, stat.S_IWRITE)
Expand Down Expand Up @@ -1143,6 +1189,7 @@ def run(
extra_gcloud_args: tuple[str, ...] | None = None,
with_cloud_run_sandbox: bool = False,
extra_packages: list[str] | None = None,
python_version: str = '3.11',
) -> None:
"""Deploys an agent to Google Cloud Run.

Expand Down Expand Up @@ -1184,13 +1231,15 @@ def run(
extra_packages: Additional local file or directory paths to stage alongside
the agent and make importable in the image. A relative path is resolved
against the current working directory.
python_version: The Python version for the container base image.
"""
_validate_trigger_options(
trigger_sources, trigger_oidc_audience, trigger_oidc_service_accounts
)
app_name = _validate_app_name(
app_name or os.path.basename(os.path.normpath(agent_folder))
)
python_version = _validate_python_version(python_version)
if parse(adk_version) >= parse('1.3.0') and not use_local_storage:
session_service_uri = session_service_uri or 'memory://'
artifact_service_uri = artifact_service_uri or 'memory://'
Expand Down Expand Up @@ -1267,6 +1316,7 @@ def run(
staged_extra_packages, temp_folder
),
extra_env_vars='',
python_version=python_version,
)
dockerfile_path = os.path.join(temp_folder, 'Dockerfile')
os.makedirs(temp_folder, exist_ok=True)
Expand Down Expand Up @@ -1326,6 +1376,7 @@ def to_cloud_run(
extra_gcloud_args: tuple[str, ...] | None = None,
with_cloud_run_sandbox: bool = False,
extra_packages: list[str] | None = None,
python_version: str = '3.11',
) -> None:
"""Deploys an agent to Google Cloud Run (deprecated)."""
warnings.warn(
Expand Down Expand Up @@ -1362,6 +1413,7 @@ def to_cloud_run(
extra_gcloud_args=extra_gcloud_args,
with_cloud_run_sandbox=with_cloud_run_sandbox,
extra_packages=extra_packages,
python_version=python_version,
)


Expand Down Expand Up @@ -1422,6 +1474,7 @@ def to_agent_engine(
extra_packages: Optional[list[str]] = None,
worker_pool: Optional[str] = None,
service_account: Optional[str] = None,
python_version: Optional[str] = None,
) -> None:
"""Deploys an agent to Gemini Enterprise Agent Platform.

Expand Down Expand Up @@ -1500,12 +1553,19 @@ def to_agent_engine(
``GOOGLE_CLOUD_SERVICE_ACCOUNT`` in the ``.env`` file and
``service_account`` in ``.agent_engine_config.json`` when both are
present. When omitted, Agent Engine uses its default service agent.
python_version (str): Optional. The Python version for the container base
image. Overrides ``python_version`` in ``.agent_engine_config.json``
when both are present. When neither is set, ``3.11`` is used.
"""
_validate_trigger_options(
trigger_sources, trigger_oidc_audience, trigger_oidc_service_accounts
)
app_name = os.path.basename(os.path.normpath(agent_folder))
_validate_app_name(app_name)
# Fail on a bad flag value before the temp folder is replaced and the agent
# source copied; the config-file value is resolved once the file is read.
if python_version is not None:
python_version = _validate_python_version(python_version)
display_name = display_name or app_name
parent_folder = os.path.dirname(os.path.normpath(agent_folder))
if adk_app_object:
Expand Down Expand Up @@ -1602,6 +1662,9 @@ def to_agent_engine(
agent_config['description'] = description

_apply_worker_pool_to_agent_config(agent_config, worker_pool)
python_version = _resolve_python_version_from_agent_config(
agent_config, python_version
)

config_extra_packages = agent_config.pop('extra_packages', None) or []
# CLI entries resolve against the invocation dir; config-file entries
Expand Down Expand Up @@ -1868,6 +1931,7 @@ def create_dockerfile_for_agent_engine(resource_name: str) -> None:
staged_extra_packages, temp_folder_path
),
extra_env_vars=extra_env_vars,
python_version=python_version,
)
with open('Dockerfile', 'w', encoding='utf-8') as f:
f.write(dockerfile_content)
Expand Down Expand Up @@ -1946,6 +2010,7 @@ def to_gke(
'ClusterIP', 'NodePort', 'LoadBalancer'
] = 'ClusterIP',
extra_packages: Optional[list[str]] = None,
python_version: str = '3.11',
) -> None:
"""Deploys an agent to Google Kubernetes Engine(GKE).

Expand Down Expand Up @@ -1977,6 +2042,7 @@ def to_gke(
extra_packages: Additional local file or directory paths to stage alongside
the agent and make importable in the image. A relative path is resolved
against the current working directory.
python_version: The Python version for the container base image.
"""
_validate_trigger_options(
trigger_sources, trigger_oidc_audience, trigger_oidc_service_accounts
Expand All @@ -1994,6 +2060,7 @@ def to_gke(

app_name = app_name or os.path.basename(os.path.normpath(agent_folder))
_validate_app_name(app_name)
python_version = _validate_python_version(python_version)
if parse(adk_version) >= parse('1.3.0') and not use_local_storage:
session_service_uri = session_service_uri or 'memory://'
artifact_service_uri = artifact_service_uri or 'memory://'
Expand Down Expand Up @@ -2070,6 +2137,7 @@ def to_gke(
staged_extra_packages, temp_folder
),
extra_env_vars='',
python_version=python_version,
)
dockerfile_path = os.path.join(temp_folder, 'Dockerfile')
os.makedirs(temp_folder, exist_ok=True)
Expand Down
39 changes: 39 additions & 0 deletions src/google/adk/cli/cli_tools_click.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,16 @@ def deploy_options(command: _F) -> _F:
" 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 container image"
" (default: 3.11)."
),
),
click.argument(
"agent",
type=click.Path(
Expand Down Expand Up @@ -2703,6 +2713,7 @@ def cli_deploy_cloud_run(
provider_args: tuple[str, ...] = (),
env: tuple[str, ...] = (),
extra_packages: tuple[str, ...] = (),
python_version: str = "3.11",
) -> None:
"""Deploys an agent to Cloud Run.

Expand Down Expand Up @@ -2754,6 +2765,7 @@ def cli_deploy_cloud_run(
extra_gcloud_args=tuple(gcloud_args),
with_cloud_run_sandbox=with_cloud_run_sandbox,
extra_packages=list(extra_packages),
python_version=python_version,
)
except (click.ClickException, click.Abort):
raise
Expand Down Expand Up @@ -2785,6 +2797,7 @@ def cli_deploy_docker(
a2a: bool = False,
provider_args: tuple[str, ...] = (),
env: tuple[str, ...] = (),
python_version: str = "3.11",
) -> None:
"""Deploys an agent to a local Docker container."""
_warn_if_with_ui(with_ui)
Expand Down Expand Up @@ -2813,6 +2826,7 @@ def cli_deploy_docker(
trigger_sources=None,
provider_args=provider_args,
env=env,
python_version=python_version,
)
except (click.ClickException, click.Abort):
raise
Expand Down Expand Up @@ -3078,6 +3092,17 @@ def cli_migrate_session(
" the version in the dev environment)"
),
)
@click.option(
"--python_version",
type=str,
default=None,
help=(
"Optional. The Python version used in the container image. Overrides"
" `python_version` in `.agent_engine_config.json` when both are set."
" (default: `python_version` from `.agent_engine_config.json`, or"
" 3.11)"
),
)
@click.option(
"--extra_packages",
multiple=True,
Expand Down Expand Up @@ -3155,6 +3180,7 @@ def cli_deploy_agent_engine(
trigger_oidc_audience: str | None = None,
trigger_oidc_service_accounts: str | None = None,
service_account: str | None = None,
python_version: str | None = None,
) -> None:
"""Deploys an agent to Agent Engine.

Expand Down Expand Up @@ -3220,6 +3246,7 @@ def cli_deploy_agent_engine(
extra_packages=list(extra_packages),
worker_pool=worker_pool,
service_account=service_account,
python_version=python_version,
)
except (click.ClickException, click.Abort):
raise
Expand Down Expand Up @@ -3339,6 +3366,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 container image"
" (default: 3.11)."
),
)
# Kept as raw str (not parsed to list) — interpolated directly into Dockerfile CMD.
@click.option(
"--trigger_sources",
Expand Down Expand Up @@ -3413,6 +3450,7 @@ def cli_deploy_gke(
trigger_oidc_audience: str | None = None,
trigger_oidc_service_accounts: str | None = None,
extra_packages: tuple[str, ...] = (),
python_version: str = "3.11",
) -> None:
"""Deploys an agent to GKE.

Expand Down Expand Up @@ -3452,6 +3490,7 @@ def cli_deploy_gke(
trigger_oidc_audience=trigger_oidc_audience,
trigger_oidc_service_accounts=trigger_oidc_service_accounts,
extra_packages=list(extra_packages),
python_version=python_version,
)
except (click.ClickException, click.Abort):
raise
Expand Down
32 changes: 31 additions & 1 deletion src/google/adk/cli/deployers/_dockerfile_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
r'^[A-Za-z0-9_-][A-Za-z0-9_.-]{0,62}$'
)

_DEFAULT_PYTHON_VERSION: Final[str] = '3.11'

_PYTHON_VERSION_PATTERN: Final[re.Pattern[str]] = re.compile(
r'^3\.(1[1-9]|[2-9][0-9])(\.[0-9]+)?$'
)


def _validate_app_name(app_name: str) -> str:
"""Validates the deploy app name before it is written into a Dockerfile.
Expand All @@ -55,6 +61,27 @@ def _validate_app_name(app_name: str) -> str:
return app_name


def _validate_python_version(python_version: str) -> str:
"""Validates the Python version before it is written into a Dockerfile.

Args:
python_version: The Python version (e.g. '3.11', '3.12', '3.12.1').

Returns:
The validated Python version, unchanged.

Raises:
click.ClickException: If the Python version is not a valid 3.11+ version
string.
"""
if not _PYTHON_VERSION_PATTERN.fullmatch(python_version):
raise click.ClickException(
f'Invalid python_version {python_version!r}. Expected a Python 3.11+'
' version string such as "3.11", "3.12", or "3.12.1".'
)
return python_version


def _validate_dockerfile_literal(name: str, value: Any) -> str:
"""Validates a value interpolated into a single-line Dockerfile instruction.

Expand Down Expand Up @@ -98,7 +125,7 @@ def _to_exec_form(instruction: str, args: list[str]) -> str:


_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 @@ -191,6 +218,7 @@ def _render_dockerfile(
express_mode_option: str = '',
extra_packages_copy: str = '',
extra_env_vars: str = '',
python_version: str = _DEFAULT_PYTHON_VERSION,
) -> str:
"""Renders the Dockerfile used by every `adk deploy` target.

Expand Down Expand Up @@ -222,6 +250,7 @@ def _render_dockerfile(
express_mode_option: `--express_mode` or an empty string.
extra_packages_copy: Extra `COPY`/`ENV` instructions, or an empty string.
extra_env_vars: Extra `ENV` lines, or an empty string.
python_version: The Python version for the base image (default: '3.11').

Returns:
The Dockerfile contents.
Expand Down Expand Up @@ -263,6 +292,7 @@ def _render_dockerfile(
)

return _DOCKERFILE_TEMPLATE.format(
python_version=_validate_python_version(python_version),
app_name=app_name,
port=port,
install_adk=install_adk,
Expand Down
Loading
Loading