gen3-dataops-toolkit 2.0.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.
- g3dt/__init__.py +0 -0
- g3dt/cli/__init__.py +5 -0
- g3dt/cli/_internal/__init__.py +1 -0
- g3dt/cli/_internal/dispatch.py +428 -0
- g3dt/cli/_internal/registry.py +65 -0
- g3dt/cli/_internal/resolve.py +22 -0
- g3dt/cli/_internal/runner.py +76 -0
- g3dt/cli/_internal/safety.py +110 -0
- g3dt/cli/config_cmds.py +202 -0
- g3dt/cli/delete_cmds.py +101 -0
- g3dt/cli/dict_cmds.py +102 -0
- g3dt/cli/ec2_cmds.py +114 -0
- g3dt/cli/indexd_cmds.py +57 -0
- g3dt/cli/jobs.py +83 -0
- g3dt/cli/k8s.py +54 -0
- g3dt/cli/main.py +110 -0
- g3dt/cli/metadata.py +76 -0
- g3dt/cli/synth.py +206 -0
- g3dt/config.py +393 -0
- g3dt/indexd/__init__.py +0 -0
- g3dt/indexd/indexd_registrar.py +244 -0
- g3dt/ingest/ingest.py +629 -0
- g3dt/resolver.py +163 -0
- g3dt/services/delete/delete_all_metadata_for_project.py +170 -0
- g3dt/services/delete/delete_metadata.sh +153 -0
- g3dt/services/delete/delete_metadata_by_guid.py +338 -0
- g3dt/services/dictionary/deploy_dd.sh +65 -0
- g3dt/services/dictionary/pull_dict.sh +59 -0
- g3dt/services/dictionary/upload_dictionary.py +109 -0
- g3dt/services/indexd/register_indexd.py +240 -0
- g3dt/services/k8s_ops/argocd_restart_etl.sh +140 -0
- g3dt/services/k8s_ops/argocd_restart_ms.sh +102 -0
- g3dt/services/k8s_ops/argocd_restart_schema.sh +106 -0
- g3dt/services/k8s_ops/login_to_pod.sh +110 -0
- g3dt/services/k8s_ops/restart_etl_and_ms.sh +56 -0
- g3dt/services/synthetic_data/delete_synth_metadata_sheepdog.py +183 -0
- g3dt/services/synthetic_data/full_deploy_dd_and_synth.sh +124 -0
- g3dt/services/synthetic_data/generate_synth_metadata.sh +133 -0
- g3dt/services/synthetic_data/upload_synth_metadata_sheepdog.py +165 -0
- g3dt/services/upload/metadata/upload_all_studies.sh +108 -0
- g3dt/services/upload/metadata/upload_metadata.py +152 -0
- g3dt/upload/__init__.py +1 -0
- g3dt/upload/metadata_deleter.py +265 -0
- g3dt/upload/metadata_submitter.py +1093 -0
- g3dt/upload/upload_synthdata_s3.py +164 -0
- g3dt/utils/athena_utils.py +834 -0
- g3dt/utils/dbt_utils.py +66 -0
- g3dt/utils/release_writer.py +188 -0
- g3dt/validate/validate.py +609 -0
- gen3_dataops_toolkit-2.0.0.dist-info/METADATA +125 -0
- gen3_dataops_toolkit-2.0.0.dist-info/RECORD +53 -0
- gen3_dataops_toolkit-2.0.0.dist-info/WHEEL +4 -0
- gen3_dataops_toolkit-2.0.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Confirmation and environment guards for destructive / production operations.
|
|
2
|
+
|
|
3
|
+
These mirror (and strengthen) the guards already baked into the shell scripts:
|
|
4
|
+
the test-only ``synth deploy`` guard, the prod aborts in the bulk scripts, and
|
|
5
|
+
the optional delete confirmation prompts.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from g3dt.config import env_base
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_prod(env: str) -> bool:
|
|
15
|
+
"""True if the environment name refers to production."""
|
|
16
|
+
return "prod" in env.lower()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def require_test_env(env: str) -> None:
|
|
20
|
+
"""Abort unless ``env`` is the test environment (``test`` or ``test_ec2``).
|
|
21
|
+
|
|
22
|
+
A hard guard for any command that must never run outside test. (The ``synth``
|
|
23
|
+
commands no longer use this — they allow any env and gate prod with
|
|
24
|
+
:func:`confirm_prod_strict` instead.)
|
|
25
|
+
"""
|
|
26
|
+
if env_base(env) != "test":
|
|
27
|
+
typer.secho(
|
|
28
|
+
f"Refusing: this command is only allowed for the 'test' "
|
|
29
|
+
f"environment (got '{env}').",
|
|
30
|
+
fg=typer.colors.RED,
|
|
31
|
+
err=True,
|
|
32
|
+
)
|
|
33
|
+
raise typer.Exit(2)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def abort_if_prod(env: str) -> None:
|
|
37
|
+
"""Hard abort for bulk operations that must never touch production."""
|
|
38
|
+
if is_prod(env):
|
|
39
|
+
typer.secho(
|
|
40
|
+
f"Refusing bulk operation against a production environment "
|
|
41
|
+
f"('{env}').",
|
|
42
|
+
fg=typer.colors.RED,
|
|
43
|
+
err=True,
|
|
44
|
+
)
|
|
45
|
+
raise typer.Exit(2)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def confirm_destructive(action: str, target: str, env: str, assume_yes: bool) -> None:
|
|
49
|
+
"""Gate a destructive operation with an appropriate confirmation.
|
|
50
|
+
|
|
51
|
+
* Production: ALWAYS require typing the ``target`` exactly, even with
|
|
52
|
+
``--yes`` (so automation can never silently delete prod data).
|
|
53
|
+
* Non-production: a simple y/N prompt, skippable with ``--yes``.
|
|
54
|
+
|
|
55
|
+
Confirmation always happens locally, before any EC2 dispatch (SSM has no
|
|
56
|
+
TTY), after which the remote job is invoked with ``--yes``.
|
|
57
|
+
"""
|
|
58
|
+
if is_prod(env):
|
|
59
|
+
typer.secho(
|
|
60
|
+
f"PRODUCTION {action} targeting '{target}' (env={env}).",
|
|
61
|
+
fg=typer.colors.RED,
|
|
62
|
+
bold=True,
|
|
63
|
+
)
|
|
64
|
+
# default="" so an empty entry (just pressing Enter) returns immediately
|
|
65
|
+
# and aborts, instead of click re-prompting forever.
|
|
66
|
+
typed = typer.prompt(
|
|
67
|
+
f"Type '{target}' to confirm", default="", show_default=False
|
|
68
|
+
)
|
|
69
|
+
if typed.strip() != target:
|
|
70
|
+
typer.secho(
|
|
71
|
+
"Confirmation did not match. Aborting.",
|
|
72
|
+
fg=typer.colors.RED,
|
|
73
|
+
err=True,
|
|
74
|
+
)
|
|
75
|
+
raise typer.Exit(1)
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
if assume_yes:
|
|
79
|
+
return
|
|
80
|
+
if not typer.confirm(f"{action} targeting '{target}' (env={env}). Proceed?"):
|
|
81
|
+
typer.secho("Aborted.", fg=typer.colors.YELLOW)
|
|
82
|
+
raise typer.Exit(1)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def confirm_prod_strict(action: str, env: str) -> None:
|
|
86
|
+
"""Warn and require typing the env name before any action against production.
|
|
87
|
+
|
|
88
|
+
Production is any env whose name contains ``prod`` (see :func:`is_prod`).
|
|
89
|
+
Non-production environments return immediately (no prompt). The confirmation
|
|
90
|
+
cannot be bypassed, so automation can never silently act on prod.
|
|
91
|
+
|
|
92
|
+
Used by the ``synth`` commands, which may target any configured environment.
|
|
93
|
+
"""
|
|
94
|
+
if not is_prod(env):
|
|
95
|
+
return
|
|
96
|
+
typer.secho(
|
|
97
|
+
f"PRODUCTION {action} targeting env '{env}'.",
|
|
98
|
+
fg=typer.colors.RED,
|
|
99
|
+
bold=True,
|
|
100
|
+
)
|
|
101
|
+
# default="" so an empty entry (just pressing Enter) returns immediately and
|
|
102
|
+
# aborts, instead of click re-prompting forever.
|
|
103
|
+
typed = typer.prompt(f"Type '{env}' to confirm", default="", show_default=False)
|
|
104
|
+
if typed.strip() != env:
|
|
105
|
+
typer.secho(
|
|
106
|
+
"Confirmation did not match. Aborting.",
|
|
107
|
+
fg=typer.colors.RED,
|
|
108
|
+
err=True,
|
|
109
|
+
)
|
|
110
|
+
raise typer.Exit(1)
|
g3dt/cli/config_cmds.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""`g3dt config` — discover environments, studies, and resolved settings.
|
|
2
|
+
|
|
3
|
+
Everything the toolkit uses at runtime is resolved from SSM
|
|
4
|
+
(``/{project}/{env}/...``, published by ``cdk deploy`` in
|
|
5
|
+
gen3-aws-data-pipeline). These commands make that tree browsable so an
|
|
6
|
+
operator can answer "what environments exist?" and "what will this actually
|
|
7
|
+
do?" without touching the AWS console. The only local file is the tiny
|
|
8
|
+
``g3dt.yaml`` bootstrap marker (project/region/default_env, optional per-env
|
|
9
|
+
``profiles:`` and ``studies:`` maps) — ``g3dt config set`` edits that marker,
|
|
10
|
+
nothing else.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
from g3dt import config
|
|
20
|
+
from g3dt.cli._internal.resolve import env_of, study_of
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(
|
|
23
|
+
no_args_is_help=True,
|
|
24
|
+
help="Inspect the resolved SSM config; edit the local bootstrap marker.",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command()
|
|
29
|
+
def envs() -> None:
|
|
30
|
+
"""List the environments with a deployed SSM tree for this project."""
|
|
31
|
+
try:
|
|
32
|
+
for name in config.list_envs():
|
|
33
|
+
typer.echo(name)
|
|
34
|
+
except config.ConfigError as exc:
|
|
35
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
36
|
+
raise typer.Exit(1)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.command()
|
|
40
|
+
def studies(
|
|
41
|
+
env: str = typer.Option(
|
|
42
|
+
None,
|
|
43
|
+
"--env",
|
|
44
|
+
"-e",
|
|
45
|
+
help="Also check the env's S3 registry (s3://<metadata-bucket>/config/studies.yaml).",
|
|
46
|
+
),
|
|
47
|
+
) -> None:
|
|
48
|
+
"""List the configured studies (bare names).
|
|
49
|
+
|
|
50
|
+
The registry comes from the marker's studies: block, or — pass --env —
|
|
51
|
+
from the env's S3 registry, which is what the EC2 job box uses.
|
|
52
|
+
"""
|
|
53
|
+
names = config.list_studies(env=env)
|
|
54
|
+
if not names:
|
|
55
|
+
typer.secho(
|
|
56
|
+
"No studies configured. Add a studies: block to your g3dt.yaml "
|
|
57
|
+
"marker, or upload config/studies.yaml to the env's metadata "
|
|
58
|
+
"bucket (and pass --env).",
|
|
59
|
+
fg=typer.colors.YELLOW,
|
|
60
|
+
)
|
|
61
|
+
return
|
|
62
|
+
for name in names:
|
|
63
|
+
typer.echo(name)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.command()
|
|
67
|
+
def show(
|
|
68
|
+
env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
|
|
69
|
+
study: str = typer.Option(
|
|
70
|
+
None, "--study", "-s", help="Optional study to resolve against the env."
|
|
71
|
+
),
|
|
72
|
+
full: bool = typer.Option(
|
|
73
|
+
False, "--full", help="Also dump the raw SSM subtree (every parameter)."
|
|
74
|
+
),
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Print fully-resolved settings for an env (and optionally a study).
|
|
77
|
+
|
|
78
|
+
Use this before any job to confirm the exact names the tooling will use —
|
|
79
|
+
the staging-vs-prod safety check. Everything shown is read live from the
|
|
80
|
+
env's SSM tree; nothing is local except the marker's project/profiles.
|
|
81
|
+
"""
|
|
82
|
+
e = env_of(env)
|
|
83
|
+
typer.secho(f"Environment: {e.name}", bold=True)
|
|
84
|
+
typer.echo(f" is_ec2 : {e.is_ec2}")
|
|
85
|
+
typer.echo(f" region : {e.region}")
|
|
86
|
+
typer.echo(f" aws_profile : {e.aws_profile or '(ambient credentials)'}")
|
|
87
|
+
typer.echo(f" aws_secret_name : {e.aws_secret_name}")
|
|
88
|
+
typer.echo(f" dictionary_version : {e.dictionary_version}")
|
|
89
|
+
typer.echo(f" schema_s3_uri : {e.schema_s3_uri}")
|
|
90
|
+
typer.echo(f" schema_repo : {e.schema_repo}")
|
|
91
|
+
typer.echo(f" domain : {e.domain}")
|
|
92
|
+
typer.echo(f" app_name : {e.app_name}")
|
|
93
|
+
typer.echo(f" namespace : {e.namespace}")
|
|
94
|
+
typer.echo(f" cluster_name : {e.cluster_name}")
|
|
95
|
+
typer.echo(f" ec2_instance_id : {e.ec2_instance_id}")
|
|
96
|
+
if study:
|
|
97
|
+
s = study_of(study, env)
|
|
98
|
+
typer.secho(f"Study: {study} -> {s.key}", bold=True)
|
|
99
|
+
typer.echo(f" project_id : {s.project_id}")
|
|
100
|
+
typer.echo(f" program_id : {s.program_id}")
|
|
101
|
+
typer.echo(f" s3_metadata_path : {s.s3_metadata_path}")
|
|
102
|
+
if full:
|
|
103
|
+
from g3dt import resolver
|
|
104
|
+
|
|
105
|
+
marker = config.load_marker()
|
|
106
|
+
project = config.require_project(marker)
|
|
107
|
+
rc = resolver.resolve(
|
|
108
|
+
project, config.env_base(env),
|
|
109
|
+
profile=config.aws_profile_for(env, marker),
|
|
110
|
+
)
|
|
111
|
+
typer.secho(
|
|
112
|
+
f"\n/{rc.project}/{rc.env} ({len(rc.params)} parameters)", bold=True
|
|
113
|
+
)
|
|
114
|
+
for key in sorted(rc.params):
|
|
115
|
+
typer.echo(f" {key:<32} {rc.params[key]}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command()
|
|
119
|
+
def diff(
|
|
120
|
+
env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
|
|
121
|
+
file: Path = typer.Option(
|
|
122
|
+
...,
|
|
123
|
+
"--file",
|
|
124
|
+
"-f",
|
|
125
|
+
help="The env's INPUT file in the CDK repo, e.g. "
|
|
126
|
+
"../gen3-aws-data-pipeline/config/etl.test.json.",
|
|
127
|
+
),
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Flag drift between SSM and the committed CDK INPUT file.
|
|
130
|
+
|
|
131
|
+
Compares the mirrored app facts (``app/*``) and the toolkit pin
|
|
132
|
+
(``meta/toolkitVersion``) in SSM against ``config/<project>.<env>.json``.
|
|
133
|
+
A difference means "someone edited the JSON but didn't `cdk deploy`" (or
|
|
134
|
+
vice-versa). Exits 1 on drift, so it can gate CI.
|
|
135
|
+
"""
|
|
136
|
+
from g3dt import resolver
|
|
137
|
+
|
|
138
|
+
marker = config.load_marker()
|
|
139
|
+
project = config.require_project(marker)
|
|
140
|
+
try:
|
|
141
|
+
rc = resolver.resolve(
|
|
142
|
+
project, config.env_base(env),
|
|
143
|
+
profile=config.aws_profile_for(env, marker),
|
|
144
|
+
)
|
|
145
|
+
except config.ConfigError as exc:
|
|
146
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
147
|
+
raise typer.Exit(1)
|
|
148
|
+
|
|
149
|
+
inputs = json.loads(file.read_text())
|
|
150
|
+
gen3 = inputs.get("gen3", {})
|
|
151
|
+
# camelCase input field -> snake_case SSM leaf (the CDK's mirror contract)
|
|
152
|
+
camel_to_leaf = {
|
|
153
|
+
"dictionaryVersion": "dictionary_version",
|
|
154
|
+
"awsSecretName": "aws_secret_name",
|
|
155
|
+
"schemaS3Uri": "schema_s3_uri",
|
|
156
|
+
"domain": "domain",
|
|
157
|
+
"appName": "app_name",
|
|
158
|
+
"namespace": "namespace",
|
|
159
|
+
"clusterName": "cluster_name",
|
|
160
|
+
"schemaRepo": "schema_repo",
|
|
161
|
+
}
|
|
162
|
+
drift = False
|
|
163
|
+
|
|
164
|
+
def check(label: str, file_value, ssm_value) -> None:
|
|
165
|
+
nonlocal drift
|
|
166
|
+
if file_value != ssm_value:
|
|
167
|
+
drift = True
|
|
168
|
+
typer.secho(
|
|
169
|
+
f" DRIFT {label}: file={file_value!r} ssm={ssm_value!r}",
|
|
170
|
+
fg=typer.colors.YELLOW,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
for camel, leaf in camel_to_leaf.items():
|
|
174
|
+
check(f"gen3.{camel}", gen3.get(camel), rc.get(f"app/{leaf}"))
|
|
175
|
+
check("toolkitVersion", inputs.get("toolkitVersion"), rc.get("meta/toolkitVersion"))
|
|
176
|
+
|
|
177
|
+
if not drift:
|
|
178
|
+
typer.secho(
|
|
179
|
+
f"No drift: SSM /{project}/{config.env_base(env)} matches {file}.",
|
|
180
|
+
fg=typer.colors.GREEN,
|
|
181
|
+
)
|
|
182
|
+
raise typer.Exit(1 if drift else 0)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@app.command("set")
|
|
186
|
+
def set_value(
|
|
187
|
+
key: str = typer.Argument(..., help="Bootstrap key: project, region, default_env."),
|
|
188
|
+
value: str = typer.Argument(..., help="New value, e.g. etl."),
|
|
189
|
+
) -> None:
|
|
190
|
+
"""Set one bootstrap key in the local g3dt.yaml marker.
|
|
191
|
+
|
|
192
|
+
Only the bootstrap (project/region/default_env) lives locally. Deployed
|
|
193
|
+
settings — dictionary_version, domain, buckets, ... — are CDK INPUTS: edit
|
|
194
|
+
config/<project>.<env>.json in gen3-aws-data-pipeline and `cdk deploy`;
|
|
195
|
+
the values flow to SSM, which is what every consumer reads.
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
old, new, path = config.set_marker_value(key, value)
|
|
199
|
+
except config.ConfigError as exc:
|
|
200
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
201
|
+
raise typer.Exit(1)
|
|
202
|
+
typer.secho(f"Updated {key}: {old} -> {new} ({path})", fg=typer.colors.GREEN)
|
g3dt/cli/delete_cmds.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""`g3dt delete` — destructive metadata removal (data-plane).
|
|
2
|
+
|
|
3
|
+
A single ``delete metadata`` command handles one or many studies, sequentially,
|
|
4
|
+
in a single job. ``--version`` is required: pass a specific version (e.g.
|
|
5
|
+
``0.9.8``) to remove just that version (resolved via an Athena GUID lookup), or
|
|
6
|
+
``all`` to remove every version.
|
|
7
|
+
|
|
8
|
+
Every command confirms before acting. Production always requires typing the
|
|
9
|
+
target id, even with ``--yes``. Deleting ALL versions always prompts, even with
|
|
10
|
+
``--yes``. Confirmation happens locally before any EC2 dispatch (SSM has no
|
|
11
|
+
TTY), after which the remote job runs non-interactively.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
from g3dt.cli._internal import dispatch, safety
|
|
18
|
+
from g3dt.cli._internal.dispatch import Target
|
|
19
|
+
from g3dt.cli._internal.resolve import study_of
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(no_args_is_help=True, help="Delete metadata from Gen3 (destructive).")
|
|
22
|
+
|
|
23
|
+
_DELETE_METADATA = "services/delete/delete_metadata.sh"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command()
|
|
27
|
+
def metadata(
|
|
28
|
+
studies: str = typer.Option(
|
|
29
|
+
..., "--studies", help="Comma-separated studies, e.g. ausdiab,caughtcad."
|
|
30
|
+
),
|
|
31
|
+
env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
|
|
32
|
+
version: str = typer.Option(
|
|
33
|
+
None,
|
|
34
|
+
"--version",
|
|
35
|
+
help="Metadata version to delete, e.g. 0.9.8, or 'all' for every version.",
|
|
36
|
+
),
|
|
37
|
+
node: str = typer.Option(None, "--node", help="Delete only this node type."),
|
|
38
|
+
yes: bool = typer.Option(
|
|
39
|
+
False, "--yes", "-y", help="Skip the non-prod prompt (specific-version only)."
|
|
40
|
+
),
|
|
41
|
+
on: Target = typer.Option(Target.local, "--on", help="Run local or on ec2."),
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Delete study metadata for one or more studies, sequentially, in one job.
|
|
44
|
+
|
|
45
|
+
Studies are processed one at a time. A study that exists but has no data at
|
|
46
|
+
the requested version is skipped, and the job continues to the next study.
|
|
47
|
+
"""
|
|
48
|
+
if version is None:
|
|
49
|
+
typer.secho(
|
|
50
|
+
"--version is required: specify a version (e.g. 0.9.8) or 'all' "
|
|
51
|
+
"to delete every version.",
|
|
52
|
+
fg=typer.colors.RED,
|
|
53
|
+
err=True,
|
|
54
|
+
)
|
|
55
|
+
raise typer.Exit(2)
|
|
56
|
+
|
|
57
|
+
names = [s.strip() for s in studies.split(",") if s.strip()]
|
|
58
|
+
keys = [study_of(name, env).key for name in names]
|
|
59
|
+
target = ",".join(keys)
|
|
60
|
+
all_versions = version.strip().lower() == "all"
|
|
61
|
+
|
|
62
|
+
if all_versions:
|
|
63
|
+
# Deleting every version is the most destructive path: always prompt
|
|
64
|
+
# (pass assume_yes=False so --yes can't bypass it; prod still types the
|
|
65
|
+
# target).
|
|
66
|
+
safety.confirm_destructive("deletion of ALL VERSIONS", target, env, False)
|
|
67
|
+
else:
|
|
68
|
+
safety.confirm_destructive(f"deletion of v{version}", target, env, yes)
|
|
69
|
+
|
|
70
|
+
def build_args(env_name):
|
|
71
|
+
a = [
|
|
72
|
+
"--studies",
|
|
73
|
+
target,
|
|
74
|
+
"--env",
|
|
75
|
+
env_name,
|
|
76
|
+
"--version",
|
|
77
|
+
"all" if all_versions else version,
|
|
78
|
+
]
|
|
79
|
+
if node:
|
|
80
|
+
a += ["--node", node]
|
|
81
|
+
return a
|
|
82
|
+
|
|
83
|
+
def remote_cli(env_name):
|
|
84
|
+
# --yes: confirmation already happened locally; the remote job must
|
|
85
|
+
# not prompt (SSM has no TTY). The remote re-check is version-specific
|
|
86
|
+
# only, and 'all' was already confirmed above.
|
|
87
|
+
a = [
|
|
88
|
+
"delete", "metadata",
|
|
89
|
+
"--studies", studies,
|
|
90
|
+
"--env", env_name,
|
|
91
|
+
"--version", "all" if all_versions else version,
|
|
92
|
+
"--yes",
|
|
93
|
+
]
|
|
94
|
+
if node:
|
|
95
|
+
a += ["--node", node]
|
|
96
|
+
return a
|
|
97
|
+
|
|
98
|
+
dispatch.run_or_dispatch(
|
|
99
|
+
on, env, _DELETE_METADATA, build_args, "delete-metadata",
|
|
100
|
+
interpreter="bash", remote_cli=remote_cli,
|
|
101
|
+
)
|
g3dt/cli/dict_cmds.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""`g3dt dict` — data dictionary operations (pull / upload / deploy).
|
|
2
|
+
|
|
3
|
+
All local: dictionary deploy restarts Gen3 schema microservices via the ArgoCD
|
|
4
|
+
SSO browser flow, which only works interactively on the laptop.
|
|
5
|
+
|
|
6
|
+
The schema repo is an env input (``app/schema_repo`` in SSM), so any project
|
|
7
|
+
can point at its own dictionary repo. Downloads land in ``~/.g3dt/schemas/``
|
|
8
|
+
(the toolkit is installable-only — nothing is written into the package).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
|
|
16
|
+
from g3dt.config import script_env
|
|
17
|
+
from g3dt.cli._internal import runner
|
|
18
|
+
from g3dt.cli._internal.resolve import env_of
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(no_args_is_help=True, help="Data dictionary operations (local).")
|
|
21
|
+
|
|
22
|
+
#: Raw-GitHub URL template; the trailing path is the schema repo's layout
|
|
23
|
+
#: convention (see AustralianBioCommons/acdc-schema-json), not a project name.
|
|
24
|
+
_DICT_URL_TMPL = (
|
|
25
|
+
"https://raw.githubusercontent.com/{repo}/"
|
|
26
|
+
"refs/tags/{version}/dictionary/prod_dict/acdc_schema.json"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
SCHEMA_DIR = Path("~/.g3dt/schemas").expanduser()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _version(env_cfg, override):
|
|
33
|
+
return override or env_cfg.dictionary_version
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def dict_url(env_cfg, version: str) -> str:
|
|
37
|
+
return _DICT_URL_TMPL.format(repo=env_cfg.schema_repo, version=version)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command()
|
|
41
|
+
def pull(
|
|
42
|
+
env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
|
|
43
|
+
version: str = typer.Option(
|
|
44
|
+
None, "--version", help="Dictionary git tag (default: the env's version)."
|
|
45
|
+
),
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Download the dictionary JSON from the env's schema repo.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
g3dt dict pull --env test
|
|
51
|
+
g3dt dict pull --env staging --version v1.1.5
|
|
52
|
+
"""
|
|
53
|
+
e = env_of(env)
|
|
54
|
+
url = dict_url(e, _version(e, version))
|
|
55
|
+
runner.run(
|
|
56
|
+
runner.bash_script("services/dictionary/pull_dict.sh", url),
|
|
57
|
+
env=script_env(e),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.command()
|
|
62
|
+
def upload(
|
|
63
|
+
env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
|
|
64
|
+
version: str = typer.Option(
|
|
65
|
+
None, "--version", help="Dictionary git tag (default: the env's version)."
|
|
66
|
+
),
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Upload the (already pulled) dictionary JSON to the env's S3 location."""
|
|
69
|
+
e = env_of(env)
|
|
70
|
+
v = _version(e, version)
|
|
71
|
+
local_file = str(SCHEMA_DIR / f"acdc_schema_{v}.json")
|
|
72
|
+
s3_uri = f"s3://{e.schema_s3_uri}"
|
|
73
|
+
args = [local_file, s3_uri]
|
|
74
|
+
if e.aws_profile:
|
|
75
|
+
args.append(e.aws_profile)
|
|
76
|
+
runner.run(
|
|
77
|
+
runner.python_script("services/dictionary/upload_dictionary.py", *args),
|
|
78
|
+
env=script_env(e),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@app.command()
|
|
83
|
+
def deploy(
|
|
84
|
+
env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Pull + upload the dictionary and restart Gen3 schema microservices.
|
|
87
|
+
|
|
88
|
+
Wraps services/dictionary/deploy_dd.sh. Requires an interactive ArgoCD SSO
|
|
89
|
+
login, so it runs locally only.
|
|
90
|
+
|
|
91
|
+
The deployed version is the env's `dictionary_version` — a CDK INPUT. To
|
|
92
|
+
change it, edit config/<project>.<env>.json in gen3-aws-data-pipeline and
|
|
93
|
+
`cdk deploy` (the value flows to SSM), then re-run this command.
|
|
94
|
+
|
|
95
|
+
Examples:
|
|
96
|
+
g3dt dict deploy --env test
|
|
97
|
+
"""
|
|
98
|
+
e = env_of(env)
|
|
99
|
+
runner.run(
|
|
100
|
+
runner.bash_script("services/dictionary/deploy_dd.sh", env),
|
|
101
|
+
env=script_env(e),
|
|
102
|
+
)
|
g3dt/cli/ec2_cmds.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""`g3dt ec2` — manage the env's EC2 job box (up / down / status).
|
|
2
|
+
|
|
3
|
+
The box is created per environment by the CDK (ec2-job-runner stack) and is
|
|
4
|
+
SSM-managed: no SSH key or bootstrap script is needed. Its instance id is
|
|
5
|
+
resolved from the env's own SSM tree (``ec2/instanceId``), so targeting
|
|
6
|
+
another environment's box is structurally impossible.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from g3dt import config
|
|
15
|
+
from g3dt.cli._internal.resolve import env_of
|
|
16
|
+
from g3dt.upload.metadata_submitter import create_boto3_session
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(no_args_is_help=True, help="Manage the env's EC2 job box.")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _resolve(env: str):
|
|
22
|
+
"""Return ``(auth_env, instance_id)`` for the env's box."""
|
|
23
|
+
e = env_of(config.env_base(env))
|
|
24
|
+
if not e.ec2_instance_id:
|
|
25
|
+
typer.secho(
|
|
26
|
+
f"No ec2/instanceId published for env '{e.name}'. Has the "
|
|
27
|
+
f"ec2-job-runner stack been deployed? (cdk deploy in "
|
|
28
|
+
f"gen3-aws-data-pipeline)",
|
|
29
|
+
fg=typer.colors.RED,
|
|
30
|
+
err=True,
|
|
31
|
+
)
|
|
32
|
+
raise typer.Exit(2)
|
|
33
|
+
return e, e.ec2_instance_id
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _session(e):
|
|
37
|
+
return create_boto3_session(aws_profile=e.aws_profile, aws_region=e.region)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command()
|
|
41
|
+
def up(
|
|
42
|
+
env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
|
|
43
|
+
wait: bool = typer.Option(
|
|
44
|
+
True, "--wait/--no-wait", help="Wait until the box registers with SSM."
|
|
45
|
+
),
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Start the env's job box and print how to reach it.
|
|
48
|
+
|
|
49
|
+
Plain ``start_instances`` — the box was fully bootstrapped by CDK
|
|
50
|
+
user-data (toolkit installed, marker written), so there is nothing to
|
|
51
|
+
re-provision. Normal access is SSM (no SSH); a session command is printed
|
|
52
|
+
as soon as the box is reachable.
|
|
53
|
+
"""
|
|
54
|
+
e, instance_id = _resolve(env)
|
|
55
|
+
session = _session(e)
|
|
56
|
+
session.client("ec2").start_instances(InstanceIds=[instance_id])
|
|
57
|
+
typer.secho(f"Start requested for {instance_id} ({e.name}).", fg=typer.colors.GREEN)
|
|
58
|
+
if not wait:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
ssm = session.client("ssm")
|
|
62
|
+
typer.echo("Waiting for the box to register with SSM...")
|
|
63
|
+
for _ in range(60): # up to ~5 minutes
|
|
64
|
+
info = ssm.describe_instance_information(
|
|
65
|
+
Filters=[{"Key": "InstanceIds", "Values": [instance_id]}]
|
|
66
|
+
).get("InstanceInformationList", [])
|
|
67
|
+
if info and info[0].get("PingStatus") == "Online":
|
|
68
|
+
profile = f" --profile {e.aws_profile}" if e.aws_profile else ""
|
|
69
|
+
typer.secho(f"{instance_id} is reachable.", fg=typer.colors.GREEN)
|
|
70
|
+
typer.echo(" session : aws ssm start-session "
|
|
71
|
+
f"--target {instance_id}{profile}")
|
|
72
|
+
typer.echo(f" dispatch: g3dt <cmd> --env {config.env_base(env)} --on ec2")
|
|
73
|
+
return
|
|
74
|
+
time.sleep(5)
|
|
75
|
+
typer.secho(
|
|
76
|
+
f"{instance_id} started but has not registered with SSM yet; "
|
|
77
|
+
f"check `g3dt ec2 status --env {env}` in a minute.",
|
|
78
|
+
fg=typer.colors.YELLOW,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@app.command()
|
|
83
|
+
def down(
|
|
84
|
+
env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Stop the env's job box.
|
|
87
|
+
|
|
88
|
+
(An idle box also stops itself: the CDK auto-stop alarm fires after 24h
|
|
89
|
+
under 1% CPU.)
|
|
90
|
+
"""
|
|
91
|
+
e, instance_id = _resolve(env)
|
|
92
|
+
_session(e).client("ec2").stop_instances(InstanceIds=[instance_id])
|
|
93
|
+
typer.secho(f"Stop requested for {instance_id}.", fg=typer.colors.GREEN)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.command()
|
|
97
|
+
def status(
|
|
98
|
+
env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Show the box's EC2 state and SSM reachability."""
|
|
101
|
+
e, instance_id = _resolve(env)
|
|
102
|
+
session = _session(e)
|
|
103
|
+
resp = session.client("ec2").describe_instances(InstanceIds=[instance_id])
|
|
104
|
+
state = "unknown"
|
|
105
|
+
for res in resp.get("Reservations", []):
|
|
106
|
+
for inst in res.get("Instances", []):
|
|
107
|
+
state = inst.get("State", {}).get("Name", "unknown")
|
|
108
|
+
ssm_state = "not registered"
|
|
109
|
+
info = session.client("ssm").describe_instance_information(
|
|
110
|
+
Filters=[{"Key": "InstanceIds", "Values": [instance_id]}]
|
|
111
|
+
).get("InstanceInformationList", [])
|
|
112
|
+
if info:
|
|
113
|
+
ssm_state = f"ssm {info[0].get('PingStatus', 'unknown').lower()}"
|
|
114
|
+
typer.echo(f"{instance_id}: {state} ({ssm_state})")
|
g3dt/cli/indexd_cmds.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""`g3dt indexd` — register S3 files with Gen3 indexd (data-plane).
|
|
2
|
+
|
|
3
|
+
Long-running, so it supports ``--on ec2``.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from g3dt.cli._internal import dispatch
|
|
12
|
+
from g3dt.cli._internal.dispatch import Target
|
|
13
|
+
from g3dt.cli._internal.resolve import study_of
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(no_args_is_help=True, help="Register files with Gen3 indexd.")
|
|
16
|
+
|
|
17
|
+
_REGISTER = "services/indexd/register_indexd.py"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command()
|
|
21
|
+
def register(
|
|
22
|
+
s3_paths: List[str] = typer.Option(
|
|
23
|
+
..., "--s3-paths", help="One or more S3 prefixes to scan (repeatable)."
|
|
24
|
+
),
|
|
25
|
+
study: str = typer.Option(..., "--study", "-s", help="Study, e.g. edcad."),
|
|
26
|
+
env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
|
|
27
|
+
dry_run: bool = typer.Option(
|
|
28
|
+
False, "--dry-run", help="Scan + write file_metadata only; skip indexd."
|
|
29
|
+
),
|
|
30
|
+
on: Target = typer.Option(Target.local, "--on", help="Run local or on ec2."),
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Scan S3 prefixes and register the files with Gen3 indexd.
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
g3dt indexd register --s3-paths s3://bucket/edcad/ --study edcad --env staging
|
|
36
|
+
g3dt indexd register --s3-paths s3://b/a/ --s3-paths s3://b/c/ --study edcad --env staging --on ec2
|
|
37
|
+
"""
|
|
38
|
+
s = study_of(study, env)
|
|
39
|
+
|
|
40
|
+
def build_args(env_name):
|
|
41
|
+
a = ["--s3-paths", *s3_paths, "--study", s.key, "--env", env_name]
|
|
42
|
+
if dry_run:
|
|
43
|
+
a.append("--dry-run")
|
|
44
|
+
return a
|
|
45
|
+
|
|
46
|
+
def remote_cli(env_name):
|
|
47
|
+
a: list = ["indexd", "register"]
|
|
48
|
+
for p in s3_paths:
|
|
49
|
+
a += ["--s3-paths", p]
|
|
50
|
+
a += ["--study", study, "--env", env_name]
|
|
51
|
+
if dry_run:
|
|
52
|
+
a.append("--dry-run")
|
|
53
|
+
return a
|
|
54
|
+
|
|
55
|
+
dispatch.run_or_dispatch(
|
|
56
|
+
on, env, _REGISTER, build_args, "indexd-register", remote_cli=remote_cli,
|
|
57
|
+
)
|