Skip to content

Commit 6806dea

Browse files
jackwotherspooncopybara-github
authored andcommitted
feat: passthrough extra args for adk deploy cloud_run as Cloud Run args
Merge #2544 The command `adk deploy cloud_run` supports limited `gcloud run deploy` args 😢. Which makes the command fine for simple deployments... It should support all current and future Cloud Run deployment args for the command to be widely adopted. This can easily be done by passing through all extra args passed to `adk deploy cloud_run` to gcloud... This PR assumes any extra args/flags passed after `AGENT_PATH` are gcloud flags. ## Example ```sh # ADK flags adk deploy cloud_run \ --project=$GOOGLE_CLOUD_PROJECT \ --region=$GOOGLE_CLOUD_LOCATION \ $AGENT_PATH \ # Use the -- separator for gcloud args -- \ --min-instances=2 \ --no-allow-unauthenticated ``` This gives full Cloud Run feature support to ADK users 🤖 🚀 ## Test Plan To test you can just build locally or pip install feature branch directly: ``` uv venv uv pip install git+https://github.com/jackwotherspoon/adk-python.git ``` Deploy to Cloud Run using additional arguments following `AGENT_PATH`, such as `--min-instance=2` or `--description="Cloud Run test"`: ```sh uv run adk deploy cloud_run \ --project=$GOOGLE_CLOUD_PROJECT \ --region=$GOOGLE_CLOUD_LOCATION \ --with_ui \ $AGENT_PATH \ -- \ --labels=test-label=adk \ --min-instances=2 ``` You can click on the Cloud Run service after deployment and check the service yaml, you should see the additional label etc. <img width="1612" height="622" alt="image" src="https://github.com/user-attachments/assets/596a260a-0052-460b-9642-c18900ccf7c9" /> Fixes #2351 COPYBARA_INTEGRATE_REVIEW=#2544 from jackwotherspoon:main 184a4d7 PiperOrigin-RevId: 799252544
1 parent 2b2f0b5 commit 6806dea

File tree

5 files changed

+451
-23
lines changed

5 files changed

+451
-23
lines changed

src/google/adk/cli/cli_deploy.py

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,52 @@ def _resolve_project(project_in_option: Optional[str]) -> str:
9595
return project
9696

9797

98+
def _validate_gcloud_extra_args(
99+
extra_gcloud_args: Optional[tuple[str, ...]], adk_managed_args: set[str]
100+
) -> None:
101+
"""Validates that extra gcloud args don't conflict with ADK-managed args.
102+
103+
This function dynamically checks for conflicts based on the actual args
104+
that ADK will set, rather than using a hardcoded list.
105+
106+
Args:
107+
extra_gcloud_args: User-provided extra arguments for gcloud.
108+
adk_managed_args: Set of argument names that ADK will set automatically.
109+
Should include '--' prefix (e.g., '--project').
110+
111+
Raises:
112+
click.ClickException: If any conflicts are found.
113+
"""
114+
if not extra_gcloud_args:
115+
return
116+
117+
# Parse user arguments into a set of argument names for faster lookup
118+
user_arg_names = set()
119+
for arg in extra_gcloud_args:
120+
if arg.startswith('--'):
121+
# Handle both '--arg=value' and '--arg value' formats
122+
arg_name = arg.split('=')[0]
123+
user_arg_names.add(arg_name)
124+
125+
# Check for conflicts with ADK-managed args
126+
conflicts = user_arg_names.intersection(adk_managed_args)
127+
128+
if conflicts:
129+
conflict_list = ', '.join(f"'{arg}'" for arg in sorted(conflicts))
130+
if len(conflicts) == 1:
131+
raise click.ClickException(
132+
f"The argument {conflict_list} conflicts with ADK's automatic"
133+
' configuration. ADK will set this argument automatically, so please'
134+
' remove it from your command.'
135+
)
136+
else:
137+
raise click.ClickException(
138+
f"The arguments {conflict_list} conflict with ADK's automatic"
139+
' configuration. ADK will set these arguments automatically, so'
140+
' please remove them from your command.'
141+
)
142+
143+
98144
def _get_service_option_by_adk_version(
99145
adk_version: str,
100146
session_uri: Optional[str],
@@ -141,6 +187,7 @@ def to_cloud_run(
141187
artifact_service_uri: Optional[str] = None,
142188
memory_service_uri: Optional[str] = None,
143189
a2a: bool = False,
190+
extra_gcloud_args: Optional[tuple[str, ...]] = None,
144191
):
145192
"""Deploys an agent to Google Cloud Run.
146193
@@ -234,26 +281,56 @@ def to_cloud_run(
234281
click.echo('Deploying to Cloud Run...')
235282
region_options = ['--region', region] if region else []
236283
project = _resolve_project(project)
237-
subprocess.run(
238-
[
239-
'gcloud',
240-
'run',
241-
'deploy',
242-
service_name,
243-
'--source',
244-
temp_folder,
245-
'--project',
246-
project,
247-
*region_options,
248-
'--port',
249-
str(port),
250-
'--verbosity',
251-
log_level.lower() if log_level else verbosity,
252-
'--labels',
253-
'created-by=adk',
254-
],
255-
check=True,
256-
)
284+
285+
# Build the set of args that ADK will manage
286+
adk_managed_args = {'--source', '--project', '--port', '--verbosity'}
287+
if region:
288+
adk_managed_args.add('--region')
289+
290+
# Validate that extra gcloud args don't conflict with ADK-managed args
291+
_validate_gcloud_extra_args(extra_gcloud_args, adk_managed_args)
292+
293+
# Build the command with extra gcloud args
294+
gcloud_cmd = [
295+
'gcloud',
296+
'run',
297+
'deploy',
298+
service_name,
299+
'--source',
300+
temp_folder,
301+
'--project',
302+
project,
303+
*region_options,
304+
'--port',
305+
str(port),
306+
'--verbosity',
307+
log_level.lower() if log_level else verbosity,
308+
]
309+
310+
# Handle labels specially - merge user labels with ADK label
311+
user_labels = []
312+
extra_args_without_labels = []
313+
314+
if extra_gcloud_args:
315+
for arg in extra_gcloud_args:
316+
if arg.startswith('--labels='):
317+
# Extract user-provided labels
318+
user_labels_value = arg[9:] # Remove '--labels=' prefix
319+
user_labels.append(user_labels_value)
320+
else:
321+
extra_args_without_labels.append(arg)
322+
323+
# Combine ADK label with user labels
324+
all_labels = ['created-by=adk']
325+
all_labels.extend(user_labels)
326+
labels_arg = ','.join(all_labels)
327+
328+
gcloud_cmd.extend(['--labels', labels_arg])
329+
330+
# Add any remaining extra passthrough args
331+
gcloud_cmd.extend(extra_args_without_labels)
332+
333+
subprocess.run(gcloud_cmd, check=True)
257334
finally:
258335
click.echo(f'Cleaning up the temp folder: {temp_folder}')
259336
shutil.rmtree(temp_folder)

src/google/adk/cli/cli_tools_click.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -858,7 +858,13 @@ def cli_api_server(
858858
server.run()
859859

860860

861-
@deploy.command("cloud_run")
861+
@deploy.command(
862+
"cloud_run",
863+
context_settings={
864+
"allow_extra_args": True,
865+
"allow_interspersed_args": False,
866+
},
867+
)
862868
@click.option(
863869
"--project",
864870
type=str,
@@ -971,7 +977,9 @@ def cli_api_server(
971977
# TODO: Add eval_storage_uri option back when evals are supported in Cloud Run.
972978
@adk_services_options()
973979
@deprecated_adk_services_options()
980+
@click.pass_context
974981
def cli_deploy_cloud_run(
982+
ctx,
975983
agent: str,
976984
project: Optional[str],
977985
region: Optional[str],
@@ -996,9 +1004,13 @@ def cli_deploy_cloud_run(
9961004
9971005
AGENT: The path to the agent source code folder.
9981006
999-
Example:
1007+
Use '--' to separate gcloud arguments from adk arguments.
1008+
1009+
Examples:
10001010
10011011
adk deploy cloud_run --project=[project] --region=[region] path/to/my_agent
1012+
1013+
adk deploy cloud_run --project=[project] --region=[region] path/to/my_agent -- --no-allow-unauthenticated --min-instances=2
10021014
"""
10031015
if verbosity:
10041016
click.secho(
@@ -1010,6 +1022,36 @@ def cli_deploy_cloud_run(
10101022

10111023
session_service_uri = session_service_uri or session_db_url
10121024
artifact_service_uri = artifact_service_uri or artifact_storage_uri
1025+
1026+
# Parse arguments to separate gcloud args (after --) from regular args
1027+
gcloud_args = []
1028+
if "--" in ctx.args:
1029+
separator_index = ctx.args.index("--")
1030+
gcloud_args = ctx.args[separator_index + 1 :]
1031+
regular_args = ctx.args[:separator_index]
1032+
1033+
# If there are regular args before --, that's an error
1034+
if regular_args:
1035+
click.secho(
1036+
"Error: Unexpected arguments after agent path and before '--':"
1037+
f" {' '.join(regular_args)}. \nOnly arguments after '--' are passed"
1038+
" to gcloud.",
1039+
fg="red",
1040+
err=True,
1041+
)
1042+
ctx.exit(2)
1043+
else:
1044+
# No -- separator, treat all args as an error to enforce the new behavior
1045+
if ctx.args:
1046+
click.secho(
1047+
f"Error: Unexpected arguments: {' '.join(ctx.args)}. \nUse '--' to"
1048+
" separate gcloud arguments, e.g.: adk deploy cloud_run [options]"
1049+
" agent_path -- --min-instances=2",
1050+
fg="red",
1051+
err=True,
1052+
)
1053+
ctx.exit(2)
1054+
10131055
try:
10141056
cli_deploy.to_cloud_run(
10151057
agent_folder=agent,
@@ -1029,6 +1071,7 @@ def cli_deploy_cloud_run(
10291071
artifact_service_uri=artifact_service_uri,
10301072
memory_service_uri=memory_service_uri,
10311073
a2a=a2a,
1074+
extra_gcloud_args=tuple(gcloud_args),
10321075
)
10331076
except Exception as e:
10341077
click.secho(f"Deploy failed: {e}", fg="red", err=True)

tests/unittests/cli/test_cli_tools_click_option_mismatch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def test_adk_deploy_cloud_run():
137137
cloud_run_command,
138138
cli_deploy_cloud_run.callback,
139139
"deploy cloud_run",
140-
ignore_params={"verbose"},
140+
ignore_params={"verbose", "ctx"},
141141
)
142142

143143

tests/unittests/cli/utils/test_cli_deploy.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,3 +636,76 @@ def mock_subprocess_run(*args, **kwargs):
636636

637637
# 4. Verify cleanup
638638
assert str(rmtree_recorder.get_last_call_args()[0]) == str(tmp_path)
639+
640+
641+
# Label merging tests
642+
@pytest.mark.parametrize(
643+
"extra_gcloud_args, expected_labels",
644+
[
645+
# No user labels - should only have default ADK label
646+
(None, "created-by=adk"),
647+
([], "created-by=adk"),
648+
# Single user label
649+
(["--labels=env=test"], "created-by=adk,env=test"),
650+
# Multiple user labels in same argument
651+
(
652+
["--labels=env=test,team=myteam"],
653+
"created-by=adk,env=test,team=myteam",
654+
),
655+
# User labels mixed with other args
656+
(
657+
["--memory=1Gi", "--labels=env=test", "--cpu=1"],
658+
"created-by=adk,env=test",
659+
),
660+
# Multiple --labels arguments
661+
(
662+
["--labels=env=test", "--labels=team=myteam"],
663+
"created-by=adk,env=test,team=myteam",
664+
),
665+
# Labels with other passthrough args
666+
(
667+
["--timeout=300", "--labels=env=prod", "--max-instances=10"],
668+
"created-by=adk,env=prod",
669+
),
670+
],
671+
)
672+
def test_cloud_run_label_merging(
673+
monkeypatch: pytest.MonkeyPatch,
674+
agent_dir: Callable[[bool, bool], Path],
675+
tmp_path: Path,
676+
extra_gcloud_args: list[str] | None,
677+
expected_labels: str,
678+
) -> None:
679+
"""Test that user labels are properly merged with the default ADK label."""
680+
src_dir = agent_dir(False, False)
681+
run_recorder = _Recorder()
682+
683+
monkeypatch.setattr(subprocess, "run", run_recorder)
684+
monkeypatch.setattr(shutil, "rmtree", lambda x: None)
685+
686+
# Execute the function under test
687+
cli_deploy.to_cloud_run(
688+
agent_folder=str(src_dir),
689+
project="test-project",
690+
region="us-central1",
691+
service_name="test-service",
692+
app_name="test-app",
693+
temp_folder=str(tmp_path),
694+
port=8080,
695+
trace_to_cloud=False,
696+
with_ui=False,
697+
log_level="info",
698+
verbosity="info",
699+
adk_version="1.0.0",
700+
extra_gcloud_args=tuple(extra_gcloud_args) if extra_gcloud_args else None,
701+
)
702+
703+
# Verify that the gcloud command was called
704+
assert len(run_recorder.calls) == 1
705+
gcloud_args = run_recorder.get_last_call_args()[0]
706+
707+
# Find the labels argument
708+
labels_idx = gcloud_args.index("--labels")
709+
actual_labels = gcloud_args[labels_idx + 1]
710+
711+
assert actual_labels == expected_labels

0 commit comments

Comments
 (0)