From ed6612fb3c0b797aec3b9ab6cf9144a6144312e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Wed, 29 Jul 2026 13:17:45 +0400 Subject: [PATCH] =?UTF-8?q?v1.1.25:=20full=20Terraform=20parity=20?= =?UTF-8?q?=E2=80=94=20normalize,=20resolveRefSvc,=20send=20ALL=20params,?= =?UTF-8?q?=2030min=20timeout,=20svc=20in=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/app.py | 2 +- site/routes/api_test.py | 150 ++++++++++++++++++++++++++++------------ 2 files changed, 105 insertions(+), 47 deletions(-) diff --git a/site/app.py b/site/app.py index a7dc2df..e4e9c84 100644 --- a/site/app.py +++ b/site/app.py @@ -18,7 +18,7 @@ from routes.api import bp as api_bp from routes.api_test import bp as api_test_bp # Версия — меняется при КАЖДОМ изменении кода. Показывается в топбаре UI. -VERSION = "1.1.24" +VERSION = "1.1.25" app = Flask(__name__, template_folder="templates", static_folder="static") app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc") diff --git a/site/routes/api_test.py b/site/routes/api_test.py index 64a2feb..1bbcf58 100644 --- a/site/routes/api_test.py +++ b/site/routes/api_test.py @@ -199,47 +199,8 @@ def api_test(): print(f"[TRACKER ERROR] add failed: {e}", flush=True) traceback.print_exc() - # Отправка пользовательских параметров - for pid, pval in params.items(): - if pval: # только непустые - client.post("/instanceOperationCfsParams", - {"instanceOperationUid": op_uid, "svcOperationCfsParamId": int(pid), "paramValue": str(pval)}) - - # Досылка required-параметров (шаг 6 Terraform) - # Берём из РЕАЛЬНОЙ операции — не из шаблона (там нет paramValue) - try: - op_det = client.get(f"/instanceOperations/{op_uid}?fields=cfsParams") - for p in op_det.get("instanceOperation", {}).get("cfsParams", []): - pid_str = str(p["svcOperationCfsParamId"]) - if p.get("isRequired") and pid_str not in params: - val = p.get("paramValue") or p.get("defaultValue") - dd = p.get("dataDescriptor") - if val is not None and val != "": - client.post("/instanceOperationCfsParams", { - "instanceOperationUid": op_uid, - "svcOperationCfsParamId": p["svcOperationCfsParamId"], - "paramValue": str(val) - }) - elif isinstance(dd, dict): - sub_obj = {} - for sk, sv in dd.items(): - sd = sv.get("paramValue") or sv.get("defaultValue", "") - if sv.get("isRequired") and sd: - sub_obj[sk] = sd - if sub_obj: - client.post("/instanceOperationCfsParams", { - "instanceOperationUid": op_uid, - "svcOperationCfsParamId": p["svcOperationCfsParamId"], - "paramValue": json.dumps(sub_obj) - }) - except Exception: - pass - - # Валидация перед запуском - try: - client.get(f"/instanceOperations/{op_uid}/validate-cfs") - except Exception: - pass + # Отправка параметров (шаги 3-7 Terraform) + _send_params_terraform(client, op_uid, params) client.post(f"/instanceOperations/{op_uid}/run") @@ -266,9 +227,7 @@ def api_test(): op_uid = _find_uid(op_resp) or _uid_from_location(op_resp.get("_location", "")) if not op_uid: return jsonify({"status": "FAIL", "error": "Не удалось получить opUid"}), 500 - for pid, pval in params.items(): - client.post("/instanceOperationCfsParams", - {"instanceOperationUid": op_uid, "svcOperationCfsParamId": int(pid), "paramValue": str(pval)}) + _send_params_terraform(client, op_uid, params) client.post(f"/instanceOperations/{op_uid}/run") # фоном ждать завершения @@ -291,6 +250,105 @@ _op_results = {} _MAX_OP_RESULTS = 500 +def _normalize_value(val, data_type, data_descriptor): + """normalizeUniversalValueV6 — Terraform-equivalent. + Пустые значения нормализуются по типу: integer→"0", boolean→"false", + array→"[]", map/json→"{}", map-fixed→сборка из sub-param defaults.""" + val = (val or "").strip() + if val.lower() == "null" or val == '""': + val = "" + dt = (data_type or "").lower() + # map-fixed/map с DataDescriptor: пусто или {} → собрать из sub-param defaults + if ("map" in dt) and isinstance(data_descriptor, dict) and data_descriptor: + if not val or val == "{}": + sub_obj = {} + for sk, sv in data_descriptor.items(): + sd = sv.get("defaultValue") + sub_obj[sk] = str(sd) if sd is not None else "" + return json.dumps(sub_obj) + return val + if val: + return val + # Пустое — нормализовать по типу + if "array" in dt: + return "[]" + if "map" in dt or "json" in dt: + return "{}" + if "integer" in dt or "int" in dt: + return "0" + if "boolean" in dt or "bool" in dt: + return "false" + return val # пустая строка — отправляем как есть (Terraform тоже шлёт) + + +def _resolve_ref_svc(client, cfs_params, params): + """resolveRefSvcParamValues — Terraform step 4. + Для параметров с refSvcId в dataDescriptor: если значение пустое — + подставить UUID существующего инстанса этого сервиса.""" + instances = None # lazy load + for p in cfs_params: + dd = p.get("dataDescriptor") + if not isinstance(dd, dict): + continue + pid = str(p["svcOperationCfsParamId"]) + user_val = json.loads(params.get(pid, "{}")) if pid in params else {} + for sk, sv in dd.items(): + ref_id = sv.get("refSvcId") + if not ref_id: + continue + cur = user_val.get(sk, "") + if cur and cur != '""': + continue # уже заполнено + if instances is None: + try: + instances = get_instances(client) + except Exception: + return + match = next((i for i in instances if i.get("serviceId") == ref_id and i.get("explainedStatus") not in ("deleted",)), None) + if match: + user_val[sk] = match["instanceUid"] + if user_val and pid in params: + params[pid] = json.dumps(user_val) + + +def _send_params_terraform(client, op_uid, params): + """Terraform steps 3-7: cfsParams → resolveRefSvc → send user → send all unsent → validate. + Работает для CREATE и non-CREATE одинаково.""" + # Шаг 3: GET cfsParams реальной операции + op_det = client.get(f"/instanceOperations/{op_uid}?fields=cfsParams") + cfs_params = op_det.get("instanceOperation", {}).get("cfsParams", []) + # Шаг 4: resolveRefSvcParamValues + _resolve_ref_svc(client, cfs_params, params) + # Шаг 5: отправить ВСЕ пользовательские params (включая пустые!) + sent = set() + for pid, pval in params.items(): + client.post("/instanceOperationCfsParams", { + "instanceOperationUid": op_uid, + "svcOperationCfsParamId": int(pid), + "paramValue": str(pval) + }) + sent.add(int(pid)) + # Шаг 6: дослать ВСЕ неотправленные params из cfsParams + for p in cfs_params: + pid = p["svcOperationCfsParamId"] + if pid in sent: + continue + # nil-check: paramValue может быть None (отличаем от "") + val = p.get("paramValue") + if val is None: + val = p.get("defaultValue") + if val is None: + val = "" + val = _normalize_value(str(val), p.get("dataType", ""), p.get("dataDescriptor")) + client.post("/instanceOperationCfsParams", { + "instanceOperationUid": op_uid, + "svcOperationCfsParamId": pid, + "paramValue": val + }) + # Шаг 7: validate-cfs (ошибка НЕ глотается — возвращается вызывающему) + client.get(f"/instanceOperations/{op_uid}/validate-cfs") + + def _cleanup_op_results(): """Удалить старые записи: старше 1 часа или сверх лимита.""" import time @@ -321,11 +379,11 @@ def _finish_op(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_ import time _cleanup_op_results() # очистить старые записи перед добавлением новой t0 = time.time() - deadline = t0 + 300 + deadline = t0 + 1800 while time.time() < deadline: try: try: - data = client.get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages") + data = client.get(f"/instanceOperations/{op_uid}?fields=dtFinish,isSuccessful,errorLog,isInProgress,duration,stages,svc") except Exception: time.sleep(5) continue