feat: from_stands.py — конвертер STANDS YAML → polygon configs (37 сервисов)
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
"""
|
||||
from_stands.py — конвертер STANDS YAML → polygon service config.
|
||||
|
||||
Читает YAML из STANDS_DIR, генерирует конфиги в SERVICES_DIR.
|
||||
Запуск: python from_stands.py <STANDS_DIR> [SERVICES_DIR]
|
||||
"""
|
||||
|
||||
import html
|
||||
import os
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
# Расширение для YAML-файлов (может быть .yaml или .yml)
|
||||
YAML_EXTS = (".yaml", ".yml")
|
||||
|
||||
# Поля которые переносятся 1:1 из STANDS YAML
|
||||
TOP_LEVEL_KEYS = (
|
||||
"name", "service_id", "service_display_name",
|
||||
"service_short_name", "service_man",
|
||||
)
|
||||
|
||||
# Стандартные 6 операций инстанса
|
||||
INSTANCE_OPERATIONS = frozenset({
|
||||
"create", "modify", "delete", "suspend", "resume", "redeploy",
|
||||
})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Утилиты
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _ue(s):
|
||||
"""html.unescape строки."""
|
||||
if isinstance(s, str):
|
||||
return html.unescape(s)
|
||||
return s
|
||||
|
||||
|
||||
def _pluralize(word):
|
||||
"""Простейшая плюрализация: user→users, database→databases."""
|
||||
if word.endswith("s"):
|
||||
return word
|
||||
if word.endswith("y") and word[-2] not in "aeiou":
|
||||
return word[:-1] + "ies"
|
||||
return word + "s"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Конвертация параметров
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _convert_param(p, is_modifiable_default=False):
|
||||
"""Конвертирует один параметр STANDS → polygon cfsParam.
|
||||
|
||||
Возвращает dict с полями:
|
||||
svcOperationCfsParamId, svcOperationCfsParam, dataType,
|
||||
isRequired, defaultValue, valueList, isModifiable,
|
||||
dataDescriptor (если есть sub_params)
|
||||
"""
|
||||
dt = _ue(p.get("data_type", "string"))
|
||||
required = p.get("required", False)
|
||||
default = _ue(str(p.get("default", "")))
|
||||
is_modifiable = p.get("is_modifiable", is_modifiable_default)
|
||||
|
||||
out = {
|
||||
"svcOperationCfsParamId": p["id"],
|
||||
"svcOperationCfsParam": p["code"],
|
||||
"dataType": dt,
|
||||
"isRequired": required,
|
||||
"defaultValue": default,
|
||||
"isModifiable": is_modifiable,
|
||||
}
|
||||
|
||||
# valueList — может быть None, тогда не включаем (input, не select)
|
||||
vl = p.get("value_list")
|
||||
if vl is not None:
|
||||
out["valueList"] = [_ue(str(v)) for v in vl]
|
||||
|
||||
# refSvcId
|
||||
ref = p.get("refSvcId")
|
||||
if ref is not None:
|
||||
out["refSvcId"] = ref
|
||||
|
||||
# dataDescriptor из sub_params
|
||||
sub = p.get("sub_params")
|
||||
if sub:
|
||||
out["dataDescriptor"] = _convert_sub_params(sub)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _convert_sub_params(sub_params):
|
||||
"""Конвертирует sub_params → dataDescriptor.
|
||||
|
||||
Формат выхода:
|
||||
{code: {dataType, defaultValue, isRequired, valueList?, isModifiable?}}
|
||||
"""
|
||||
dd = {}
|
||||
for sp in sub_params:
|
||||
code = sp["code"]
|
||||
dt = _ue(sp.get("data_type", "string"))
|
||||
default = _ue(str(sp.get("default", "")))
|
||||
|
||||
entry = {
|
||||
"dataType": dt,
|
||||
"defaultValue": default,
|
||||
"isRequired": sp.get("required", False),
|
||||
"isModifiable": sp.get("is_modifiable", False),
|
||||
}
|
||||
|
||||
vl = sp.get("value_list")
|
||||
if vl is not None:
|
||||
entry["valueList"] = [_ue(str(v)) for v in vl]
|
||||
|
||||
# Рекурсия: sub_params внутри sub_params (редко, но возможно)
|
||||
subsub = sp.get("sub_params")
|
||||
if subsub:
|
||||
entry["dataDescriptor"] = _convert_sub_params(subsub)
|
||||
|
||||
dd[code] = entry
|
||||
|
||||
return dd
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Конвертация операций
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _convert_operations(operations):
|
||||
"""Конвертирует список операций STANDS → polygon.
|
||||
|
||||
Возвращает (ops_list, state_out_template):
|
||||
ops_list — список operation-диктов
|
||||
state_out_template — {plural: {}} для subresource create-операций
|
||||
"""
|
||||
ops_list = []
|
||||
state_out_template = {}
|
||||
|
||||
for op in operations:
|
||||
op_id = op["id"]
|
||||
op_name = op.get("name", "")
|
||||
op_kind = op.get("kind", "instance")
|
||||
op_action = op.get("action", "")
|
||||
|
||||
ops_list.append({
|
||||
"svcOperationId": op_id,
|
||||
"operation": op_name,
|
||||
"kind": op_kind,
|
||||
"action": op_action,
|
||||
})
|
||||
|
||||
# subresource create → добавить в state_out_template
|
||||
if op_kind == "subresource" and op_action == "create":
|
||||
# subresource имя из operation name: create_user → user
|
||||
sub_name = op_name.replace("create_", "")
|
||||
plural = _pluralize(sub_name)
|
||||
state_out_template[plural] = {}
|
||||
|
||||
return ops_list, state_out_template
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Конвертация одного сервиса
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def convert_service(stands_yaml_path):
|
||||
"""Читает STANDS YAML, возвращает polygon-конфиг как dict."""
|
||||
with open(stands_yaml_path, "r", encoding="utf-8") as f:
|
||||
raw = yaml.safe_load(f)
|
||||
|
||||
if not raw:
|
||||
return None
|
||||
|
||||
out = {}
|
||||
|
||||
# 1. Поля верхнего уровня (1:1)
|
||||
for key in TOP_LEVEL_KEYS:
|
||||
val = raw.get(key)
|
||||
if val is not None:
|
||||
if isinstance(val, str):
|
||||
val = _ue(val)
|
||||
out[key] = val
|
||||
|
||||
# 2. lifecycle
|
||||
lc = raw.get("lifecycle", {})
|
||||
if lc:
|
||||
out["lifecycle"] = {
|
||||
"suspendOnDestroyDefault": lc.get("suspend_on_destroy_default", False),
|
||||
"adoptExistingOnCreateDefault": lc.get("adopt_existing_on_create_default", False),
|
||||
}
|
||||
|
||||
# 3. Операции
|
||||
operations = raw.get("operations", [])
|
||||
ops_list, state_out_template = _convert_operations(operations)
|
||||
out["operations"] = ops_list
|
||||
out["stateOut"] = state_out_template
|
||||
|
||||
# 4. cfsParams — собираем из ВСЕХ операций, dedup по svcOperationCfsParamId
|
||||
cfs_params = {}
|
||||
cfs_params_by_op = {} # op_id → [param, ...]
|
||||
for op in operations:
|
||||
op_id = op["id"]
|
||||
params_for_op = []
|
||||
for p in op.get("params", []):
|
||||
pid = p["id"]
|
||||
if pid not in cfs_params:
|
||||
is_mod = p.get("is_modifiable", op.get("action") != "create")
|
||||
cfs_params[pid] = _convert_param(p, is_modifiable_default=is_mod)
|
||||
params_for_op.append(pid)
|
||||
cfs_params_by_op[op_id] = params_for_op
|
||||
|
||||
out["cfsParams"] = list(cfs_params.values())
|
||||
out["cfsParamsByOp"] = cfs_params_by_op
|
||||
|
||||
# 5. stateParams — дефолты из create-операции (включая map-fixed)
|
||||
state_params = {}
|
||||
create_op = None
|
||||
for op in operations:
|
||||
if op.get("action") == "create" and op.get("kind") == "instance":
|
||||
create_op = op
|
||||
break
|
||||
|
||||
if create_op:
|
||||
for p in create_op.get("params", []):
|
||||
code = p["code"]
|
||||
default = _ue(str(p.get("default", "")))
|
||||
dt = _ue(p.get("data_type", "string"))
|
||||
sub = p.get("sub_params")
|
||||
|
||||
if sub:
|
||||
# map-fixed / map с sub_params — генерируем JSON из defaults
|
||||
state_params[code] = _build_default_json(sub)
|
||||
else:
|
||||
state_params[code] = default
|
||||
|
||||
out["stateParams"] = state_params
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _build_default_json(sub_params):
|
||||
"""Строит JSON-строку из defaults sub_params."""
|
||||
parts = []
|
||||
for sp in sub_params:
|
||||
code = sp["code"]
|
||||
default = _ue(str(sp.get("default", "")))
|
||||
dt = _ue(sp.get("data_type", "string"))
|
||||
|
||||
subsub = sp.get("sub_params")
|
||||
if subsub:
|
||||
val = _build_default_json(subsub)
|
||||
elif "bool" in dt:
|
||||
val = default.lower() if default.lower() in ("true", "false") else "false"
|
||||
elif "int" in dt or "integer" in dt:
|
||||
try:
|
||||
int(default)
|
||||
val = default
|
||||
except (ValueError, TypeError):
|
||||
val = "0"
|
||||
else:
|
||||
# string — quoted
|
||||
escaped = default.replace('"', '\\"')
|
||||
val = f'"{escaped}"'
|
||||
|
||||
parts.append(f'"{code}": {val}')
|
||||
|
||||
return "{" + ", ".join(parts) + "}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Точка входа
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python from_stands.py <STANDS_DIR> [SERVICES_DIR]")
|
||||
print(" STANDS_DIR — путь к STANDS YAML (напр. ../STANDS/test/resources_yaml)")
|
||||
print(" SERVICES_DIR — путь для выхода (по умолчанию ./services)")
|
||||
sys.exit(1)
|
||||
|
||||
stands_dir = sys.argv[1]
|
||||
services_dir = sys.argv[2] if len(sys.argv) > 2 else os.path.join(os.path.dirname(__file__) or ".", "services")
|
||||
|
||||
if not os.path.isdir(stands_dir):
|
||||
print(f"ERROR: STANDS_DIR not found: {stands_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
os.makedirs(services_dir, exist_ok=True)
|
||||
|
||||
# Найти все YAML-файлы
|
||||
yaml_files = sorted(
|
||||
f for f in os.listdir(stands_dir)
|
||||
if f.lower().endswith(YAML_EXTS)
|
||||
)
|
||||
|
||||
if not yaml_files:
|
||||
print(f"No YAML files found in {stands_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Found {len(yaml_files)} YAML files in {stands_dir}")
|
||||
print(f"Output: {services_dir}/")
|
||||
print()
|
||||
|
||||
converted = 0
|
||||
errors = 0
|
||||
|
||||
for fname in yaml_files:
|
||||
src = os.path.join(stands_dir, fname)
|
||||
dst = os.path.join(services_dir, fname)
|
||||
|
||||
try:
|
||||
cfg = convert_service(src)
|
||||
if cfg is None:
|
||||
print(f" SKIP {fname}: empty YAML")
|
||||
continue
|
||||
|
||||
with open(dst, "w", encoding="utf-8") as f:
|
||||
yaml.dump(
|
||||
cfg, f,
|
||||
allow_unicode=True,
|
||||
default_flow_style=False,
|
||||
sort_keys=False,
|
||||
width=200,
|
||||
)
|
||||
|
||||
# Быстрая проверка результата
|
||||
svc_name = cfg.get("name", "?")
|
||||
ops_count = len(cfg.get("operations", []))
|
||||
params_count = len(cfg.get("cfsParams", []))
|
||||
state_out = list(cfg.get("stateOut", {}).keys())
|
||||
subresources = " +".join(state_out) if state_out else "—"
|
||||
|
||||
print(f" OK {fname:40s} svc={svc_name:20s} ops={ops_count} params={params_count} stateOut={subresources}")
|
||||
converted += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f" FAIL {fname}: {e}")
|
||||
errors += 1
|
||||
|
||||
print()
|
||||
print(f"Done: {converted} converted, {errors} errors")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user