v1.0.63: autotest namespace and duplicate prevention
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Архитектура app-autotest — полный документ
|
# Архитектура app-autotest — полный документ
|
||||||
|
|
||||||
v1.0.62, 27.07.2026
|
v1.0.63, 27.07.2026
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -350,6 +350,7 @@ def detect_endpoint(token):
|
|||||||
| 17 | v1.0.57 | Хрупкость финального состояния кнопки | Повторяемый код финализации статуса | `setFinishedState()` централизует финальный UI-состояние |
|
| 17 | v1.0.57 | Хрупкость финального состояния кнопки | Повторяемый код финализации статуса | `setFinishedState()` централизует финальный UI-состояние |
|
||||||
| 18 | v1.0.59 | После F5 список терял tracked-инстанс | `api_operations()` не добирал tracked-сирот обратно из трекера | `tracked_by_uid` + добавление отсутствующих tracked-инстансов |
|
| 18 | v1.0.59 | После F5 список терял tracked-инстанс | `api_operations()` не добирал tracked-сирот обратно из трекера | `tracked_by_uid` + добавление отсутствующих tracked-инстансов |
|
||||||
| 19 | v1.0.60 | CREATE не появлялся сразу в списке после OK | UI делал diff и мог не перерисовать список целиком | Полная перерисовка `inst-list` в `refreshInstances()` |
|
| 19 | v1.0.60 | CREATE не появлялся сразу в списке после OK | UI делал diff и мог не перерисовать список целиком | Полная перерисовка `inst-list` в `refreshInstances()` |
|
||||||
|
| 20 | v1.0.63 | Дубли и чужие инстансы | Принадлежность определялась не по namespace | `autotest-` prefix + уникализация `displayName` при CREATE |
|
||||||
|
|
||||||
### ✅ Закрыто в v1.0.56
|
### ✅ Закрыто в v1.0.56
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
# История разработки app-autotest
|
# История разработки app-autotest
|
||||||
|
|
||||||
|
## v1.0.63 (27.07.2026) — autotest namespace for instances
|
||||||
|
|
||||||
|
### Что изменилось
|
||||||
|
- В `site/templates/index.html` CREATE теперь генерирует `displayName` с префиксом `autotest-` и проверяет его на уникальность среди текущих инстансов.
|
||||||
|
- В `site/routes/api_test.py` backend тоже нормализует `displayName` с этим префиксом и, если нужно, добавляет суффикс для уникальности перед созданием в облаке.
|
||||||
|
- В `site/routes/main.py` список инстансов фильтруется по namespace `autotest-`, чтобы в UI попадали только тестовые инстансы приложения.
|
||||||
|
- В `DOCS/ARCHITECTURE.md` зафиксирован переход на namespace-based фильтрацию.
|
||||||
|
|
||||||
|
### Что это чинит
|
||||||
|
- Дубли `displayName` при CREATE.
|
||||||
|
- Чужие инстансы в списке.
|
||||||
|
- Зависимость от локального трекера как от основного признака принадлежности.
|
||||||
|
|
||||||
## v1.0.62 (27.07.2026) — immediate visual refresh after CREATE
|
## v1.0.62 (27.07.2026) — immediate visual refresh after CREATE
|
||||||
|
|
||||||
### Что изменилось
|
### Что изменилось
|
||||||
|
|||||||
+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.62"
|
VERSION = "1.0.63"
|
||||||
|
|
||||||
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")
|
||||||
|
|||||||
+30
-1
@@ -1,4 +1,5 @@
|
|||||||
from flask import Blueprint, current_app, jsonify, request
|
from flask import Blueprint, current_app, jsonify, request
|
||||||
|
import uuid
|
||||||
|
|
||||||
from api.http_client import HttpClient, detect_endpoint
|
from api.http_client import HttpClient, detect_endpoint
|
||||||
from operations.get_services import get_services, get_service_detail
|
from operations.get_services import get_services, get_service_detail
|
||||||
@@ -8,6 +9,33 @@ from operations.tracker import remove as tracker_remove
|
|||||||
|
|
||||||
bp = Blueprint("api_test", __name__)
|
bp = Blueprint("api_test", __name__)
|
||||||
|
|
||||||
|
AUTOTEST_PREFIX = "autotest-"
|
||||||
|
|
||||||
|
|
||||||
|
def _with_prefix(name):
|
||||||
|
name = (name or "").strip() or "instance"
|
||||||
|
return name if name.startswith(AUTOTEST_PREFIX) else f"{AUTOTEST_PREFIX}{name}"
|
||||||
|
|
||||||
|
|
||||||
|
def _unique_display_name(client, requested_name):
|
||||||
|
base_name = _with_prefix(requested_name)
|
||||||
|
try:
|
||||||
|
existing = {
|
||||||
|
i.get("displayName", "")
|
||||||
|
for i in get_instances(client)
|
||||||
|
if str(i.get("displayName", "")).startswith(AUTOTEST_PREFIX)
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
existing = set()
|
||||||
|
|
||||||
|
if base_name not in existing:
|
||||||
|
return base_name
|
||||||
|
|
||||||
|
while True:
|
||||||
|
candidate = f"{base_name}-{uuid.uuid4().hex[:6]}"
|
||||||
|
if candidate not in existing:
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
token = request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"]
|
token = request.cookies.get("token") or current_app.config["NUBES_API_TOKEN"]
|
||||||
@@ -107,6 +135,7 @@ def api_test():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if op_name == "create":
|
if op_name == "create":
|
||||||
|
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)
|
||||||
instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(resp.get("_location", ""))
|
instance_uid = resp.get("instanceUid") or _find_uid(resp) or _uid_from_location(resp.get("_location", ""))
|
||||||
@@ -135,7 +164,7 @@ def api_test():
|
|||||||
|
|
||||||
# фоном ждать завершения
|
# фоном ждать завершения
|
||||||
threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, True), daemon=True).start()
|
threading.Thread(target=_finish_op, args=(client, op_uid, instance_uid, svc_id, display_name, op_name, svc_op_id, True), 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})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if not instance_uid:
|
if not instance_uid:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from operations.tracker import list_all as tracker_list
|
|||||||
from runner import load_config
|
from runner import load_config
|
||||||
|
|
||||||
bp = Blueprint("main", __name__)
|
bp = Blueprint("main", __name__)
|
||||||
|
AUTOTEST_PREFIX = "autotest-"
|
||||||
|
|
||||||
|
|
||||||
def _mask(s):
|
def _mask(s):
|
||||||
@@ -143,6 +144,7 @@ def api_operations(svc_id):
|
|||||||
nubes_uids = {i["instanceUid"] for i in instances}
|
nubes_uids = {i["instanceUid"] for i in instances}
|
||||||
svc_instances = [i for i in instances
|
svc_instances = [i for i in instances
|
||||||
if i.get("instanceUid") in tracked_uids
|
if i.get("instanceUid") in tracked_uids
|
||||||
|
and str(i.get("displayName", "")).startswith(AUTOTEST_PREFIX)
|
||||||
and i.get("explainedStatus") not in ("deleted",)]
|
and i.get("explainedStatus") not in ("deleted",)]
|
||||||
# Если Nubes уже не отдаёт tracked-инстанс после reload, добавляем его из трекера.
|
# Если Nubes уже не отдаёт tracked-инстанс после reload, добавляем его из трекера.
|
||||||
for uid, t in tracked_by_uid.items():
|
for uid, t in tracked_by_uid.items():
|
||||||
|
|||||||
@@ -99,6 +99,7 @@
|
|||||||
<script>
|
<script>
|
||||||
let svcInstances=[], selectedInst=null, selectedOp=null, pollTimer=null;
|
let svcInstances=[], selectedInst=null, selectedOp=null, pollTimer=null;
|
||||||
const SVC_ID=1;
|
const SVC_ID=1;
|
||||||
|
const AUTOTEST_PREFIX='autotest-';
|
||||||
|
|
||||||
selectService(SVC_ID);
|
selectService(SVC_ID);
|
||||||
|
|
||||||
@@ -151,6 +152,18 @@ function startCreate(){
|
|||||||
showParams(18,'create');
|
showParams(18,'create');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeCreateDisplayName(){
|
||||||
|
const existing = new Set((svcInstances||[])
|
||||||
|
.map(i => i.displayName || '')
|
||||||
|
.filter(name => name.startsWith(AUTOTEST_PREFIX)));
|
||||||
|
const suffix = Date.now().toString(36);
|
||||||
|
let candidate = `${AUTOTEST_PREFIX}${suffix}`;
|
||||||
|
while(existing.has(candidate)){
|
||||||
|
candidate = `${AUTOTEST_PREFIX}${suffix}-${Math.random().toString(36).slice(2, 6)}`;
|
||||||
|
}
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
function runOp(opName,opId){
|
function runOp(opName,opId){
|
||||||
stopPoll();
|
stopPoll();
|
||||||
selectedOp={opId,opName,svcId:SVC_ID};
|
selectedOp={opId,opName,svcId:SVC_ID};
|
||||||
@@ -176,7 +189,7 @@ function showParams(opId,opName){
|
|||||||
|
|
||||||
fetch('/api/params/'+opId).then(r=>r.json()).then(params=>{
|
fetch('/api/params/'+opId).then(r=>r.json()).then(params=>{
|
||||||
const form=document.getElementById('params-form');
|
const form=document.getElementById('params-form');
|
||||||
const displayName=opName==='create'?('autotest-1-'+Date.now().toString(36)):'';
|
const displayName=opName==='create'?makeCreateDisplayName():'';
|
||||||
form.innerHTML=(opName==='create'?`<div class="param-row"><label class="req">displayName</label><input type="text" id="param-displayname" value="${displayName}"></div>`:'')
|
form.innerHTML=(opName==='create'?`<div class="param-row"><label class="req">displayName</label><input type="text" id="param-displayname" value="${displayName}"></div>`:'')
|
||||||
+ params.map(p=>{
|
+ params.map(p=>{
|
||||||
const req=p.isRequired;
|
const req=p.isRequired;
|
||||||
|
|||||||
Reference in New Issue
Block a user