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,338 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
import logging
|
|
4
|
+
import argparse
|
|
5
|
+
import yaml
|
|
6
|
+
from g3dt.upload.metadata_submitter import (
|
|
7
|
+
create_boto3_session,
|
|
8
|
+
get_gen3_api_key_aws_secret,
|
|
9
|
+
infer_api_endpoint_from_jwt,
|
|
10
|
+
create_gen3_submission_class,
|
|
11
|
+
)
|
|
12
|
+
from g3dt.upload.metadata_deleter import (
|
|
13
|
+
query_metadata_upload_guids,
|
|
14
|
+
delete_records_by_guid,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# ANSI colour codes (matching metadata_submitter.py style)
|
|
18
|
+
GREEN = "\033[92m"
|
|
19
|
+
RED = "\033[91m"
|
|
20
|
+
YELLOW = "\033[93m"
|
|
21
|
+
BLUE = "\033[94m"
|
|
22
|
+
RESET = "\033[0m"
|
|
23
|
+
|
|
24
|
+
EXCLUDE_NODES = [
|
|
25
|
+
"program",
|
|
26
|
+
"project",
|
|
27
|
+
"acknowledgement",
|
|
28
|
+
"publication",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
# Exit code that signals "study exists but has no data at this version —
|
|
32
|
+
# skipped". A bulk caller (services/delete/delete_metadata.sh) treats this as a
|
|
33
|
+
# skip-and-continue rather than a failure. Only emitted with --skip-if-empty.
|
|
34
|
+
SKIP_EXIT_CODE = 3
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def setup_logger():
|
|
38
|
+
logger = logging.getLogger()
|
|
39
|
+
logger.setLevel(logging.INFO)
|
|
40
|
+
if not logger.handlers:
|
|
41
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
42
|
+
formatter = logging.Formatter(
|
|
43
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
44
|
+
)
|
|
45
|
+
handler.setFormatter(formatter)
|
|
46
|
+
logger.addHandler(handler)
|
|
47
|
+
return logger
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Shared config resolution (SSM-backed) — see src/g3dt/config.py
|
|
51
|
+
from g3dt import config as g3dt_config, resolver # noqa: E402
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_import_order(import_order_path, exclude_nodes=None):
|
|
55
|
+
"""
|
|
56
|
+
Reads the DataImportOrder.txt file and returns the node list
|
|
57
|
+
in deletion order (reversed, with excluded nodes removed).
|
|
58
|
+
"""
|
|
59
|
+
if exclude_nodes is None:
|
|
60
|
+
exclude_nodes = EXCLUDE_NODES
|
|
61
|
+
with open(import_order_path, 'r', encoding='utf-8') as f:
|
|
62
|
+
nodes = [line.strip() for line in f if line.strip()]
|
|
63
|
+
nodes = [n for n in nodes if n not in exclude_nodes]
|
|
64
|
+
nodes.reverse()
|
|
65
|
+
return nodes
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main():
|
|
69
|
+
logger = setup_logger()
|
|
70
|
+
|
|
71
|
+
parser = argparse.ArgumentParser(
|
|
72
|
+
description=(
|
|
73
|
+
"Delete Gen3 metadata records by GUID. Queries the Athena "
|
|
74
|
+
"metadata_upload table for matching records per node "
|
|
75
|
+
"(in reverse DataImportOrder) and deletes them from Gen3."
|
|
76
|
+
),
|
|
77
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
78
|
+
)
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--study",
|
|
81
|
+
required=True,
|
|
82
|
+
help=(
|
|
83
|
+
"Study key (bare or env-suffixed) "
|
|
84
|
+
"(e.g. ausdiab, caughtcad, edcad, cdah)"
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"--env",
|
|
89
|
+
required=True,
|
|
90
|
+
help="Environment to use (selects AWS secret, profile, etc.)",
|
|
91
|
+
)
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--version",
|
|
94
|
+
required=True,
|
|
95
|
+
help="Metadata version to filter on (e.g. 0.8.1)",
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--import-order",
|
|
99
|
+
default="DataImportOrder.txt",
|
|
100
|
+
help="Path to DataImportOrder.txt",
|
|
101
|
+
)
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
"--node",
|
|
104
|
+
default=None,
|
|
105
|
+
help=(
|
|
106
|
+
"Delete only a specific node (e.g. 'subject'). "
|
|
107
|
+
"If omitted, all nodes are processed in reverse "
|
|
108
|
+
"DataImportOrder."
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"--prompt",
|
|
113
|
+
action="store_true",
|
|
114
|
+
default=False,
|
|
115
|
+
help="Prompt for confirmation before deleting.",
|
|
116
|
+
)
|
|
117
|
+
parser.add_argument(
|
|
118
|
+
"--batch-size",
|
|
119
|
+
type=int,
|
|
120
|
+
default=40,
|
|
121
|
+
help="Number of UUIDs per DELETE request.",
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--batch-delay",
|
|
125
|
+
type=float,
|
|
126
|
+
default=0.5,
|
|
127
|
+
help="Seconds to pause between batches.",
|
|
128
|
+
)
|
|
129
|
+
parser.add_argument(
|
|
130
|
+
"--verbose",
|
|
131
|
+
action="store_true",
|
|
132
|
+
default=False,
|
|
133
|
+
help="Log full API response JSON for each request.",
|
|
134
|
+
)
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"--delay",
|
|
137
|
+
type=float,
|
|
138
|
+
default=1.0,
|
|
139
|
+
help="Seconds to wait between nodes.",
|
|
140
|
+
)
|
|
141
|
+
parser.add_argument(
|
|
142
|
+
"--skip-if-empty",
|
|
143
|
+
action="store_true",
|
|
144
|
+
default=False,
|
|
145
|
+
help=(
|
|
146
|
+
"If the study has no data at the given version, exit with the skip "
|
|
147
|
+
"code (3) instead of 0. Used by the bulk delete loop to "
|
|
148
|
+
"skip-and-continue rather than treat it as a failure."
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
args = parser.parse_args()
|
|
153
|
+
|
|
154
|
+
if args.verbose:
|
|
155
|
+
logger.setLevel(logging.DEBUG)
|
|
156
|
+
|
|
157
|
+
# Env facts + resource names from SSM; the study registry from the marker
|
|
158
|
+
# or s3://<metadata-bucket>/config/studies.yaml.
|
|
159
|
+
try:
|
|
160
|
+
env_cfg = g3dt_config.resolve_env(args.env)
|
|
161
|
+
except g3dt_config.ConfigError as exc:
|
|
162
|
+
logger.error(str(exc))
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
try:
|
|
165
|
+
study_cfg = g3dt_config.resolve_study(args.study, args.env)
|
|
166
|
+
except g3dt_config.ConfigError as exc:
|
|
167
|
+
if args.skip_if_empty:
|
|
168
|
+
logger.warning(
|
|
169
|
+
"Study '%s' not found in configuration — skipping.", args.study
|
|
170
|
+
)
|
|
171
|
+
sys.exit(SKIP_EXIT_CODE)
|
|
172
|
+
logger.error(str(exc))
|
|
173
|
+
sys.exit(1)
|
|
174
|
+
|
|
175
|
+
project_id = study_cfg.project_id
|
|
176
|
+
program_id = study_cfg.program_id
|
|
177
|
+
|
|
178
|
+
aws_secret_name = env_cfg.aws_secret_name
|
|
179
|
+
aws_profile = env_cfg.aws_profile
|
|
180
|
+
aws_region = env_cfg.region
|
|
181
|
+
|
|
182
|
+
rc = resolver.resolve(
|
|
183
|
+
g3dt_config.require_project(),
|
|
184
|
+
g3dt_config.env_base(args.env),
|
|
185
|
+
profile=aws_profile,
|
|
186
|
+
)
|
|
187
|
+
# Upload-tracking table: conventional name in the env's metadata DB
|
|
188
|
+
# (exactly like the CDK's `releases` table).
|
|
189
|
+
database = rc.metadata_db
|
|
190
|
+
table = g3dt_config.METADATA_UPLOAD_TABLE
|
|
191
|
+
athena_s3_output = rc.athena_output_location
|
|
192
|
+
workgroup = rc.athena_workgroup
|
|
193
|
+
|
|
194
|
+
# Construct compound project_id for Athena query
|
|
195
|
+
compound_project_id = f"{program_id}-{project_id}"
|
|
196
|
+
|
|
197
|
+
logger.info(
|
|
198
|
+
"Study: %s | Env: %s | Project: %s | Version: %s",
|
|
199
|
+
args.study,
|
|
200
|
+
args.env,
|
|
201
|
+
compound_project_id,
|
|
202
|
+
args.version,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# AWS and Gen3 authentication
|
|
206
|
+
session = create_boto3_session(aws_profile=aws_profile)
|
|
207
|
+
api_key = get_gen3_api_key_aws_secret(
|
|
208
|
+
secret_name=aws_secret_name,
|
|
209
|
+
region_name=aws_region,
|
|
210
|
+
session=session,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Derive api_endpoint from JWT
|
|
214
|
+
api_endpoint = infer_api_endpoint_from_jwt(api_key['api_key'])
|
|
215
|
+
logger.info("Derived API endpoint: %s", api_endpoint)
|
|
216
|
+
|
|
217
|
+
# Create Gen3Submission instance
|
|
218
|
+
sub = create_gen3_submission_class(api_key)
|
|
219
|
+
|
|
220
|
+
# Determine node list
|
|
221
|
+
if args.node:
|
|
222
|
+
nodes_to_delete = [args.node]
|
|
223
|
+
logger.info(
|
|
224
|
+
"%s[SINGLE NODE]%s Targeting node: %s",
|
|
225
|
+
BLUE, RESET, args.node,
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
nodes_to_delete = load_import_order(args.import_order)
|
|
229
|
+
logger.info(
|
|
230
|
+
"Loaded %s nodes from %s (deletion order, "
|
|
231
|
+
"excluding %s)",
|
|
232
|
+
len(nodes_to_delete),
|
|
233
|
+
args.import_order,
|
|
234
|
+
EXCLUDE_NODES,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if args.prompt:
|
|
238
|
+
confirm = input(
|
|
239
|
+
f"Proceed with deletion for project "
|
|
240
|
+
f"{compound_project_id}, version {args.version}, "
|
|
241
|
+
f"{len(nodes_to_delete)} node(s)? (yes/no): "
|
|
242
|
+
).strip().lower()
|
|
243
|
+
if confirm != "yes":
|
|
244
|
+
logger.info("Deletion cancelled by user.")
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
# Process each node
|
|
248
|
+
total_deleted = 0
|
|
249
|
+
total_skipped = 0
|
|
250
|
+
total_nodes = len(nodes_to_delete)
|
|
251
|
+
|
|
252
|
+
for idx, node in enumerate(nodes_to_delete, start=1):
|
|
253
|
+
logger.info(
|
|
254
|
+
"%s[Node %d/%d]%s | Project: %-10s | "
|
|
255
|
+
"Node: %-25s | Querying...",
|
|
256
|
+
BLUE, idx, total_nodes, RESET,
|
|
257
|
+
compound_project_id, node,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
df = query_metadata_upload_guids(
|
|
261
|
+
database=database,
|
|
262
|
+
table=table,
|
|
263
|
+
project_id=compound_project_id,
|
|
264
|
+
api_endpoint=api_endpoint,
|
|
265
|
+
version=args.version,
|
|
266
|
+
athena_s3_output=athena_s3_output,
|
|
267
|
+
workgroup=workgroup,
|
|
268
|
+
aws_region=aws_region,
|
|
269
|
+
aws_profile=aws_profile,
|
|
270
|
+
node=node,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if df.empty:
|
|
274
|
+
logger.info(
|
|
275
|
+
"%s[SKIP]%s | Project: %-10s | "
|
|
276
|
+
"Node: %-25s | No records found",
|
|
277
|
+
YELLOW, RESET,
|
|
278
|
+
compound_project_id, node,
|
|
279
|
+
)
|
|
280
|
+
total_skipped += 1
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
uuids = df['gen3_guid'].dropna().unique().tolist()
|
|
284
|
+
logger.info(
|
|
285
|
+
"%s[DELETE]%s | Project: %-10s | "
|
|
286
|
+
"Node: %-25s | Records: %s",
|
|
287
|
+
BLUE, RESET,
|
|
288
|
+
compound_project_id, node, len(uuids),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
delete_records_by_guid(
|
|
292
|
+
gen3_submission=sub,
|
|
293
|
+
program_id=program_id,
|
|
294
|
+
project_id=project_id,
|
|
295
|
+
uuids=uuids,
|
|
296
|
+
batch_size=args.batch_size,
|
|
297
|
+
batch_delay=args.batch_delay,
|
|
298
|
+
verbose=args.verbose,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
logger.info(
|
|
302
|
+
"%s[SUCCESS]%s | Project: %-10s | "
|
|
303
|
+
"Node: %-25s | Deleted: %s",
|
|
304
|
+
GREEN, RESET,
|
|
305
|
+
compound_project_id, node, len(uuids),
|
|
306
|
+
)
|
|
307
|
+
total_deleted += len(uuids)
|
|
308
|
+
|
|
309
|
+
if idx < total_nodes:
|
|
310
|
+
time.sleep(args.delay)
|
|
311
|
+
|
|
312
|
+
logger.info(
|
|
313
|
+
"=========================================="
|
|
314
|
+
)
|
|
315
|
+
logger.info(
|
|
316
|
+
"Deletion complete. Total deleted: %s | "
|
|
317
|
+
"Nodes skipped: %s | Nodes processed: %s",
|
|
318
|
+
total_deleted,
|
|
319
|
+
total_skipped,
|
|
320
|
+
total_nodes - total_skipped,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# No records matched the requested version across any node. This usually
|
|
324
|
+
# means the version was never uploaded (or the data was uploaded without a
|
|
325
|
+
# version), so surface an actionable hint rather than a silent "0 deleted".
|
|
326
|
+
if total_deleted == 0:
|
|
327
|
+
logger.warning(
|
|
328
|
+
"Data version '%s' not found for study '%s'. Ensure each data node "
|
|
329
|
+
"has a `data_version` property for versioning to work.",
|
|
330
|
+
args.version,
|
|
331
|
+
args.study,
|
|
332
|
+
)
|
|
333
|
+
if args.skip_if_empty:
|
|
334
|
+
sys.exit(SKIP_EXIT_CODE)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
if __name__ == "__main__":
|
|
338
|
+
main()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Exit on any error
|
|
4
|
+
set -e
|
|
5
|
+
set -o pipefail
|
|
6
|
+
|
|
7
|
+
# 1. Pull the dictionary at a specific version.
|
|
8
|
+
# 2. Upload the dictionary to S3.
|
|
9
|
+
# 3. Restart microservices (schema).
|
|
10
|
+
|
|
11
|
+
# Usage: bash deploy_dd.sh <profile>
|
|
12
|
+
# <profile> is display-only; all configuration comes from G3DT_* environment
|
|
13
|
+
# variables exported by the g3dt CLI (g3dt.config.script_env).
|
|
14
|
+
PROFILE=$1
|
|
15
|
+
|
|
16
|
+
if [ -z "$PROFILE" ]; then
|
|
17
|
+
echo "Usage: $0 <profile> (test|staging|prod) — run via the g3dt CLI"
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Defining script paths (sibling scripts ship together inside the package)
|
|
22
|
+
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
|
23
|
+
SERVICE_DIR="${SCRIPT_DIR}/.."
|
|
24
|
+
|
|
25
|
+
# Configuration from G3DT_* env vars (fail loudly if a required one is missing)
|
|
26
|
+
VERSION="${G3DT_DICTIONARY_VERSION:?G3DT_DICTIONARY_VERSION not set — run via the g3dt CLI}"
|
|
27
|
+
SCHEMA_S3_URI="${G3DT_SCHEMA_S3_URI:?G3DT_SCHEMA_S3_URI not set — run via the g3dt CLI}"
|
|
28
|
+
DOMAIN="${G3DT_DOMAIN:?G3DT_DOMAIN not set — run via the g3dt CLI}"
|
|
29
|
+
APP_NAME="${G3DT_APP_NAME:?G3DT_APP_NAME not set — run via the g3dt CLI}"
|
|
30
|
+
NAMESPACE="${G3DT_NAMESPACE:?G3DT_NAMESPACE not set — run via the g3dt CLI}"
|
|
31
|
+
CLUSTER_NAME="${G3DT_CLUSTER_NAME:?G3DT_CLUSTER_NAME not set — run via the g3dt CLI}"
|
|
32
|
+
SCHEMA_REPO="${G3DT_SCHEMA_REPO:?G3DT_SCHEMA_REPO not set — run via the g3dt CLI}"
|
|
33
|
+
REGION="${G3DT_REGION:-ap-southeast-2}"
|
|
34
|
+
EKS_ARN="${G3DT_EKS_ARN:-}"
|
|
35
|
+
# Downloaded schemas live outside the installed package.
|
|
36
|
+
SCHEMA_DIR="${G3DT_SCHEMA_DIR:-$HOME/.g3dt/schemas}"
|
|
37
|
+
ARGO_SCRIPT_DIR="${SERVICE_DIR}/k8s_ops"
|
|
38
|
+
|
|
39
|
+
# Never export an empty AWS_PROFILE (empty means ambient credentials).
|
|
40
|
+
if [ -n "${G3DT_AWS_PROFILE:-}" ]; then
|
|
41
|
+
export AWS_PROFILE="${G3DT_AWS_PROFILE}"
|
|
42
|
+
echo "==== [0] Configuring AWS PROFILE as '${AWS_PROFILE}' for profile '${PROFILE}' ===="
|
|
43
|
+
else
|
|
44
|
+
echo "==== [0] No AWS profile set for '${PROFILE}' — using ambient AWS credentials ===="
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
echo "Updating kubeconfig for cluster: ${CLUSTER_NAME}"
|
|
48
|
+
# eks_arn is optional; only pass --role-arn when it is set.
|
|
49
|
+
ROLE_ARG=""
|
|
50
|
+
if [ -n "${EKS_ARN}" ] && [ "${EKS_ARN}" != "null" ]; then
|
|
51
|
+
ROLE_ARG="--role-arn ${EKS_ARN}"
|
|
52
|
+
fi
|
|
53
|
+
if ! aws eks update-kubeconfig --name "${CLUSTER_NAME}" --region "${REGION}" ${ROLE_ARG}; then
|
|
54
|
+
echo "Error: Failed to update kubeconfig for cluster ${CLUSTER_NAME}"
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
echo "==== [1] Pulling dictionary for version ${VERSION} ===="
|
|
59
|
+
bash "${SERVICE_DIR}/dictionary/pull_dict.sh" "https://raw.githubusercontent.com/${SCHEMA_REPO}/refs/tags/${VERSION}/dictionary/prod_dict/acdc_schema.json"
|
|
60
|
+
|
|
61
|
+
echo "==== [2] Uploading dictionary to S3: s3://${SCHEMA_S3_URI} ===="
|
|
62
|
+
python3 "${SERVICE_DIR}/dictionary/upload_dictionary.py" "${SCHEMA_DIR}/acdc_schema_${VERSION}.json" "s3://${SCHEMA_S3_URI}"
|
|
63
|
+
|
|
64
|
+
echo "==== [3] Restarting microservices (schema) ===="
|
|
65
|
+
bash "${ARGO_SCRIPT_DIR}/argocd_restart_schema.sh" -d "${DOMAIN}" -a "${APP_NAME}" -n "${NAMESPACE}"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
show_help() {
|
|
4
|
+
echo "Usage: $0 <dict_url> [output_file]"
|
|
5
|
+
echo
|
|
6
|
+
echo "Download a dictionary JSON file from the specified URL."
|
|
7
|
+
echo
|
|
8
|
+
echo "Arguments:"
|
|
9
|
+
echo " dict_url The URL to download the dictionary JSON from."
|
|
10
|
+
echo " output_file Optional. The file to save the dictionary as (default: acdc_schema.json)."
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if [[ "$1" == "-h" || "$1" == "--help" || "$#" -lt 1 ]]; then
|
|
14
|
+
show_help
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
DICT_URL="$1"
|
|
19
|
+
|
|
20
|
+
# Downloads are written outside the installed package (never into site-packages).
|
|
21
|
+
SCHEMA_DIR="${G3DT_SCHEMA_DIR:-$HOME/.g3dt/schemas}"
|
|
22
|
+
mkdir -p "$SCHEMA_DIR"
|
|
23
|
+
|
|
24
|
+
# Extract version tag after '/tags/' or '/tag/' or '/refs/tags/' in the URL
|
|
25
|
+
if [[ "$DICT_URL" =~ /tags/([^/]+)/ ]]; then
|
|
26
|
+
VERSION="${BASH_REMATCH[1]}"
|
|
27
|
+
elif [[ "$DICT_URL" =~ /refs/tags/([^/]+)/ ]]; then
|
|
28
|
+
VERSION="${BASH_REMATCH[1]}"
|
|
29
|
+
else
|
|
30
|
+
VERSION="unknown"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Set output filename, including version tag if not overridden by user
|
|
34
|
+
if [[ -n "$2" ]]; then
|
|
35
|
+
OUTPUT_BASENAME="$2"
|
|
36
|
+
else
|
|
37
|
+
# Get the filename from the URL
|
|
38
|
+
BASE_NAME=$(basename "$DICT_URL")
|
|
39
|
+
# Insert version before file extension if possible
|
|
40
|
+
if [[ "$BASE_NAME" == *.* ]]; then
|
|
41
|
+
EXTENSION="${BASE_NAME##*.}"
|
|
42
|
+
NAME_NO_EXT="${BASE_NAME%.*}"
|
|
43
|
+
OUTPUT_BASENAME="${NAME_NO_EXT}_${VERSION}.${EXTENSION}"
|
|
44
|
+
else
|
|
45
|
+
OUTPUT_BASENAME="${BASE_NAME}_${VERSION}"
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
OUTPUT_FILE="${SCHEMA_DIR}/${OUTPUT_BASENAME}"
|
|
50
|
+
|
|
51
|
+
echo "Downloading dictionary from $DICT_URL..."
|
|
52
|
+
wget -O "$OUTPUT_FILE" "$DICT_URL"
|
|
53
|
+
|
|
54
|
+
if [ $? -eq 0 ]; then
|
|
55
|
+
echo "Download complete: $OUTPUT_FILE"
|
|
56
|
+
else
|
|
57
|
+
echo "Download failed." >&2
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
from botocore.exceptions import ClientError
|
|
6
|
+
|
|
7
|
+
logging.basicConfig(
|
|
8
|
+
level=logging.INFO,
|
|
9
|
+
format="%(asctime)s %(levelname)s %(message)s",
|
|
10
|
+
handlers=[logging.StreamHandler()]
|
|
11
|
+
)
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_s3_client(profile_name=None):
|
|
16
|
+
"""
|
|
17
|
+
Creates an S3 client with optional AWS profile.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
profile_name (str, optional): AWS profile name.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
boto3.client: S3 client.
|
|
24
|
+
"""
|
|
25
|
+
if profile_name:
|
|
26
|
+
session = boto3.Session(profile_name=profile_name)
|
|
27
|
+
return session.client("s3")
|
|
28
|
+
return boto3.client("s3")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_dict_version(dict_file_path):
|
|
32
|
+
"""
|
|
33
|
+
Extracts the dictionary version from the provided JSON/YAML combo settings.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
dict_file_path (str): Path to the dictionary file.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
str: Dictionary version.
|
|
40
|
+
"""
|
|
41
|
+
with open(dict_file_path, "r", encoding="utf-8") as f:
|
|
42
|
+
dict_data = json.load(f)
|
|
43
|
+
return dict_data.get("_settings.yaml", {}).get("_dict_version", None)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def upload_dict_to_s3(dict_file_path, s3_target_uri, dict_version, profile_name=None):
|
|
47
|
+
"""
|
|
48
|
+
Uploads a dictionary file to S3 with metadata.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
dict_file_path (str): Local dictionary path.
|
|
52
|
+
s3_target_uri (str): URI like s3://bucket/key.
|
|
53
|
+
dict_version (str): Dictionary version string.
|
|
54
|
+
profile_name (str, optional): AWS profile name.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
bool: True on success, False otherwise.
|
|
58
|
+
"""
|
|
59
|
+
if not s3_target_uri.startswith("s3://"):
|
|
60
|
+
logger.error(f"Invalid S3 URI: {s3_target_uri}")
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
s3_path = s3_target_uri[len("s3://") :]
|
|
65
|
+
if "/" not in s3_path:
|
|
66
|
+
logger.error(f"S3 URI missing key: {s3_target_uri}")
|
|
67
|
+
return False
|
|
68
|
+
bucket, key = s3_path.split("/", 1)
|
|
69
|
+
# Set S3 metadata key to "version" instead of "dict_version"
|
|
70
|
+
extra_args = {"Metadata": {"version": dict_version or "unknown"}}
|
|
71
|
+
s3_client = get_s3_client(profile_name)
|
|
72
|
+
s3_client.upload_file(dict_file_path, bucket, key, ExtraArgs=extra_args)
|
|
73
|
+
logger.info(
|
|
74
|
+
f"Successfully uploaded '{dict_file_path}' (version: {dict_version}) to {s3_target_uri}"
|
|
75
|
+
)
|
|
76
|
+
return True
|
|
77
|
+
except ClientError as e:
|
|
78
|
+
logger.error(f"Failed to upload file to {s3_target_uri}: {e}")
|
|
79
|
+
raise
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.error(f"Unexpected error uploading file: {e}")
|
|
82
|
+
raise
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main():
|
|
86
|
+
if len(sys.argv) < 3 or len(sys.argv) > 4:
|
|
87
|
+
print(
|
|
88
|
+
"Usage: python upload_dictionary.py <local_file_path> <s3_uri> [aws_profile]",
|
|
89
|
+
file=sys.stderr,
|
|
90
|
+
)
|
|
91
|
+
sys.exit(1)
|
|
92
|
+
|
|
93
|
+
local_file_path = sys.argv[1]
|
|
94
|
+
s3_uri = sys.argv[2]
|
|
95
|
+
profile_name = sys.argv[3] if len(sys.argv) == 4 else None
|
|
96
|
+
|
|
97
|
+
dict_version = get_dict_version(local_file_path)
|
|
98
|
+
if dict_version is None:
|
|
99
|
+
logger.error(f"Could not determine dictionary version from {local_file_path}")
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
|
|
102
|
+
success = upload_dict_to_s3(local_file_path, s3_uri, dict_version, profile_name)
|
|
103
|
+
if not success:
|
|
104
|
+
sys.exit(1)
|
|
105
|
+
# You may add a completion message if desired
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
main()
|