Skip to content

Commit f52831c

Browse files
yeesiancopybara-github
authored andcommitted
feat: Enable building of images from ADK CLI
Co-authored-by: Yeesian Ng <ysian@google.com> PiperOrigin-RevId: 897874029
1 parent abcf14c commit f52831c

7 files changed

Lines changed: 517 additions & 94 deletions

File tree

src/google/adk/cli/cli_build.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Logic for the `adk build` command."""
16+
17+
from __future__ import annotations
18+
19+
import os
20+
import shutil
21+
import subprocess
22+
import tempfile
23+
from datetime import datetime
24+
from typing import Optional
25+
26+
import click
27+
from .utils import build_utils
28+
from .utils import gcp_utils
29+
30+
31+
def run_build_directory(
32+
agent_folder: str,
33+
project: Optional[str],
34+
region: Optional[str],
35+
repository: str,
36+
image_name: Optional[str],
37+
tag: str,
38+
adk_version: str,
39+
log_level: str = "INFO",
40+
):
41+
"""Builds an agent image and pushes it to Artifact Registry.
42+
43+
Args:
44+
agent_folder: Path to the agent source code.
45+
project: GCP project ID.
46+
region: GCP region.
47+
repository: Artifact Registry repository name.
48+
image_name: Name of the image. Defaults to agent folder name.
49+
tag: Image tag.
50+
adk_version: ADK version to use in the image.
51+
log_level: Gcloud logging verbosity.
52+
"""
53+
project = gcp_utils.resolve_project(project)
54+
if not project:
55+
raise ValueError(
56+
"GCP Project ID must be specified or configured in gcloud."
57+
)
58+
59+
if not region:
60+
# Try to get default region from gcloud config if not provided
61+
result = subprocess.run(
62+
[gcp_utils.GCLOUD_CMD, "config", "get-value", "compute/region"],
63+
capture_output=True,
64+
text=True,
65+
check=False,
66+
)
67+
region = result.stdout.strip() or "us-central1"
68+
click.echo(f"Using region: {region}")
69+
70+
app_name = os.path.basename(agent_folder.rstrip("/"))
71+
image_name = image_name or app_name
72+
73+
temp_folder = os.path.join(
74+
tempfile.gettempdir(),
75+
"adk_build_src",
76+
datetime.now().strftime("%Y%m%d_%H%M%S"),
77+
)
78+
79+
try:
80+
click.echo(f"Staging build files in {temp_folder}...")
81+
agent_src_path = os.path.join(temp_folder, "agents", app_name)
82+
shutil.copytree(agent_folder, agent_src_path)
83+
84+
requirements_txt_path = os.path.join(agent_src_path, "requirements.txt")
85+
install_agent_deps = (
86+
f'RUN pip install -r "/app/agents/{app_name}/requirements.txt"'
87+
if os.path.exists(requirements_txt_path)
88+
else "# No requirements.txt found."
89+
)
90+
91+
dockerfile_content = build_utils.DOCKERFILE_TEMPLATE.format(
92+
gcp_project_id=project,
93+
gcp_region=region,
94+
app_name=app_name,
95+
port=8080, # Default port for container images
96+
command="api_server",
97+
install_agent_deps=install_agent_deps,
98+
service_option=build_utils.get_service_option_by_adk_version(
99+
adk_version, None, None, None, False
100+
),
101+
trace_to_cloud_option="",
102+
otel_to_cloud_option="",
103+
allow_origins_option="",
104+
adk_version=adk_version,
105+
host_option="--host=0.0.0.0",
106+
a2a_option="",
107+
trigger_sources_option="",
108+
)
109+
110+
dockerfile_path = os.path.join(temp_folder, "Dockerfile")
111+
os.makedirs(temp_folder, exist_ok=True)
112+
with open(dockerfile_path, "w", encoding="utf-8") as f:
113+
f.write(dockerfile_content)
114+
115+
# image URL format: [REGION]-docker.pkg.dev/[PROJECT]/[REPOSITORY]/[IMAGE]:[TAG]
116+
full_image_url = (
117+
f"{region}-docker.pkg.dev/{project}/{repository}/{image_name}:{tag}"
118+
)
119+
120+
click.secho(f"\nBuilding image: {full_image_url}", bold=True)
121+
subprocess.run(
122+
[
123+
gcp_utils.GCLOUD_CMD,
124+
"builds",
125+
"submit",
126+
"--tag",
127+
full_image_url,
128+
"--project",
129+
project,
130+
"--verbosity",
131+
log_level.lower(),
132+
temp_folder,
133+
],
134+
check=True,
135+
)
136+
click.secho("\n✅ Image built and pushed successfully.", fg="green")
137+
138+
finally:
139+
if os.path.exists(temp_folder):
140+
shutil.rmtree(temp_folder)

src/google/adk/cli/cli_deploy.py

Lines changed: 8 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@
2929
import click
3030
from packaging.version import parse
3131

32-
_IS_WINDOWS = os.name == 'nt'
33-
_GCLOUD_CMD = 'gcloud.cmd' if _IS_WINDOWS else 'gcloud'
34-
_LOCAL_STORAGE_FLAG_MIN_VERSION: Final[str] = '1.21.0'
3532
_AGENT_ENGINE_REQUIREMENT: Final[str] = (
3633
'google-cloud-aiplatform[adk,agent_engines]'
3734
)
@@ -63,44 +60,6 @@ def _ensure_agent_engine_dependency(requirements_txt_path: str) -> None:
6360
f.write(_AGENT_ENGINE_REQUIREMENT + '\n')
6461

6562

66-
_DOCKERFILE_TEMPLATE: Final[str] = """
67-
FROM python:3.11-slim
68-
WORKDIR /app
69-
70-
# Create a non-root user
71-
RUN adduser --disabled-password --gecos "" myuser
72-
73-
# Switch to the non-root user
74-
USER myuser
75-
76-
# Set up environment variables - Start
77-
ENV PATH="/home/myuser/.local/bin:$PATH"
78-
79-
ENV GOOGLE_GENAI_USE_VERTEXAI=1
80-
ENV GOOGLE_CLOUD_PROJECT={gcp_project_id}
81-
ENV GOOGLE_CLOUD_LOCATION={gcp_region}
82-
83-
# Set up environment variables - End
84-
85-
# Install ADK - Start
86-
RUN pip install google-adk=={adk_version}
87-
# Install ADK - End
88-
89-
# Copy agent - Start
90-
91-
# Set permission
92-
COPY --chown=myuser:myuser "agents/{app_name}/" "/app/agents/{app_name}/"
93-
94-
# Copy agent - End
95-
96-
# Install Agent Deps - Start
97-
{install_agent_deps}
98-
# Install Agent Deps - End
99-
100-
EXPOSE {port}
101-
102-
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"
103-
"""
10463

10564
_AGENT_ENGINE_APP_TEMPLATE: Final[str] = """
10665
import os
@@ -409,17 +368,9 @@ def _ensure_agent_engine_dependency(requirements_txt_path: str) -> None:
409368

410369

411370
def _resolve_project(project_in_option: Optional[str]) -> str:
412-
if project_in_option:
413-
return project_in_option
414-
415-
result = subprocess.run(
416-
[_GCLOUD_CMD, 'config', 'get-value', 'project'],
417-
check=True,
418-
capture_output=True,
419-
text=True,
420-
)
421-
project = result.stdout.strip()
422-
click.echo(f'Use default project: {project}')
371+
project = gcp_utils.resolve_project(project_in_option)
372+
if not project_in_option:
373+
click.echo(f'Use default project: {project}')
423374
return project
424375

425376

@@ -585,43 +536,6 @@ def _validate_agent_import(
585536
sys.modules.pop(key, None)
586537

587538

588-
def _get_service_option_by_adk_version(
589-
adk_version: str,
590-
session_uri: Optional[str],
591-
artifact_uri: Optional[str],
592-
memory_uri: Optional[str],
593-
use_local_storage: Optional[bool] = None,
594-
) -> str:
595-
"""Returns service option string based on adk_version."""
596-
parsed_version = parse(adk_version)
597-
options: list[str] = []
598-
599-
if parsed_version >= parse('1.3.0'):
600-
if session_uri:
601-
options.append(f'--session_service_uri={session_uri}')
602-
if artifact_uri:
603-
options.append(f'--artifact_service_uri={artifact_uri}')
604-
if memory_uri:
605-
options.append(f'--memory_service_uri={memory_uri}')
606-
else:
607-
if session_uri:
608-
options.append(f'--session_db_url={session_uri}')
609-
if parsed_version >= parse('1.2.0') and artifact_uri:
610-
options.append(f'--artifact_storage_uri={artifact_uri}')
611-
612-
if use_local_storage is not None and parsed_version >= parse(
613-
_LOCAL_STORAGE_FLAG_MIN_VERSION
614-
):
615-
# Only valid when session/artifact URIs are unset; otherwise the CLI
616-
# rejects the combination to avoid confusing precedence.
617-
if session_uri is None and artifact_uri is None:
618-
options.append((
619-
'--use_local_storage'
620-
if use_local_storage
621-
else '--no_use_local_storage'
622-
))
623-
624-
return ' '.join(options)
625539

626540

627541
def to_cloud_run(
@@ -719,14 +633,14 @@ def to_cloud_run(
719633
trigger_sources_option = (
720634
f'--trigger_sources={trigger_sources}' if trigger_sources else ''
721635
)
722-
dockerfile_content = _DOCKERFILE_TEMPLATE.format(
636+
dockerfile_content = build_utils.DOCKERFILE_TEMPLATE.format(
723637
gcp_project_id=project,
724638
gcp_region=region,
725639
app_name=app_name,
726640
port=port,
727641
command='web' if with_ui else 'api_server',
728642
install_agent_deps=install_agent_deps,
729-
service_option=_get_service_option_by_adk_version(
643+
service_option=build_utils.get_service_option_by_adk_version(
730644
adk_version,
731645
session_service_uri,
732646
artifact_service_uri,
@@ -764,7 +678,7 @@ def to_cloud_run(
764678

765679
# Build the command with extra gcloud args
766680
gcloud_cmd = [
767-
_GCLOUD_CMD,
681+
gcp_utils.GCLOUD_CMD,
768682
'run',
769683
'deploy',
770684
service_name,
@@ -1261,14 +1175,14 @@ def to_gke(
12611175
click.secho('\nSTEP 2: Generating deployment files...', bold=True)
12621176
click.echo(' - Creating Dockerfile...')
12631177
host_option = '--host=0.0.0.0' if adk_version > '0.5.0' else ''
1264-
dockerfile_content = _DOCKERFILE_TEMPLATE.format(
1178+
dockerfile_content = build_utils.DOCKERFILE_TEMPLATE.format(
12651179
gcp_project_id=project,
12661180
gcp_region=region,
12671181
app_name=app_name,
12681182
port=port,
12691183
command='web' if with_ui else 'api_server',
12701184
install_agent_deps=install_agent_deps,
1271-
service_option=_get_service_option_by_adk_version(
1185+
service_option=build_utils.get_service_option_by_adk_version(
12721186
adk_version,
12731187
session_service_uri,
12741188
artifact_service_uri,

0 commit comments

Comments
 (0)