Задача Егора 2ч.
This commit is contained in:
27
docs/API.md
27
docs/API.md
@@ -1190,16 +1190,20 @@ GET /api/workload/teachers?departmentId=1&startDate=2026-05-20&endDate=2026-06-0
|
||||
него используется нормализованный `domain`.
|
||||
|
||||
**Логика:**
|
||||
1. Создаёт временный HikariCP pool, ещё не доступный маршрутизатору.
|
||||
2. Открывает соединение и явно проверяет его готовность.
|
||||
3. Выполняет Flyway-валидацию и миграции tenant-БД.
|
||||
4. Читает актуальный `tenants-secret`, применяет только upsert запрошенного `domain` и
|
||||
1. До мутации разбирает и при необходимости полностью применяет текущую mounted-проекцию
|
||||
tenant-конфигурации как безопасный baseline; ошибка подготовки возвращает `503`.
|
||||
2. Создаёт временный HikariCP pool, ещё не доступный маршрутизатору.
|
||||
3. Открывает соединение и явно проверяет его готовность.
|
||||
4. Выполняет Flyway-валидацию и миграции tenant-БД.
|
||||
5. Читает актуальный `tenants-secret`, применяет только upsert запрошенного `domain` и
|
||||
выполняет условный `PUT` с прочитанным Kubernetes `resourceVersion`.
|
||||
5. При конфликте повторно читает Secret и заново применяет свою мутацию с ограниченным
|
||||
6. При конфликте повторно читает Secret и заново применяет свою мутацию с ограниченным
|
||||
retry/backoff; неизменившаяся конфигурация не записывается повторно.
|
||||
6. Одной атомарной публикацией заменяет связку `TenantConfig + DataSource`.
|
||||
7. Передаёт прежний pool на отложенное закрытие после завершения активных запросов
|
||||
7. Одной атомарной публикацией заменяет связку `TenantConfig + DataSource`.
|
||||
8. Передаёт прежний pool на отложенное закрытие после завершения активных запросов
|
||||
либо по истечении защитного таймаута.
|
||||
9. Возвращает внутри backend `TenantLifecycleMutationResult` с подтверждённой
|
||||
`TenantSecretUpdateReceipt` для согласования mounted-проекции.
|
||||
|
||||
Backend соединяется с Kubernetes API только через проверенный service-account CA и
|
||||
hostname verification. Если безопасно сохранить tenant-конфигурацию не удалось, операция
|
||||
@@ -1236,6 +1240,15 @@ HTTP-статусы: `400` для некорректного payload и `503` д
|
||||
компенсация также допускается только для подтверждённой версии и не затирает более новое
|
||||
изменение другого pod. Неизвестный `domain` возвращает `404`, lifecycle-ошибка — `503`.
|
||||
|
||||
Для `POST` и `DELETE` baseline готовится до API-мутации под общим lifecycle monitor.
|
||||
После успеха backend использует семантические `previousTenants` и `committedTenants` из
|
||||
`TenantSecretUpdateReceipt`. Fence создаётся только для реального изменения общего Secret
|
||||
(`persisted=true`, `changed=true`): известные старые и промежуточные снимки временно
|
||||
откладываются, ожидаемый committed-снимок применяется полностью. Persisted no-op и локальная
|
||||
операция fence не создают, а неизвестный merged snapshot другого pod синхронизируется сразу.
|
||||
SHA-256 файла подтверждается только после полного успешного sync; ошибки повторяются с
|
||||
экспоненциальной задержкой от 30 до 300 секунд.
|
||||
|
||||
### `POST /api/database/test`
|
||||
|
||||
Тест подключения к произвольной БД (без регистрации тенанта).
|
||||
|
||||
@@ -81,7 +81,7 @@ sequenceDiagram
|
||||
| `TenantRoutingDataSource` | Маршрутизирует запросы и атомарно публикует неизменяемый снимок `TenantConfig + DataSource` |
|
||||
| `TenantDataSourceConfig` | Загружает конфигурацию тенантов из JSON-файла, создаёт HikariCP пулы |
|
||||
| `TenantWebMvcConfig` | Регистрирует `TenantInterceptor` и `AuthorizationInterceptor` в MVC-слое |
|
||||
| `TenantConfigWatcher` | Периодически (каждые 30 сек) перечитывает `tenants.json`, синхронизирует тенантов |
|
||||
| `TenantConfigWatcher` | Периодически перечитывает `tenants.json`, сравнивает полный нормализованный снимок и повторяет неудачную синхронизацию с ограниченным backoff |
|
||||
| `TenantConfigStore` | Задаёт явные атомарные операции upsert/remove и условную компенсацию persisted-конфигурации |
|
||||
| `KubernetesTenantSecretUpdater` | Реализует `TenantConfigStore` через GET и условный PUT Kubernetes Secret по `resourceVersion`, проверяя TLS по service-account CA |
|
||||
| `TenantLifecycleService` | Сериализует prepare/validate/migrate/persist/swap/remove и выполняет компенсацию при отказе |
|
||||
@@ -128,10 +128,11 @@ sequenceDiagram
|
||||
|
||||
### Жизненный цикл тенанта
|
||||
|
||||
1. **Добавление или обновление через API:** `TenantLifecycleService` нормализует конфигурацию,
|
||||
создаёт непубликуемый HikariCP candidate, проверяет реальное соединение и выполняет
|
||||
Flyway. Затем `TenantConfigStore` читает актуальный Secret и применяет к нему только
|
||||
upsert запрошенного домена.
|
||||
1. **Добавление или обновление через API:** перед мутацией `TenantConfigWatcher` разбирает,
|
||||
нормализует и при необходимости полностью применяет текущую mounted-проекцию как baseline.
|
||||
Ошибка подготовки baseline прерывает API-операцию. Затем `TenantLifecycleService` создаёт
|
||||
непубликуемый HikariCP candidate, проверяет реальное соединение, выполняет Flyway, а
|
||||
`TenantConfigStore` читает актуальный Secret и применяет к нему только upsert домена.
|
||||
2. **Межподовая запись:** `KubernetesTenantSecretUpdater` выполняет `GET` текущего
|
||||
`tenants-secret`, нормализует и сортирует список, после чего отправляет полный объект
|
||||
условным `PUT` с прочитанным `metadata.resourceVersion`. При `409 Conflict` актуальное
|
||||
@@ -140,6 +141,9 @@ sequenceDiagram
|
||||
не отправляет лишний `PUT`.
|
||||
3. **Атомарная локальная публикация:** только после успешной или подтверждённой повторным
|
||||
чтением персистенции одной публикацией заменяется связка `TenantConfig + DataSource`.
|
||||
Успешный lifecycle возвращает внутренний `TenantLifecycleMutationResult` с конфигурацией
|
||||
и `TenantSecretUpdateReceipt`, содержащей признаки `persisted`/`changed`, а также
|
||||
семантические снимки `previousTenants` и `committedTenants`.
|
||||
4. **Безопасный отказ и компенсация:** ошибка credentials, соединения, Flyway или Secret
|
||||
закрывает candidate, не меняя действующий route. Компенсация прежним снимком разрешена
|
||||
только для подтверждённой записи и только пока текущий Secret сохраняет выданный ей
|
||||
@@ -151,7 +155,10 @@ sequenceDiagram
|
||||
а прежний Hikari pool закрывается после завершения активных подключений либо по истечении
|
||||
настраиваемого grace timeout.
|
||||
6. **Синхронизация подов:** `TenantConfigWatcher` каждые 30 секунд проверяет смонтированный
|
||||
`tenants.json` и применяет добавление/удаление под тем же локальным lifecycle-monitor.
|
||||
`tenants.json` и сравнивает все поля нормализованного `TenantConfig`: `name`, `domain`,
|
||||
`url`, `username` и `password`. Для нового или изменённого тенанта lifecycle без повторной
|
||||
записи Secret выполняет prepare → проверку соединения → Flyway → атомарный swap; отсутствующие
|
||||
домены удаляются под тем же локальным monitor.
|
||||
7. **Удаление:** `DELETE /api/database/tenants/{domain}` применяет к актуальному Secret только
|
||||
удаление указанного домена через тот же условный `PUT`, затем атомарно исключает tenant
|
||||
из маршрутизации и передаёт pool на drain. Для отказа локального удаления действуют те же
|
||||
@@ -171,13 +178,25 @@ monitor по-прежнему не считается межподовой бл
|
||||
цепочки и обязательную проверку hostname. Trust-all fallback отсутствует: ошибка CA или TLS
|
||||
завершает персистенцию безопасным отказом.
|
||||
|
||||
`TenantConfigWatcher` сравнивает содержимое смонтированного из Secret `tenants.json` по
|
||||
SHA-256, а не по `String.hashCode()`, чтобы изменение файла не пропускалось из-за
|
||||
32-битной коллизии. На текущем этапе watcher активирует новые домены и удаляет отсутствующие,
|
||||
но ещё не заменяет подключение существующего домена при изменении его URL или credentials.
|
||||
Полная синхронизация такого изменения относится к проблеме №13; до её выполнения новое
|
||||
значение сбрасывает readiness этого tenant в состояние ожидания, а pod не должен считаться
|
||||
готовым по старому подключению.
|
||||
`TenantConfigWatcher` использует SHA-256 содержимого `tenants.json` как идентификатор файловой
|
||||
ревизии, но решения о запаздывающих проекциях принимает по нормализованным семантическим
|
||||
снимкам `name`/`domain`/`url`/`username`/`password`. Хеш становится последним применённым
|
||||
только после успешного разбора и полного lifecycle sync. Ошибка сохраняет прежний хеш,
|
||||
снимает readiness и запускает экспоненциальные повторы через 30–300 секунд; другая неудачная
|
||||
файловая ревизия получает собственную попытку без ожидания backoff предыдущей.
|
||||
|
||||
После API-операции snapshot fence устанавливается только если `TenantSecretUpdateReceipt`
|
||||
подтверждает реальное общее изменение: `persisted=true` и `changed=true`. Снимок до записи
|
||||
и ранее ожидавшиеся committed-снимки временно считаются deferred, а новый committed-снимок —
|
||||
ожидаемым. Поэтому при быстрых мутациях H0 → H1 → H2 запаздывающие H0 и H1 не откатывают
|
||||
runtime, а H2 полностью синхронизируется и снимает fence. Неизвестный объединённый снимок,
|
||||
которого нет среди deferred-состояний, также немедленно проходит полный sync и не теряет
|
||||
изменение другого pod. Локальная персистенция (`persisted=false`) и подтверждённый no-op
|
||||
(`changed=false`) fence не создают.
|
||||
|
||||
Если ошибка readiness произошла уже после успешного swap, следующий watcher retry видит
|
||||
совпадающую активную конфигурацию, проверяет действующее соединение и восстанавливает
|
||||
readiness без повторной миграции и второго swap.
|
||||
|
||||
### Liveness и readiness
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── package.json # Dependency-free проверки frontend через node:test
|
||||
├── tests/
|
||||
│ └── dashboard-conflicts.test.mjs # Регрессии дат и состояний проверки конфликтов
|
||||
├── index.html # 🔐 Страница авторизации (общая)
|
||||
├── script.js # Логика авторизации
|
||||
├── style.css # Стили страницы авторизации
|
||||
@@ -34,6 +37,7 @@ frontend/
|
||||
│ ├── js/
|
||||
│ │ ├── main.js # Инициализация, маршрутизация, навигация
|
||||
│ │ ├── api.js # HTTP-обёртка (fetch + Authorization)
|
||||
│ │ ├── dashboard-conflicts.js # Чистые функции дат, загрузки и состояний Red Zone
|
||||
│ │ ├── utils.js # Утилиты
|
||||
│ │ ├── otel.js # OpenTelemetry (клиентская телеметрия, только прод)
|
||||
│ │ └── views/ # Модули представлений
|
||||
@@ -125,6 +129,7 @@ frontend/
|
||||
|
||||
| Tab | Описание | API |
|
||||
|-----|----------|-----|
|
||||
| `dashboard` | Сводные метрики и проверка конфликтов текущей недели с явным статусом полноты данных | `/api/departments`, `/api/classrooms`, `/api/groups`, `/api/users/teachers`, `/api/schedule/search`, `/api/admin/time-slots` |
|
||||
| `teacher-requests` | Очередь заявок кафедр на создание преподавателей с редактированием перед одобрением | `/api/teacher-requests`, `/api/departments` |
|
||||
| `users` | CRUD пользователей | `/api/users` |
|
||||
| `groups` | CRUD групп, мультифильтр списка по формам обучения, настройка 0/2/3 подгрупп для лабораторных и назначения графиков | `/api/groups`, `/api/subgroups` |
|
||||
@@ -139,6 +144,7 @@ frontend/
|
||||
|
||||
### Особенности админских вкладок
|
||||
|
||||
- Вкладка `dashboard` формирует date-only значения из локальных компонентов даты, а текущую неделю — от отдельного объекта понедельника до `понедельник + 6 дней`. Расписания кафедр загружаются независимо через `Promise.allSettled`: `COMPLETE` означает ответы всех кафедр, `PARTIAL` — только части, `NOT_RUN` — отсутствие пригодных ответов или кафедр. Зелёная карточка «Конфликты расписания не обнаружены» разрешена только для `COMPLETE` без найденных конфликтов; частичный результат всегда остаётся предупреждением, а полный отказ показывается как «Проверка не выполнена». Технические причины отказов в DOM не выводятся.
|
||||
- Вкладка `groups` загружает кафедры, специальности, профили, учебные годы и календарные графики. Список групп открывается через `/api/groups?includeArchived=true`, поэтому в таблице видны активные, будущие, завершившие обучение и архивные группы со статусом. Группа создаётся через `/api/groups` с `specialtyId` и `specialtyProfileId`, а модалка редактирования использует широкую сетку полей без внутреннего пустого скролла. Блок подгрупп использует `/api/subgroups` и `/api/groups/{id}/subgroups`, а блок назначений использует `/api/groups/{id}/calendar-assignments`. После назначения графика в таблице назначений сразу выводятся дисциплины графика, сгруппированные по номерам семестров. В селекты подгрупп и назначений попадают только группы с `active=true`.
|
||||
- Вкладка `teacher-requests` показывает pending-заявки кафедр на создание преподавателей. Администратор может скорректировать кафедру, логин, ФИО и должность, задать пароль минимум 8 символов, затем одобрить заявку через `/api/teacher-requests/{id}/approve` или отклонить её через `/api/teacher-requests/{id}/reject`. Для роли `ADMIN` счётчик pending-заявок выводится в пункте меню «Заявки» и рядом с заголовком страницы, чтобы очередь была видна без открытия вкладки.
|
||||
- Вкладка `department-workspace` в блоке преподавателей объединяет данные `/api/department/teachers` и `/api/workload/teachers`: каждый преподаватель показывается одной карточкой с должностью и нагрузкой за выбранный период, преподаватели без занятий получают нулевую нагрузку, а преподаватели из расписания добавляются без дублей. Если дата начала периода выбрана позже даты окончания, поле окончания очищается, а расчёт нагрузки ждёт корректный период.
|
||||
@@ -208,6 +214,19 @@ export const api = {
|
||||
|
||||
---
|
||||
|
||||
## Frontend-тесты
|
||||
|
||||
Регрессионные проверки frontend используют встроенный `node:test` без сторонних npm-зависимостей (Node.js 18+). Тесты `dashboard-conflicts.test.mjs` покрывают локальные даты `Europe/Moscow` в интервале 00:00–03:00, первые дни месяца, переход года, полную/частичную/не выполненную загрузку и запрет ложного зелёного статуса.
|
||||
|
||||
Команды выполняются из каталога `frontend/`:
|
||||
|
||||
```bash
|
||||
npm test # unit-тесты дат, загрузки кафедр и UI-состояний
|
||||
npm run check # синтаксис модулей дашборда + unit-тесты
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Аутентификация (Frontend)
|
||||
|
||||
### Страница входа (`/index.html`)
|
||||
|
||||
@@ -126,16 +126,24 @@ env:
|
||||
снимают readiness; резервная H2-БД не считается готовой tenant-БД.
|
||||
|
||||
При добавлении тенанта через API:
|
||||
1. `TenantLifecycleService` создаёт отдельный candidate pool, проверяет соединение и Flyway
|
||||
2. `KubernetesTenantSecretUpdater` выполняет `GET` актуального Secret и применяет к списку
|
||||
1. `TenantConfigWatcher` до мутации разбирает и при необходимости применяет текущую
|
||||
mounted-проекцию как безопасный baseline; ошибка подготовки прерывает операцию
|
||||
2. `TenantLifecycleService` создаёт отдельный candidate pool, проверяет соединение и Flyway
|
||||
3. `KubernetesTenantSecretUpdater` выполняет `GET` актуального Secret и применяет к списку
|
||||
только upsert запрошенного домена
|
||||
3. полный объект Secret отправляется условным `PUT` с текущим `metadata.resourceVersion`;
|
||||
4. полный объект Secret отправляется условным `PUT` с текущим `metadata.resourceVersion`;
|
||||
при `409 Conflict` выполняются повторное чтение и повторное применение своей мутации
|
||||
4. `TenantRoutingDataSource` атомарно публикует candidate только после успешной либо
|
||||
5. `TenantRoutingDataSource` атомарно публикует candidate только после успешной либо
|
||||
подтверждённой повторным чтением персистенции
|
||||
5. старый pool перестаёт принимать новые запросы и закрывается после drain/grace timeout
|
||||
6. `TenantConfigWatcher` на остальных pod видит обновлённый файл и каждые 30 секунд
|
||||
применяет добавление или удаление домена
|
||||
6. старый pool перестаёт принимать новые запросы и передаётся на закрытие после
|
||||
drain/grace timeout
|
||||
7. lifecycle возвращает `TenantLifecycleMutationResult` с `TenantSecretUpdateReceipt`;
|
||||
semantic fence регистрируется только для фактически записанного изменения
|
||||
(`persisted=true`, `changed=true`)
|
||||
8. `TenantConfigWatcher` на остальных pod видит обновлённый файл и каждые 30 секунд
|
||||
синхронизирует полный нормализованный `TenantConfig`, включая изменения `name`, `domain`,
|
||||
`url`, `username` и `password`; новый или изменённый pool проходит проверку соединения, Flyway
|
||||
и атомарный swap без повторной записи Secret
|
||||
|
||||
Удаление использует тот же persistence-first порядок и применяет к актуальному Secret только
|
||||
remove указанного домена. Число попыток записи ограничено тремя, задержка между повторами
|
||||
@@ -147,10 +155,17 @@ backend восстанавливает прежний снимок только
|
||||
Неопределённый сетевой результат сначала сверяется повторным `GET`; совпавшее целевое
|
||||
состояние принимается без квитанции, допускающей небезопасный автоматический откат.
|
||||
|
||||
Watcher пока не заменяет подключение существующего домена при изменении URL или credentials.
|
||||
Такое изменение сбрасывает readiness, но полное применение на другом pod относится к
|
||||
проблеме №13. До её исправления нельзя считать watcher механизмом полной синхронизации всех
|
||||
полей `TenantConfig`.
|
||||
Watcher подтверждает SHA-256 файловой ревизии только после полного успешного применения
|
||||
снимка. При ошибке прежний применённый хеш сохраняется, readiness снимается, а синхронизация
|
||||
повторяется с экспоненциальной задержкой от 30 до 300 секунд.
|
||||
|
||||
Защита от задержки kubelet основана не на raw hash, а на полном нормализованном снимке.
|
||||
Квитанция фактической persisted-мутации задаёт `previousTenants` и `committedTenants`:
|
||||
previous и промежуточные committed-снимки временно deferred, последний committed ожидается.
|
||||
Поэтому последовательность H0 → H1 → H2 не откатывается при доставке H0/H1, а H2 применяется.
|
||||
Неизвестный merged snapshot, не совпадающий с deferred-состояниями, применяется сразу.
|
||||
Persisted no-op и локальная операция fence не создают. Если sync оборвался после swap на
|
||||
публикации readiness, retry проверяет уже активное соединение и не выполняет второй swap.
|
||||
|
||||
Kubernetes-клиент доверяет только service-account CA и проверяет hostname API server.
|
||||
Поскольку реализация использует HTTP `GET` и `PUT`, минимальная Role должна разрешать только
|
||||
|
||||
Reference in New Issue
Block a user