agent-starter-pack 0.7.1__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {agent_starter_pack-0.7.1.dist-info → agent_starter_pack-0.9.0.dist-info}/METADATA +7 -6
  2. {agent_starter_pack-0.7.1.dist-info → agent_starter_pack-0.9.0.dist-info}/RECORD +63 -59
  3. agents/README.md +7 -0
  4. agents/adk_base/{template/.templateconfig.yaml → .template/templateconfig.yaml} +3 -1
  5. agents/adk_base/notebooks/adk_app_testing.ipynb +8 -6
  6. agents/adk_gemini_fullstack/{template/.templateconfig.yaml → .template/templateconfig.yaml} +3 -2
  7. agents/adk_gemini_fullstack/notebooks/adk_app_testing.ipynb +8 -6
  8. agents/agentic_rag/{template/.templateconfig.yaml → .template/templateconfig.yaml} +2 -1
  9. agents/agentic_rag/notebooks/adk_app_testing.ipynb +8 -6
  10. agents/crewai_coding_crew/{template/.templateconfig.yaml → .template/templateconfig.yaml} +1 -1
  11. llm.txt +7 -0
  12. src/base_template/Makefile +5 -1
  13. src/base_template/README.md +2 -2
  14. src/base_template/deployment/cd/deploy-to-prod.yaml +1 -16
  15. src/base_template/deployment/cd/staging.yaml +4 -19
  16. src/base_template/deployment/terraform/apis.tf +2 -2
  17. src/base_template/deployment/terraform/build_triggers.tf +5 -5
  18. src/base_template/deployment/terraform/dev/apis.tf +8 -1
  19. src/base_template/deployment/terraform/dev/variables.tf +3 -1
  20. src/base_template/deployment/terraform/iam.tf +8 -8
  21. src/base_template/deployment/terraform/locals.tf +9 -2
  22. src/base_template/deployment/terraform/log_sinks.tf +2 -2
  23. src/base_template/deployment/terraform/service_accounts.tf +3 -3
  24. src/base_template/deployment/terraform/storage.tf +7 -7
  25. src/base_template/deployment/terraform/variables.tf +3 -0
  26. src/base_template/pyproject.toml +4 -3
  27. src/cli/commands/create.py +191 -41
  28. src/cli/commands/list.py +158 -0
  29. src/cli/commands/setup_cicd.py +2 -2
  30. src/cli/main.py +2 -0
  31. src/cli/utils/cicd.py +2 -2
  32. src/cli/utils/remote_template.py +254 -0
  33. src/cli/utils/template.py +134 -25
  34. src/deployment_targets/agent_engine/app/agent_engine_app.py +7 -7
  35. src/deployment_targets/agent_engine/tests/load_test/README.md +1 -6
  36. src/deployment_targets/agent_engine/tests/load_test/load_test.py +13 -3
  37. src/deployment_targets/cloud_run/Dockerfile +3 -0
  38. src/deployment_targets/cloud_run/app/server.py +18 -0
  39. src/deployment_targets/cloud_run/deployment/terraform/dev/service.tf +231 -0
  40. src/deployment_targets/cloud_run/deployment/terraform/service.tf +360 -0
  41. src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +8 -5
  42. src/deployment_targets/cloud_run/tests/load_test/README.md +2 -2
  43. src/deployment_targets/cloud_run/tests/load_test/load_test.py +21 -17
  44. src/frontends/adk_gemini_fullstack/frontend/src/App.tsx +2 -3
  45. src/resources/docs/adk-cheatsheet.md +1 -1
  46. src/resources/locks/uv-adk_base-agent_engine.lock +873 -236
  47. src/resources/locks/uv-adk_base-cloud_run.lock +1169 -283
  48. src/resources/locks/uv-adk_gemini_fullstack-agent_engine.lock +873 -236
  49. src/resources/locks/uv-adk_gemini_fullstack-cloud_run.lock +1169 -283
  50. src/resources/locks/uv-agentic_rag-agent_engine.lock +508 -373
  51. src/resources/locks/uv-agentic_rag-cloud_run.lock +668 -469
  52. src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +582 -587
  53. src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +791 -733
  54. src/resources/locks/uv-langgraph_base_react-agent_engine.lock +587 -478
  55. src/resources/locks/uv-langgraph_base_react-cloud_run.lock +799 -627
  56. src/resources/locks/uv-live_api-cloud_run.lock +803 -603
  57. src/resources/setup_cicd/github.tf +2 -2
  58. src/utils/lock_utils.py +1 -1
  59. src/deployment_targets/cloud_run/uv.lock +0 -6952
  60. {agent_starter_pack-0.7.1.dist-info → agent_starter_pack-0.9.0.dist-info}/WHEEL +0 -0
  61. {agent_starter_pack-0.7.1.dist-info → agent_starter_pack-0.9.0.dist-info}/entry_points.txt +0 -0
  62. {agent_starter_pack-0.7.1.dist-info → agent_starter_pack-0.9.0.dist-info}/licenses/LICENSE +0 -0
  63. /agents/langgraph_base_react/{template/.templateconfig.yaml → .template/templateconfig.yaml} +0 -0
  64. /agents/live_api/{template/.templateconfig.yaml → .template/templateconfig.yaml} +0 -0
src/cli/utils/template.py CHANGED
@@ -29,6 +29,9 @@ from rich.prompt import IntPrompt, Prompt
29
29
  from src.cli.utils.version import get_current_version
30
30
 
31
31
  from .datastores import DATASTORES
32
+ from .remote_template import (
33
+ get_base_template_name,
34
+ )
32
35
 
33
36
  ADK_FILES = ["app/__init__.py"]
34
37
  NON_ADK_FILES: list[str] = []
@@ -69,7 +72,7 @@ class TemplateConfig:
69
72
 
70
73
 
71
74
  OVERWRITE_FOLDERS = ["app", "frontend", "tests", "notebooks"]
72
- TEMPLATE_CONFIG_FILE = ".templateconfig.yaml"
75
+ TEMPLATE_CONFIG_FILE = "templateconfig.yaml"
73
76
  DEPLOYMENT_FOLDERS = ["cloud_run", "agent_engine"]
74
77
  DEFAULT_FRONTEND = "streamlit"
75
78
 
@@ -94,7 +97,7 @@ def get_available_agents(deployment_target: str | None = None) -> dict:
94
97
 
95
98
  for agent_dir in agents_dir.iterdir():
96
99
  if agent_dir.is_dir() and not agent_dir.name.startswith("__"):
97
- template_config_path = agent_dir / "template" / ".templateconfig.yaml"
100
+ template_config_path = agent_dir / ".template" / "templateconfig.yaml"
98
101
  if template_config_path.exists():
99
102
  try:
100
103
  with open(template_config_path) as f:
@@ -154,15 +157,20 @@ def load_template_config(template_dir: pathlib.Path) -> dict[str, Any]:
154
157
  return {}
155
158
 
156
159
 
157
- def get_deployment_targets(agent_name: str) -> list:
160
+ def get_deployment_targets(
161
+ agent_name: str, remote_config: dict[str, Any] | None = None
162
+ ) -> list:
158
163
  """Get available deployment targets for the selected agent."""
159
- template_path = (
160
- pathlib.Path(__file__).parent.parent.parent.parent
161
- / "agents"
162
- / agent_name
163
- / "template"
164
- )
165
- config = load_template_config(template_path)
164
+ if remote_config:
165
+ config = remote_config
166
+ else:
167
+ template_path = (
168
+ pathlib.Path(__file__).parent.parent.parent.parent
169
+ / "agents"
170
+ / agent_name
171
+ / ".template"
172
+ )
173
+ config = load_template_config(template_path)
166
174
 
167
175
  if not config:
168
176
  return []
@@ -171,9 +179,11 @@ def get_deployment_targets(agent_name: str) -> list:
171
179
  return targets if isinstance(targets, list) else [targets]
172
180
 
173
181
 
174
- def prompt_deployment_target(agent_name: str) -> str:
182
+ def prompt_deployment_target(
183
+ agent_name: str, remote_config: dict[str, Any] | None = None
184
+ ) -> str:
175
185
  """Ask user to select a deployment target for the agent."""
176
- targets = get_deployment_targets(agent_name)
186
+ targets = get_deployment_targets(agent_name, remote_config=remote_config)
177
187
 
178
188
  # Define deployment target friendly names and descriptions
179
189
  TARGET_INFO = {
@@ -206,6 +216,36 @@ def prompt_deployment_target(agent_name: str) -> str:
206
216
  return targets[choice - 1]
207
217
 
208
218
 
219
+ def prompt_session_type_selection() -> str:
220
+ """Ask user to select a session type for Cloud Run deployment."""
221
+ console = Console()
222
+
223
+ session_types = {
224
+ "in_memory": {
225
+ "display_name": "In-memory session",
226
+ "description": "Session data stored in memory - ideal for stateless applications",
227
+ },
228
+ "alloydb": {
229
+ "display_name": "AlloyDB",
230
+ "description": "Use AlloyDB for session management. Comes with terraform resources for deployment.",
231
+ },
232
+ }
233
+
234
+ console.print("\n> Please select a session type:")
235
+ for idx, (_key, info) in enumerate(session_types.items(), 1):
236
+ console.print(
237
+ f"{idx}. [bold]{info['display_name']}[/] - [dim]{info['description']}[/]"
238
+ )
239
+
240
+ choice = IntPrompt.ask(
241
+ "\nEnter the number of your session type choice",
242
+ default=1,
243
+ show_default=True,
244
+ )
245
+
246
+ return list(session_types.keys())[choice - 1]
247
+
248
+
209
249
  def prompt_datastore_selection(
210
250
  agent_name: str, from_cli_flag: bool = False
211
251
  ) -> str | None:
@@ -242,7 +282,7 @@ def prompt_datastore_selection(
242
282
  pathlib.Path(__file__).parent.parent.parent.parent
243
283
  / "agents"
244
284
  / agent_name
245
- / "template"
285
+ / ".template"
246
286
  )
247
287
  config = load_template_config(template_path)
248
288
 
@@ -319,7 +359,7 @@ def prompt_datastore_selection(
319
359
  def get_template_path(agent_name: str, debug: bool = False) -> pathlib.Path:
320
360
  """Get the absolute path to the agent template directory."""
321
361
  current_dir = pathlib.Path(__file__).parent.parent.parent.parent
322
- template_path = current_dir / "agents" / agent_name / "template"
362
+ template_path = current_dir / "agents" / agent_name / ".template"
323
363
  if debug:
324
364
  logging.debug(f"Looking for template in: {template_path}")
325
365
  logging.debug(f"Template exists: {template_path.exists()}")
@@ -365,7 +405,10 @@ def process_template(
365
405
  deployment_target: str | None = None,
366
406
  include_data_ingestion: bool = False,
367
407
  datastore: str | None = None,
408
+ session_type: str | None = None,
368
409
  output_dir: pathlib.Path | None = None,
410
+ remote_template_path: pathlib.Path | None = None,
411
+ remote_config: dict[str, Any] | None = None,
369
412
  ) -> None:
370
413
  """Process the template directory and create a new project.
371
414
 
@@ -375,15 +418,33 @@ def process_template(
375
418
  project_name: Name of the project to create
376
419
  deployment_target: Optional deployment target (agent_engine or cloud_run)
377
420
  include_data_ingestion: Whether to include data pipeline components
421
+ datastore: Optional datastore type for data ingestion
422
+ session_type: Optional session type for cloud_run deployment
378
423
  output_dir: Optional output directory path, defaults to current directory
424
+ remote_template_path: Optional path to remote template for overlay
425
+ remote_config: Optional remote template configuration
379
426
  """
380
427
  logging.debug(f"Processing template from {template_dir}")
381
428
  logging.debug(f"Project name: {project_name}")
382
429
  logging.debug(f"Include pipeline: {datastore}")
383
430
  logging.debug(f"Output directory: {output_dir}")
384
431
 
385
- # Get paths
386
- agent_path = template_dir.parent # Get parent of template dir
432
+ # Handle remote vs local templates
433
+ is_remote = remote_template_path is not None
434
+
435
+ if is_remote:
436
+ # For remote templates, determine the base template
437
+ base_template_name = get_base_template_name(remote_config or {})
438
+ agent_path = (
439
+ pathlib.Path(__file__).parent.parent.parent.parent
440
+ / "agents"
441
+ / base_template_name
442
+ )
443
+ logging.debug(f"Remote template using base: {base_template_name}")
444
+ else:
445
+ # For local templates, use the existing logic
446
+ agent_path = template_dir.parent # Get parent of template dir
447
+
387
448
  logging.debug(f"agent path: {agent_path}")
388
449
  logging.debug(f"agent path exists: {agent_path.exists()}")
389
450
  logging.debug(
@@ -456,7 +517,7 @@ def process_template(
456
517
  copy_frontend_files(frontend_type, project_template)
457
518
  logging.debug(f"4. Processed frontend files for type: {frontend_type}")
458
519
 
459
- # 5. Finally, copy agent-specific files to override everything else
520
+ # 5. Copy agent-specific files to override base template
460
521
  if agent_path.exists():
461
522
  for folder in OVERWRITE_FOLDERS:
462
523
  agent_folder = agent_path / folder
@@ -467,11 +528,27 @@ def process_template(
467
528
  agent_folder, project_folder, agent_name, overwrite=True
468
529
  )
469
530
 
531
+ # 6. Finally, overlay remote template files if present
532
+ if is_remote and remote_template_path:
533
+ logging.debug(
534
+ f"6. Overlaying remote template files from {remote_template_path}"
535
+ )
536
+ copy_files(
537
+ remote_template_path,
538
+ project_template,
539
+ agent_name=agent_name,
540
+ overwrite=True,
541
+ )
542
+
470
543
  # Load and validate template config first
471
- template_path = pathlib.Path(template_dir)
472
- config = load_template_config(template_path)
544
+ if is_remote:
545
+ config = remote_config or {}
546
+ else:
547
+ template_path = pathlib.Path(template_dir)
548
+ config = load_template_config(template_path)
549
+
473
550
  if not config:
474
- raise ValueError(f"Could not load template config from {template_path}")
551
+ raise ValueError("Could not load template config")
475
552
 
476
553
  # Validate deployment target
477
554
  available_targets = config.get("settings", {}).get("deployment_targets", [])
@@ -483,8 +560,8 @@ def process_template(
483
560
  f"Invalid deployment target '{deployment_target}'. Available targets: {available_targets}"
484
561
  )
485
562
 
486
- # Load template config
487
- template_config = load_template_config(pathlib.Path(template_dir))
563
+ # Use the already loaded config
564
+ template_config = config
488
565
 
489
566
  # Check if data processing should be included
490
567
  if include_data_ingestion and datastore:
@@ -527,6 +604,7 @@ def process_template(
527
604
  "settings": settings,
528
605
  "tags": tags,
529
606
  "deployment_target": deployment_target or "",
607
+ "session_type": session_type or "",
530
608
  "frontend_type": frontend_type,
531
609
  "extra_dependencies": [extra_deps],
532
610
  "data_ingestion": include_data_ingestion,
@@ -591,9 +669,36 @@ def process_template(
591
669
  file_path.unlink()
592
670
  logging.debug(f"Deleted {file_path}")
593
671
 
594
- # After copying template files, handle the lock file
595
- if deployment_target:
596
- # Get the source lock file path
672
+ # Handle pyproject.toml and uv.lock files
673
+ if is_remote and remote_template_path:
674
+ # For remote templates, use their pyproject.toml and uv.lock if they exist
675
+ remote_pyproject = remote_template_path / "pyproject.toml"
676
+ remote_uv_lock = remote_template_path / "uv.lock"
677
+
678
+ if remote_pyproject.exists():
679
+ shutil.copy2(
680
+ remote_pyproject, final_destination / "pyproject.toml"
681
+ )
682
+ logging.debug("Used pyproject.toml from remote template")
683
+
684
+ if remote_uv_lock.exists():
685
+ shutil.copy2(remote_uv_lock, final_destination / "uv.lock")
686
+ logging.debug("Used uv.lock from remote template")
687
+ elif deployment_target:
688
+ # Fallback to base template lock file
689
+ base_template_name = get_base_template_name(remote_config or {})
690
+ lock_path = (
691
+ pathlib.Path(__file__).parent.parent.parent.parent
692
+ / "src"
693
+ / "resources"
694
+ / "locks"
695
+ / f"uv-{base_template_name}-{deployment_target}.lock"
696
+ )
697
+ if lock_path.exists():
698
+ shutil.copy2(lock_path, final_destination / "uv.lock")
699
+ logging.debug(f"Used fallback lock file from {lock_path}")
700
+ elif deployment_target:
701
+ # For local templates, use the existing logic
597
702
  lock_path = (
598
703
  pathlib.Path(__file__).parent.parent.parent.parent
599
704
  / "src"
@@ -672,8 +777,12 @@ def copy_files(
672
777
  return True
673
778
  if "__pycache__" in str(path) or path.name == "__pycache__":
674
779
  return True
780
+ if ".git" in path.parts:
781
+ return True
675
782
  if agent_name is not None and should_exclude_path(path, agent_name):
676
783
  return True
784
+ if path.is_dir() and path.name == ".template":
785
+ return True
677
786
  return False
678
787
 
679
788
  if src.is_dir():
@@ -12,14 +12,13 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- # mypy: disable-error-code="attr-defined"
15
+ # mypy: disable-error-code="attr-defined,arg-type"
16
16
  {%- if "adk" in cookiecutter.tags %}
17
17
  import copy
18
18
  import datetime
19
19
  import json
20
20
  import logging
21
21
  import os
22
- from collections.abc import Mapping, Sequence
23
22
  from typing import Any
24
23
 
25
24
  import google.auth
@@ -57,7 +56,7 @@ class AgentEngineApp(AdkApp):
57
56
  feedback_obj = Feedback.model_validate(feedback)
58
57
  self.logger.log_struct(feedback_obj.model_dump(), severity="INFO")
59
58
 
60
- def register_operations(self) -> Mapping[str, Sequence]:
59
+ def register_operations(self) -> dict[str, list[str]]:
61
60
  """Registers the operations of the Agent.
62
61
 
63
62
  Extends the base operations to include feedback registration functionality.
@@ -69,9 +68,10 @@ class AgentEngineApp(AdkApp):
69
68
  def clone(self) -> "AgentEngineApp":
70
69
  """Returns a clone of the ADK application."""
71
70
  template_attributes = self._tmpl_attrs
71
+
72
72
  return self.__class__(
73
- agent=copy.deepcopy(template_attributes.get("agent")),
74
- enable_tracing=template_attributes.get("enable_tracing"),
73
+ agent=copy.deepcopy(template_attributes["agent"]),
74
+ enable_tracing=bool(template_attributes.get("enable_tracing", False)),
75
75
  session_service_builder=template_attributes.get("session_service_builder"),
76
76
  artifact_service_builder=template_attributes.get(
77
77
  "artifact_service_builder"
@@ -83,7 +83,7 @@ import datetime
83
83
  import json
84
84
  import logging
85
85
  import os
86
- from collections.abc import Iterable, Mapping, Sequence
86
+ from collections.abc import Iterable, Mapping
87
87
  from typing import (
88
88
  Any,
89
89
  )
@@ -182,7 +182,7 @@ class AgentEngineApp:
182
182
  feedback_obj = Feedback.model_validate(feedback)
183
183
  self.logger.log_struct(feedback_obj.model_dump(), severity="INFO")
184
184
 
185
- def register_operations(self) -> Mapping[str, Sequence]:
185
+ def register_operations(self) -> dict[str, list[str]]:
186
186
  """Registers the operations of the Agent.
187
187
 
188
188
  This mapping defines how different operation modes (e.g., "", "stream")
@@ -18,12 +18,7 @@ Follow these steps to execute load tests:
18
18
  It's recommended to use a separate terminal tab and create a virtual environment for Locust to avoid conflicts with your application's Python environment.
19
19
 
20
20
  ```bash
21
- # Create and activate virtual environment
22
- python3 -m venv .locust_env
23
- source .locust_env/bin/activate
24
-
25
- # Install required packages
26
- pip install locust==2.31.1 "google-cloud-aiplatform[langchain,reasoningengine]>=1.77.0"
21
+ python3 -m venv .locust_env && source .locust_env/bin/activate && pip install locust==2.31.1
27
22
  ```
28
23
 
29
24
  **3. Execute the Load Test:**
@@ -91,15 +91,25 @@ class ChatStreamUser(HttpUser):
91
91
  events = []
92
92
  for line in response.iter_lines():
93
93
  if line:
94
- event = json.loads(line)
95
- events.append(event)
94
+ line_str = line.decode("utf-8")
95
+ events.append(line_str)
96
+
97
+ if "429 Too Many Requests" in line_str:
98
+ self.environment.events.request.fire(
99
+ request_type="POST",
100
+ name=f"{url_path} rate_limited 429s",
101
+ response_time=0,
102
+ response_length=len(line),
103
+ response=response,
104
+ context={},
105
+ )
96
106
  end_time = time.time()
97
107
  total_time = end_time - start_time
98
108
  self.environment.events.request.fire(
99
109
  request_type="POST",
100
110
  name="/stream_messages end",
101
111
  response_time=total_time * 1000, # Convert to milliseconds
102
- response_length=len(json.dumps(events)),
112
+ response_length=len(events),
103
113
  response=response,
104
114
  context={},
105
115
  )
@@ -24,6 +24,9 @@ COPY ./app ./app
24
24
 
25
25
  RUN uv sync --frozen
26
26
 
27
+ ARG COMMIT_SHA=""
28
+ ENV COMMIT_SHA=${COMMIT_SHA}
29
+
27
30
  EXPOSE 8080
28
31
 
29
32
  CMD ["uv", "run", "uvicorn", "app.server:app", "--host", "0.0.0.0", "--port", "8080"]
@@ -43,11 +43,29 @@ provider.add_span_processor(processor)
43
43
  trace.set_tracer_provider(provider)
44
44
 
45
45
  AGENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
46
+
47
+ {%- if cookiecutter.session_type == "alloydb" %}
48
+ # AlloyDB session configuration
49
+ db_user = os.environ.get("DB_USER", "postgres")
50
+ db_name = os.environ.get("DB_NAME", "postgres")
51
+ db_pass = os.environ.get("DB_PASS")
52
+ db_host = os.environ.get("DB_HOST")
53
+
54
+ # Set session_service_uri if database credentials are available
55
+ session_service_uri = None
56
+ if db_host and db_pass:
57
+ session_service_uri = f"postgresql://{db_user}:{db_pass}@{db_host}:5432/{db_name}"
58
+ {%- else %}
59
+ # In-memory session configuration - no persistent storage
60
+ session_service_uri = None
61
+ {%- endif %}
62
+
46
63
  app: FastAPI = get_fast_api_app(
47
64
  agents_dir=AGENT_DIR,
48
65
  web=True,
49
66
  artifact_service_uri=bucket_name,
50
67
  allow_origins=allow_origins,
68
+ session_service_uri=session_service_uri,
51
69
  )
52
70
  app.title = "{{cookiecutter.project_name}}"
53
71
  app.description = "API for interacting with the Agent {{cookiecutter.project_name}}"
@@ -0,0 +1,231 @@
1
+ # Copyright 2025 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
+ # Get project information to access the project number
16
+ data "google_project" "project" {
17
+ project_id = var.dev_project_id
18
+ }
19
+
20
+
21
+ {%- if "adk" in cookiecutter.tags and cookiecutter.session_type == "alloydb" %}
22
+
23
+ # VPC Network for AlloyDB
24
+ resource "google_compute_network" "default" {
25
+ name = "${var.project_name}-alloydb-network"
26
+ project = var.dev_project_id
27
+ auto_create_subnetworks = false
28
+ depends_on = [resource.google_project_service.services]
29
+ }
30
+
31
+ # Subnet for AlloyDB
32
+ resource "google_compute_subnetwork" "default" {
33
+ name = "${var.project_name}-alloydb-network"
34
+ ip_cidr_range = "10.0.0.0/24"
35
+ region = var.region
36
+ network = google_compute_network.default.id
37
+ project = var.dev_project_id
38
+
39
+ # This is required for Cloud Run VPC connectors
40
+ purpose = "PRIVATE"
41
+
42
+ private_ip_google_access = true
43
+ }
44
+
45
+ # Private IP allocation for AlloyDB
46
+ resource "google_compute_global_address" "private_ip_alloc" {
47
+ name = "${var.project_name}-private-ip"
48
+ project = var.dev_project_id
49
+ address_type = "INTERNAL"
50
+ purpose = "VPC_PEERING"
51
+ prefix_length = 16
52
+ network = google_compute_network.default.id
53
+
54
+ depends_on = [resource.google_project_service.services]
55
+ }
56
+
57
+ # VPC connection for AlloyDB
58
+ resource "google_service_networking_connection" "vpc_connection" {
59
+ network = google_compute_network.default.id
60
+ service = "servicenetworking.googleapis.com"
61
+ reserved_peering_ranges = [google_compute_global_address.private_ip_alloc.name]
62
+ }
63
+
64
+ # AlloyDB Cluster
65
+ resource "google_alloydb_cluster" "session_db_cluster" {
66
+ project = var.dev_project_id
67
+ cluster_id = "${var.project_name}-alloydb-cluster"
68
+ location = var.region
69
+
70
+ network_config {
71
+ network = google_compute_network.default.id
72
+ }
73
+
74
+ depends_on = [
75
+ google_service_networking_connection.vpc_connection
76
+ ]
77
+ }
78
+
79
+ # AlloyDB Instance
80
+ resource "google_alloydb_instance" "session_db_instance" {
81
+ cluster = google_alloydb_cluster.session_db_cluster.name
82
+ instance_id = "${var.project_name}-alloydb-instance"
83
+ instance_type = "PRIMARY"
84
+
85
+ availability_type = "REGIONAL" # Regional redundancy
86
+
87
+ machine_config {
88
+ cpu_count = 2
89
+ }
90
+ }
91
+
92
+ # Generate a random password for the database user
93
+ resource "random_password" "db_password" {
94
+ length = 16
95
+ special = true
96
+ override_special = "!#$%&*()-_=+[]{}<>:?"
97
+ }
98
+
99
+ # Store the password in Secret Manager
100
+ resource "google_secret_manager_secret" "db_password" {
101
+ project = var.dev_project_id
102
+ secret_id = "${var.project_name}-db-password"
103
+
104
+ replication {
105
+ auto {}
106
+ }
107
+
108
+ depends_on = [resource.google_project_service.services]
109
+ }
110
+
111
+ resource "google_secret_manager_secret_version" "db_password" {
112
+ secret = google_secret_manager_secret.db_password.id
113
+ secret_data = random_password.db_password.result
114
+ }
115
+
116
+ resource "google_alloydb_user" "db_user" {
117
+ cluster = google_alloydb_cluster.session_db_cluster.name
118
+ user_id = "postgres"
119
+ user_type = "ALLOYDB_BUILT_IN"
120
+ password = random_password.db_password.result
121
+ database_roles = ["alloydbsuperuser"]
122
+
123
+ depends_on = [google_alloydb_instance.session_db_instance]
124
+ }
125
+
126
+ {%- endif %}
127
+
128
+
129
+ resource "google_cloud_run_v2_service" "app" {
130
+ name = var.project_name
131
+ location = var.region
132
+ project = var.dev_project_id
133
+ deletion_protection = false
134
+ ingress = "INGRESS_TRAFFIC_ALL"
135
+
136
+ template {
137
+ containers {
138
+ image = "us-docker.pkg.dev/cloudrun/container/hello"
139
+
140
+ resources {
141
+ limits = {
142
+ cpu = "4"
143
+ memory = "8Gi"
144
+ }
145
+ }
146
+ {%- if cookiecutter.data_ingestion %}
147
+ {%- if cookiecutter.datastore_type == "vertex_ai_search" %}
148
+
149
+ env {
150
+ name = "DATA_STORE_ID"
151
+ value = resource.google_discovery_engine_data_store.data_store_dev.data_store_id
152
+ }
153
+
154
+ env {
155
+ name = "DATA_STORE_REGION"
156
+ value = var.data_store_region
157
+ }
158
+ {%- elif cookiecutter.datastore_type == "vertex_ai_vector_search" %}
159
+ env {
160
+ name = "VECTOR_SEARCH_INDEX"
161
+ value = resource.google_vertex_ai_index.vector_search_index.id
162
+ }
163
+
164
+ env {
165
+ name = "VECTOR_SEARCH_INDEX_ENDPOINT"
166
+ value = resource.google_vertex_ai_index_endpoint.vector_search_index_endpoint.id
167
+ }
168
+
169
+ env {
170
+ name = "VECTOR_SEARCH_BUCKET"
171
+ value = "gs://${resource.google_storage_bucket.data_ingestion_PIPELINE_GCS_ROOT.name}"
172
+ }
173
+ {%- endif %}
174
+ {%- endif %}
175
+
176
+ {%- if "adk" in cookiecutter.tags and cookiecutter.session_type == "alloydb" %}
177
+
178
+ env {
179
+ name = "DB_HOST"
180
+ value = google_alloydb_instance.session_db_instance.ip_address
181
+ }
182
+
183
+ env {
184
+ name = "DB_PASS"
185
+ value_source {
186
+ secret_key_ref {
187
+ secret = google_secret_manager_secret.db_password.secret_id
188
+ version = "latest"
189
+ }
190
+ }
191
+ }
192
+ {%- endif %}
193
+ }
194
+
195
+ service_account = google_service_account.cloud_run_app_sa.email
196
+ max_instance_request_concurrency = 40
197
+
198
+ scaling {
199
+ min_instance_count = 1
200
+ max_instance_count = 10
201
+ }
202
+
203
+ session_affinity = true
204
+
205
+ {%- if "adk" in cookiecutter.tags and cookiecutter.session_type == "alloydb" %}
206
+ # VPC access for AlloyDB connectivity
207
+ vpc_access {
208
+ network_interfaces {
209
+ network = google_compute_network.default.id
210
+ subnetwork = google_compute_subnetwork.default.id
211
+ }
212
+ }
213
+ {%- endif %}
214
+ }
215
+
216
+ traffic {
217
+ type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
218
+ percent = 100
219
+ }
220
+
221
+ # This lifecycle block prevents Terraform from overwriting the container image when it's
222
+ # updated by Cloud Run deployments outside of Terraform (e.g., via CI/CD pipelines)
223
+ lifecycle {
224
+ ignore_changes = [
225
+ template[0].containers[0].image,
226
+ ]
227
+ }
228
+
229
+ # Make dependencies conditional to avoid errors.
230
+ depends_on = [resource.google_project_service.services]
231
+ }