dander-platform 0.1.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.
- dander/__init__.py +12 -0
- dander/_bigquery_retry.py +54 -0
- dander/bootstrap/__init__.py +37 -0
- dander/bootstrap/admin.py +276 -0
- dander/bootstrap/project.py +327 -0
- dander/bootstrap/terraform.py +356 -0
- dander/bootstrap/verify.py +986 -0
- dander/catalog/README.md +48 -0
- dander/catalog/__init__.py +42 -0
- dander/catalog/dataplex.py +210 -0
- dander/catalog/publisher.py +21 -0
- dander/catalog/registry.py +31 -0
- dander/catalog/spine.py +252 -0
- dander/catalog/store.py +213 -0
- dander/cli/__init__.py +1 -0
- dander/cli/main.py +1588 -0
- dander/concurrency.py +78 -0
- dander/core/__init__.py +1 -0
- dander/core/config.py +22 -0
- dander/core/interfaces.py +32 -0
- dander/dev/__init__.py +1 -0
- dander/dev/synthetic_vendor.py +242 -0
- dander/evidence/__init__.py +19 -0
- dander/evidence/models.py +139 -0
- dander/executor.py +289 -0
- dander/ingestion/README.md +30 -0
- dander/ingestion/__init__.py +44 -0
- dander/ingestion/config.py +43 -0
- dander/ingestion/dlt_backed.py +304 -0
- dander/ingestion/enterprise.py +196 -0
- dander/ingestion/pagination.py +157 -0
- dander/ingestion/source.py +362 -0
- dander/pipeline/README.md +491 -0
- dander/pipeline/__init__.py +102 -0
- dander/pipeline/compiler.py +519 -0
- dander/pipeline/errors.py +248 -0
- dander/pipeline/graph.py +1027 -0
- dander/pipeline/graph_ops.py +413 -0
- dander/pipeline/node_config.py +358 -0
- dander/pipeline/request_spec.py +301 -0
- dander/project/__init__.py +26 -0
- dander/project/config.py +235 -0
- dander/project/scaffold.py +93 -0
- dander/py.typed +0 -0
- dander/runtime.py +458 -0
- dander/sandbox.py +265 -0
- dander/schema.py +36 -0
- dander/security/__init__.py +35 -0
- dander/security/api_bearer.py +20 -0
- dander/security/api_key.py +27 -0
- dander/security/base.py +32 -0
- dander/security/no_auth.py +21 -0
- dander/security/oauth.py +157 -0
- dander/security/oauth1.py +104 -0
- dander/security/oauth_jwt.py +172 -0
- dander/security/secret_manager.py +136 -0
- dander/state/__init__.py +37 -0
- dander/state/lease.py +406 -0
- dander/state/run_history.py +472 -0
- dander/state/watermark.py +259 -0
- dander/templates/project/.dockerignore +7 -0
- dander/templates/project/.gitignore +6 -0
- dander/templates/project/Dockerfile +19 -0
- dander/templates/project/README.md +22 -0
- dander/templates/project/connectors/greenhouse_job_board.yaml +56 -0
- dander/templates/project/dander.yaml +25 -0
- dander/templates/project/infra/.terraform.lock.hcl +64 -0
- dander/templates/project/infra/README.md +131 -0
- dander/templates/project/infra/__init__.py +1 -0
- dander/templates/project/infra/bootstrap-admin/.terraform.lock.hcl +22 -0
- dander/templates/project/infra/bootstrap-admin/README.md +74 -0
- dander/templates/project/infra/bootstrap-admin/main.tf +176 -0
- dander/templates/project/infra/bootstrap-admin/outputs.tf +24 -0
- dander/templates/project/infra/bootstrap-admin/variables.tf +75 -0
- dander/templates/project/infra/bootstrap-admin/versions.tf +19 -0
- dander/templates/project/infra/functions/__init__.py +1 -0
- dander/templates/project/infra/functions/stop_billing/__init__.py +1 -0
- dander/templates/project/infra/functions/stop_billing/handler.py +66 -0
- dander/templates/project/infra/functions/stop_billing/main.py +37 -0
- dander/templates/project/infra/functions/stop_billing/requirements.txt +3 -0
- dander/templates/project/infra/main.tf +133 -0
- dander/templates/project/infra/modules/bigquery/main.tf +13 -0
- dander/templates/project/infra/modules/bigquery/outputs.tf +4 -0
- dander/templates/project/infra/modules/bigquery/variables.tf +16 -0
- dander/templates/project/infra/modules/cost-guard/README.md +12 -0
- dander/templates/project/infra/modules/cost-guard/main.tf +206 -0
- dander/templates/project/infra/modules/cost-guard/outputs.tf +14 -0
- dander/templates/project/infra/modules/cost-guard/variables.tf +45 -0
- dander/templates/project/infra/modules/cost-guard/versions.tf +21 -0
- dander/templates/project/infra/modules/github-wif/README.md +6 -0
- dander/templates/project/infra/modules/github-wif/main.tf +79 -0
- dander/templates/project/infra/modules/github-wif/outputs.tf +9 -0
- dander/templates/project/infra/modules/github-wif/variables.tf +30 -0
- dander/templates/project/infra/modules/github-wif/versions.tf +10 -0
- dander/templates/project/infra/modules/scheduled-job/README.md +37 -0
- dander/templates/project/infra/modules/scheduled-job/main.tf +374 -0
- dander/templates/project/infra/modules/scheduled-job/outputs.tf +48 -0
- dander/templates/project/infra/modules/scheduled-job/variables.tf +141 -0
- dander/templates/project/infra/modules/scheduled-job/versions.tf +10 -0
- dander/templates/project/infra/modules/secret-manager/README.md +5 -0
- dander/templates/project/infra/modules/secret-manager/main.tf +45 -0
- dander/templates/project/infra/modules/secret-manager/outputs.tf +4 -0
- dander/templates/project/infra/modules/secret-manager/variables.tf +25 -0
- dander/templates/project/infra/modules/secret-manager/versions.tf +10 -0
- dander/templates/project/infra/outputs.tf +37 -0
- dander/templates/project/infra/ownership-cutover.tf +11 -0
- dander/templates/project/infra/sandbox.auto.tfvars.example +35 -0
- dander/templates/project/infra/variables.tf +211 -0
- dander/templates/project/infra/versions.tf +44 -0
- dander/templates/project/models/staging/stg_greenhouse__jobs.sql +29 -0
- dander/templates/project/models/staging/stg_greenhouse__jobs.yml +50 -0
- dander/transform/README.md +26 -0
- dander/transform/__init__.py +35 -0
- dander/transform/config.py +179 -0
- dander/transform/model.py +67 -0
- dander/transform/project.py +184 -0
- dander/transform/runner.py +335 -0
- dander/writer/README.md +37 -0
- dander/writer/__init__.py +43 -0
- dander/writer/base.py +76 -0
- dander/writer/bigquery.py +913 -0
- dander/writer/storage_write.py +294 -0
- dander_platform-0.1.0.dist-info/METADATA +627 -0
- dander_platform-0.1.0.dist-info/RECORD +127 -0
- dander_platform-0.1.0.dist-info/WHEEL +4 -0
- dander_platform-0.1.0.dist-info/entry_points.txt +3 -0
- dander_platform-0.1.0.dist-info/licenses/LICENSE +176 -0
dander/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Dander — an opinionated, GCP-native data platform (ingest + transform + catalog).
|
|
2
|
+
|
|
3
|
+
Keep this module import-light: importing ``dander`` must not require the heavy optional
|
|
4
|
+
dependencies. Import from the subpackages (``dander.ingestion``, ``dander.writer``, ...) directly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
__version__ = version("dander-platform")
|
|
11
|
+
except PackageNotFoundError: # pragma: no cover - only an unpackaged source tree
|
|
12
|
+
__version__ = "0+unknown"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Narrow retry support for BigQuery transaction contention."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import random
|
|
7
|
+
from time import sleep
|
|
8
|
+
from typing import TYPE_CHECKING, Protocol
|
|
9
|
+
|
|
10
|
+
from google.api_core.exceptions import BadRequest
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
|
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
|
16
|
+
_CONCURRENT_UPDATE_MESSAGES = (
|
|
17
|
+
"transaction is aborted due to concurrent update against table",
|
|
18
|
+
"could not serialize access to table",
|
|
19
|
+
)
|
|
20
|
+
_MAX_ATTEMPTS = 5
|
|
21
|
+
_BASE_DELAY_SECONDS = 0.25
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _Job(Protocol):
|
|
25
|
+
def result(self) -> object:
|
|
26
|
+
"""Wait for the submitted BigQuery job."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run_mutation_with_retry[JobT: _Job](submit: Callable[[], JobT]) -> JobT:
|
|
30
|
+
"""Run a mutation, retrying only BigQuery's concurrent-update transaction abort."""
|
|
31
|
+
for attempt in range(1, _MAX_ATTEMPTS + 1):
|
|
32
|
+
try:
|
|
33
|
+
job = submit()
|
|
34
|
+
job.result()
|
|
35
|
+
return job
|
|
36
|
+
except BadRequest as error:
|
|
37
|
+
message = str(error).lower()
|
|
38
|
+
concurrent_update = any(
|
|
39
|
+
signature in message for signature in _CONCURRENT_UPDATE_MESSAGES
|
|
40
|
+
)
|
|
41
|
+
if not concurrent_update or attempt == _MAX_ATTEMPTS:
|
|
42
|
+
raise
|
|
43
|
+
delay = (_BASE_DELAY_SECONDS * (2 ** (attempt - 1))) + random.uniform(
|
|
44
|
+
0,
|
|
45
|
+
_BASE_DELAY_SECONDS,
|
|
46
|
+
)
|
|
47
|
+
_LOGGER.warning(
|
|
48
|
+
"BigQuery transaction contention; retrying mutation in %.2fs (attempt %d/%d)",
|
|
49
|
+
delay,
|
|
50
|
+
attempt + 1,
|
|
51
|
+
_MAX_ATTEMPTS,
|
|
52
|
+
)
|
|
53
|
+
sleep(delay)
|
|
54
|
+
raise AssertionError("BigQuery mutation retry loop exited unexpectedly")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Infrastructure bootstrap adapters."""
|
|
2
|
+
|
|
3
|
+
from dander.bootstrap.admin import AdministrativeBootstrap, AdministrativeBootstrapError
|
|
4
|
+
from dander.bootstrap.project import (
|
|
5
|
+
ProjectBootstrapError,
|
|
6
|
+
RuntimeImagePublisher,
|
|
7
|
+
StateBucketBootstrap,
|
|
8
|
+
active_admin_member,
|
|
9
|
+
wait_for_service_account_impersonation,
|
|
10
|
+
)
|
|
11
|
+
from dander.bootstrap.terraform import TerraformBootstrap, TerraformBootstrapError
|
|
12
|
+
from dander.bootstrap.verify import (
|
|
13
|
+
DeploymentSummary,
|
|
14
|
+
DeploymentVerificationError,
|
|
15
|
+
DeploymentVerifier,
|
|
16
|
+
VerificationCheck,
|
|
17
|
+
VerificationStatus,
|
|
18
|
+
write_summary,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"AdministrativeBootstrap",
|
|
23
|
+
"AdministrativeBootstrapError",
|
|
24
|
+
"DeploymentSummary",
|
|
25
|
+
"DeploymentVerificationError",
|
|
26
|
+
"DeploymentVerifier",
|
|
27
|
+
"ProjectBootstrapError",
|
|
28
|
+
"RuntimeImagePublisher",
|
|
29
|
+
"StateBucketBootstrap",
|
|
30
|
+
"TerraformBootstrap",
|
|
31
|
+
"TerraformBootstrapError",
|
|
32
|
+
"VerificationCheck",
|
|
33
|
+
"VerificationStatus",
|
|
34
|
+
"write_summary",
|
|
35
|
+
"active_admin_member",
|
|
36
|
+
"wait_for_service_account_impersonation",
|
|
37
|
+
]
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Stage-zero administrative Terraform bootstrap."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
_PROJECT_ID = re.compile(r"^[a-z][a-z0-9-]{4,28}[a-z0-9]$")
|
|
14
|
+
_BUCKET_NAME = re.compile(r"^[a-z0-9][a-z0-9._-]{1,61}[a-z0-9]$")
|
|
15
|
+
_REGION = re.compile(r"^[a-z][a-z0-9-]{1,62}[a-z0-9]$")
|
|
16
|
+
_LOCATION = re.compile(r"^[A-Za-z][A-Za-z0-9-]{0,62}$")
|
|
17
|
+
_BILLING_ACCOUNT = re.compile(r"^[0-9A-F]{6}-[0-9A-F]{6}-[0-9A-F]{6}$")
|
|
18
|
+
_PRINCIPAL = re.compile(r"^(?:user|serviceAccount|group):[^\r\n]+$")
|
|
19
|
+
_SERVICE_ACCOUNT_ID = re.compile(r"^[a-z][a-z0-9-]{4,28}[a-z0-9]$")
|
|
20
|
+
_GITHUB_REPOSITORY = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
|
|
21
|
+
_GITHUB_REF = re.compile(r"^refs/(?:heads|tags)/[A-Za-z0-9._/-]+$")
|
|
22
|
+
_STAGE_ZERO_STATE_PREFIX = "dander/bootstrap-admin/state"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AdministrativeBootstrapError(RuntimeError):
|
|
26
|
+
"""Raised when stage-zero Terraform cannot safely complete."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AdministrativeBootstrap:
|
|
30
|
+
"""Create only the state bucket and identity preconditions for platform Terraform."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, infra_dir: Path, operator_artifact_dir: Path) -> None:
|
|
33
|
+
self._infra_dir = infra_dir.resolve()
|
|
34
|
+
self._repository_dir = self._infra_dir.parent.parent
|
|
35
|
+
self._operator_artifact_dir = operator_artifact_dir.expanduser().resolve()
|
|
36
|
+
if self._is_within(self._operator_artifact_dir, self._repository_dir):
|
|
37
|
+
raise AdministrativeBootstrapError(
|
|
38
|
+
"Operator artifact directory must be outside the repository checkout"
|
|
39
|
+
)
|
|
40
|
+
self._tf_data_dir = self._operator_artifact_dir / "terraform-data"
|
|
41
|
+
self._plan_path = self._operator_artifact_dir / "dander-admin-bootstrap.tfplan"
|
|
42
|
+
|
|
43
|
+
def execute(
|
|
44
|
+
self,
|
|
45
|
+
*,
|
|
46
|
+
project: str,
|
|
47
|
+
state_bucket: str,
|
|
48
|
+
admin_member: str,
|
|
49
|
+
apply: bool,
|
|
50
|
+
region: str = "us-central1",
|
|
51
|
+
state_location: str = "US",
|
|
52
|
+
bootstrap_service_account_id: str = "dander-bootstrap",
|
|
53
|
+
billing_account_id: str = "",
|
|
54
|
+
github_repository: str = "",
|
|
55
|
+
github_ref: str = "refs/heads/main",
|
|
56
|
+
adopt_state_bucket: bool = False,
|
|
57
|
+
) -> Path:
|
|
58
|
+
"""Plan stage zero and optionally apply that exact saved plan.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
project: GCP project receiving the administrative resources.
|
|
62
|
+
state_bucket: Globally unique bucket name for platform Terraform state.
|
|
63
|
+
admin_member: Approved principal allowed to impersonate the bootstrap identity.
|
|
64
|
+
apply: Whether to apply the saved plan after planning.
|
|
65
|
+
region: Provider region.
|
|
66
|
+
state_location: GCS location for the state bucket.
|
|
67
|
+
bootstrap_service_account_id: Account id for the bootstrap service account.
|
|
68
|
+
billing_account_id: Optional billing account for budget-management access.
|
|
69
|
+
github_repository: Optional repository allowed to use proof-workflow WIF.
|
|
70
|
+
github_ref: Exact branch or tag ref allowed to use proof-workflow WIF.
|
|
71
|
+
adopt_state_bucket: Import a pre-created stage-zero backend bucket when absent from
|
|
72
|
+
state. Used only by the batteries-included ``dander init`` flow.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Path to the saved stage-zero plan.
|
|
76
|
+
"""
|
|
77
|
+
self._validate(
|
|
78
|
+
project=project,
|
|
79
|
+
state_bucket=state_bucket,
|
|
80
|
+
admin_member=admin_member,
|
|
81
|
+
region=region,
|
|
82
|
+
state_location=state_location,
|
|
83
|
+
bootstrap_service_account_id=bootstrap_service_account_id,
|
|
84
|
+
billing_account_id=billing_account_id,
|
|
85
|
+
github_repository=github_repository,
|
|
86
|
+
github_ref=github_ref,
|
|
87
|
+
)
|
|
88
|
+
self._prepare_operator_directories()
|
|
89
|
+
plan_path = self._plan_path
|
|
90
|
+
self._run(
|
|
91
|
+
"terraform",
|
|
92
|
+
"init",
|
|
93
|
+
"-reconfigure",
|
|
94
|
+
"-input=false",
|
|
95
|
+
f"-backend-config=bucket={state_bucket}",
|
|
96
|
+
f"-backend-config=prefix={_STAGE_ZERO_STATE_PREFIX}",
|
|
97
|
+
)
|
|
98
|
+
if adopt_state_bucket:
|
|
99
|
+
self._adopt_state_bucket(
|
|
100
|
+
project=project,
|
|
101
|
+
state_bucket=state_bucket,
|
|
102
|
+
admin_member=admin_member,
|
|
103
|
+
region=region,
|
|
104
|
+
state_location=state_location,
|
|
105
|
+
bootstrap_service_account_id=bootstrap_service_account_id,
|
|
106
|
+
billing_account_id=billing_account_id,
|
|
107
|
+
github_repository=github_repository,
|
|
108
|
+
github_ref=github_ref,
|
|
109
|
+
)
|
|
110
|
+
self._run(
|
|
111
|
+
"terraform",
|
|
112
|
+
"plan",
|
|
113
|
+
f"-var=project_id={project}",
|
|
114
|
+
f"-var=state_bucket={state_bucket}",
|
|
115
|
+
f"-var=admin_member={admin_member}",
|
|
116
|
+
f"-var=region={region}",
|
|
117
|
+
f"-var=state_location={state_location}",
|
|
118
|
+
f"-var=bootstrap_service_account_id={bootstrap_service_account_id}",
|
|
119
|
+
f"-var=billing_account_id={billing_account_id}",
|
|
120
|
+
f"-var=github_repository={github_repository}",
|
|
121
|
+
f"-var=github_ref={github_ref}",
|
|
122
|
+
f"-out={plan_path}",
|
|
123
|
+
)
|
|
124
|
+
try:
|
|
125
|
+
if not plan_path.is_file() or plan_path.is_symlink():
|
|
126
|
+
raise AdministrativeBootstrapError(
|
|
127
|
+
f"Terraform did not create a regular saved plan at {plan_path}"
|
|
128
|
+
)
|
|
129
|
+
plan_path.chmod(0o600)
|
|
130
|
+
except OSError as error:
|
|
131
|
+
raise AdministrativeBootstrapError(
|
|
132
|
+
f"Could not secure the saved Terraform plan at {plan_path}"
|
|
133
|
+
) from error
|
|
134
|
+
if apply:
|
|
135
|
+
self._run("terraform", "apply", str(plan_path))
|
|
136
|
+
return plan_path
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _is_within(candidate: Path, parent: Path) -> bool:
|
|
140
|
+
return candidate == parent or parent in candidate.parents
|
|
141
|
+
|
|
142
|
+
def _prepare_operator_directories(self) -> None:
|
|
143
|
+
try:
|
|
144
|
+
if self._tf_data_dir.is_symlink():
|
|
145
|
+
raise AdministrativeBootstrapError("terraform-data must not be a symlink")
|
|
146
|
+
if self._tf_data_dir.exists() and not self._tf_data_dir.is_dir():
|
|
147
|
+
raise AdministrativeBootstrapError("terraform-data must be a directory")
|
|
148
|
+
resolved_tf_data_dir = self._tf_data_dir.resolve()
|
|
149
|
+
if not self._is_within(resolved_tf_data_dir, self._operator_artifact_dir):
|
|
150
|
+
raise AdministrativeBootstrapError(
|
|
151
|
+
"Resolved terraform-data must remain inside the operator artifact directory"
|
|
152
|
+
)
|
|
153
|
+
if self._is_within(resolved_tf_data_dir, self._repository_dir):
|
|
154
|
+
raise AdministrativeBootstrapError(
|
|
155
|
+
"Resolved terraform-data must remain outside the repository checkout"
|
|
156
|
+
)
|
|
157
|
+
if self._plan_path.is_symlink():
|
|
158
|
+
raise AdministrativeBootstrapError("Saved Terraform plan must not be a symlink")
|
|
159
|
+
if self._plan_path.exists() and not self._plan_path.is_file():
|
|
160
|
+
raise AdministrativeBootstrapError("Saved Terraform plan must be a regular file")
|
|
161
|
+
self._operator_artifact_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
162
|
+
self._operator_artifact_dir.chmod(0o700)
|
|
163
|
+
self._tf_data_dir.mkdir(mode=0o700, exist_ok=True)
|
|
164
|
+
self._tf_data_dir.chmod(0o700)
|
|
165
|
+
if self._plan_path.exists():
|
|
166
|
+
self._plan_path.chmod(0o600)
|
|
167
|
+
self._plan_path.unlink()
|
|
168
|
+
except OSError as error:
|
|
169
|
+
raise AdministrativeBootstrapError(
|
|
170
|
+
"Could not create or secure the operator artifact directory"
|
|
171
|
+
) from error
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _validate(
|
|
175
|
+
*,
|
|
176
|
+
project: str,
|
|
177
|
+
state_bucket: str,
|
|
178
|
+
admin_member: str,
|
|
179
|
+
region: str,
|
|
180
|
+
state_location: str,
|
|
181
|
+
bootstrap_service_account_id: str,
|
|
182
|
+
billing_account_id: str,
|
|
183
|
+
github_repository: str,
|
|
184
|
+
github_ref: str,
|
|
185
|
+
) -> None:
|
|
186
|
+
for label, value, pattern in (
|
|
187
|
+
("project", project, _PROJECT_ID),
|
|
188
|
+
("state bucket", state_bucket, _BUCKET_NAME),
|
|
189
|
+
("admin member", admin_member, _PRINCIPAL),
|
|
190
|
+
("region", region, _REGION),
|
|
191
|
+
("state location", state_location, _LOCATION),
|
|
192
|
+
("bootstrap service-account id", bootstrap_service_account_id, _SERVICE_ACCOUNT_ID),
|
|
193
|
+
):
|
|
194
|
+
if not pattern.fullmatch(value):
|
|
195
|
+
raise AdministrativeBootstrapError(f"Invalid {label}: {value!r}")
|
|
196
|
+
if billing_account_id and not _BILLING_ACCOUNT.fullmatch(billing_account_id):
|
|
197
|
+
raise AdministrativeBootstrapError(
|
|
198
|
+
"Billing account must use XXXXXX-XXXXXX-XXXXXX format"
|
|
199
|
+
)
|
|
200
|
+
if github_repository and not _GITHUB_REPOSITORY.fullmatch(github_repository):
|
|
201
|
+
raise AdministrativeBootstrapError("Invalid GitHub repository")
|
|
202
|
+
if not _GITHUB_REF.fullmatch(github_ref):
|
|
203
|
+
raise AdministrativeBootstrapError("Invalid GitHub ref")
|
|
204
|
+
|
|
205
|
+
def _run(self, *args: str) -> None:
|
|
206
|
+
environment = os.environ.copy()
|
|
207
|
+
environment["TF_DATA_DIR"] = str(self._tf_data_dir)
|
|
208
|
+
try:
|
|
209
|
+
subprocess.run(
|
|
210
|
+
args,
|
|
211
|
+
cwd=self._infra_dir,
|
|
212
|
+
env=environment,
|
|
213
|
+
umask=0o077,
|
|
214
|
+
check=True,
|
|
215
|
+
)
|
|
216
|
+
except FileNotFoundError as error:
|
|
217
|
+
raise AdministrativeBootstrapError(
|
|
218
|
+
"Terraform is not installed or is not available on PATH"
|
|
219
|
+
) from error
|
|
220
|
+
except subprocess.CalledProcessError as error:
|
|
221
|
+
raise AdministrativeBootstrapError(
|
|
222
|
+
f"Terraform command failed with exit code {error.returncode}"
|
|
223
|
+
) from error
|
|
224
|
+
|
|
225
|
+
def _adopt_state_bucket(
|
|
226
|
+
self,
|
|
227
|
+
*,
|
|
228
|
+
project: str,
|
|
229
|
+
state_bucket: str,
|
|
230
|
+
admin_member: str,
|
|
231
|
+
region: str,
|
|
232
|
+
state_location: str,
|
|
233
|
+
bootstrap_service_account_id: str,
|
|
234
|
+
billing_account_id: str,
|
|
235
|
+
github_repository: str,
|
|
236
|
+
github_ref: str,
|
|
237
|
+
) -> None:
|
|
238
|
+
if (
|
|
239
|
+
self._run_status("terraform", "state", "show", "google_storage_bucket.terraform_state")
|
|
240
|
+
== 0
|
|
241
|
+
):
|
|
242
|
+
return
|
|
243
|
+
self._run(
|
|
244
|
+
"terraform",
|
|
245
|
+
"import",
|
|
246
|
+
f"-var=project_id={project}",
|
|
247
|
+
f"-var=state_bucket={state_bucket}",
|
|
248
|
+
f"-var=admin_member={admin_member}",
|
|
249
|
+
f"-var=region={region}",
|
|
250
|
+
f"-var=state_location={state_location}",
|
|
251
|
+
f"-var=bootstrap_service_account_id={bootstrap_service_account_id}",
|
|
252
|
+
f"-var=billing_account_id={billing_account_id}",
|
|
253
|
+
f"-var=github_repository={github_repository}",
|
|
254
|
+
f"-var=github_ref={github_ref}",
|
|
255
|
+
"google_storage_bucket.terraform_state",
|
|
256
|
+
state_bucket,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def _run_status(self, *args: str) -> int:
|
|
260
|
+
environment = os.environ.copy()
|
|
261
|
+
environment["TF_DATA_DIR"] = str(self._tf_data_dir)
|
|
262
|
+
try:
|
|
263
|
+
completed = subprocess.run(
|
|
264
|
+
args,
|
|
265
|
+
cwd=self._infra_dir,
|
|
266
|
+
env=environment,
|
|
267
|
+
umask=0o077,
|
|
268
|
+
check=False,
|
|
269
|
+
stdout=subprocess.DEVNULL,
|
|
270
|
+
stderr=subprocess.DEVNULL,
|
|
271
|
+
)
|
|
272
|
+
except FileNotFoundError as error:
|
|
273
|
+
raise AdministrativeBootstrapError(
|
|
274
|
+
"Terraform is not installed or is not available on PATH"
|
|
275
|
+
) from error
|
|
276
|
+
return completed.returncode
|