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.
- datacommons_admin-0.0.1/PKG-INFO +15 -0
- datacommons_admin-0.0.1/README.md +3 -0
- datacommons_admin-0.0.1/datacommons_admin/__init__.py +1 -0
- datacommons_admin-0.0.1/datacommons_admin/admin_cli.py +553 -0
- datacommons_admin-0.0.1/datacommons_admin/infra_templates.py +75 -0
- datacommons_admin-0.0.1/datacommons_admin/ingest_cli.py +142 -0
- datacommons_admin-0.0.1/datacommons_admin/ingestion_helper_client.py +108 -0
- datacommons_admin-0.0.1/datacommons_admin/ingestion_job_client.py +145 -0
- datacommons_admin-0.0.1/datacommons_admin/tf_utils.py +134 -0
- datacommons_admin-0.0.1/datacommons_admin/version.py +15 -0
- datacommons_admin-0.0.1/datacommons_admin.egg-info/PKG-INFO +15 -0
- datacommons_admin-0.0.1/datacommons_admin.egg-info/SOURCES.txt +16 -0
- datacommons_admin-0.0.1/datacommons_admin.egg-info/dependency_links.txt +1 -0
- datacommons_admin-0.0.1/datacommons_admin.egg-info/requires.txt +3 -0
- datacommons_admin-0.0.1/datacommons_admin.egg-info/top_level.txt +1 -0
- datacommons_admin-0.0.1/pyproject.toml +25 -0
- datacommons_admin-0.0.1/setup.cfg +4 -0
- datacommons_admin-0.0.1/tests/test_admin_cli.py +378 -0
|
@@ -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 @@
|
|
|
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
|
+
"""
|