kdcube-cli 0.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.
- kdcube_cli/__init__.py +1 -0
- kdcube_cli/banner.py +476 -0
- kdcube_cli/cli.py +5468 -0
- kdcube_cli/export_live_bundles.py +500 -0
- kdcube_cli/frontend_config.py +387 -0
- kdcube_cli/installer.py +4959 -0
- kdcube_cli/tty_keys.py +74 -0
- kdcube_cli-0.0.0.dist-info/METADATA +5 -0
- kdcube_cli-0.0.0.dist-info/RECORD +12 -0
- kdcube_cli-0.0.0.dist-info/WHEEL +5 -0
- kdcube_cli-0.0.0.dist-info/licenses/LICENSE +21 -0
- kdcube_cli-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import posixpath
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_MANAGED_BUNDLE_METADATA_FILE = ".kdcube-managed-bundle.json"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def aws_cli_env(*, region: str | None, profile: str | None) -> dict[str, str]:
|
|
18
|
+
env = os.environ.copy()
|
|
19
|
+
if region:
|
|
20
|
+
env["AWS_REGION"] = region
|
|
21
|
+
env["AWS_DEFAULT_REGION"] = region
|
|
22
|
+
if profile:
|
|
23
|
+
env["AWS_PROFILE"] = profile
|
|
24
|
+
return env
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def aws_secret_json(
|
|
28
|
+
*,
|
|
29
|
+
secret_id: str,
|
|
30
|
+
region: str | None,
|
|
31
|
+
profile: str | None,
|
|
32
|
+
required: bool,
|
|
33
|
+
) -> dict[str, object] | None:
|
|
34
|
+
cmd = [
|
|
35
|
+
"aws",
|
|
36
|
+
"secretsmanager",
|
|
37
|
+
"get-secret-value",
|
|
38
|
+
"--secret-id",
|
|
39
|
+
secret_id,
|
|
40
|
+
"--query",
|
|
41
|
+
"SecretString",
|
|
42
|
+
"--output",
|
|
43
|
+
"text",
|
|
44
|
+
]
|
|
45
|
+
proc = subprocess.run(
|
|
46
|
+
cmd,
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True,
|
|
49
|
+
env=aws_cli_env(region=region, profile=profile),
|
|
50
|
+
)
|
|
51
|
+
if proc.returncode != 0:
|
|
52
|
+
stderr = (proc.stderr or "").strip()
|
|
53
|
+
if not required and "ResourceNotFoundException" in stderr:
|
|
54
|
+
return None
|
|
55
|
+
raise SystemExit(f"Failed to read AWS secret {secret_id}: {stderr or proc.stdout.strip()}")
|
|
56
|
+
raw = (proc.stdout or "").strip()
|
|
57
|
+
if not raw or raw == "None":
|
|
58
|
+
return {} if required else None
|
|
59
|
+
try:
|
|
60
|
+
payload = json.loads(raw)
|
|
61
|
+
except Exception as exc:
|
|
62
|
+
raise SystemExit(f"AWS secret {secret_id} does not contain valid JSON.") from exc
|
|
63
|
+
if not isinstance(payload, dict):
|
|
64
|
+
raise SystemExit(f"AWS secret {secret_id} must contain a JSON object.")
|
|
65
|
+
return payload
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def resolve_aws_sm_prefix(*, tenant: str, project: str, explicit: str | None) -> str:
|
|
69
|
+
if explicit and explicit.strip():
|
|
70
|
+
return explicit.strip().strip("/")
|
|
71
|
+
tenant_text = str(tenant or "").strip()
|
|
72
|
+
project_text = str(project or "").strip()
|
|
73
|
+
if not tenant_text or not project_text:
|
|
74
|
+
raise SystemExit("--tenant and --project are required unless --aws-sm-prefix is provided.")
|
|
75
|
+
return f"kdcube/{tenant_text}/{project_text}"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _empty_bundles_secrets_payload() -> dict[str, object]:
|
|
79
|
+
return {
|
|
80
|
+
"bundles": {
|
|
81
|
+
"version": "1",
|
|
82
|
+
"items": [],
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _format_bytes(size: int) -> str:
|
|
88
|
+
units = ["B", "KB", "MB", "GB"]
|
|
89
|
+
value = float(max(size, 0))
|
|
90
|
+
unit = units[0]
|
|
91
|
+
for unit in units:
|
|
92
|
+
if value < 1024 or unit == units[-1]:
|
|
93
|
+
break
|
|
94
|
+
value /= 1024
|
|
95
|
+
if unit == "B":
|
|
96
|
+
return f"{int(value)} {unit}"
|
|
97
|
+
return f"{value:.1f} {unit}"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _get_nested(data: object, *parts: str) -> object:
|
|
101
|
+
cur = data
|
|
102
|
+
for part in parts:
|
|
103
|
+
if not isinstance(cur, dict):
|
|
104
|
+
return None
|
|
105
|
+
cur = cur.get(part)
|
|
106
|
+
return cur
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _strip_env_value(value: object) -> str:
|
|
110
|
+
if value is None:
|
|
111
|
+
return ""
|
|
112
|
+
text = str(value).strip()
|
|
113
|
+
if len(text) >= 2 and text[0] == text[-1] and text[0] in {"'", '"'}:
|
|
114
|
+
text = text[1:-1]
|
|
115
|
+
return text.strip()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _load_env_values(path: Path) -> dict[str, str]:
|
|
119
|
+
values: dict[str, str] = {}
|
|
120
|
+
if not path.exists():
|
|
121
|
+
return values
|
|
122
|
+
for line in path.read_text().splitlines():
|
|
123
|
+
stripped = line.strip()
|
|
124
|
+
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
|
125
|
+
continue
|
|
126
|
+
key, value = stripped.split("=", 1)
|
|
127
|
+
key = key.strip()
|
|
128
|
+
if key:
|
|
129
|
+
values[key] = _strip_env_value(value)
|
|
130
|
+
return values
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _descriptor_item_refs(data: object) -> list[tuple[str, dict[str, object]]]:
|
|
134
|
+
if not isinstance(data, dict):
|
|
135
|
+
return []
|
|
136
|
+
raw_bundles = data.get("bundles")
|
|
137
|
+
if isinstance(raw_bundles, dict):
|
|
138
|
+
raw_items = raw_bundles.get("items")
|
|
139
|
+
if isinstance(raw_items, list):
|
|
140
|
+
return [
|
|
141
|
+
(str(item.get("id") or "").strip(), item)
|
|
142
|
+
for item in raw_items
|
|
143
|
+
if isinstance(item, dict)
|
|
144
|
+
]
|
|
145
|
+
items: list[tuple[str, dict[str, object]]] = []
|
|
146
|
+
for key, value in raw_bundles.items():
|
|
147
|
+
if key in {"items", "version", "default_bundle_id"} or not isinstance(value, dict):
|
|
148
|
+
continue
|
|
149
|
+
items.append((str(value.get("id") or key).strip(), value))
|
|
150
|
+
return items
|
|
151
|
+
if isinstance(raw_bundles, list):
|
|
152
|
+
return [
|
|
153
|
+
(str(item.get("id") or "").strip(), item)
|
|
154
|
+
for item in raw_bundles
|
|
155
|
+
if isinstance(item, dict)
|
|
156
|
+
]
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _norm_container_path(value: object) -> str:
|
|
161
|
+
text = str(value or "").strip().replace("\\", "/")
|
|
162
|
+
if not text:
|
|
163
|
+
return ""
|
|
164
|
+
normalized = posixpath.normpath(text)
|
|
165
|
+
if not normalized.startswith("/"):
|
|
166
|
+
normalized = "/" + normalized
|
|
167
|
+
return normalized
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _container_path_to_host_path(
|
|
171
|
+
raw_path: object,
|
|
172
|
+
*,
|
|
173
|
+
container_root: str,
|
|
174
|
+
host_root: str,
|
|
175
|
+
) -> str | None:
|
|
176
|
+
path = _norm_container_path(raw_path)
|
|
177
|
+
root = _norm_container_path(container_root)
|
|
178
|
+
host = str(host_root or "").strip()
|
|
179
|
+
if not path or not root or not host:
|
|
180
|
+
return None
|
|
181
|
+
if path == root:
|
|
182
|
+
return str(Path(host).expanduser())
|
|
183
|
+
if not path.startswith(root.rstrip("/") + "/"):
|
|
184
|
+
return None
|
|
185
|
+
rel = path[len(root.rstrip("/")) :].lstrip("/")
|
|
186
|
+
return str(Path(host).expanduser().joinpath(*rel.split("/")))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _is_platform_managed_example_path(
|
|
190
|
+
raw_path: object,
|
|
191
|
+
*,
|
|
192
|
+
bundle_id: str,
|
|
193
|
+
container_managed_root: str,
|
|
194
|
+
host_managed_root: str,
|
|
195
|
+
) -> bool:
|
|
196
|
+
path = _norm_container_path(raw_path)
|
|
197
|
+
root = _norm_container_path(container_managed_root)
|
|
198
|
+
if not path or not root:
|
|
199
|
+
return False
|
|
200
|
+
if path != root and not path.startswith(root.rstrip("/") + "/"):
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
host_path = _container_path_to_host_path(
|
|
204
|
+
raw_path,
|
|
205
|
+
container_root=container_managed_root,
|
|
206
|
+
host_root=host_managed_root,
|
|
207
|
+
)
|
|
208
|
+
if host_path:
|
|
209
|
+
metadata_path = Path(host_path).expanduser() / _MANAGED_BUNDLE_METADATA_FILE
|
|
210
|
+
try:
|
|
211
|
+
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
|
|
212
|
+
if str(metadata.get("kind") or "") == "shared_example_bundle":
|
|
213
|
+
return True
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
leaf = posixpath.basename(path.rstrip("/"))
|
|
218
|
+
bid = str(bundle_id or "").strip()
|
|
219
|
+
return bool(bid and (leaf == bid or leaf.startswith(f"{bid}__")))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _strip_platform_managed_source_fields(item: dict) -> list[str]:
|
|
223
|
+
removed: list[str] = []
|
|
224
|
+
for key in ("path", "repo", "ref", "subdir", "git_commit", "module", "name", "description", "singleton"):
|
|
225
|
+
if key in item:
|
|
226
|
+
item.pop(key, None)
|
|
227
|
+
removed.append(key)
|
|
228
|
+
return removed
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _normalize_exported_bundle_paths(
|
|
232
|
+
data: object,
|
|
233
|
+
*,
|
|
234
|
+
config_dir: Path | None,
|
|
235
|
+
) -> list[dict[str, str]]:
|
|
236
|
+
"""Convert runtime-local descriptor paths back to seed-descriptor paths.
|
|
237
|
+
|
|
238
|
+
Runtime descriptors consumed by Docker use container-visible paths such as
|
|
239
|
+
/bundles/... or /managed-bundles/.... Exported seed descriptors should use
|
|
240
|
+
host paths for unmanaged local-path bundles. Platform-managed example bundles
|
|
241
|
+
are exported without materialized paths; the runtime resolves those built-ins
|
|
242
|
+
from the bundle id. Git-backed descriptors should keep their repo/ref/subdir
|
|
243
|
+
fields and must not export an incidental materialized path.
|
|
244
|
+
"""
|
|
245
|
+
if not isinstance(data, dict):
|
|
246
|
+
return []
|
|
247
|
+
|
|
248
|
+
assembly_path = config_dir / "assembly.yaml" if config_dir is not None else None
|
|
249
|
+
assembly = yaml.safe_load(assembly_path.read_text()) if assembly_path is not None and assembly_path.exists() else {}
|
|
250
|
+
env = _load_env_values(config_dir / ".env") if config_dir is not None else {}
|
|
251
|
+
|
|
252
|
+
host_bundles = str(_get_nested(assembly, "paths", "host_bundles_path") or env.get("HOST_BUNDLES_PATH") or "").strip()
|
|
253
|
+
container_bundles = str(
|
|
254
|
+
_get_nested(assembly, "platform", "services", "proc", "bundles", "bundles_root")
|
|
255
|
+
or env.get("BUNDLES_ROOT")
|
|
256
|
+
or "/bundles"
|
|
257
|
+
).strip()
|
|
258
|
+
host_managed = str(
|
|
259
|
+
_get_nested(assembly, "paths", "host_managed_bundles_path")
|
|
260
|
+
or env.get("HOST_MANAGED_BUNDLES_PATH")
|
|
261
|
+
or ""
|
|
262
|
+
).strip()
|
|
263
|
+
container_managed = str(
|
|
264
|
+
_get_nested(assembly, "platform", "services", "proc", "bundles", "managed_bundles_root")
|
|
265
|
+
or env.get("MANAGED_BUNDLES_ROOT")
|
|
266
|
+
or "/managed-bundles"
|
|
267
|
+
).strip()
|
|
268
|
+
|
|
269
|
+
translations: list[dict[str, str]] = []
|
|
270
|
+
for bundle_id, item in _descriptor_item_refs(data):
|
|
271
|
+
if item.get("repo"):
|
|
272
|
+
if "path" in item:
|
|
273
|
+
old = str(item.pop("path") or "")
|
|
274
|
+
translations.append(
|
|
275
|
+
{
|
|
276
|
+
"bundle_id": bundle_id,
|
|
277
|
+
"action": "removed_git_path",
|
|
278
|
+
"runtime_path": old,
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
raw_path = str(item.get("path") or "").strip()
|
|
284
|
+
if not raw_path:
|
|
285
|
+
continue
|
|
286
|
+
if _is_platform_managed_example_path(
|
|
287
|
+
raw_path,
|
|
288
|
+
bundle_id=bundle_id,
|
|
289
|
+
container_managed_root=container_managed,
|
|
290
|
+
host_managed_root=host_managed,
|
|
291
|
+
):
|
|
292
|
+
removed = _strip_platform_managed_source_fields(item)
|
|
293
|
+
translations.append(
|
|
294
|
+
{
|
|
295
|
+
"bundle_id": bundle_id,
|
|
296
|
+
"action": "removed_platform_managed_path",
|
|
297
|
+
"runtime_path": raw_path,
|
|
298
|
+
"removed_fields": ",".join(removed),
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
continue
|
|
302
|
+
if _container_path_to_host_path(
|
|
303
|
+
raw_path,
|
|
304
|
+
container_root=container_managed,
|
|
305
|
+
host_root=host_managed,
|
|
306
|
+
) is not None:
|
|
307
|
+
translations.append(
|
|
308
|
+
{
|
|
309
|
+
"bundle_id": bundle_id,
|
|
310
|
+
"action": "kept_managed_path",
|
|
311
|
+
"runtime_path": raw_path,
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
continue
|
|
315
|
+
host_path = _container_path_to_host_path(
|
|
316
|
+
raw_path,
|
|
317
|
+
container_root=container_bundles,
|
|
318
|
+
host_root=host_bundles,
|
|
319
|
+
)
|
|
320
|
+
source = "bundles"
|
|
321
|
+
if host_path is None:
|
|
322
|
+
continue
|
|
323
|
+
item["path"] = host_path
|
|
324
|
+
translations.append(
|
|
325
|
+
{
|
|
326
|
+
"bundle_id": bundle_id,
|
|
327
|
+
"action": "translated_path",
|
|
328
|
+
"source": source,
|
|
329
|
+
"runtime_path": raw_path,
|
|
330
|
+
"host_path": host_path,
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
return translations
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _export_live_bundle_descriptors_from_files(
|
|
337
|
+
console: Console,
|
|
338
|
+
*,
|
|
339
|
+
bundles_path: Path,
|
|
340
|
+
bundles_secrets_path: Path | None,
|
|
341
|
+
out_dir: Path,
|
|
342
|
+
) -> None:
|
|
343
|
+
if not bundles_path.exists():
|
|
344
|
+
raise SystemExit(f"Local bundles descriptor not found: {bundles_path}")
|
|
345
|
+
|
|
346
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
347
|
+
out_bundles_path = out_dir / "bundles.yaml"
|
|
348
|
+
bundles_data = yaml.safe_load(bundles_path.read_text()) or {}
|
|
349
|
+
translations = _normalize_exported_bundle_paths(
|
|
350
|
+
bundles_data,
|
|
351
|
+
config_dir=bundles_path.parent,
|
|
352
|
+
)
|
|
353
|
+
out_bundles_path.write_text(
|
|
354
|
+
yaml.safe_dump(bundles_data, sort_keys=False, allow_unicode=True)
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
out_bundles_secrets_path = out_dir / "bundles.secrets.yaml"
|
|
358
|
+
if bundles_secrets_path is not None and bundles_secrets_path.exists():
|
|
359
|
+
out_bundles_secrets_path.write_text(bundles_secrets_path.read_text())
|
|
360
|
+
else:
|
|
361
|
+
out_bundles_secrets_path.write_text(
|
|
362
|
+
yaml.safe_dump(_empty_bundles_secrets_payload(), sort_keys=False, allow_unicode=True)
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
console.print("[green]Exported effective live bundle descriptors.[/green]")
|
|
366
|
+
console.print(f"[dim]Authority:[/dim] mounted local descriptors")
|
|
367
|
+
console.print(f"[dim]source bundles.yaml:[/dim] {bundles_path}")
|
|
368
|
+
if bundles_secrets_path is not None and bundles_secrets_path.exists():
|
|
369
|
+
console.print(f"[dim]source bundles.secrets.yaml:[/dim] {bundles_secrets_path}")
|
|
370
|
+
else:
|
|
371
|
+
console.print("[dim]source bundles.secrets.yaml:[/dim] <not configured>")
|
|
372
|
+
if translations:
|
|
373
|
+
console.print("[dim]path normalization:[/dim]")
|
|
374
|
+
for item in translations:
|
|
375
|
+
action = item.get("action")
|
|
376
|
+
bundle_id = item.get("bundle_id") or "<unknown>"
|
|
377
|
+
if action == "removed_git_path":
|
|
378
|
+
console.print(f"[dim] {bundle_id}: removed git materialized path[/dim]")
|
|
379
|
+
elif action == "removed_platform_managed_path":
|
|
380
|
+
console.print(
|
|
381
|
+
f"[dim] {bundle_id}: removed platform-managed materialized path "
|
|
382
|
+
f"{item.get('runtime_path')}[/dim]"
|
|
383
|
+
)
|
|
384
|
+
elif action == "kept_managed_path":
|
|
385
|
+
console.print(f"[dim] {bundle_id}: kept managed cache path {item.get('runtime_path')}[/dim]")
|
|
386
|
+
elif action == "translated_path":
|
|
387
|
+
console.print(
|
|
388
|
+
f"[dim] {bundle_id}: {item.get('runtime_path')} -> {item.get('host_path')}[/dim]"
|
|
389
|
+
)
|
|
390
|
+
console.print(
|
|
391
|
+
f"[green]created bundles.yaml:[/green] {out_bundles_path} "
|
|
392
|
+
f"({_format_bytes(out_bundles_path.stat().st_size)})"
|
|
393
|
+
)
|
|
394
|
+
console.print(
|
|
395
|
+
f"[green]created bundles.secrets.yaml:[/green] {out_bundles_secrets_path} "
|
|
396
|
+
f"({_format_bytes(out_bundles_secrets_path.stat().st_size)})"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def export_live_bundle_descriptors(
|
|
401
|
+
console: Console,
|
|
402
|
+
*,
|
|
403
|
+
tenant: str,
|
|
404
|
+
project: str,
|
|
405
|
+
out_dir: Path,
|
|
406
|
+
aws_region: str | None,
|
|
407
|
+
aws_profile: str | None,
|
|
408
|
+
aws_sm_prefix: str | None,
|
|
409
|
+
bundles_path: Path | None = None,
|
|
410
|
+
bundles_secrets_path: Path | None = None,
|
|
411
|
+
) -> None:
|
|
412
|
+
if bundles_path is not None:
|
|
413
|
+
_export_live_bundle_descriptors_from_files(
|
|
414
|
+
console,
|
|
415
|
+
bundles_path=bundles_path,
|
|
416
|
+
bundles_secrets_path=bundles_secrets_path,
|
|
417
|
+
out_dir=out_dir,
|
|
418
|
+
)
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
prefix = resolve_aws_sm_prefix(tenant=tenant, project=project, explicit=aws_sm_prefix)
|
|
422
|
+
meta_secret_id = f"{prefix}/bundles-meta"
|
|
423
|
+
meta = aws_secret_json(
|
|
424
|
+
secret_id=meta_secret_id,
|
|
425
|
+
region=aws_region,
|
|
426
|
+
profile=aws_profile,
|
|
427
|
+
required=True,
|
|
428
|
+
)
|
|
429
|
+
bundle_ids = [
|
|
430
|
+
str(item).strip()
|
|
431
|
+
for item in (meta.get("bundle_ids") or [])
|
|
432
|
+
if str(item).strip()
|
|
433
|
+
]
|
|
434
|
+
if not bundle_ids:
|
|
435
|
+
raise SystemExit(
|
|
436
|
+
f"{meta_secret_id} does not contain bundle_ids. "
|
|
437
|
+
"Bootstrap the authoritative bundle descriptor store first by applying a live bundle update or reload-authority."
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
default_bundle_id = str(meta.get("default_bundle_id") or "").strip() or None
|
|
441
|
+
bundles_items: list[dict[str, object]] = []
|
|
442
|
+
bundles_secrets_items: list[dict[str, object]] = []
|
|
443
|
+
|
|
444
|
+
for bundle_id in bundle_ids:
|
|
445
|
+
descriptor = aws_secret_json(
|
|
446
|
+
secret_id=f"{prefix}/bundles/{bundle_id}/descriptor",
|
|
447
|
+
region=aws_region,
|
|
448
|
+
profile=aws_profile,
|
|
449
|
+
required=True,
|
|
450
|
+
) or {}
|
|
451
|
+
secrets = aws_secret_json(
|
|
452
|
+
secret_id=f"{prefix}/bundles/{bundle_id}/secrets",
|
|
453
|
+
region=aws_region,
|
|
454
|
+
profile=aws_profile,
|
|
455
|
+
required=False,
|
|
456
|
+
) or {}
|
|
457
|
+
descriptor_payload = {"id": bundle_id, **descriptor}
|
|
458
|
+
props = descriptor_payload.pop("props", None)
|
|
459
|
+
if isinstance(props, dict) and props:
|
|
460
|
+
descriptor_payload["config"] = props
|
|
461
|
+
bundles_items.append(descriptor_payload)
|
|
462
|
+
bundles_secrets_items.append({"id": bundle_id, "secrets": secrets})
|
|
463
|
+
|
|
464
|
+
bundles_payload: dict[str, object] = {"bundles": {"version": "1", "items": bundles_items}}
|
|
465
|
+
if default_bundle_id:
|
|
466
|
+
bundles_payload["bundles"]["default_bundle_id"] = default_bundle_id
|
|
467
|
+
translations = _normalize_exported_bundle_paths(
|
|
468
|
+
bundles_payload,
|
|
469
|
+
config_dir=None,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
bundles_secrets_payload: dict[str, object] = {
|
|
473
|
+
"bundles": {"version": "1", "items": bundles_secrets_items}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
477
|
+
bundles_path = out_dir / "bundles.yaml"
|
|
478
|
+
bundles_secrets_path = out_dir / "bundles.secrets.yaml"
|
|
479
|
+
bundles_path.write_text(yaml.safe_dump(bundles_payload, sort_keys=False, allow_unicode=True))
|
|
480
|
+
bundles_secrets_path.write_text(
|
|
481
|
+
yaml.safe_dump(bundles_secrets_payload, sort_keys=False, allow_unicode=True)
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
console.print("[green]Exported effective live bundle descriptors.[/green]")
|
|
485
|
+
console.print(f"[dim]AWS SM prefix:[/dim] {prefix}")
|
|
486
|
+
if translations:
|
|
487
|
+
console.print("[dim]path normalization:[/dim]")
|
|
488
|
+
for item in translations:
|
|
489
|
+
action = item.get("action")
|
|
490
|
+
bundle_id = item.get("bundle_id") or "<unknown>"
|
|
491
|
+
if action == "removed_git_path":
|
|
492
|
+
console.print(f"[dim] {bundle_id}: removed git materialized path[/dim]")
|
|
493
|
+
console.print(
|
|
494
|
+
f"[green]created bundles.yaml:[/green] {bundles_path} "
|
|
495
|
+
f"({_format_bytes(bundles_path.stat().st_size)})"
|
|
496
|
+
)
|
|
497
|
+
console.print(
|
|
498
|
+
f"[green]created bundles.secrets.yaml:[/green] {bundles_secrets_path} "
|
|
499
|
+
f"({_format_bytes(bundles_secrets_path.stat().st_size)})"
|
|
500
|
+
)
|