Skip to content

Commit 3d6d09a

Browse files
authored
feat: refactor CLI to use direct API calls (GoogleCloudPlatform#713)
* feat: refactor CLI to use direct API calls - Add get_project_number() function using Resource Manager API - Replace gcloud projects describe calls with direct API calls in cicd.py - Add _build_api_headers() helper in register_gemini_enterprise.py - Include User-Agent and x-goog-api-client headers in Discovery Engine API calls * fix: improve error handling and make API helper functions public - Make get_user_agent and get_x_goog_api_client_header public in gcp.py - Add RESOURCE_MANAGER_API_BASE constant for API endpoint URL - Use specific exceptions (PermissionError, ValueError) in get_project_number - Update cicd.py to handle new exception types from get_project_number - Update register_gemini_enterprise.py to use public function names * fix: remove extra ::: from development guide
1 parent cc37d9f commit 3d6d09a

File tree

5 files changed

+108
-43
lines changed

5 files changed

+108
-43
lines changed

agent_starter_pack/cli/commands/register_gemini_enterprise.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
from rich.console import Console
3131

3232
from agent_starter_pack.cli.utils.command import run_gcloud_command
33+
from agent_starter_pack.cli.utils.gcp import (
34+
get_user_agent,
35+
get_x_goog_api_client_header,
36+
)
3337

3438
# TOML parser - use standard library for Python 3.11+, fallback to tomli
3539
if sys.version_info >= (3, 11):
@@ -298,6 +302,32 @@ def get_access_token() -> str:
298302
raise RuntimeError("Failed to get access token") from e
299303

300304

305+
def _build_api_headers(
306+
access_token: str,
307+
project_id: str,
308+
content_type: bool = False,
309+
) -> dict[str, str]:
310+
"""Build headers for Discovery Engine API requests with user-agent.
311+
312+
Args:
313+
access_token: Google Cloud access token
314+
project_id: GCP project ID or number for billing
315+
content_type: Whether to include Content-Type header (for POST/PATCH)
316+
317+
Returns:
318+
Headers dictionary
319+
"""
320+
headers = {
321+
"Authorization": f"Bearer {access_token}",
322+
"x-goog-user-project": project_id,
323+
"User-Agent": get_user_agent(),
324+
"x-goog-api-client": get_x_goog_api_client_header(),
325+
}
326+
if content_type:
327+
headers["Content-Type"] = "application/json"
328+
return headers
329+
330+
301331
def get_identity_token() -> str:
302332
"""Get Google Cloud identity token.
303333
@@ -604,10 +634,7 @@ def list_gemini_enterprise_apps(
604634
f"{base_endpoint}/v1alpha/projects/{project_number}/"
605635
f"locations/{location}/collections/default_collection/engines"
606636
)
607-
headers = {
608-
"Authorization": f"Bearer {access_token}",
609-
"x-goog-user-project": project_number,
610-
}
637+
headers = _build_api_headers(access_token, project_number)
611638

612639
response = requests.get(url, headers=headers, timeout=30)
613640
response.raise_for_status()
@@ -937,11 +964,7 @@ def register_a2a_agent(
937964
f"locations/{as_location}/collections/{collection}/engines/{engine_id}/"
938965
"assistants/default_assistant/agents"
939966
)
940-
headers = {
941-
"Authorization": f"Bearer {access_token}",
942-
"Content-Type": "application/json",
943-
"x-goog-user-project": project_id,
944-
}
967+
headers = _build_api_headers(access_token, project_id, content_type=True)
945968

946969
# Build payload with A2A agent definition
947970
payload = {
@@ -1106,11 +1129,7 @@ def register_agent(
11061129
)
11071130

11081131
# Request headers
1109-
headers = {
1110-
"Authorization": f"Bearer {access_token}",
1111-
"Content-Type": "application/json",
1112-
"x-goog-user-project": project_id,
1113-
}
1132+
headers = _build_api_headers(access_token, project_id, content_type=True)
11141133

11151134
# Request body
11161135
payload: dict = {

agent_starter_pack/cli/utils/cicd.py

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from rich.prompt import IntPrompt, Prompt
3030

3131
from agent_starter_pack.cli.utils.command import get_gcloud_cmd
32+
from agent_starter_pack.cli.utils.gcp import get_project_number
3233

3334
console = Console()
3435

@@ -149,18 +150,7 @@ def create_github_connection(
149150

150151
# Get the Cloud Build service account and grant permissions with retry logic
151152
try:
152-
project_number_result = run_command(
153-
[
154-
"gcloud",
155-
"projects",
156-
"describe",
157-
project_id,
158-
"--format=value(projectNumber)",
159-
],
160-
capture_output=True,
161-
check=True,
162-
)
163-
project_number = project_number_result.stdout.strip()
153+
project_number = get_project_number(project_id)
164154
cloud_build_sa = (
165155
f"service-{project_number}@gcp-sa-cloudbuild.iam.gserviceaccount.com"
166156
)
@@ -219,7 +209,14 @@ def create_github_connection(
219209
"⏳ Waiting for IAM permissions to propagate (this typically takes 5-10 seconds)..."
220210
)
221211
time.sleep(10) # Give IAM time to propagate before proceeding
222-
except subprocess.CalledProcessError as e:
212+
except (PermissionError, ValueError) as e:
213+
console.print(
214+
f"⚠️ Could not setup Cloud Build service account: {e}", style="yellow"
215+
)
216+
console.print(
217+
"You may need to manually grant the permissions if the connection creation fails."
218+
)
219+
except Exception as e:
223220
console.print(
224221
f"⚠️ Could not setup Cloud Build service account: {e}", style="yellow"
225222
)
@@ -477,16 +474,7 @@ def ensure_apis_enabled(project_id: str, apis: list[str]) -> None:
477474
capture_output=True,
478475
)
479476

480-
project_number = run_command(
481-
[
482-
"gcloud",
483-
"projects",
484-
"describe",
485-
project_id,
486-
"--format=value(projectNumber)",
487-
],
488-
capture_output=True,
489-
).stdout.strip()
477+
project_number = get_project_number(project_id)
490478

491479
cloudbuild_sa = (
492480
f"service-{project_number}@gcp-sa-cloudbuild.iam.gserviceaccount.com"
@@ -507,11 +495,21 @@ def ensure_apis_enabled(project_id: str, apis: list[str]) -> None:
507495
)
508496
console.print("✅ Permissions granted to Cloud Build service account")
509497

498+
except (PermissionError, ValueError) as e:
499+
console.print(
500+
f"❌ Failed to set up service account permissions: {e!s}", style="bold red"
501+
)
502+
raise
510503
except subprocess.CalledProcessError as e:
511504
console.print(
512505
f"❌ Failed to set up service account permissions: {e!s}", style="bold red"
513506
)
514507
raise
508+
except Exception as e:
509+
console.print(
510+
f"❌ Failed to set up service account permissions: {e!s}", style="bold red"
511+
)
512+
raise
515513

516514
# Add a small delay to allow API enablement and IAM changes to propagate
517515
time.sleep(10)

agent_starter_pack/cli/utils/gcp.py

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
from agent_starter_pack.cli.utils.command import run_gcloud_command
2929
from agent_starter_pack.cli.utils.version import PACKAGE_NAME, get_current_version
3030

31+
# API endpoint constants
32+
RESOURCE_MANAGER_API_BASE = "https://cloudresourcemanager.googleapis.com"
33+
3134
# Lazy console - only create when needed
3235
_console = None
3336

@@ -49,16 +52,16 @@ def _get_console() -> Console:
4952
)
5053

5154

52-
def _get_user_agent(context: str | None = None) -> str:
55+
def get_user_agent(context: str | None = None) -> str:
5356
"""Returns a custom user agent string."""
5457
version = get_current_version()
5558
prefix = "ag" if context == "agent-garden" else ""
5659
return f"{prefix}{version}-{PACKAGE_NAME}/{prefix}{version}-{PACKAGE_NAME}"
5760

5861

59-
def _get_x_goog_api_client_header(context: str | None = None) -> str:
62+
def get_x_goog_api_client_header(context: str | None = None) -> str:
6063
"""Build x-goog-api-client header matching SDK format."""
61-
user_agent = _get_user_agent(context)
64+
user_agent = get_user_agent(context)
6265
python_version = (
6366
f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
6467
)
@@ -112,8 +115,8 @@ def _test_vertex_connection(
112115
Returns:
113116
Tuple of (success, error_message)
114117
"""
115-
user_agent = _get_user_agent(context)
116-
x_goog_api_client = _get_x_goog_api_client_header(context)
118+
user_agent = get_user_agent(context)
119+
x_goog_api_client = get_x_goog_api_client_header(context)
117120

118121
try:
119122
response = requests.post(
@@ -268,3 +271,44 @@ def get_account() -> str:
268271
):
269272
raise Exception(_AUTH_ERROR_MESSAGE) from e
270273
raise
274+
275+
276+
def get_project_number(project_id: str) -> str:
277+
"""Get project number from project ID using Resource Manager API.
278+
279+
Args:
280+
project_id: GCP project ID
281+
282+
Returns:
283+
Project number as string
284+
285+
Raises:
286+
PermissionError: If access is denied to the project
287+
ValueError: If the project is not found
288+
requests.exceptions.HTTPError: For other API failures
289+
"""
290+
_, _, token = _get_credentials_and_token()
291+
292+
user_agent = get_user_agent()
293+
x_goog_api_client = get_x_goog_api_client_header()
294+
295+
response = requests.get(
296+
f"{RESOURCE_MANAGER_API_BASE}/v1/projects/{project_id}",
297+
headers={
298+
"Authorization": f"Bearer {token}",
299+
"User-Agent": user_agent,
300+
"x-goog-api-client": x_goog_api_client,
301+
},
302+
timeout=30,
303+
)
304+
305+
if response.status_code == 403:
306+
raise PermissionError(
307+
f"Permission denied accessing project '{project_id}'. "
308+
"Ensure you have the required permissions."
309+
)
310+
if response.status_code == 404:
311+
raise ValueError(f"Project '{project_id}' not found.")
312+
313+
response.raise_for_status()
314+
return response.json()["projectNumber"]

docs/guide/development-guide.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ go get <package> # Add dependency
9292
go mod tidy # Clean up dependencies
9393
```
9494
:::
95-
:::
9695

9796
> Note: The specific UI playground launched by `make playground` depends on the agent template you selected during creation.
9897

docs/package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)