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
157 changes: 157 additions & 0 deletions src/google/adk/cli/cli_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Logic for the `adk build` command."""

from __future__ import annotations

import os
import shutil
import subprocess
import tempfile
from datetime import datetime
from typing import Optional

import click
from .utils import build_utils
from .utils import gcp_utils


def build_image(
agent_folder: str,
project: Optional[str],
region: Optional[str],
repository: str,
image_name: Optional[str],
tag: str,
adk_version: str,
log_level: str = "INFO",
):
"""Builds an agent image and pushes it to Artifact Registry.

Args:
agent_folder: Path to the agent source code.
project: GCP project ID.
region: GCP region.
repository: Artifact Registry repository name.
image_name: Name of the image. Defaults to agent folder name.
tag: Image tag.
adk_version: ADK version to use in the image.
log_level: Gcloud logging verbosity.
"""
project = gcp_utils.resolve_project(project)
env_vars = {}
# Attempt to read the env variables from .env in the dir (if any).
env_file = os.path.join(agent_folder, '.env')
if os.path.exists(env_file):
from dotenv import dotenv_values

click.echo(f'Reading environment variables from {env_file}')
env_vars = dotenv_values(env_file)
if 'GOOGLE_CLOUD_PROJECT' in env_vars:
env_project = env_vars.pop('GOOGLE_CLOUD_PROJECT')
if env_project:
if project:
click.secho(
'Ignoring GOOGLE_CLOUD_PROJECT in .env as `--project` was'
' explicitly passed and takes precedence',
fg='yellow',
)
else:
project = env_project
click.echo(f'{project=} set by GOOGLE_CLOUD_PROJECT in {env_file}')
if 'GOOGLE_CLOUD_LOCATION' in env_vars:
env_region = env_vars.get('GOOGLE_CLOUD_LOCATION')
if env_region:
if region:
click.secho(
'Ignoring GOOGLE_CLOUD_LOCATION in .env as `--region` was'
' explicitly passed and takes precedence',
fg='yellow',
)
else:
region = env_region
click.echo(f'{region=} set by GOOGLE_CLOUD_LOCATION in {env_file}')

app_name = os.path.basename(agent_folder.rstrip("/"))
image_name = image_name or app_name

temp_folder = os.path.join(
tempfile.gettempdir(),
"adk_build_src",
datetime.now().strftime("%Y%m%d_%H%M%S"),
)

try:
click.echo(f"Staging build files in {temp_folder}...")
agent_src_path = os.path.join(temp_folder, "agents", app_name)
shutil.copytree(agent_folder, agent_src_path)

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."
)

dockerfile_content = build_utils.DOCKERFILE_TEMPLATE.format(
gcp_project_id=project,
gcp_region=region,
app_name=app_name,
port=8080, # Default port for container images
command="api_server",
install_agent_deps=install_agent_deps,
service_option=build_utils.get_service_option_by_adk_version(
adk_version, None, None, None, False
),
trace_to_cloud_option="",
otel_to_cloud_option="",
allow_origins_option="",
adk_version=adk_version,
host_option="--host=0.0.0.0",
a2a_option="",
trigger_sources_option="",
)

dockerfile_path = os.path.join(temp_folder, "Dockerfile")
os.makedirs(temp_folder, exist_ok=True)
with open(dockerfile_path, "w", encoding="utf-8") as f:
f.write(dockerfile_content)

# image URL format: [REGION]-docker.pkg.dev/[PROJECT]/[REPOSITORY]/[IMAGE]:[TAG]
full_image_url = (
f"{region}-docker.pkg.dev/{project}/{repository}/{image_name}:{tag}"
)

click.secho(f"\nBuilding image: {full_image_url}", bold=True)
subprocess.run(
[
gcp_utils.GCLOUD_CMD,
"builds",
"submit",
"--tag",
full_image_url,
"--project",
project,
"--verbosity",
log_level.lower(),
temp_folder,
],
check=True,
)
click.secho("\n✅ Image built and pushed successfully.", fg="green")

finally:
if os.path.exists(temp_folder):
shutil.rmtree(temp_folder)
139 changes: 38 additions & 101 deletions src/google/adk/cli/cli_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,9 @@

import click
from packaging.version import parse
from google.adk.cli.utils import build_utils
from google.adk.cli.utils import gcp_utils

_IS_WINDOWS = os.name == 'nt'
_GCLOUD_CMD = 'gcloud.cmd' if _IS_WINDOWS else 'gcloud'
_LOCAL_STORAGE_FLAG_MIN_VERSION: Final[str] = '1.21.0'
_AGENT_ENGINE_REQUIREMENT: Final[str] = (
'google-cloud-aiplatform[adk,agent_engines]'
)
Expand Down Expand Up @@ -63,45 +62,6 @@ def _ensure_agent_engine_dependency(requirements_txt_path: str) -> None:
f.write(_AGENT_ENGINE_REQUIREMENT + '\n')


_DOCKERFILE_TEMPLATE: Final[str] = """
FROM python:3.11-slim
WORKDIR /app

# Create a non-root user
RUN adduser --disabled-password --gecos "" myuser

# Switch to the non-root user
USER myuser

# Set up environment variables - Start
ENV PATH="/home/myuser/.local/bin:$PATH"

ENV GOOGLE_GENAI_USE_VERTEXAI=1
ENV GOOGLE_CLOUD_PROJECT={gcp_project_id}
ENV GOOGLE_CLOUD_LOCATION={gcp_region}

# Set up environment variables - End

# Install ADK - Start
RUN pip install google-adk=={adk_version}
# Install ADK - End

# Copy agent - Start

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

# 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} "/app/agents"
"""

_AGENT_ENGINE_APP_TEMPLATE: Final[str] = """
import os
import vertexai
Expand Down Expand Up @@ -409,17 +369,9 @@ def _ensure_agent_engine_dependency(requirements_txt_path: str) -> None:


def _resolve_project(project_in_option: Optional[str]) -> str:
if project_in_option:
return project_in_option

result = subprocess.run(
[_GCLOUD_CMD, 'config', 'get-value', 'project'],
check=True,
capture_output=True,
text=True,
)
project = result.stdout.strip()
click.echo(f'Use default project: {project}')
project = gcp_utils.resolve_project(project_in_option)
if not project_in_option:
click.echo(f'Use default project: {project}')
return project


Expand Down Expand Up @@ -585,45 +537,6 @@ def _validate_agent_import(
sys.modules.pop(key, None)


def _get_service_option_by_adk_version(
adk_version: str,
session_uri: Optional[str],
artifact_uri: Optional[str],
memory_uri: Optional[str],
use_local_storage: Optional[bool] = None,
) -> str:
"""Returns service option string based on adk_version."""
parsed_version = parse(adk_version)
options: list[str] = []

if parsed_version >= parse('1.3.0'):
if session_uri:
options.append(f'--session_service_uri={session_uri}')
if artifact_uri:
options.append(f'--artifact_service_uri={artifact_uri}')
if memory_uri:
options.append(f'--memory_service_uri={memory_uri}')
else:
if session_uri:
options.append(f'--session_db_url={session_uri}')
if parsed_version >= parse('1.2.0') and artifact_uri:
options.append(f'--artifact_storage_uri={artifact_uri}')

if use_local_storage is not None and parsed_version >= parse(
_LOCAL_STORAGE_FLAG_MIN_VERSION
):
# Only valid when session/artifact URIs are unset; otherwise the CLI
# rejects the combination to avoid confusing precedence.
if session_uri is None and artifact_uri is None:
options.append((
'--use_local_storage'
if use_local_storage
else '--no_use_local_storage'
))

return ' '.join(options)


def to_cloud_run(
*,
agent_folder: str,
Expand Down Expand Up @@ -719,14 +632,14 @@ def to_cloud_run(
trigger_sources_option = (
f'--trigger_sources={trigger_sources}' if trigger_sources else ''
)
dockerfile_content = _DOCKERFILE_TEMPLATE.format(
dockerfile_content = build_utils.DOCKERFILE_TEMPLATE.format(
gcp_project_id=project,
gcp_region=region,
app_name=app_name,
port=port,
command='web' if with_ui else 'api_server',
install_agent_deps=install_agent_deps,
service_option=_get_service_option_by_adk_version(
service_option=build_utils.get_service_option_by_adk_version(
adk_version,
session_service_uri,
artifact_service_uri,
Expand Down Expand Up @@ -764,7 +677,7 @@ def to_cloud_run(

# Build the command with extra gcloud args
gcloud_cmd = [
_GCLOUD_CMD,
gcp_utils.GCLOUD_CMD,
'run',
'deploy',
service_name,
Expand Down Expand Up @@ -828,6 +741,7 @@ def to_agent_engine(
env_file: Optional[str] = None,
agent_engine_config_file: Optional[str] = None,
skip_agent_import_validation: bool = True,
image_uri: Optional[str] = None,
):
"""Deploys an agent to Vertex AI Agent Engine.

Expand Down Expand Up @@ -895,7 +809,34 @@ def to_agent_engine(
skip the pre-deployment import validation of `agent.py`. This can be
useful when the local environment does not have the same dependencies as
the deployment environment.
image_uri (str): Optional. The Artifact Registry Docker image URI (e.g.,
us-central1-docker.pkg.dev/my-project/my-repo/my-image:tag) of the
container image to be deployed to Agent Engine. If specified, the
deployment will skip the build step and deploy the image directly to
Agent Engine, and the other source files will be ignored.
"""
import vertexai
from ..utils._google_client_headers import get_tracking_headers

if image_uri:
click.echo(f'Deploying agent engine from image: {image_uri}')
project = _resolve_project(project)
client = vertexai.Client(
project=project,
location=region,
http_options={'headers': get_tracking_headers()},
)
config = {'container_spec': {'image_uri': image_uri}, 'class_methods': []}
if display_name:
config['display_name'] = display_name
if description:
config['description'] = description
agent_engine = client.agent_engines.create(config=config)
click.secho(
f'✅ Created agent engine: {agent_engine.api_resource.name}',
fg='green',
)
return
app_name = os.path.basename(agent_folder)
display_name = display_name or app_name
parent_folder = os.path.dirname(agent_folder)
Expand Down Expand Up @@ -1072,10 +1013,6 @@ def to_agent_engine(
# Set env_vars in agent_config to None if it is not set.
agent_config['env_vars'] = agent_config.get('env_vars', env_vars)

import vertexai

from ..utils._google_client_headers import get_tracking_headers

if project and region:
click.echo('Initializing Vertex AI...')
client = vertexai.Client(
Expand Down Expand Up @@ -1261,14 +1198,14 @@ def to_gke(
click.secho('\nSTEP 2: Generating deployment files...', bold=True)
click.echo(' - Creating Dockerfile...')
host_option = '--host=0.0.0.0' if adk_version > '0.5.0' else ''
dockerfile_content = _DOCKERFILE_TEMPLATE.format(
dockerfile_content = build_utils.DOCKERFILE_TEMPLATE.format(
gcp_project_id=project,
gcp_region=region,
app_name=app_name,
port=port,
command='web' if with_ui else 'api_server',
install_agent_deps=install_agent_deps,
service_option=_get_service_option_by_adk_version(
service_option=build_utils.get_service_option_by_adk_version(
adk_version,
session_service_uri,
artifact_service_uri,
Expand Down
Loading
Loading