#!/usr/bin/env python3 """ Дополнительный генератор: обогащает *_outputs.md реальными ключами из output_inventory (собранного со стендов). Обновляет только секцию "## Реальные поля из облака" — не трогает остальное. Запуск (из корня репо на VM): python3 devops/04b_enrich_outputs_from_inventory.py [--dry-run] """ import json, yaml, glob, os, sys BASE = "/home/naeel/terra/terraform" YAML_DIR = f"{BASE}/devops/profiles/test/generated/resources_yaml" INV_DIR = f"{BASE}/devops/profiles/test/generated/output_inventory" DOCS_DIR = f"{BASE}/docs/30_registry/resources" SECTION_MARKER = "## Реальные поля из облака" def flatten(obj, prefix=""): """Рекурсивно собирает dot-notation ключи из вложенного dict/list.""" keys = [] if isinstance(obj, dict): for k, v in obj.items(): full = f"{prefix}.{k}" if prefix else k keys.append(full) if isinstance(v, (dict, list)): keys.extend(flatten(v, full)) elif isinstance(obj, list): for i, v in enumerate(obj): full = f"{prefix}.{i}" if prefix else str(i) if isinstance(v, (dict, list)): keys.extend(flatten(v, full)) # скалярные элементы списка не добавляем — нет смысла return keys def best_instance(instances): """Выбираем лучший инстанс: running с данными > suspended с данными.""" candidates = [i for i in instances if i.get("out") or i.get("params")] if not candidates: return None running = [i for i in candidates if i.get("status") == "running"] return running[-1] if running else candidates[-1] def build_section(inst): params = inst.get("params") or {} out = inst.get("out") or {} vault = inst.get("vault_keys") or [] source = inst.get("source", "test") name = inst.get("name", "?") status = inst.get("status", "?") lines = [ f"{SECTION_MARKER} ({source} / {name} / {status})\n", "\n", "Поля ниже получены из реальных инстансов этого сервиса в облаке.\n", "Используйте их как готовые ключи для `state_out_flat` и `vault_secrets`.\n", ] if params: lines.append("\n### `state_params` ключи\n\n") for k in params.keys(): lines.append(f'- `state_params["{k}"]`\n') flat_out = flatten(out) if flat_out: lines.append("\n### `state_out_flat` ключи\n\n") for k in flat_out: lines.append(f'- `state_out_flat["{k}"]`\n') if vault: lines.append("\n### `vault_secrets` ключи\n\n") for k in vault: lines.append(f'- `vault_secrets["{k}"]`\n') return "".join(lines) def main(): dry_run = "--dry-run" in sys.argv # 1. service_id → name (из YAML) svc_to_name = {} for f in glob.glob(f"{YAML_DIR}/*.yaml"): try: d = yaml.safe_load(open(f)) sid = d.get("service_id") name = d.get("name") or d.get("service_short_name") if sid and name: svc_to_name[sid] = name except Exception: pass updated = 0 skipped = 0 for inv_file in sorted(glob.glob(f"{INV_DIR}/svc_*.json")): if "index" in inv_file: continue inv = json.load(open(inv_file)) svc_id = inv.get("serviceId") inst = best_instance(inv.get("instances", [])) if not inst: print(f" skip svc={svc_id}: нет данных") skipped += 1 continue name = svc_to_name.get(svc_id) if not name: print(f" skip svc={svc_id}: нет YAML маппинга") skipped += 1 continue out_path = f"{DOCS_DIR}/{name}_outputs.md" if not os.path.exists(out_path): print(f" skip svc={svc_id} ({name}): нет файла {name}_outputs.md") skipped += 1 continue new_section = build_section(inst) content = open(out_path).read() if SECTION_MARKER in content: idx = content.index(SECTION_MARKER) new_content = content[:idx] + new_section else: new_content = content.rstrip("\n") + "\n\n" + new_section if dry_run: print(f" dry-run svc={svc_id} ({name}): {out_path}") print(f" params={list((inst.get('params') or {}).keys())}") print(f" out_flat_count={len(flatten(inst.get('out') or {}))}") print(f" vault={inst.get('vault_keys', [])}") else: with open(out_path, "w") as f: f.write(new_content) print(f" updated svc={svc_id} ({name})") updated += 1 print(f"\nДone: {updated} updated, {skipped} skipped") if __name__ == "__main__": main()