datacommons-admin 0.0.1__tar.gz

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.
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: datacommons-admin
3
+ Version: 0.0.1
4
+ Summary: Data Commons Admin CLI
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Homepage, https://github.com/datacommonsorg/datacommons
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: click>=8.1.7
10
+ Requires-Dist: google-cloud-storage>=2.13.0
11
+ Requires-Dist: pyopenssl>=24.0.0
12
+
13
+ # datacommons-admin
14
+
15
+ This package provides the `admin` command group consumed by the top-level `datacommons` CLI for initializing and managing Data Commons platform instances in GCP.
@@ -0,0 +1,3 @@
1
+ # datacommons-admin
2
+
3
+ This package provides the `admin` command group consumed by the top-level `datacommons` CLI for initializing and managing Data Commons platform instances in GCP.
@@ -0,0 +1 @@
1
+ """CLI package for administering Data Commons instances in GCP."""
@@ -0,0 +1,553 @@
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
+ from pathlib import Path
16
+ import re
17
+ import sys
18
+ import urllib.request
19
+ from typing import Any, Tuple
20
+
21
+ import click
22
+ from google.api_core import exceptions
23
+ from google.cloud import storage
24
+
25
+ from datacommons_admin.infra_templates import (
26
+ BACKEND_TF_TEMPLATE,
27
+ README_TEMPLATE,
28
+ REMOTE_STATE_TEMPLATE,
29
+ )
30
+
31
+
32
+ DEFAULT_BUCKET_LOCATION = "US"
33
+ GITHUB_RAW_BASE_URL = "https://raw.githubusercontent.com/datacommonsorg/datacommons"
34
+ GITHUB_REPO_URL = "https://github.com/datacommonsorg/datacommons.git"
35
+
36
+
37
+ def _get_default_bucket_name(namespace: str, project_id: str) -> str:
38
+ """Returns the default Google Cloud Storage bucket name for Terraform state."""
39
+ return f"tf-state-{namespace}-{project_id}"
40
+
41
+
42
+ def _get_default_state_prefix(namespace: str) -> str:
43
+ """Returns the default Google Cloud Storage object prefix for Terraform state."""
44
+ return f"terraform/state/{namespace}"
45
+
46
+
47
+ def _log_resolved_value(label: str, value: str, is_default: bool, indent: int = 2):
48
+ """Logs a value with a bullet if default, or a green checkmark if from flag."""
49
+ prefix = " " * indent
50
+ padded_label = label.ljust(12)
51
+ if is_default:
52
+ click.echo(f"{prefix}- {padded_label}: {value} (Default)")
53
+ else:
54
+ click.secho(f"{prefix}✔", fg="green", nl=False)
55
+ click.echo(f" {padded_label}: {value} (from flag)")
56
+
57
+
58
+ def _prompt(text: str, indent: int = 2, **kwargs):
59
+ """Prints the cyan [?] prompt symbol and calls click.prompt."""
60
+ click.secho(" " * indent + "[?]", fg="cyan", bold=True, nl=False)
61
+ prompt_text = text if text.startswith(" ") else f" {text}"
62
+ return click.prompt(prompt_text, **kwargs)
63
+
64
+
65
+ def _confirm(text: str, indent: int = 2, **kwargs):
66
+ """Prints the cyan [?] prompt symbol and calls click.confirm."""
67
+ click.secho(" " * indent + "[?]", fg="cyan", bold=True, nl=False)
68
+ prompt_text = text if text.startswith(" ") else f" {text}"
69
+ return click.confirm(prompt_text, **kwargs)
70
+
71
+
72
+ def _create_and_configure_bucket(
73
+ storage_client,
74
+ bucket_name: str,
75
+ project_id: str,
76
+ location: str = DEFAULT_BUCKET_LOCATION,
77
+ ) -> bool:
78
+ """Prompts and creates a Google Cloud Storage bucket, enables versioning, and sets IAM policy.
79
+
80
+ Returns True if created, False if cancelled.
81
+ """
82
+ click.echo(f" - {'Status'.ljust(12)}: Not found")
83
+ click.echo(f" - {'Project'.ljust(12)}: {project_id}")
84
+ _log_resolved_value("Location", location, location == DEFAULT_BUCKET_LOCATION)
85
+
86
+ if not _confirm("Create this bucket?", default=True):
87
+ return False
88
+
89
+ click.secho(
90
+ f" Creating bucket gs://{bucket_name} in project {project_id} with location {location}...",
91
+ fg="bright_black",
92
+ )
93
+ new_bucket = storage_client.create_bucket(bucket_name, location=location)
94
+ new_bucket.iam_configuration.uniform_bucket_level_access_enabled = True
95
+ new_bucket.versioning_enabled = True
96
+ new_bucket.patch()
97
+ click.secho(f" Enabling versioning...", fg="bright_black")
98
+ click.secho(" Configuring bucket IAM policy...", fg="bright_black")
99
+ policy = new_bucket.get_iam_policy(requested_policy_version=3)
100
+ policy["roles/storage.objectAdmin"].add(f"projectEditor:{project_id}")
101
+ policy["roles/storage.objectAdmin"].add(f"projectOwner:{project_id}")
102
+ new_bucket.set_iam_policy(policy)
103
+ return True
104
+
105
+
106
+ def _abort_bucket_setup(is_default: bool):
107
+ click.secho(
108
+ " No bucket available for remote Terraform state storage. Cancelling setup.",
109
+ fg="red",
110
+ )
111
+ if is_default:
112
+ click.secho(
113
+ " Hint: Use --no-tf-remote-state to keep Terraform state local, or --tf-state-bucket to override the bucket name. See --help for more.",
114
+ fg="bright_black",
115
+ )
116
+ # Use sys.exit(1) instead of click.Abort() to avoid being caught by broad
117
+ # except Exception blocks in the caller and to avoid Click's "Aborted!" message.
118
+ sys.exit(1)
119
+
120
+
121
+ def _ensure_bucket_ready(
122
+ storage_client,
123
+ bucket_name: str,
124
+ project_id: str,
125
+ location: str = DEFAULT_BUCKET_LOCATION,
126
+ is_default: bool = False,
127
+ ) -> bool:
128
+ """Checks if bucket exists and is ready to use, or creates it if missing.
129
+
130
+ Returns True if the bucket is ready to use, False if the user wants to try another name.
131
+ """
132
+ try:
133
+ bucket = storage_client.get_bucket(bucket_name)
134
+ click.echo(f" - {'Status'.ljust(12)}: Found")
135
+ # Only prompt to reuse if it was the default bucket
136
+ if is_default:
137
+ if not _confirm("Use this bucket?", default=True):
138
+ _abort_bucket_setup(is_default)
139
+ else:
140
+ click.echo(" Proceeding...")
141
+ return True
142
+ except exceptions.NotFound:
143
+ if _create_and_configure_bucket(
144
+ storage_client, bucket_name, project_id, location
145
+ ):
146
+ return True
147
+ else:
148
+ _abort_bucket_setup(is_default)
149
+
150
+
151
+ def _configure_remote_state(
152
+ project_id: str,
153
+ namespace: str,
154
+ bucket_name: str = "",
155
+ location: str = DEFAULT_BUCKET_LOCATION,
156
+ ) -> str:
157
+ """Handles Google Cloud Storage state bucket verification, creation, and IAM setup."""
158
+ try:
159
+ storage_client = storage.Client(project=project_id)
160
+ except Exception as e:
161
+ raise click.ClickException(
162
+ f"Failed to initialize Google Cloud Storage client for project '{project_id}': {e}. "
163
+ "Ensure you are authenticated via 'gcloud auth application-default login'."
164
+ )
165
+
166
+ is_default = False
167
+ if not bucket_name:
168
+ bucket_name = _get_default_bucket_name(namespace, project_id)
169
+ is_default = True
170
+
171
+ click.echo(
172
+ "Setting up Google Cloud Storage bucket for storing terraform state remotely:"
173
+ )
174
+ _log_resolved_value("Name", bucket_name, is_default)
175
+
176
+ try:
177
+ ready = _ensure_bucket_ready(
178
+ storage_client, bucket_name, project_id, location, is_default
179
+ )
180
+ except exceptions.Unauthorized as e:
181
+ raise click.ClickException(
182
+ f"Authentication failed: {e}\n"
183
+ "Please ensure you are authenticated. Run 'gcloud auth application-default login' and try again."
184
+ )
185
+ except exceptions.Forbidden as e:
186
+ raise click.ClickException(
187
+ f"Permission denied: {e}\n"
188
+ f"Please ensure your account has 'Storage Admin' or 'Project Editor' permissions in project '{project_id}'."
189
+ )
190
+ except click.Abort:
191
+ click.echo("")
192
+ sys.exit(1)
193
+ except Exception as e:
194
+ click.secho(
195
+ f" Error: Failed to access or create bucket gs://{bucket_name}.",
196
+ fg="red",
197
+ bold=True,
198
+ )
199
+ click.secho(f" {e}", fg="red")
200
+ raise click.ClickException("Setup cancelled.")
201
+
202
+ if not ready:
203
+ raise click.ClickException("Setup cancelled.")
204
+ return bucket_name
205
+
206
+
207
+ def _get_github_templates(ref: str) -> tuple[str, str, str, str]:
208
+ """Fetches variables.tf, main.tf, outputs.tf, and terraform.tfvars.template from GitHub for the given ref."""
209
+ base_url = f"{GITHUB_RAW_BASE_URL}/{ref}/infra/dcp"
210
+
211
+ def fetch(filename: str) -> str:
212
+ url = f"{base_url}/{filename}"
213
+
214
+ req = urllib.request.Request(url, headers={"User-Agent": "DataCommons-CLI"})
215
+ with urllib.request.urlopen(req, timeout=10) as response:
216
+ return response.read().decode("utf-8")
217
+
218
+ return (
219
+ fetch("variables.tf"),
220
+ fetch("main.tf"),
221
+ fetch("outputs.tf"),
222
+ fetch("terraform.tfvars.template"),
223
+ )
224
+
225
+
226
+ @click.group()
227
+ def admin() -> None:
228
+ """Manage a Data Commons Platform instance in Google Cloud"""
229
+
230
+
231
+ def _resolve_project_config(
232
+ project_id: str, namespace: str, force: bool
233
+ ) -> Tuple[str, str, Path]:
234
+ """Resolves project ID and namespace, and determines target directory."""
235
+ if project_id:
236
+ _log_resolved_value("Project ID", project_id, is_default=False)
237
+ if namespace:
238
+ _log_resolved_value("Namespace", namespace, is_default=False)
239
+
240
+ resolved_project_id = project_id.strip()
241
+ if not resolved_project_id:
242
+ resolved_project_id = _prompt(
243
+ "Google Cloud Platform project ID", type=str
244
+ ).strip()
245
+ if not resolved_project_id:
246
+ raise click.ClickException("GCP project ID must not be empty.")
247
+
248
+ resolved_namespace = namespace.strip()
249
+ while True:
250
+ if not resolved_namespace:
251
+ resolved_namespace = _prompt("Namespace", type=str).strip()
252
+ if not resolved_namespace:
253
+ click.secho("Error: Namespace must not be empty.", fg="red")
254
+ continue
255
+
256
+ target_dir = Path.cwd() / resolved_namespace
257
+ if target_dir.exists() and not force:
258
+ click.secho(
259
+ f"Error: Folder '{resolved_namespace}' already exists locally. "
260
+ "Please specify a different namespace, or use --force to overwrite.",
261
+ fg="yellow",
262
+ )
263
+ resolved_namespace = ""
264
+ continue
265
+
266
+ break
267
+
268
+ return resolved_project_id, resolved_namespace, target_dir
269
+
270
+
271
+ def _check_existing_files(target_dir: Path, use_remote_state: bool, force: bool):
272
+ """Checks if target files already exist and raises error if they do (and not force)."""
273
+ main_tf_path = target_dir / "main.tf"
274
+ tfvars_path = target_dir / "terraform.tfvars"
275
+ readme_path = target_dir / "README.md"
276
+ backend_tf_path = target_dir / "backend.tf"
277
+
278
+ paths_to_check = [main_tf_path, tfvars_path, readme_path]
279
+ if use_remote_state:
280
+ paths_to_check.append(backend_tf_path)
281
+
282
+ existing_paths = [path for path in paths_to_check if path.exists()]
283
+ if existing_paths and not force:
284
+ existing_labels = ", ".join(str(path) for path in existing_paths)
285
+ raise click.ClickException(
286
+ f"Refusing to overwrite existing file(s): {existing_labels}. "
287
+ "Use --force to overwrite."
288
+ )
289
+
290
+
291
+ def _setup_dcp_config_dir(
292
+ target_dir: Path,
293
+ project_id: str,
294
+ namespace: str,
295
+ bucket_name: str,
296
+ tf_state_prefix: str,
297
+ dc_api_key: str,
298
+ ref: str,
299
+ use_remote_state: bool,
300
+ ):
301
+ """Downloads and populates Terraform templates."""
302
+
303
+ api_key = dc_api_key.strip()
304
+ if not api_key:
305
+ api_key = _prompt(
306
+ "Data Commons API key (from apikeys.datacommons.org)",
307
+ type=str,
308
+ default="",
309
+ show_default=False,
310
+ ).strip()
311
+
312
+ if not api_key:
313
+ click.secho(
314
+ " [!] Warning: Data Commons API key was skipped. You must add it to terraform.tfvars before running terraform apply.",
315
+ fg="yellow",
316
+ bold=True,
317
+ )
318
+
319
+ target_dir.mkdir(parents=True, exist_ok=True)
320
+ click.secho(f"Creating directory: {target_dir}", fg="bright_black")
321
+
322
+ try:
323
+ variables_content, main_content, outputs_content, tfvars_example = (
324
+ _get_github_templates(ref)
325
+ )
326
+
327
+ # Update the stack module source to point to GitHub
328
+ resolved_source = f"git::{GITHUB_REPO_URL}//infra/dcp/modules/stack?ref={ref}"
329
+ main_content = re.sub(
330
+ r'source\s*=\s*["\']\./modules/stack["\']',
331
+ f'source = "{resolved_source}"',
332
+ main_content,
333
+ )
334
+
335
+ # Write the files
336
+ (target_dir / "variables.tf").write_text(variables_content, encoding="utf-8")
337
+ (target_dir / "main.tf").write_text(main_content, encoding="utf-8")
338
+ (target_dir / "outputs.tf").write_text(outputs_content, encoding="utf-8")
339
+
340
+ # Modify tfvars_example with actual values
341
+ tfvars_content = tfvars_example
342
+ tfvars_content = tfvars_content.replace('"$$PROJECT_ID$$"', f'"{project_id}"')
343
+ tfvars_content = tfvars_content.replace('"$$NAMESPACE$$"', f'"{namespace}"')
344
+ if api_key:
345
+ tfvars_content = tfvars_content.replace('"$$DC_API_KEY$$"', f'"{api_key}"')
346
+
347
+ (target_dir / "terraform.tfvars").write_text(tfvars_content, encoding="utf-8")
348
+
349
+ except Exception as e:
350
+ raise click.ClickException(f"Failed to initialize Terraform templates: {e}")
351
+
352
+ remote_state_info = ""
353
+ if use_remote_state and bucket_name:
354
+ remote_state_info = REMOTE_STATE_TEMPLATE.format(
355
+ bucket_name=bucket_name, prefix=tf_state_prefix
356
+ )
357
+
358
+ (target_dir / "README.md").write_text(
359
+ README_TEMPLATE.format(remote_state_section=remote_state_info), encoding="utf-8"
360
+ )
361
+ if use_remote_state and bucket_name:
362
+ (target_dir / "backend.tf").write_text(
363
+ BACKEND_TF_TEMPLATE.format(
364
+ bucket_name=bucket_name,
365
+ prefix=tf_state_prefix,
366
+ ),
367
+ encoding="utf-8",
368
+ )
369
+
370
+ click.secho("Downloaded and populated Terraform templates.", fg="bright_black")
371
+
372
+
373
+ @admin.command()
374
+ @click.option(
375
+ "--project-id",
376
+ default="",
377
+ help="Google Cloud Platform project ID used for all resources related to your Data Commons instance.",
378
+ )
379
+ @click.option(
380
+ "--namespace", default="", help="Namespace prefix for provisioned resources."
381
+ )
382
+ @click.option("--dc-api-key", default="", help="Data Commons API key.")
383
+ @click.option(
384
+ "--ref", default="main", show_default=True, help="Git ref for module source."
385
+ )
386
+ @click.option(
387
+ "--force", is_flag=True, help="Overwrite existing generated files if present."
388
+ )
389
+ @click.option(
390
+ "--tf-remote-state/--no-tf-remote-state",
391
+ default=True,
392
+ help="Enable or disable Terraform remote state management in Google Cloud Storage. Disabling ignores other --tf-state-* flags.",
393
+ )
394
+ @click.option(
395
+ "--tf-state-bucket",
396
+ default="",
397
+ help="Google Cloud Storage bucket for Terraform remote state. Generates a default name if omitted. Prompts to create the bucket if it is missing.",
398
+ )
399
+ @click.option(
400
+ "--tf-state-bucket-location",
401
+ default=DEFAULT_BUCKET_LOCATION,
402
+ show_default=True,
403
+ help="Google Cloud Storage bucket location if a new bucket needs to be created.",
404
+ )
405
+ @click.option(
406
+ "--tf-state-prefix",
407
+ default="",
408
+ help="Google Cloud Storage object prefix for Terraform state file (default: terraform/state/{namespace}).",
409
+ )
410
+ def init(
411
+ project_id: str,
412
+ namespace: str,
413
+ dc_api_key: str,
414
+ ref: str,
415
+ force: bool,
416
+ tf_remote_state: bool,
417
+ tf_state_bucket: str,
418
+ tf_state_bucket_location: str,
419
+ tf_state_prefix: str,
420
+ ) -> None:
421
+ """Initialize Terraform scaffolding for Data Commons administration/infrastructure."""
422
+ click.secho("Data Commons Admin Init", fg="cyan", bold=True)
423
+
424
+ # 1. Project Configs
425
+ click.secho("\n[Project Configuration]", fg="cyan", bold=True)
426
+ click.secho("Configuring project settings...", fg="bright_black")
427
+ resolved_project_id, resolved_namespace, target_dir = _resolve_project_config(
428
+ project_id, namespace, force
429
+ )
430
+
431
+ # 2. Terraform Setup
432
+ click.secho("\n[Terraform Backend Setup]", fg="cyan", bold=True)
433
+ click.secho("Configuring backend for Terraform state...", fg="bright_black")
434
+ if not tf_remote_state:
435
+ click.echo(" Using local backend for Terraform state.")
436
+
437
+ # Refuse to overwrite existing files unless --force is specified
438
+ _check_existing_files(target_dir, tf_remote_state, force)
439
+
440
+ resolved_bucket_name = (
441
+ _configure_remote_state(
442
+ resolved_project_id,
443
+ resolved_namespace,
444
+ tf_state_bucket,
445
+ tf_state_bucket_location,
446
+ )
447
+ if tf_remote_state
448
+ else ""
449
+ )
450
+
451
+ resolved_tf_state_prefix = tf_state_prefix.strip() or _get_default_state_prefix(
452
+ resolved_namespace
453
+ )
454
+
455
+ # 3. DCP config dir setup
456
+ click.secho("\n[DCP Configuration]", fg="cyan", bold=True)
457
+ click.secho("Setting up configuration files...", fg="bright_black")
458
+ _setup_dcp_config_dir(
459
+ target_dir,
460
+ resolved_project_id,
461
+ resolved_namespace,
462
+ resolved_bucket_name,
463
+ resolved_tf_state_prefix,
464
+ dc_api_key,
465
+ ref,
466
+ tf_remote_state,
467
+ )
468
+
469
+ click.secho(
470
+ f"Customize variables in {resolved_namespace}/terraform.tfvars as needed.",
471
+ fg="green",
472
+ )
473
+ click.secho(
474
+ f"Refer to {resolved_namespace}/README.md for more info and next steps.",
475
+ fg="green",
476
+ )
477
+
478
+
479
+ def _setup_ingestion_client() -> Tuple[Any, str, str]:
480
+ click.secho(
481
+ "Fetching Ingestion Helper URI, Orchestrator Service Account, and Spanner details from Terraform outputs...",
482
+ fg="bright_black",
483
+ )
484
+
485
+ from datacommons_admin.tf_utils import (
486
+ get_dcp_ingestion_helper_uri,
487
+ get_dcp_orchestrator_service_account_email,
488
+ get_dcp_spanner_instance_id,
489
+ get_dcp_spanner_database_id,
490
+ )
491
+ from datacommons_admin.ingestion_helper_client import IngestionHelperClient
492
+
493
+ uri = get_dcp_ingestion_helper_uri()
494
+ sa_email = get_dcp_orchestrator_service_account_email()
495
+ instance_id = get_dcp_spanner_instance_id()
496
+ database_id = get_dcp_spanner_database_id()
497
+
498
+ click.secho(f"Found Ingestion Helper URI: {uri}", fg="green")
499
+ click.secho(f"Found Orchestrator Service Account: {sa_email}", fg="green")
500
+ click.secho(
501
+ f"Found Spanner Database Instance: {instance_id} / Database ID: {database_id}",
502
+ fg="green",
503
+ )
504
+
505
+ client = IngestionHelperClient(uri, service_account_email=sa_email)
506
+ return client, instance_id, database_id
507
+
508
+
509
+ def _run_seed_db(client: Any, instance_id: str, database_id: str) -> None:
510
+ click.secho(
511
+ f"Seeding Spanner database '{instance_id}/{database_id}' via the Ingestion Helper service (this may take a few moments)...",
512
+ fg="bright_black",
513
+ )
514
+ result = client.seed_database()
515
+ click.secho("Successfully seeded Spanner database!", fg="green", bold=True)
516
+ if "message" in result:
517
+ click.secho(f"Details: {result['message']}", fg="bright_black")
518
+
519
+
520
+ @admin.command(name="init-db")
521
+ @click.option(
522
+ "--init-only", is_flag=True, help="Only initialize the database without seeding."
523
+ )
524
+ def init_db(init_only: bool) -> None:
525
+ """Initialize (and by default seed) the Spanner database via the DCP Ingestion Helper service."""
526
+ click.secho("Datacommons Admin Init-DB", fg="cyan", bold=True)
527
+ client, instance_id, database_id = _setup_ingestion_client()
528
+
529
+ click.secho(
530
+ f"Initializing Spanner database '{instance_id}/{database_id}' via the Ingestion Helper service (this may take a few moments)...",
531
+ fg="bright_black",
532
+ )
533
+ result = client.initialize_database()
534
+
535
+ click.secho("Successfully initialized Spanner database!", fg="green", bold=True)
536
+ if "message" in result:
537
+ click.secho(f"Details: {result['message']}", fg="bright_black")
538
+
539
+ if not init_only:
540
+ _run_seed_db(client, instance_id, database_id)
541
+
542
+
543
+ @admin.command(name="seed-db")
544
+ def seed_db() -> None:
545
+ """Seed the Spanner database via the DCP Ingestion Helper service."""
546
+ click.secho("Datacommons Admin Seed-DB", fg="cyan", bold=True)
547
+ client, instance_id, database_id = _setup_ingestion_client()
548
+ _run_seed_db(client, instance_id, database_id)
549
+
550
+
551
+ from datacommons_admin.ingest_cli import ingest
552
+
553
+ admin.add_command(ingest)
@@ -0,0 +1,75 @@
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
+
16
+ REMOTE_STATE_TEMPLATE = """
17
+ ## Remote State Management
18
+
19
+ Terraform state is configured to be stored remotely in Google Cloud Storage:
20
+ - **Bucket**: `gs://{bucket_name}`
21
+ - **Prefix**: `{prefix}`
22
+
23
+ This enables team collaboration, state locking, and automated pipeline deployments. The bucket is configured with Uniform Bucket-Level Access and object versioning.
24
+ """
25
+
26
+ README_TEMPLATE = """# Data Commons Platform Terraform Setup
27
+
28
+ This directory contains Terraform configuration for a new Data Commons Platform instance.
29
+
30
+ Data Commons is an open knowledge graph for integrating and querying structured data across domains.
31
+ The Data Commons Platform is the deployable infrastructure stack that runs Data Commons services in your GCP project.
32
+ {remote_state_section}
33
+ ## What This Terraform Creates
34
+
35
+ This setup deploys core Data Commons Platform infrastructure on GCP using the `infra/dcp` module, including Cloud Run services, Cloud Spanner resources, IAM bindings, and supporting service configuration.
36
+
37
+ ## Configure Variables
38
+
39
+ Set environment-specific values in `terraform.tfvars` (for example `project_id`, `namespace`, and `dc_api_key`), and update module arguments in `main.tf` if you want to enable or tune additional features.
40
+
41
+ ## Learn More About Variables
42
+
43
+ For the full list of supported module inputs and defaults, see:
44
+ - https://github.com/datacommonsorg/datacommons/blob/main/infra/dcp/variables.tf
45
+ - https://github.com/datacommonsorg/datacommons/blob/main/infra/dcp/outputs.tf
46
+ - https://github.com/datacommonsorg/datacommons/blob/main/infra/dcp/terraform.tfvars.template
47
+ - https://github.com/datacommonsorg/datacommons/blob/main/infra/dcp/README.md
48
+
49
+ ## Next Steps
50
+
51
+ 1. Review `terraform.tfvars` and update values if needed.
52
+ 2. Initialize Terraform:
53
+ ```bash
54
+ terraform init
55
+ ```
56
+ 3. Preview infrastructure changes:
57
+ ```bash
58
+ terraform plan
59
+ ```
60
+ 4. Deploy infrastructure:
61
+ ```bash
62
+ terraform apply
63
+ ```
64
+
65
+ Generated using the Data Commons CLI tool:
66
+ https://github.com/datacommonsorg/datacommons
67
+ """
68
+
69
+ BACKEND_TF_TEMPLATE = """terraform {{
70
+ backend "gcs" {{
71
+ bucket = "{bucket_name}"
72
+ prefix = "{prefix}"
73
+ }}
74
+ }}
75
+ """