v1.0.74: version bump before push
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Архитектура app-autotest — полный документ
|
# Архитектура app-autotest — полный документ
|
||||||
|
|
||||||
v1.0.71, 27.07.2026
|
v1.0.74, 27.07.2026
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -10,6 +10,12 @@ Flask-приложение для автотестов операций серв
|
|||||||
Деплой: Nubes pythonk8s (gunicorn), тестовый стенд `atest.pythonk8s.dev.nubes.ru`.
|
Деплой: Nubes pythonk8s (gunicorn), тестовый стенд `atest.pythonk8s.dev.nubes.ru`.
|
||||||
Репозиторий: `https://gitea.services.ngcloud.ru/forcloud/app-autotest.git`.
|
Репозиторий: `https://gitea.services.ngcloud.ru/forcloud/app-autotest.git`.
|
||||||
|
|
||||||
|
Текущий срез потока операций:
|
||||||
|
- CREATE и обычные операции разделены в UI и backend.
|
||||||
|
- CREATE использует фиксированный префикс `autotest-`, non-create работает по выбранному инстансу.
|
||||||
|
- Финальный статус показывает статус, операцию, длительность и имя инстанса.
|
||||||
|
- Список autotest-инстансов строится cloud-first; tracker нужен только как короткий fallback после CREATE.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Структура файлов
|
## 2. Структура файлов
|
||||||
|
|||||||
@@ -1,5 +1,28 @@
|
|||||||
# История разработки app-autotest
|
# История разработки app-autotest
|
||||||
|
|
||||||
|
# v1.0.74 (27.07.2026) — version bump for push
|
||||||
|
|
||||||
|
### Что изменилось
|
||||||
|
- В `site/templates/index.html` create-операция отделена от обычных операций: CREATE использует фиксированный префикс `autotest-`, а non-create берёт имя выбранного инстанса.
|
||||||
|
- Из non-create ветки убран случайный fallback `tut` и общий create-style `displayName`.
|
||||||
|
- В `site/routes/api_test.py` backend больше не подставляет create-имя по умолчанию для обычных операций и возвращает `displayName` для финального статуса.
|
||||||
|
- В `site/routes/main.py` список autotest-инстансов теперь строится cloud-first без старой склейки через map по имени; tracker остался только как временный fallback, если cloud ещё не вернул новый инстанс.
|
||||||
|
|
||||||
|
### Что это чинит
|
||||||
|
- Путаницу между CREATE и обычными операциями.
|
||||||
|
- Ситуацию, когда в UI появлялся лишний `tut`.
|
||||||
|
- Дубли `running/creating` на одном autotest-инстансе.
|
||||||
|
- Неясный финальный статус, где было видно операцию, но не было понятно, над каким инстансом она выполнялась.
|
||||||
|
|
||||||
|
# v1.0.72 (27.07.2026) — service list keeps one autotest row per displayName
|
||||||
|
|
||||||
|
### Что изменилось
|
||||||
|
- В `site/routes/main.py` сервисный список теперь cloud-first и не склеивает разные строки в одну map.
|
||||||
|
- Tracker остаётся только как fallback, если cloud ещё не вернул конкретный autotest-инстанс.
|
||||||
|
|
||||||
|
### Что это чинит
|
||||||
|
- Дубли `running/creating` на одном и том же autotest-инстансе.
|
||||||
|
|
||||||
# v1.0.71 (27.07.2026) — finished status shows instance name
|
# v1.0.71 (27.07.2026) — finished status shows instance name
|
||||||
|
|
||||||
### Что изменилось
|
### Что изменилось
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ from routes.main import bp as main_bp
|
|||||||
from routes.api import bp as api_bp
|
from routes.api import bp as api_bp
|
||||||
from routes.api_test import bp as api_test_bp
|
from routes.api_test import bp as api_test_bp
|
||||||
|
|
||||||
VERSION = "1.0.71"
|
VERSION = "1.0.74"
|
||||||
|
|
||||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||||||
app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc")
|
app.config["NUBES_API_ENDPOINT"] = os.getenv("NUBES_API_ENDPOINT", "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc")
|
||||||
|
|||||||
@@ -129,12 +129,14 @@ def api_test():
|
|||||||
svc_op_id = data["svcOperationId"]
|
svc_op_id = data["svcOperationId"]
|
||||||
params = data.get("params", {})
|
params = data.get("params", {})
|
||||||
instance_uid = data.get("instanceUid")
|
instance_uid = data.get("instanceUid")
|
||||||
display_name = data.get("displayName", f"autotest-{svc_id}")
|
display_name = data.get("displayName") or ""
|
||||||
|
|
||||||
client = _client()
|
client = _client()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if op_name == "create":
|
if op_name == "create":
|
||||||
|
if not display_name:
|
||||||
|
display_name = f"autotest-{svc_id}"
|
||||||
display_name = _unique_display_name(client, display_name)
|
display_name = _unique_display_name(client, display_name)
|
||||||
payload = {"serviceId": svc_id, "displayName": display_name, "descr": ""}
|
payload = {"serviceId": svc_id, "displayName": display_name, "descr": ""}
|
||||||
resp = client.post("/instances", payload)
|
resp = client.post("/instances", payload)
|
||||||
@@ -190,8 +192,9 @@ def api_test():
|
|||||||
|
|
||||||
# фоном ждать завершения
|
# фоном ждать завершения
|
||||||
is_delete = (op_name == "delete")
|
is_delete = (op_name == "delete")
|
||||||
|
display_name = display_name or instance_uid or f"instance-{svc_id}"
|
||||||
threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, False, is_delete), daemon=True).start()
|
threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, False, is_delete), daemon=True).start()
|
||||||
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid})
|
return jsonify({"status": "RUNNING", "opUid": op_uid, "instanceUid": instance_uid, "displayName": display_name})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|||||||
+13
-17
@@ -156,39 +156,35 @@ def api_operations(svc_id):
|
|||||||
tracked_by_uid = {t["instanceUid"]: t for t in tracked if t["svcId"] == svc_id}
|
tracked_by_uid = {t["instanceUid"]: t for t in tracked if t["svcId"] == svc_id}
|
||||||
instances = get_instances(client)
|
instances = get_instances(client)
|
||||||
nubes_uids = {i["instanceUid"] for i in instances}
|
nubes_uids = {i["instanceUid"] for i in instances}
|
||||||
svc_instances_by_name = {}
|
|
||||||
|
|
||||||
def _store_instance(item):
|
svc_instances = []
|
||||||
display_name = str(item.get("displayName", "") or "")
|
cloud_names = set()
|
||||||
if not display_name.startswith(AUTOTEST_PREFIX):
|
|
||||||
return
|
|
||||||
existing = svc_instances_by_name.get(display_name)
|
|
||||||
if existing is None:
|
|
||||||
svc_instances_by_name[display_name] = item
|
|
||||||
return
|
|
||||||
if existing.get("status") == "creating" and item.get("status") != "creating":
|
|
||||||
svc_instances_by_name[display_name] = item
|
|
||||||
|
|
||||||
for i in instances:
|
for i in instances:
|
||||||
if not str(i.get("displayName", "")).startswith(AUTOTEST_PREFIX):
|
display_name = str(i.get("displayName", "") or "")
|
||||||
|
if not display_name.startswith(AUTOTEST_PREFIX):
|
||||||
continue
|
continue
|
||||||
if i.get("explainedStatus") in ("deleted",):
|
if i.get("explainedStatus") in ("deleted",):
|
||||||
continue
|
continue
|
||||||
item = dict(i)
|
item = dict(i)
|
||||||
item["status"] = _resolve_instance_status(item, tracked_by_uid.get(i.get("instanceUid")))
|
item["status"] = _resolve_instance_status(item, tracked_by_uid.get(i.get("instanceUid")))
|
||||||
_store_instance(item)
|
svc_instances.append(item)
|
||||||
# Если Nubes ещё не отдаёт только что созданный autotest-инстанс, добавляем его из трекера.
|
cloud_names.add(display_name)
|
||||||
|
|
||||||
|
# tracker — только временный fallback, если cloud ещё не отдал конкретный autotest-инстанс
|
||||||
for uid, t in tracked_by_uid.items():
|
for uid, t in tracked_by_uid.items():
|
||||||
if uid not in nubes_uids:
|
if uid not in nubes_uids:
|
||||||
_store_instance({
|
display_name = str(t.get("displayName", "") or "")
|
||||||
|
if display_name in cloud_names:
|
||||||
|
continue
|
||||||
|
svc_instances.append({
|
||||||
"instanceUid": uid,
|
"instanceUid": uid,
|
||||||
"displayName": t["displayName"],
|
"displayName": display_name,
|
||||||
"serviceId": svc_id,
|
"serviceId": svc_id,
|
||||||
"svc": detail.get("svc", ""),
|
"svc": detail.get("svc", ""),
|
||||||
"explainedStatus": "",
|
"explainedStatus": "",
|
||||||
"status": "creating",
|
"status": "creating",
|
||||||
})
|
})
|
||||||
svc_instances = list(svc_instances_by_name.values())
|
|
||||||
return jsonify({"svc": detail.get("svc", ""), "operations": ops, "instances": svc_instances})
|
return jsonify({"svc": detail.get("svc", ""), "operations": ops, "instances": svc_instances})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|||||||
@@ -263,10 +263,10 @@ function setRunningState(opName, displayName){
|
|||||||
|
|
||||||
async function executeOp(params){
|
async function executeOp(params){
|
||||||
stopPoll();
|
stopPoll();
|
||||||
// Захватить displayName ДО очистки формы
|
const isCreate=selectedOp?.opName==='create';
|
||||||
const displayNameInput=document.getElementById('param-displayname');
|
const displayNameInput=document.getElementById('param-displayname');
|
||||||
const displayNameSuffix=(displayNameInput?.value||'').trim() || 'tut';
|
const displayNameSuffix=(displayNameInput?.value||'').trim();
|
||||||
const displayName=`${AUTOTEST_PREFIX}${displayNameSuffix}`;
|
const displayName=isCreate ? `${AUTOTEST_PREFIX}${displayNameSuffix || makeCreateDisplayName()}` : findInstName();
|
||||||
document.getElementById('params-card').style.display='block';
|
document.getElementById('params-card').style.display='block';
|
||||||
document.getElementById('params-form').innerHTML='';
|
document.getElementById('params-form').innerHTML='';
|
||||||
const btn=document.getElementById('btn-test');
|
const btn=document.getElementById('btn-test');
|
||||||
@@ -284,13 +284,13 @@ async function executeOp(params){
|
|||||||
svcOperationId:selectedOp.opId,
|
svcOperationId:selectedOp.opId,
|
||||||
params,
|
params,
|
||||||
instanceUid:selectedInst||'',
|
instanceUid:selectedInst||'',
|
||||||
displayName
|
displayName:isCreate ? displayName : ''
|
||||||
})});
|
})});
|
||||||
const d=await r.json();
|
const d=await r.json();
|
||||||
if(d.status==='FAIL'){document.getElementById('test-status').innerHTML=` <span class="badge">FAIL</span> ${d.error||''}`;btn.disabled=false;return;}
|
if(d.status==='FAIL'){document.getElementById('test-status').innerHTML=` <span class="badge">FAIL</span> ${d.error||''}`;btn.disabled=false;return;}
|
||||||
// start polling
|
// start polling
|
||||||
const opUid=d.opUid;
|
const opUid=d.opUid;
|
||||||
const instanceName=d.displayName||displayName;
|
const instanceName=d.displayName||displayName||findInstName();
|
||||||
pollTimer=setInterval(async()=>{
|
pollTimer=setInterval(async()=>{
|
||||||
const sr=await fetch('/api/test/status/'+opUid);
|
const sr=await fetch('/api/test/status/'+opUid);
|
||||||
const sd=await sr.json();
|
const sd=await sr.json();
|
||||||
|
|||||||
Reference in New Issue
Block a user