Compare commits
9 Commits
a78a93f2f0
...
87f3d98621
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87f3d98621 | ||
|
|
a759155b15 | ||
|
|
0180b450b3 | ||
|
|
c3f477788e | ||
|
|
7b5053f42a | ||
|
|
6b4cd5b6c8 | ||
|
|
8bb7fe97eb | ||
| db0218f4ca | |||
| da7c90fa6a |
@@ -6,10 +6,7 @@ on:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
concurrency:
|
||||
group: magistr-production
|
||||
cancel-in-progress: false
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: gitea.zuev.company
|
||||
@@ -28,20 +25,24 @@ jobs:
|
||||
- name: Получить исходный код
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Проверить запуск CI-контейнеров без bind mount
|
||||
run: bash scripts/test-run-ci-container.sh
|
||||
|
||||
- name: Backend unit и component tests
|
||||
run: >-
|
||||
docker run --rm
|
||||
-v "${{ github.workspace }}/backend:/workspace"
|
||||
-w /workspace
|
||||
"${{ env.MAVEN_IMAGE }}"
|
||||
run: |
|
||||
bash scripts/run-ci-container.sh \
|
||||
backend \
|
||||
"${{ env.MAVEN_IMAGE }}" \
|
||||
-- \
|
||||
mvn -Dapi.version=1.44 '-Dtest=!*IntegrationTest' test
|
||||
|
||||
- name: Frontend static и unit tests
|
||||
run: >-
|
||||
docker run --rm
|
||||
-v "${{ github.workspace }}/frontend:/workspace"
|
||||
-w /workspace
|
||||
"${{ env.NODE_IMAGE }}"
|
||||
run: |
|
||||
bash scripts/run-ci-container.sh \
|
||||
frontend \
|
||||
"${{ env.NODE_IMAGE }}" \
|
||||
--copy backend /backend \
|
||||
-- \
|
||||
sh -c "npm ci && npm run check"
|
||||
|
||||
- name: Проверить Docker Compose
|
||||
@@ -144,11 +145,14 @@ jobs:
|
||||
name: Проверить опубликованные образы
|
||||
needs: [build-and-push-backend, build-and-push-frontend]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 35
|
||||
env:
|
||||
BACKEND_REF: ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }}@${{ needs.build-and-push-backend.outputs.digest }}
|
||||
FRONTEND_REF: ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }}@${{ needs.build-and-push-frontend.outputs.digest }}
|
||||
steps:
|
||||
- name: Получить исходный код
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Войти в Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
@@ -157,19 +161,23 @@ jobs:
|
||||
password: ${{ secrets.ZUEV_TOKEN }}
|
||||
|
||||
- name: Проверить backend на HIGH и CRITICAL уязвимости
|
||||
run: >-
|
||||
docker run --rm
|
||||
-v "$HOME/.docker:/root/.docker:ro"
|
||||
"${{ env.TRIVY_IMAGE }}"
|
||||
image --scanners vuln --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed
|
||||
run: |
|
||||
bash scripts/run-ci-container.sh \
|
||||
"$HOME/.docker" \
|
||||
"${{ env.TRIVY_IMAGE }}" \
|
||||
--env DOCKER_CONFIG=/workspace \
|
||||
-- \
|
||||
image --timeout 15m --scanners vuln --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \
|
||||
"$BACKEND_REF"
|
||||
|
||||
- name: Проверить frontend на HIGH и CRITICAL уязвимости
|
||||
run: >-
|
||||
docker run --rm
|
||||
-v "$HOME/.docker:/root/.docker:ro"
|
||||
"${{ env.TRIVY_IMAGE }}"
|
||||
image --scanners vuln --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed
|
||||
run: |
|
||||
bash scripts/run-ci-container.sh \
|
||||
"$HOME/.docker" \
|
||||
"${{ env.TRIVY_IMAGE }}" \
|
||||
--env DOCKER_CONFIG=/workspace \
|
||||
-- \
|
||||
image --timeout 15m --scanners vuln --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \
|
||||
"$FRONTEND_REF"
|
||||
|
||||
deploy-to-k8s:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Обзор проекта
|
||||
|
||||
Проект представляет собой систему управления университетским расписанием.
|
||||
- **Backend**: Java 17, Spring Boot 3.2.5 (Мультитенантная архитектура: отдельная БД для каждого клиента)
|
||||
- **Backend**: Java 17, Spring Boot 3.5.16 (Мультитенантная архитектура: отдельная БД для каждого клиента)
|
||||
- **Frontend**: Vanilla JavaScript + HTML/CSS (без фреймворков)
|
||||
- **Database**: PostgreSQL (множество БД, управляются через Flyway)
|
||||
- **Локальный URL**: localhost:80
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Журнал исправления проблем
|
||||
|
||||
Обновлено: 2026-07-19 (Europe/Moscow).
|
||||
Обновлено: 2026-07-29 (Europe/Moscow).
|
||||
|
||||
## Область и неизменяемые данные
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
| `V2__subgroups_active_unique_name.sql` | `68f5525a50ddba4f8800a63cab3cd0f84779e301c4a1f29a6e0399f97fbdb30b` |
|
||||
|
||||
Текущая единая baseline-миграция: `V1__init.sql`, SHA-256
|
||||
`fffedf66e8bf7d25d2d8c1b1a6146b45dd5b7e52233d05a68de8bf62c817f7a7`.
|
||||
`a984718777f1b4695868549e3c805c8b22edbb82fbe0b8731563adf5f11e1742`.
|
||||
Упоминания V2–V7 в исторических записях ниже описывают последовательность разработки до
|
||||
консолидации и не означают наличие этих файлов в текущем дереве.
|
||||
|
||||
@@ -1047,21 +1047,41 @@
|
||||
- Полностью закрыты и проверены 32 пункта. Для № 1 остаются только операторские ротация,
|
||||
provisioning, rollout и очистка истории, которые исходный prompt прямо запрещает
|
||||
выполнять без отдельных production-полномочий.
|
||||
- Полный backend PostgreSQL/Testcontainers-прогон: 277 тестов, 0 failures, 0 errors,
|
||||
- Полный backend PostgreSQL/Testcontainers-прогон: 300 тестов, 0 failures, 0 errors,
|
||||
0 skipped. Четыре frontend test-suite, `npm run check`, Compose validation,
|
||||
Workflow YAML, artifact/deploy shell-тесты и `git diff --check` прошли.
|
||||
- `kubectl kustomize ../k8s`, production secret-scan, синтаксис deploy-скрипта и расширенный
|
||||
artifact-scan прошли. В production-манифестах отсутствуют mutable `latest` и `:main`.
|
||||
- Каталог Flyway содержит ровно `V1__init.sql`; V2–V4 и другие дополнительные миграции
|
||||
отсутствуют. Финальный SHA-256 V1:
|
||||
`fffedf66e8bf7d25d2d8c1b1a6146b45dd5b7e52233d05a68de8bf62c817f7a7`.
|
||||
`a984718777f1b4695868549e3c805c8b22edbb82fbe0b8731563adf5f11e1742`.
|
||||
- Исправление нумерации недель календарного графика также перенесено из промежуточной V2
|
||||
непосредственно в V1. Чистое применение baseline и формула недель
|
||||
`понедельник–воскресенье` подтверждены
|
||||
`AcademicCalendarWeekNumberBaselineIntegrationTest`.
|
||||
|
||||
## Точка продолжения
|
||||
|
||||
Текущий этап: **реализация и проверка всех разрешённых изменений завершены**.
|
||||
Текущий этап: **репозиторная реализация завершена, production подготовлен к первому
|
||||
контролируемому rollout**.
|
||||
|
||||
Оставшееся операторское действие владельца:
|
||||
29 июля 2026 года по отдельному разрешению владельца выполнена production-подготовка:
|
||||
|
||||
1. при готовности production-полномочий выполнить `docs/SECURITY_RUNBOOK.md`: ротацию,
|
||||
provisioning, rollout и очистку истории для № 1. Эти действия не выполнялись
|
||||
автоматически и не требуются для завершения репозиторной реализации.
|
||||
- старый backend остановлен на `0` реплик;
|
||||
- обе tenant-БД (`magistr`, `n8n`) доступны, их схемы `public` пусты, у application role
|
||||
есть права `CREATE` на схему и БД;
|
||||
- в `app-secret` без вывода значения добавлен новый случайный `JWT_SECRET`;
|
||||
- из действующей конфигурации без вывода значений созданы переходные `tenants-secret` и
|
||||
`otel-postgres-secret`;
|
||||
- push в `main` больше не запускает production deploy: job разрешён только для ручного
|
||||
`workflow_dispatch`;
|
||||
- server-side dry-run выявил immutable `roleRef` старого RoleBinding; в
|
||||
`../k8s/rbac.yaml` новый binding переименован в `backend-tenant-secret-binding`, после
|
||||
чего `kubectl apply --dry-run=server -k ../k8s` прошёл.
|
||||
|
||||
Оставшиеся операторские действия:
|
||||
|
||||
1. Закоммитить и отправить изменения в `main`, дождаться checks, сборки и Trivy scan.
|
||||
2. Выполнить первый rollout по digest через `../k8s/deploy.sh apply`.
|
||||
3. После проверки удалить старые ConfigMap/RBAC-объекты, ротировать перенесённые DB/OTel
|
||||
credentials и выполнить согласованную очистку истории по `docs/SECURITY_RUNBOOK.md`.
|
||||
|
||||
100
STARTUP_GUIDE.md
100
STARTUP_GUIDE.md
@@ -58,53 +58,97 @@
|
||||
Полный локальный сброс БД выполняется командой `docker compose down -v`, но она удалит
|
||||
все локальные данные. Обычная остановка без удаления данных: `docker compose down`.
|
||||
|
||||
## 2. Первый запуск в production
|
||||
## 2. Обновление существующего production
|
||||
|
||||
1. Подготовьте K3s/Kubernetes-кластер и убедитесь, что `kubectl get nodes` работает.
|
||||
PostgreSQL в текущие манифесты не входит: заранее создайте доступную из кластера БД для
|
||||
каждого университета. Пользователь БД должен иметь права на создание и изменение схемы —
|
||||
Flyway применит `V1__init.sql` автоматически.
|
||||
Push в `main` автоматически выполняет проверки, собирает и сканирует образы, а затем
|
||||
обновляет backend/frontend в Kubernetes по проверенным immutable digest. Новые файлы
|
||||
`../k8s` CI не применяет: первый rollout и изменения самих манифестов выполняются
|
||||
оператором через `../k8s/deploy.sh`. Gitea 1.25 игнорирует `environment` и `concurrency`,
|
||||
поэтому они не используются как production-защита.
|
||||
|
||||
2. Настройте DNS для `magistr.zuev.company` и остальных tenant-доменов на публичный
|
||||
reverse proxy/Ingress. В текущем `../k8s/ingress.yaml` нет секции TLS: для HTTPS нужно
|
||||
настроить внешний Caddy с сертификатом либо добавить cert-manager/TLS в кластер.
|
||||
1. Подготовьте секреты по [`docs/SECURITY_RUNBOOK.md`](docs/SECURITY_RUNBOOK.md):
|
||||
|
||||
3. Создайте namespace и четыре обязательных Secret по
|
||||
[`docs/SECURITY_RUNBOOK.md`](docs/SECURITY_RUNBOOK.md):
|
||||
- `app-secret`: новый случайный `JWT_SECRET` не короче 32 байт, `POSTGRES_USER`,
|
||||
`POSTGRES_PASSWORD`;
|
||||
- `tenants-secret`: новый `tenants.json` с актуальными JDBC URL и ротированными паролями;
|
||||
- `otel-postgres-secret`: восемь обязательных параметров подключений Collector;
|
||||
- `gitea-registry`: доступ K3s к registry.
|
||||
|
||||
- `app-secret`: `JWT_SECRET`, `POSTGRES_USER`, `POSTGRES_PASSWORD`;
|
||||
- `tenants-secret`: файл `tenants.json`;
|
||||
- `otel-postgres-secret`: подключения Collector к БД;
|
||||
- `gitea-registry`: доступ к приватным Docker-образам.
|
||||
Для `magistr.zuev.company` нужен tenant с `"domain": "magistr"`, для
|
||||
`n8n.zuev.company` — `"domain": "n8n"`. Секреты не сохраняйте в Git или CI-логах.
|
||||
|
||||
Для `magistr.zuev.company` запись в `tenants.json` должна иметь `"domain": "magistr"`
|
||||
и реальный JDBC URL. Не сохраняйте значения Secret в Git или логах.
|
||||
2. Выполните локальные проверки, затем отправьте изменения в `main`:
|
||||
|
||||
4. В Gitea Actions добавьте секреты `ZUEV_TOKEN` (доступ к registry) и
|
||||
`KUBECONFIG_DATA` (kubeconfig в Base64). Push в `main` соберёт, проверит и опубликует
|
||||
образы. При самом первом запуске автоматический deploy может ещё не найти Deployment —
|
||||
возьмите два опубликованных digest из CI/registry и выполните bootstrap вручную:
|
||||
```bash
|
||||
K8S_DIR=../k8s bash scripts/check-production-secrets.sh
|
||||
K8S_DIR=../k8s bash scripts/check-artifact-pinning.sh
|
||||
bash scripts/test-deploy-images.sh
|
||||
kubectl kustomize ../k8s >/dev/null
|
||||
```
|
||||
|
||||
Дождитесь успешных checks, сборки и Trivy scan. Скопируйте точные backend/frontend
|
||||
ссылки вида `image@sha256:...` из CI/registry.
|
||||
|
||||
3. Объявите техническое окно и временно закройте публичный доступ во внешнем Caddy. Затем
|
||||
остановите backend:
|
||||
|
||||
```bash
|
||||
kubectl scale deployment/backend -n magistr --replicas=0
|
||||
```
|
||||
|
||||
Полностью пересоздайте **каждую** PostgreSQL tenant-БД, указанную в новом
|
||||
`tenants.json`. Недостаточно удалить `flyway_schema_history` или отдельные таблицы:
|
||||
миграции V2–V7 объединены в новую `V1__init.sql`, поэтому старая БД получит checksum
|
||||
error, а непустая БД без истории может быть ошибочно принята Flyway за baseline.
|
||||
Пользователь БД должен уметь создавать схему, функции, триггеры и расширения
|
||||
`pgcrypto`/`btree_gist`.
|
||||
|
||||
4. С рабочей станции с актуальным каталогом `../k8s` выполните первый rollout вручную:
|
||||
|
||||
```bash
|
||||
export BACKEND_IMAGE_REF='gitea.zuev.company/zuev/magistr-backend@sha256:<digest>'
|
||||
export FRONTEND_IMAGE_REF='gitea.zuev.company/zuev/magistr-frontend@sha256:<digest>'
|
||||
K8S_DIR=../k8s bash scripts/check-production-secrets.sh
|
||||
K8S_DIR=../k8s bash scripts/check-artifact-pinning.sh
|
||||
kubectl kustomize ../k8s >/dev/null
|
||||
bash ../k8s/deploy.sh apply
|
||||
```
|
||||
|
||||
5. Проверьте результат:
|
||||
Скрипт применит ConfigMap, RBAC, Secret mount, probes, OTel и Ingress, поднимет две
|
||||
реплики backend и дождётся Flyway/readiness. После этого следующие image-only
|
||||
обновления автоматически разворачиваются успешным workflow после push в `main`.
|
||||
`workflow_dispatch` остаётся дополнительным способом повторного запуска, но не
|
||||
требуется для обычной поставки.
|
||||
|
||||
5. Перед открытием публичного доступа проверьте:
|
||||
|
||||
```bash
|
||||
bash ../k8s/deploy.sh status
|
||||
kubectl rollout status deployment/backend -n magistr --timeout=300s
|
||||
kubectl rollout status deployment/frontend -n magistr --timeout=120s
|
||||
kubectl get pods -n magistr
|
||||
kubectl logs -n magistr -l app=backend --tail=200
|
||||
curl -fsS https://magistr.zuev.company/
|
||||
curl -fsSI https://magistr.zuev.company/
|
||||
```
|
||||
|
||||
После первого входа немедленно смените/отключите все тестовые пароли из `V1__init.sql`.
|
||||
Следующие push в `main` CI/CD сможет развёртывать автоматически.
|
||||
Проверьте в браузере login → reload → refresh → logout для каждого tenant-домена.
|
||||
Refresh-cookie должна иметь `HttpOnly`, `Secure`, `SameSite=Lax`, path `/api/auth`;
|
||||
access JWT не должен находиться в Local/Session Storage. До снятия технического окна
|
||||
смените или отключите все демонстрационные учётные записи из `V1__init.sql`.
|
||||
|
||||
6. После проверки удалите старый `ConfigMap/tenants-config`, отзовите старые DB/OTel
|
||||
пароли и верните обычный доступ через Caddy. В дальнейшем image-only изменения
|
||||
автоматически разворачиваются после push в `main`, но любые изменения `../k8s`
|
||||
по-прежнему нужно применять отдельно через `../k8s/deploy.sh`.
|
||||
|
||||
### Важные ограничения rollback
|
||||
|
||||
- Смена `JWT_SECRET` и сброс БД завершат все старые пользовательские сессии.
|
||||
- Readiness требует доступности и успешной миграции всех tenant-БД; одна старая или
|
||||
недоступная БД остановит rollout.
|
||||
- Автоматический rollback возвращает только два image reference. Он не откатывает БД,
|
||||
Secret, ConfigMap или RBAC; после применения новой V1 безопаснее исправлять проблему
|
||||
вперёд либо снова пересоздавать БД под старую версию.
|
||||
- CI не запускает Testcontainers integration tests: полный PostgreSQL-прогон нужно
|
||||
повторить вручную, если после зафиксированных 300 успешных тестов код ещё менялся.
|
||||
- `TRUSTED_PROXY_CIDRS=10.42.0.0/16` соответствует текущей pod-сети K3s. При смене
|
||||
cluster CIDR его нужно обновить, иначе rate limit увидит неверные клиентские IP.
|
||||
|
||||
## 3. Что могу выполнить я
|
||||
|
||||
|
||||
@@ -16,7 +16,10 @@ RUN mvn org.apache.maven.plugins:maven-dependency-plugin:3.6.1:copy \
|
||||
|
||||
FROM eclipse-temurin:17-jre-alpine@sha256:02320dd4ce20e243dfb915c686089cf9315c763084fafbb12d5c9993aee18b57
|
||||
|
||||
# Best practice: run as a non-root user
|
||||
# Устанавливаем исправления безопасности, опубликованные после сборки базового образа.
|
||||
RUN apk upgrade --no-cache
|
||||
|
||||
# Запускаем приложение от непривилегированного пользователя.
|
||||
RUN addgroup -S spring && adduser -S spring -G spring
|
||||
USER spring:spring
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.5</version>
|
||||
<version>3.5.16</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<postgresql.version>42.7.12</postgresql.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -44,6 +45,10 @@
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-database-postgresql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
|
||||
@@ -731,12 +731,31 @@ CREATE TABLE IF NOT EXISTS academic_calendar_days (
|
||||
CREATE INDEX IF NOT EXISTS idx_calendar_days_lookup
|
||||
ON academic_calendar_days(calendar_id, course_number, date);
|
||||
|
||||
CREATE OR REPLACE FUNCTION calculate_academic_calendar_week_number(
|
||||
academic_year_start DATE,
|
||||
calendar_date DATE
|
||||
)
|
||||
RETURNS INTEGER
|
||||
LANGUAGE SQL
|
||||
IMMUTABLE
|
||||
STRICT
|
||||
AS $$
|
||||
SELECT (
|
||||
(
|
||||
calendar_date
|
||||
- academic_year_start
|
||||
+ EXTRACT(ISODOW FROM academic_year_start)::INTEGER
|
||||
- 1
|
||||
) / 7
|
||||
) + 1
|
||||
$$;
|
||||
|
||||
INSERT INTO academic_calendar_days (calendar_id, course_number, date, week_number, day_of_week, activity_type_id)
|
||||
SELECT
|
||||
calendar.id,
|
||||
course.course_number,
|
||||
days.date::DATE,
|
||||
((days.date::DATE - ay.start_date) / 7) + 1,
|
||||
calculate_academic_calendar_week_number(ay.start_date, days.date::DATE),
|
||||
EXTRACT(ISODOW FROM days.date::DATE)::INT,
|
||||
activity.id
|
||||
FROM academic_calendars calendar
|
||||
@@ -1262,6 +1281,8 @@ COMMENT ON COLUMN academic_calendar_days.date IS 'Дата';
|
||||
COMMENT ON COLUMN academic_calendar_days.week_number IS 'Номер недели в учебном году';
|
||||
COMMENT ON COLUMN academic_calendar_days.day_of_week IS 'День недели ISO';
|
||||
COMMENT ON COLUMN academic_calendar_days.activity_type_id IS 'ID кода активности';
|
||||
COMMENT ON FUNCTION calculate_academic_calendar_week_number(DATE, DATE)
|
||||
IS 'Номер недели календарного учебного графика по периодам понедельник–воскресенье';
|
||||
|
||||
COMMENT ON COLUMN academic_calendar_subjects.calendar_id IS 'ID календарного учебного графика';
|
||||
COMMENT ON COLUMN academic_calendar_subjects.semester_number IS 'Номер учебного семестра внутри графика: 1, 2, 3 ...';
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
-- Недели календарного учебного графика выровнены по ISO-неделе:
|
||||
-- понедельник — первый день, воскресенье — последний.
|
||||
UPDATE academic_calendar_days AS calendar_day
|
||||
SET week_number = (
|
||||
(
|
||||
calendar_day.date
|
||||
- academic_year.start_date
|
||||
+ EXTRACT(ISODOW FROM academic_year.start_date)::INTEGER
|
||||
- 1
|
||||
) / 7
|
||||
) + 1
|
||||
FROM academic_calendars AS calendar
|
||||
JOIN academic_years AS academic_year
|
||||
ON academic_year.id = calendar.academic_year_id
|
||||
WHERE calendar_day.calendar_id = calendar.id
|
||||
AND calendar_day.week_number IS DISTINCT FROM (
|
||||
(
|
||||
calendar_day.date
|
||||
- academic_year.start_date
|
||||
+ EXTRACT(ISODOW FROM academic_year.start_date)::INTEGER
|
||||
- 1
|
||||
) / 7
|
||||
) + 1;
|
||||
@@ -15,6 +15,7 @@ import com.magistr.app.repository.DepartmentRepository;
|
||||
import com.magistr.app.repository.TeacherCreationRequestRepository;
|
||||
import com.magistr.app.repository.TeacherDepartmentAssignmentRepository;
|
||||
import com.magistr.app.repository.UserRepository;
|
||||
import com.magistr.app.service.BusinessTimeService;
|
||||
import com.magistr.app.service.ScheduleQueryService;
|
||||
import com.magistr.app.service.SubjectImportService;
|
||||
import com.magistr.app.service.TeacherDepartmentService;
|
||||
@@ -22,7 +23,10 @@ import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -34,6 +38,12 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
class DepartmentWorkspaceControllerTest {
|
||||
|
||||
private static final LocalDate TODAY = LocalDate.of(2026, 7, 29);
|
||||
private static final BusinessTimeService BUSINESS_TIME = new BusinessTimeService(
|
||||
Clock.fixed(Instant.parse("2026-07-29T09:00:00Z"), ZoneOffset.UTC),
|
||||
ZoneOffset.UTC
|
||||
);
|
||||
|
||||
@AfterEach
|
||||
void clearAuthContext() {
|
||||
AuthContext.clear();
|
||||
@@ -71,7 +81,7 @@ class DepartmentWorkspaceControllerTest {
|
||||
TeacherDepartmentService teacherDepartmentService = mock(TeacherDepartmentService.class);
|
||||
User assignedTeacher = user(10L, "assigned", "Петров Препод Петрович", 2L);
|
||||
User directTeacher = user(11L, "direct", "Препод Тест Тестович", 1L);
|
||||
when(teacherDepartmentService.findTeachersForDepartmentAtDate(1L, LocalDate.now()))
|
||||
when(teacherDepartmentService.findTeachersForDepartmentAtDate(1L, TODAY))
|
||||
.thenReturn(List.of(directTeacher, assignedTeacher));
|
||||
DepartmentWorkspaceController controller = new DepartmentWorkspaceController(
|
||||
mock(SubjectRepository.class),
|
||||
@@ -82,7 +92,8 @@ class DepartmentWorkspaceControllerTest {
|
||||
mock(TeacherCreationRequestRepository.class),
|
||||
mock(ScheduleQueryService.class),
|
||||
teacherDepartmentService,
|
||||
mock(SubjectImportService.class)
|
||||
mock(SubjectImportService.class),
|
||||
BUSINESS_TIME
|
||||
);
|
||||
|
||||
List<UserResponse> teachers = controller.getTeachers(1L);
|
||||
|
||||
@@ -9,12 +9,15 @@ import com.magistr.app.model.User;
|
||||
import com.magistr.app.repository.SubjectRepository;
|
||||
import com.magistr.app.repository.TeacherSubjectRepository;
|
||||
import com.magistr.app.repository.UserRepository;
|
||||
import com.magistr.app.service.BusinessTimeService;
|
||||
import com.magistr.app.service.TeacherDepartmentService;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -25,6 +28,11 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
class TeacherSubjectControllerTest {
|
||||
|
||||
private static final BusinessTimeService BUSINESS_TIME = new BusinessTimeService(
|
||||
Clock.fixed(Instant.parse("2026-07-29T09:00:00Z"), ZoneOffset.UTC),
|
||||
ZoneOffset.UTC
|
||||
);
|
||||
|
||||
@AfterEach
|
||||
void clearAuthContext() {
|
||||
AuthContext.clear();
|
||||
@@ -46,13 +54,14 @@ class TeacherSubjectControllerTest {
|
||||
when(teacherDepartmentService.hasAssignmentAtDate(
|
||||
teacher.getId(),
|
||||
1L,
|
||||
LocalDate.now()
|
||||
BUSINESS_TIME.today()
|
||||
)).thenReturn(true);
|
||||
TeacherSubjectController controller = new TeacherSubjectController(
|
||||
teacherSubjectRepository,
|
||||
userRepository,
|
||||
subjectRepository,
|
||||
teacherDepartmentService
|
||||
teacherDepartmentService,
|
||||
BUSINESS_TIME
|
||||
);
|
||||
AuthContext.setCurrentUser(new AuthenticatedUser(1L, "кафедра", Role.DEPARTMENT, 1L));
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.magistr.app.migration;
|
||||
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.Date;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Testcontainers
|
||||
@DisplayName("Baseline нумерации недель календарного графика")
|
||||
class AcademicCalendarWeekNumberBaselineIntegrationTest {
|
||||
|
||||
@Container
|
||||
static final PostgreSQLContainer<?> POSTGRES =
|
||||
new PostgreSQLContainer<>(com.magistr.app.testing.TestContainerImages.POSTGRES);
|
||||
|
||||
@Test
|
||||
@DisplayName("V1 считает недели по периодам понедельник–воскресенье")
|
||||
void baselineAlignsWeeksToMonday() throws SQLException {
|
||||
Flyway flyway = Flyway.configure()
|
||||
.dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
|
||||
.locations("classpath:db/migration")
|
||||
.cleanDisabled(false)
|
||||
.load();
|
||||
|
||||
flyway.clean();
|
||||
flyway.migrate();
|
||||
|
||||
assertThat(flyway.info().current()).isNotNull();
|
||||
assertThat(flyway.info().current().getVersion().getVersion()).isEqualTo("1");
|
||||
|
||||
try (Connection connection = POSTGRES.createConnection("")) {
|
||||
assertThat(calculateWeek(connection, LocalDate.of(2026, 9, 1), LocalDate.of(2026, 9, 1)))
|
||||
.isEqualTo(1);
|
||||
assertThat(calculateWeek(connection, LocalDate.of(2026, 9, 1), LocalDate.of(2026, 9, 6)))
|
||||
.isEqualTo(1);
|
||||
assertThat(calculateWeek(connection, LocalDate.of(2026, 9, 1), LocalDate.of(2026, 9, 7)))
|
||||
.isEqualTo(2);
|
||||
assertThat(calculateWeek(connection, LocalDate.of(2027, 9, 1), LocalDate.of(2027, 9, 6)))
|
||||
.isEqualTo(2);
|
||||
assertThat(calculateWeek(connection, LocalDate.of(2025, 9, 1), LocalDate.of(2025, 9, 8)))
|
||||
.isEqualTo(2);
|
||||
}
|
||||
}
|
||||
|
||||
private int calculateWeek(Connection connection,
|
||||
LocalDate academicYearStart,
|
||||
LocalDate calendarDate) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT calculate_academic_calendar_week_number(?, ?)"
|
||||
)) {
|
||||
statement.setDate(1, Date.valueOf(academicYearStart));
|
||||
statement.setDate(2, Date.valueOf(calendarDate));
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
assertThat(resultSet.next()).isTrue();
|
||||
return resultSet.getInt(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
package com.magistr.app.migration;
|
||||
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.flywaydb.core.api.MigrationVersion;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.Date;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Testcontainers
|
||||
@DisplayName("Миграция нумерации недель календарного графика")
|
||||
class AcademicCalendarWeekNumberMigrationIntegrationTest {
|
||||
|
||||
@Container
|
||||
static final PostgreSQLContainer<?> POSTGRES =
|
||||
new PostgreSQLContainer<>(com.magistr.app.testing.TestContainerImages.POSTGRES);
|
||||
|
||||
@Test
|
||||
@DisplayName("V2 переносит первый понедельник после неполной недели в неделю 2")
|
||||
void migrationAlignsWeeksToMondayFor2026And2027() throws SQLException {
|
||||
Flyway baseline = flyway("1");
|
||||
baseline.clean();
|
||||
baseline.migrate();
|
||||
|
||||
long calendar2026;
|
||||
long calendar2027;
|
||||
try (Connection connection = POSTGRES.createConnection("")) {
|
||||
SeedReferences references = loadSeedReferences(connection);
|
||||
|
||||
long year2026 = insertAcademicYear(
|
||||
connection,
|
||||
"2026-2027",
|
||||
LocalDate.of(2026, 9, 1),
|
||||
LocalDate.of(2027, 6, 30)
|
||||
);
|
||||
calendar2026 = insertCalendar(connection, year2026, references, "График 2026-2027");
|
||||
insertCalendarDay(connection, calendar2026, references.activityTypeId(),
|
||||
LocalDate.of(2026, 9, 1), 1, 2);
|
||||
insertCalendarDay(connection, calendar2026, references.activityTypeId(),
|
||||
LocalDate.of(2026, 9, 7), 1, 1);
|
||||
|
||||
long year2027 = insertAcademicYear(
|
||||
connection,
|
||||
"2027-2028",
|
||||
LocalDate.of(2027, 9, 1),
|
||||
LocalDate.of(2028, 6, 30)
|
||||
);
|
||||
calendar2027 = insertCalendar(connection, year2027, references, "График 2027-2028");
|
||||
insertCalendarDay(connection, calendar2027, references.activityTypeId(),
|
||||
LocalDate.of(2027, 9, 1), 1, 3);
|
||||
insertCalendarDay(connection, calendar2027, references.activityTypeId(),
|
||||
LocalDate.of(2027, 9, 6), 1, 1);
|
||||
}
|
||||
|
||||
Flyway latest = latestFlyway();
|
||||
latest.migrate();
|
||||
|
||||
assertThat(latest.info().current()).isNotNull();
|
||||
assertThat(latest.info().current().getVersion().getVersion()).isEqualTo("2");
|
||||
try (Connection connection = POSTGRES.createConnection("")) {
|
||||
assertThat(findWeekNumber(connection, calendar2026, LocalDate.of(2026, 9, 1))).isEqualTo(1);
|
||||
assertThat(findWeekNumber(connection, calendar2026, LocalDate.of(2026, 9, 7))).isEqualTo(2);
|
||||
assertThat(findWeekNumber(connection, calendar2027, LocalDate.of(2027, 9, 1))).isEqualTo(1);
|
||||
assertThat(findWeekNumber(connection, calendar2027, LocalDate.of(2027, 9, 6))).isEqualTo(2);
|
||||
}
|
||||
}
|
||||
|
||||
private Flyway flyway(String targetVersion) {
|
||||
return Flyway.configure()
|
||||
.dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
|
||||
.locations("classpath:db/migration")
|
||||
.target(MigrationVersion.fromVersion(targetVersion))
|
||||
.cleanDisabled(false)
|
||||
.load();
|
||||
}
|
||||
|
||||
private Flyway latestFlyway() {
|
||||
return Flyway.configure()
|
||||
.dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
|
||||
.locations("classpath:db/migration")
|
||||
.cleanDisabled(false)
|
||||
.load();
|
||||
}
|
||||
|
||||
private SeedReferences loadSeedReferences(Connection connection) throws SQLException {
|
||||
long specialtyId = queryLong(connection, "SELECT id FROM specialties ORDER BY id LIMIT 1");
|
||||
long profileId = queryLong(
|
||||
connection,
|
||||
"SELECT id FROM specialty_profiles WHERE specialty_id = ? ORDER BY id LIMIT 1",
|
||||
specialtyId
|
||||
);
|
||||
long studyFormId = queryLong(connection, "SELECT id FROM education_forms ORDER BY id LIMIT 1");
|
||||
long activityTypeId = queryLong(
|
||||
connection,
|
||||
"SELECT id FROM academic_calendar_activity_types WHERE code = 'Т'"
|
||||
);
|
||||
return new SeedReferences(specialtyId, profileId, studyFormId, activityTypeId);
|
||||
}
|
||||
|
||||
private long insertAcademicYear(Connection connection,
|
||||
String title,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
INSERT INTO academic_years (title, start_date, end_date)
|
||||
VALUES (?, ?, ?)
|
||||
RETURNING id
|
||||
""")) {
|
||||
statement.setString(1, title);
|
||||
statement.setDate(2, Date.valueOf(startDate));
|
||||
statement.setDate(3, Date.valueOf(endDate));
|
||||
return requiredLong(statement);
|
||||
}
|
||||
}
|
||||
|
||||
private long insertCalendar(Connection connection,
|
||||
long academicYearId,
|
||||
SeedReferences references,
|
||||
String title) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
INSERT INTO academic_calendars (
|
||||
title,
|
||||
academic_year_id,
|
||||
specialty_id,
|
||||
specialty_profile_id,
|
||||
study_form_id,
|
||||
course_count
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, 1)
|
||||
RETURNING id
|
||||
""")) {
|
||||
statement.setString(1, title);
|
||||
statement.setLong(2, academicYearId);
|
||||
statement.setLong(3, references.specialtyId());
|
||||
statement.setLong(4, references.profileId());
|
||||
statement.setLong(5, references.studyFormId());
|
||||
return requiredLong(statement);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertCalendarDay(Connection connection,
|
||||
long calendarId,
|
||||
long activityTypeId,
|
||||
LocalDate date,
|
||||
int weekNumber,
|
||||
int dayOfWeek) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
INSERT INTO academic_calendar_days (
|
||||
calendar_id,
|
||||
course_number,
|
||||
date,
|
||||
week_number,
|
||||
day_of_week,
|
||||
activity_type_id
|
||||
)
|
||||
VALUES (?, 1, ?, ?, ?, ?)
|
||||
""")) {
|
||||
statement.setLong(1, calendarId);
|
||||
statement.setDate(2, Date.valueOf(date));
|
||||
statement.setInt(3, weekNumber);
|
||||
statement.setInt(4, dayOfWeek);
|
||||
statement.setLong(5, activityTypeId);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private int findWeekNumber(Connection connection,
|
||||
long calendarId,
|
||||
LocalDate date) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
SELECT week_number
|
||||
FROM academic_calendar_days
|
||||
WHERE calendar_id = ?
|
||||
AND date = ?
|
||||
""")) {
|
||||
statement.setLong(1, calendarId);
|
||||
statement.setDate(2, Date.valueOf(date));
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
assertThat(resultSet.next()).isTrue();
|
||||
return resultSet.getInt(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long queryLong(Connection connection, String sql, Object... arguments) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
for (int index = 0; index < arguments.length; index++) {
|
||||
statement.setObject(index + 1, arguments[index]);
|
||||
}
|
||||
return requiredLong(statement);
|
||||
}
|
||||
}
|
||||
|
||||
private long requiredLong(PreparedStatement statement) throws SQLException {
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
assertThat(resultSet.next()).isTrue();
|
||||
return resultSet.getLong(1);
|
||||
}
|
||||
}
|
||||
|
||||
private record SeedReferences(
|
||||
long specialtyId,
|
||||
long profileId,
|
||||
long studyFormId,
|
||||
long activityTypeId
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
graph TD
|
||||
Client["🌐 Браузер"] -->|HTTPS| Caddy["Caddy Proxy"]
|
||||
Caddy -->|:80| Frontend["Frontend<br/>(Apache httpd + строгий CSP)"]
|
||||
Caddy -->|/api/*| Backend["Backend<br/>(Spring Boot 3.2.5)"]
|
||||
Caddy -->|/api/*| Backend["Backend<br/>(Spring Boot 3.5.16)"]
|
||||
|
||||
Backend --> TenantRouter{"TenantRoutingDataSource"}
|
||||
TenantRouter -->|swsu.zuev.company| DB1["PostgreSQL<br/>swsu_db"]
|
||||
@@ -27,7 +27,7 @@ graph TD
|
||||
- **JS-модули:** Vanilla JavaScript с ES6 Modules (`import`/`export`)
|
||||
- **Browser security:** same-origin runtime-ресурсы и CSP без `unsafe-inline`/`unsafe-eval`
|
||||
|
||||
### Backend (Spring Boot 3.2.5)
|
||||
### Backend (Spring Boot 3.5.16)
|
||||
- **Тип:** REST API сервер
|
||||
- **Язык:** Java 17
|
||||
- **Порт:** 8080 (внутренний)
|
||||
|
||||
@@ -913,21 +913,21 @@ lifecycle ресурсов, эффективная сетка целевого
|
||||
2. Формат имени: `V{номер}__{описание}.sql` (напр. `V1__init.sql`, `V2__add_departments.sql`)
|
||||
3. **ЗАПРЕЩЕНО** изменять уже закоммиченные файлы миграций — это сломает контрольные суммы Flyway. Исключение допускается только по прямой просьбе пользователя и при полном сбросе tenant-БД.
|
||||
4. Flyway запускается **программно** при первом обращении к БД тенанта (`TenantConfigWatcher.initDatabaseForTenant()`)
|
||||
5. Настройка `baselineOnMigrate=true` — если в БД уже есть данные, Flyway начнёт с baseline
|
||||
5. Настройка `baselineOnMigrate=true` — непустая БД без истории будет помечена baseline и
|
||||
`V1` не выполнится; поэтому текущую консолидированную V1 применяют только к полностью
|
||||
пустой tenant-схеме
|
||||
|
||||
### Текущие миграции
|
||||
|
||||
| Файл | Описание |
|
||||
|------|----------|
|
||||
| `V1__init.sql` | Полная baseline-схема: справочники, роли, refresh-сессии JWT, PostgreSQL rate limit и аудит входа, lifecycle-поля, история кафедр, календарные графики, динамическое расписание, точечные изменения с переносом даты, seed, CHECK/UNIQUE/GiST-ограничения, конкурентно безопасные триггеры и комментарии |
|
||||
| `V2__align_academic_calendar_weeks_to_monday.sql` | Перенумерация сохранённых дней календарного графика по периодам `понедельник–воскресенье`, чтобы неполная первая неделя не заполнялась датами следующей недели |
|
||||
| `V1__init.sql` | Полная baseline-схема: справочники, роли, refresh-сессии JWT, PostgreSQL rate limit и аудит входа, lifecycle-поля, история кафедр, календарные графики с нумерацией недель `понедельник–воскресенье`, динамическое расписание, точечные изменения с переносом даты, seed, CHECK/UNIQUE/GiST-ограничения, конкурентно безопасные триггеры и комментарии |
|
||||
|
||||
### Этап разработки
|
||||
|
||||
По прямому решению владельца проекта прежние разработческие миграции V2–V7 были объединены
|
||||
в baseline `V1`. Новая миграция `V2__align_academic_calendar_weeks_to_monday.sql` создана
|
||||
после фиксации baseline и накатывается поверх существующих tenant-БД без изменения
|
||||
контрольной суммы V1.
|
||||
По прямому решению владельца проекта разработческие миграции V2–V7 объединены в baseline
|
||||
`V1`. Правильная нумерация недель календарного графика также входит непосредственно в V1.
|
||||
Перед применением этой редакции требуется полностью пустая tenant-схема.
|
||||
|
||||
### Полный сброс БД (локально)
|
||||
|
||||
|
||||
@@ -86,7 +86,9 @@ Backend собирается через multi-stage сборку Maven:
|
||||
1. Этап сборки: `maven:3.9.9-eclipse-temurin-17@sha256:f58d59b...` → `mvn package`
|
||||
2. OpenTelemetry Java Agent `2.28.1` загружается как фиксированный Maven-артефакт и
|
||||
проверяется по закреплённому SHA-256
|
||||
3. Этап запуска: `eclipse-temurin:17-jre-alpine@sha256:02320dd4...` → `java -jar app.jar`
|
||||
3. Этап запуска: `eclipse-temurin:17-jre-alpine@sha256:02320dd4...`; перед добавлением
|
||||
непривилегированного пользователя устанавливаются доступные исправления Alpine через
|
||||
`apk upgrade --no-cache`, затем выполняется `java -jar app.jar`
|
||||
|
||||
### Dockerfile (Frontend)
|
||||
|
||||
@@ -96,6 +98,7 @@ RUN npm ci
|
||||
RUN npm run build:vendor
|
||||
|
||||
FROM httpd:alpine3.23@sha256:4a15e9c...
|
||||
RUN apk upgrade --no-cache
|
||||
COPY --from=frontend-assets /build/dist/vendor/ /usr/local/apache2/htdocs/vendor/
|
||||
COPY security.conf /usr/local/apache2/conf/extra/magistr-security.conf
|
||||
COPY proxy.conf /usr/local/apache2/conf/extra/magistr-proxy.conf
|
||||
@@ -106,7 +109,8 @@ tag и manifest digest. Первый этап frontend собирает зафи
|
||||
OpenTelemetry bundle; второй раздаёт только runtime-
|
||||
файлы, без `node_modules`, тестов и build-исходников. Apache подключает `mod_headers`,
|
||||
`mod_proxy` и `mod_proxy_http`; proxy сохраняет исходный `Host`, чтобы `localhost` корректно
|
||||
маршрутизировался в tenant `default`. Сервер
|
||||
маршрутизировался в tenant `default`. Перед копированием runtime-файлов устанавливаются
|
||||
доступные исправления Alpine через `apk upgrade --no-cache`. Сервер
|
||||
возвращает CSP с `script-src 'self'`, `script-src-attr 'none'`, `style-src 'self'` и точными
|
||||
SHA-256 для оставшихся статических style-атрибутов. `unsafe-inline` и `unsafe-eval` не
|
||||
используются. Дополнительно выставляются `nosniff`, `DENY`, строгий referrer policy и
|
||||
@@ -325,8 +329,8 @@ kubectl auth can-i update secret/tenants-secret \
|
||||
--as="system:serviceaccount:magistr:${BACKEND_SERVICE_ACCOUNT}" -n magistr
|
||||
```
|
||||
|
||||
Production rollout и проверка реальных pod являются внешними операциями и без отдельного
|
||||
разрешения из этой рабочей копии не выполнялись.
|
||||
Production rollout и проверка реальных pod являются внешними операциями и выполняются из
|
||||
этой рабочей копии только по отдельному разрешению владельца.
|
||||
|
||||
### Ручное обновление backend и frontend
|
||||
|
||||
@@ -361,19 +365,32 @@ Deployment-файлов запрещено.
|
||||
|
||||
Основные шаги:
|
||||
1. Backend unit/component tests, frontend static/unit tests, Compose validation, shell-тест
|
||||
immutable rollout/rollback и регрессионная проверка закрепления артефактов.
|
||||
immutable rollout/rollback и регрессионная проверка закрепления артефактов. Исходники
|
||||
backend/frontend передаются тестовым контейнерам через `docker cp`: bind mount
|
||||
`${{ github.workspace }}` не используется, потому что Docker daemon runner может
|
||||
находиться вне job-контейнера и не видеть его файловую систему. Frontend security-тест
|
||||
дополнительно получает backend в `/backend`, сохраняя ожидаемую соседнюю структуру.
|
||||
2. Только после успешных gates — login, параллельная сборка и push backend/frontend с
|
||||
SHA-tag и release tag без `latest`. Сторонние Actions закреплены полными commit SHA.
|
||||
3. BuildKit публикует для обоих образов максимальную provenance- и SBOM-attestation. Build
|
||||
jobs также возвращают registry digest каждого образа.
|
||||
4. Отдельный обязательный job сканирует опубликованные digests закреплённым Trivy `0.63.0` и
|
||||
блокирует deploy при исправимых `HIGH`/`CRITICAL` уязвимостях.
|
||||
5. Deploy job устанавливает фиксированный `kubectl v1.33.12` только после SHA-256 проверки.
|
||||
Java Agent также имеет точную версию и checksum.
|
||||
блокирует deploy при исправимых `HIGH`/`CRITICAL` уязвимостях. Docker credentials для
|
||||
приватного registry копируются во временный Trivy-контейнер через Docker API; bind mount
|
||||
домашнего каталога job-контейнера не используется. Для каждого scan задан таймаут `15m`,
|
||||
чтобы загрузка vulnerability/Java DB и анализ Java-слоёв не прерывались стандартным
|
||||
пятиминутным лимитом Trivy; общий лимит job составляет 35 минут.
|
||||
5. Push в `main` после успешных проверок, сборки, публикации и scan автоматически запускает
|
||||
deploy проверенных digest в production. Job устанавливает фиксированный
|
||||
`kubectl v1.33.12` после SHA-256 проверки. `workflow_dispatch` доступен как
|
||||
дополнительный повторный запуск, но для обычной поставки не требуется. Java Agent
|
||||
также имеет точную версию и checksum.
|
||||
6. `scripts/deploy-images.sh` принимает только `image@sha256:...`, сохраняет предыдущие
|
||||
ссылки, применяет оба digest и ждёт rollout. При отказе автоматически возвращает оба
|
||||
предыдущих образа и повторно проверяет их готовность.
|
||||
7. Workflow-wide concurrency lock не допускает одновременные production deployment.
|
||||
7. Gitea 1.25 игнорирует `environment` и `concurrency`, поэтому workflow не полагается на
|
||||
них как на защиту production. Не запускайте дополнительный `workflow_dispatch`, пока
|
||||
автоматический deploy того же commit не завершён.
|
||||
|
||||
`scripts/check-artifact-pinning.sh` проверяет Dockerfile, Compose, Actions, checksum и CI
|
||||
gates. При передаче `K8S_DIR=../k8s` он дополнительно требует digest у каждого production
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
| Компонент | Технология |
|
||||
|-----------|-----------|
|
||||
| **Backend** | Java 17, Spring Boot 3.2.5 |
|
||||
| **Backend** | Java 17, Spring Boot 3.5.16 |
|
||||
| **Frontend** | Vanilla JavaScript (ES6 Modules) + HTML/CSS |
|
||||
| **База данных** | PostgreSQL (через Flyway миграции) |
|
||||
| **Контейнеризация** | Docker, Docker Compose |
|
||||
|
||||
@@ -9,6 +9,9 @@ RUN npm run build:vendor
|
||||
|
||||
FROM httpd:alpine3.23@sha256:4a15e9c73f25334bc03cfb3c692c9adfc103bb46ca89cee1f0b9a5fcbc7b21f6
|
||||
|
||||
# Устанавливаем исправления безопасности, опубликованные после сборки базового образа.
|
||||
RUN apk upgrade --no-cache
|
||||
|
||||
COPY admin/ /usr/local/apache2/htdocs/admin/
|
||||
COPY department/ /usr/local/apache2/htdocs/department/
|
||||
COPY edu-office/ /usr/local/apache2/htdocs/edu-office/
|
||||
|
||||
@@ -1,6 +1,115 @@
|
||||
{
|
||||
"generated_at": "2026-07-22T21:09:28.978295+00:00",
|
||||
"generated_at": "2026-07-30T00:10:04.894284+00:00",
|
||||
"nodes": {
|
||||
"_gitea_workflows_docker_build_backend_image_pipeline": {
|
||||
"code_fingerprint": "3b51aee192b5af2b63ee1b7f62beca1b4524fbbd2b143c9bd29eb039db90f7bd",
|
||||
"label": "Pipeline backend-образа",
|
||||
"last": "2026-07-29T21:50:22.465033+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"date": "2026-07-29T21:50:22.465033+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "Проверить backend на HIGH и CRITICAL уязвимости: context deadline exceeded после загрузки Trivy DB"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T21:27:26.637336+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "Проверить опубликованные образы: backend Trivy HIGH/CRITICAL scan завершился exitcode 1"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T20:38:53.488440+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "The goal you specified requires a project to execute but there is no POM in this directory (/workspace)."
|
||||
}
|
||||
],
|
||||
"score": 2.991771942,
|
||||
"source_file": ".gitea/workflows/docker-build.yaml",
|
||||
"status": "preferred",
|
||||
"uses": 3
|
||||
},
|
||||
"_gitea_workflows_docker_build_build_and_push_docker_images": {
|
||||
"code_fingerprint": "3b51aee192b5af2b63ee1b7f62beca1b4524fbbd2b143c9bd29eb039db90f7bd",
|
||||
"label": "Workflow сборки и публикации Docker-образов",
|
||||
"last": "2026-07-30T00:01:24.718235+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"date": "2026-07-30T00:01:24.718235+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "кажется бэкенд уходит в рестарт, можешь сам всё посмотреть и проверить"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T22:22:22.155742+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "Проверить frontend на HIGH и CRITICAL уязвимости: 8 HIGH в Alpine runtime"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T21:50:22.465033+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "Проверить backend на HIGH и CRITICAL уязвимости: context deadline exceeded после загрузки Trivy DB"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T21:27:26.637336+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "Проверить опубликованные образы: backend Trivy HIGH/CRITICAL scan завершился exitcode 1"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T20:38:53.488440+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "The goal you specified requires a project to execute but there is no POM in this directory (/workspace)."
|
||||
}
|
||||
],
|
||||
"score": 7.977548417,
|
||||
"source_file": ".gitea/workflows/docker-build.yaml",
|
||||
"status": "preferred",
|
||||
"uses": 8
|
||||
},
|
||||
"_gitea_workflows_docker_build_deploy_to_k8s": {
|
||||
"code_fingerprint": "3b51aee192b5af2b63ee1b7f62beca1b4524fbbd2b143c9bd29eb039db90f7bd",
|
||||
"label": "Job деплоя в Kubernetes",
|
||||
"last": "2026-07-30T00:01:24.718235+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"date": "2026-07-30T00:01:24.718235+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "кажется бэкенд уходит в рестарт, можешь сам всё посмотреть и проверить"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T20:12:38.880629+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "сам можешь выполнить эти действия?"
|
||||
}
|
||||
],
|
||||
"score": 1.996058514,
|
||||
"source_file": ".gitea/workflows/docker-build.yaml",
|
||||
"status": "preferred",
|
||||
"uses": 2
|
||||
},
|
||||
"_gitea_workflows_docker_build_frontend_image_pipeline": {
|
||||
"code_fingerprint": "3b51aee192b5af2b63ee1b7f62beca1b4524fbbd2b143c9bd29eb039db90f7bd",
|
||||
"label": "Pipeline frontend-образа",
|
||||
"last": "2026-07-29T22:22:22.155742+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"date": "2026-07-29T22:22:22.155742+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "Проверить frontend на HIGH и CRITICAL уязвимости: 8 HIGH в Alpine runtime"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T21:50:22.465033+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "Проверить backend на HIGH и CRITICAL уязвимости: context deadline exceeded после загрузки Trivy DB"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T20:38:53.488440+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "The goal you specified requires a project to execute but there is no POM in this directory (/workspace)."
|
||||
}
|
||||
],
|
||||
"score": 2.992651314,
|
||||
"source_file": ".gitea/workflows/docker-build.yaml",
|
||||
"status": "preferred",
|
||||
"uses": 3
|
||||
},
|
||||
"backend_src_main_java_com_magistr_app_config_auth_authorizationinterceptor_authorizationinterceptor": {
|
||||
"code_fingerprint": "70b01b3fa22e058ee997250c52cc0affee54ab37aa5f8cc7eb74411487f1bbbf",
|
||||
"label": "AuthorizationInterceptor",
|
||||
@@ -22,7 +131,7 @@
|
||||
"q": "Почему RequireRoles связывает множество контроллеров, моделей и сервисов как cross-community bridge?"
|
||||
}
|
||||
],
|
||||
"score": 2.185520319,
|
||||
"score": 1.85377087,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/config/auth/AuthorizationInterceptor.java",
|
||||
"status": "preferred",
|
||||
"uses": 3
|
||||
@@ -38,7 +147,7 @@
|
||||
"q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?"
|
||||
}
|
||||
],
|
||||
"score": 0.776480146,
|
||||
"score": 0.658614913,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/config/auth/JwtTokenService.java",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
@@ -54,7 +163,7 @@
|
||||
"q": "Почему RequireRoles связывает множество контроллеров, моделей и сервисов как cross-community bridge?"
|
||||
}
|
||||
],
|
||||
"score": 0.653312094,
|
||||
"score": 0.554143065,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/config/auth/RequireRoles.java",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
@@ -70,7 +179,7 @@
|
||||
"q": "Как безопасно обновлять tenant DataSource без потери рабочего подключения?"
|
||||
}
|
||||
],
|
||||
"score": 0.794603611,
|
||||
"score": 0.67398734,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/config/DataInitializer.java",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
@@ -86,7 +195,7 @@
|
||||
"q": "проанализируй весь проект на баги и ошибки и дополни '/mnt/HDD/ProjectMagistr/magistr/BUG_REPORT.md'"
|
||||
}
|
||||
],
|
||||
"score": 0.75572808,
|
||||
"score": 0.641012892,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/config/tenant/ConfigMapUpdater.java",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
@@ -102,7 +211,7 @@
|
||||
"q": "Как безопасно заменить tenant DataSource без потери рабочего подключения?"
|
||||
}
|
||||
],
|
||||
"score": 0.794468884,
|
||||
"score": 0.673873064,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/config/tenant/TenantConfig.java",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
@@ -128,7 +237,7 @@
|
||||
"q": "проанализируй весь проект на баги и ошибки и дополни '/mnt/HDD/ProjectMagistr/magistr/BUG_REPORT.md'"
|
||||
}
|
||||
],
|
||||
"score": 2.344800575,
|
||||
"score": 1.988873297,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/config/tenant/TenantConfigWatcher.java",
|
||||
"status": "preferred",
|
||||
"uses": 3
|
||||
@@ -154,7 +263,7 @@
|
||||
"q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?"
|
||||
}
|
||||
],
|
||||
"score": 2.365552641,
|
||||
"score": 2.006475318,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/config/tenant/TenantRoutingDataSource.java",
|
||||
"status": "preferred",
|
||||
"uses": 3
|
||||
@@ -170,7 +279,7 @@
|
||||
"q": "Почему в выборе аудитории показаны корпус и этаж?"
|
||||
}
|
||||
],
|
||||
"score": 0.976851438,
|
||||
"score": 0.828570992,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/controller/ClassroomController.java",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
@@ -186,7 +295,7 @@
|
||||
"q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?"
|
||||
}
|
||||
],
|
||||
"score": 0.776480146,
|
||||
"score": 0.658614913,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/controller/DatabaseController.java",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
@@ -207,7 +316,7 @@
|
||||
"q": "Как безопасно заменить tenant DataSource без потери рабочего подключения?"
|
||||
}
|
||||
],
|
||||
"score": 1.589072495,
|
||||
"score": 1.347860405,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/controller/DatabaseController.java",
|
||||
"status": "preferred",
|
||||
"uses": 2
|
||||
@@ -223,7 +332,7 @@
|
||||
"q": "проанализируй весь проект на баги и ошибки и дополни '/mnt/HDD/ProjectMagistr/magistr/BUG_REPORT.md'"
|
||||
}
|
||||
],
|
||||
"score": 0.75572808,
|
||||
"score": 0.641012892,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/controller/GlobalExceptionHandler.java",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
@@ -244,7 +353,7 @@
|
||||
"q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?"
|
||||
}
|
||||
],
|
||||
"score": 1.752429602,
|
||||
"score": 1.48642084,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/controller/ScheduleController.java",
|
||||
"status": "preferred",
|
||||
"uses": 2
|
||||
@@ -260,7 +369,7 @@
|
||||
"q": "Как безопасно валидировать и сохранять точечные изменения расписания?"
|
||||
}
|
||||
],
|
||||
"score": 0.78890947,
|
||||
"score": 0.669157537,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/controller/ScheduleOverrideController.java",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
@@ -281,11 +390,27 @@
|
||||
"q": "проанализируй весь проект на баги и ошибки и дополни '/mnt/HDD/ProjectMagistr/magistr/BUG_REPORT.md'"
|
||||
}
|
||||
],
|
||||
"score": 1.549927354,
|
||||
"score": 1.314657272,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/controller/ScheduleRuleAdminController.java",
|
||||
"status": "preferred",
|
||||
"uses": 2
|
||||
},
|
||||
"backend_src_main_java_com_magistr_app_model_academiccalendar_academiccalendar": {
|
||||
"code_fingerprint": "5bdd7da771fe97443661a5bd06bed94f453ea036759c4de8c927ed68eb2b723d",
|
||||
"label": "AcademicCalendar",
|
||||
"last": "2026-07-22T21:09:57.015227+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"date": "2026-07-22T21:09:57.015227+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "хотим поменять название сервиса, придумай что-то короткое одним словом подходящее по смыслу"
|
||||
}
|
||||
],
|
||||
"score": 0.848212095,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/model/AcademicCalendar.java",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
},
|
||||
"backend_src_main_java_com_magistr_app_model_classroom_classroom": {
|
||||
"code_fingerprint": "5614d83b8b7e20fd2b76271b28a8ef11ffc137808ec2d7ed7cd23e42e0ab93ec",
|
||||
"label": "Classroom",
|
||||
@@ -297,11 +422,27 @@
|
||||
"q": "Почему в выборе аудитории показаны корпус и этаж?"
|
||||
}
|
||||
],
|
||||
"score": 0.976851438,
|
||||
"score": 0.828570992,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/model/Classroom.java",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
},
|
||||
"backend_src_main_java_com_magistr_app_model_department_department": {
|
||||
"code_fingerprint": "2948555ebc816c44c9f745ce199b0a908930bf6c82cb4a0bd46c23ffac42ea6a",
|
||||
"label": "Department",
|
||||
"last": "2026-07-22T21:09:57.015227+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"date": "2026-07-22T21:09:57.015227+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "хотим поменять название сервиса, придумай что-то короткое одним словом подходящее по смыслу"
|
||||
}
|
||||
],
|
||||
"score": 0.848212095,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/model/Department.java",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
},
|
||||
"backend_src_main_java_com_magistr_app_model_schedulerule_schedulerule": {
|
||||
"code_fingerprint": "692d61c1bfd5b46f5865174dde8cf2d962d2ccd124b5e4320e2ba8535bb29605",
|
||||
"label": "ScheduleRule",
|
||||
@@ -318,7 +459,7 @@
|
||||
"q": "Корректны ли inferred-связи вокруг ScheduleRule?"
|
||||
}
|
||||
],
|
||||
"score": 1.448240567,
|
||||
"score": 1.228405956,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/model/ScheduleRule.java",
|
||||
"status": "preferred",
|
||||
"uses": 2
|
||||
@@ -334,7 +475,7 @@
|
||||
"q": "Какие связи затрагивает полная валидация правил расписания в исправлении №7?"
|
||||
}
|
||||
],
|
||||
"score": 0.794199275,
|
||||
"score": 0.67364438,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/model/ScheduleRuleSlot.java",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
@@ -342,16 +483,37 @@
|
||||
"backend_src_main_java_com_magistr_app_model_semester_semester": {
|
||||
"code_fingerprint": "52546b552fff16ab841a63ea80fb806f6cc32edc0615ec53b3c002543d692f24",
|
||||
"label": "Semester",
|
||||
"last": "2026-07-21T19:52:13.631700+00:00",
|
||||
"last": "2026-07-22T21:09:57.015227+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"date": "2026-07-22T21:09:57.015227+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "хотим поменять название сервиса, придумай что-то короткое одним словом подходящее по смыслу"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-21T19:52:13.631700+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "в дополнительные фильры просмотра расписаний нужно добавить выбор семестра"
|
||||
}
|
||||
],
|
||||
"score": 0.975949456,
|
||||
"score": 1.676018021,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/model/Semester.java",
|
||||
"status": "preferred",
|
||||
"uses": 2
|
||||
},
|
||||
"backend_src_main_java_com_magistr_app_model_studentgroup_studentgroup": {
|
||||
"code_fingerprint": "c4a022d9dd493adcfcf8bce3abd264e78d5539b45316d9e0aaef72f1b15091ac",
|
||||
"label": "StudentGroup",
|
||||
"last": "2026-07-22T21:09:57.015227+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"date": "2026-07-22T21:09:57.015227+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "хотим поменять название сервиса, придумай что-то короткое одним словом подходящее по смыслу"
|
||||
}
|
||||
],
|
||||
"score": 0.848212095,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/model/StudentGroup.java",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
},
|
||||
@@ -366,7 +528,7 @@
|
||||
"q": "Какие связи затрагивает полная валидация правил расписания в исправлении №7?"
|
||||
}
|
||||
],
|
||||
"score": 0.794199275,
|
||||
"score": 0.67364438,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/repository/SemesterRepository.java",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
@@ -387,7 +549,7 @@
|
||||
"q": "Как безопасно валидировать и сохранять точечные изменения расписания?"
|
||||
}
|
||||
],
|
||||
"score": 1.764858926,
|
||||
"score": 1.496963464,
|
||||
"source_file": "backend/src/main/java/com/magistr/app/service/ScheduleQueryService.java",
|
||||
"status": "preferred",
|
||||
"uses": 2
|
||||
@@ -403,7 +565,7 @@
|
||||
"q": "Корректны ли inferred-связи вокруг ScheduleRule?"
|
||||
}
|
||||
],
|
||||
"score": 0.654041293,
|
||||
"score": 0.554761576,
|
||||
"source_file": "backend/src/test/java/com/magistr/app/model/ScheduleRuleTest.java",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
@@ -419,10 +581,162 @@
|
||||
"q": "Какие подсистемы связывают исправления аутентификации, tenant-конфигурации, расписания и инфраструктуры?"
|
||||
}
|
||||
],
|
||||
"score": 0.776480146,
|
||||
"score": 0.658614913,
|
||||
"source_file": "compose.yaml",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
},
|
||||
"compose_db_service": {
|
||||
"code_fingerprint": "944ed05aa3bb66491e0622baa9659fc2db3115c79076325ebe243aeb84ca09f9",
|
||||
"label": "PostgreSQL-сервис Docker Compose",
|
||||
"last": "2026-07-29T19:59:04.551881+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"date": "2026-07-29T19:59:04.551881+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "новая схема на проде на удалённом сервере создана через CREATE SCHEMA public. я проверил, давай дальше"
|
||||
}
|
||||
],
|
||||
"score": 0.995980694,
|
||||
"source_file": "compose.yaml",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
},
|
||||
"docs_database_flyway_migrations": {
|
||||
"code_fingerprint": "2f5d4ce61ba0f5d8a9141f4ac33375dfc26a68e3bdccdc4269cf2a4f45a50176",
|
||||
"label": "Flyway-миграции",
|
||||
"last": "2026-07-29T19:26:44.188240+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"date": "2026-07-29T19:26:44.188240+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "теперь напиши мне инструкцию как это всё выыкатить на прод. у мня уже поднят на проде этот сервис со старой базой данных, но я могу её затереть. ci/cd тоже настроен, по сути я прямо сейчас могу залить это в ветку мейн и оно должно раскататься. каие есть подводные камни после изменений безопасности которые мы внесли?"
|
||||
}
|
||||
],
|
||||
"score": 0.995464026,
|
||||
"source_file": "docs/DATABASE.md",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
},
|
||||
"docs_infrastructure_gitea_actions_cicd": {
|
||||
"code_fingerprint": "7d9952767f14c83ef2e2c1708d9b5f0f8fccde3a214b2d2d9d91446437fdfdd2",
|
||||
"label": "CI/CD Gitea Actions в инфраструктуре",
|
||||
"last": "2026-07-29T22:22:22.155742+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"date": "2026-07-29T22:22:22.155742+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "Проверить frontend на HIGH и CRITICAL уязвимости: 8 HIGH в Alpine runtime"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T21:50:22.465033+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "Проверить backend на HIGH и CRITICAL уязвимости: context deadline exceeded после загрузки Trivy DB"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T21:27:26.637336+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "Проверить опубликованные образы: backend Trivy HIGH/CRITICAL scan завершился exitcode 1"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T20:38:53.488440+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "The goal you specified requires a project to execute but there is no POM in this directory (/workspace)."
|
||||
}
|
||||
],
|
||||
"score": 3.990045183,
|
||||
"source_file": "docs/INFRASTRUCTURE.md",
|
||||
"status": "preferred",
|
||||
"uses": 4
|
||||
},
|
||||
"docs_infrastructure_jwt_secret_policy": {
|
||||
"code_fingerprint": "7d9952767f14c83ef2e2c1708d9b5f0f8fccde3a214b2d2d9d91446437fdfdd2",
|
||||
"label": "Политика JWT-секрета",
|
||||
"last": "2026-07-30T00:01:24.718235+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"date": "2026-07-30T00:01:24.718235+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "кажется бэкенд уходит в рестарт, можешь сам всё посмотреть и проверить"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T20:12:38.880629+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "сам можешь выполнить эти действия?"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T19:26:44.188240+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "теперь напиши мне инструкцию как это всё выыкатить на прод. у мня уже поднят на проде этот сервис со старой базой данных, но я могу её затереть. ci/cd тоже настроен, по сути я прямо сейчас могу залить это в ветку мейн и оно должно раскататься. каие есть подводные камни после изменений безопасности которые мы внесли?"
|
||||
}
|
||||
],
|
||||
"score": 2.99152254,
|
||||
"source_file": "docs/INFRASTRUCTURE.md",
|
||||
"status": "preferred",
|
||||
"uses": 3
|
||||
},
|
||||
"docs_infrastructure_kubernetes_production": {
|
||||
"code_fingerprint": "7d9952767f14c83ef2e2c1708d9b5f0f8fccde3a214b2d2d9d91446437fdfdd2",
|
||||
"label": "Production-инфраструктура Kubernetes",
|
||||
"last": "2026-07-30T00:01:24.718235+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"date": "2026-07-30T00:01:24.718235+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "кажется бэкенд уходит в рестарт, можешь сам всё посмотреть и проверить"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T20:12:38.880629+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "сам можешь выполнить эти действия?"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T19:59:04.551881+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "новая схема на проде на удалённом сервере создана через CREATE SCHEMA public. я проверил, давай дальше"
|
||||
},
|
||||
{
|
||||
"date": "2026-07-29T19:26:44.188240+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "теперь напиши мне инструкцию как это всё выыкатить на прод. у мня уже поднят на проде этот сервис со старой базой данных, но я могу её затереть. ci/cd тоже настроен, по сути я прямо сейчас могу залить это в ветку мейн и оно должно раскататься. каие есть подводные камни после изменений безопасности которые мы внесли?"
|
||||
}
|
||||
],
|
||||
"score": 3.987503234,
|
||||
"source_file": "docs/INFRASTRUCTURE.md",
|
||||
"status": "preferred",
|
||||
"uses": 4
|
||||
},
|
||||
"docs_infrastructure_tenants_configmap": {
|
||||
"code_fingerprint": "7d9952767f14c83ef2e2c1708d9b5f0f8fccde3a214b2d2d9d91446437fdfdd2",
|
||||
"label": "Tenants ConfigMap",
|
||||
"last": "2026-07-29T19:26:44.188240+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"date": "2026-07-29T19:26:44.188240+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "теперь напиши мне инструкцию как это всё выыкатить на прод. у мня уже поднят на проде этот сервис со старой базой данных, но я могу её затереть. ci/cd тоже настроен, по сути я прямо сейчас могу залить это в ветку мейн и оно должно раскататься. каие есть подводные камни после изменений безопасности которые мы внесли?"
|
||||
}
|
||||
],
|
||||
"score": 0.995464026,
|
||||
"source_file": "docs/INFRASTRUCTURE.md",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
},
|
||||
"springbootapplication": {
|
||||
"code_fingerprint": "",
|
||||
"label": "SpringBootApplication",
|
||||
"last": "2026-07-29T21:27:26.637336+00:00",
|
||||
"provenance": [
|
||||
{
|
||||
"date": "2026-07-29T21:27:26.637336+00:00",
|
||||
"outcome": "useful",
|
||||
"q": "Проверить опубликованные образы: backend Trivy HIGH/CRITICAL scan завершился exitcode 1"
|
||||
}
|
||||
],
|
||||
"score": 0.997393869,
|
||||
"source_file": "",
|
||||
"status": "tentative",
|
||||
"uses": 1
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
type: "query"
|
||||
date: "2026-07-29T19:26:44.188240+00:00"
|
||||
question: "теперь напиши мне инструкцию как это всё выыкатить на прод. у мня уже поднят на проде этот сервис со старой базой данных, но я могу её затереть. ci/cd тоже настроен, по сути я прямо сейчас могу залить это в ветку мейн и оно должно раскататься. каие есть подводные камни после изменений безопасности которые мы внесли?"
|
||||
contributor: "graphify"
|
||||
outcome: "useful"
|
||||
source_nodes: ["Workflow сборки и публикации Docker-образов", "Production-инфраструктура Kubernetes", "Flyway-миграции", "Политика JWT-секрета", "Tenants ConfigMap"]
|
||||
---
|
||||
|
||||
# Q: теперь напиши мне инструкцию как это всё выыкатить на прод. у мня уже поднят на проде этот сервис со старой базой данных, но я могу её затереть. ci/cd тоже настроен, по сути я прямо сейчас могу залить это в ветку мейн и оно должно раскататься. каие есть подводные камни после изменений безопасности которые мы внесли?
|
||||
|
||||
## Answer
|
||||
|
||||
Expanded from original query via vocab: [auth, cookie, database, docker, flyway, jwt, kubernetes, production, refresh, secret, secure, workflow]. Прямой push в main для первого rollout небезопасен: workflow меняет только image digests, а актуальные ../k8s манифесты находятся вне Git-корня и не применяются. В действующем кластере отсутствуют tenants-secret и otel-postgres-secret, а app-secret не содержит JWT_SECRET. Для консолидированной V1 нужно полностью пересоздать все tenant-БД, затем выполнить первый rollout вручную по digest через ../k8s/deploy.sh. Инструкция и ограничения rollback обновлены в STARTUP_GUIDE.md.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Signal: useful
|
||||
|
||||
## Source Nodes
|
||||
|
||||
- Workflow сборки и публикации Docker-образов
|
||||
- Production-инфраструктура Kubernetes
|
||||
- Flyway-миграции
|
||||
- Политика JWT-секрета
|
||||
- Tenants ConfigMap
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
type: "query"
|
||||
date: "2026-07-29T19:59:04.551881+00:00"
|
||||
question: "новая схема на проде на удалённом сервере создана через CREATE SCHEMA public. я проверил, давай дальше"
|
||||
contributor: "graphify"
|
||||
outcome: "useful"
|
||||
source_nodes: ["Workflow сборки и публикации Docker-образов", "Production-инфраструктура Kubernetes", "PostgreSQL-сервис Docker Compose"]
|
||||
---
|
||||
|
||||
# Q: новая схема на проде на удалённом сервере создана через CREATE SCHEMA public. я проверил, давай дальше
|
||||
|
||||
## Answer
|
||||
|
||||
Expanded from original query via vocab: [auth, database, flyway, jwt, kubernetes, postgres, production, secret, workflow]. Граф связал workflow сборки immutable Docker-образов, production-инфраструктуру Kubernetes и tenant PostgreSQL. Проверка исходников и кластера подтвердила: консолидированная V1 проходит на чистом PostgreSQL, push в main запускает image-only rollout, а новые манифесты ../k8s нужно применить вручную. До push необходимо создать JWT_SECRET в app-secret, tenants-secret и otel-postgres-secret; значения секретов не читались и не изменялись.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Signal: useful
|
||||
|
||||
## Source Nodes
|
||||
|
||||
- Workflow сборки и публикации Docker-образов
|
||||
- Production-инфраструктура Kubernetes
|
||||
- PostgreSQL-сервис Docker Compose
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
type: "query"
|
||||
date: "2026-07-29T20:12:38.880629+00:00"
|
||||
question: "сам можешь выполнить эти действия?"
|
||||
contributor: "graphify"
|
||||
outcome: "useful"
|
||||
source_nodes: ["Workflow сборки и публикации Docker-образов", "Production-инфраструктура Kubernetes", "Job деплоя в Kubernetes", "Политика JWT-секрета"]
|
||||
---
|
||||
|
||||
# Q: сам можешь выполнить эти действия?
|
||||
|
||||
## Answer
|
||||
|
||||
Expanded from original query via vocab: [workflow, production, kubernetes, secret, jwt, tenant, postgres, otel, gitea, main, registry, flyway]. Первый traversal по перегруженному main был нерелевантен; повторный запрос без main связал Workflow сборки и публикации Docker-образов, Production-инфраструктуру Kubernetes, Job деплоя в Kubernetes и Политику JWT-секрета. По отдельному разрешению старый backend масштабирован до 0, обе tenant-БД read-only проверены как пустые и имеющие CREATE, в app-secret добавлен новый JWT_SECRET без вывода значения, созданы переходные tenants-secret и otel-postgres-secret. Workflow изменён так, что push выполняет build/scan, а deploy разрешён только ручному workflow_dispatch. Server dry-run выявил immutable roleRef; внешний rbac.yaml исправлен новым именем backend-tenant-secret-binding, повторный dry-run прошёл.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Signal: useful
|
||||
|
||||
## Source Nodes
|
||||
|
||||
- Workflow сборки и публикации Docker-образов
|
||||
- Production-инфраструктура Kubernetes
|
||||
- Job деплоя в Kubernetes
|
||||
- Политика JWT-секрета
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
type: "query"
|
||||
date: "2026-07-29T20:38:53.488440+00:00"
|
||||
question: "The goal you specified requires a project to execute but there is no POM in this directory (/workspace)."
|
||||
contributor: "graphify"
|
||||
outcome: "useful"
|
||||
source_nodes: ["Workflow сборки и публикации Docker-образов", "Pipeline backend-образа", "Pipeline frontend-образа", "CI/CD Gitea Actions в инфраструктуре"]
|
||||
---
|
||||
|
||||
# Q: The goal you specified requires a project to execute but there is no POM in this directory (/workspace).
|
||||
|
||||
## Answer
|
||||
|
||||
Expanded from original query via graph vocab: [actions, backend, build, docker, frontend, gitea, pipeline, test, workflow, workspace]. Корень сбоя: workflow передавал пути job-контейнера в bind mounts внешнего Docker daemon. Исправление: scripts/run-ci-container.sh создаёт временный контейнер, копирует исходники через Docker API, запускает команду и сохраняет её exit code; frontend дополнительно получает backend в /backend, а Trivy получает Docker config через DOCKER_CONFIG=/workspace без bind mount. Проверено полным Maven gate (274 теста) и frontend gate (63 теста).
|
||||
|
||||
## Outcome
|
||||
|
||||
- Signal: useful
|
||||
|
||||
## Source Nodes
|
||||
|
||||
- Workflow сборки и публикации Docker-образов
|
||||
- Pipeline backend-образа
|
||||
- Pipeline frontend-образа
|
||||
- CI/CD Gitea Actions в инфраструктуре
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
type: "query"
|
||||
date: "2026-07-29T21:27:26.637336+00:00"
|
||||
question: "Проверить опубликованные образы: backend Trivy HIGH/CRITICAL scan завершился exitcode 1"
|
||||
contributor: "graphify"
|
||||
outcome: "useful"
|
||||
source_nodes: ["Workflow сборки и публикации Docker-образов", "Pipeline backend-образа", "CI/CD Gitea Actions в инфраструктуре", "SpringBootApplication"]
|
||||
---
|
||||
|
||||
# Q: Проверить опубликованные образы: backend Trivy HIGH/CRITICAL scan завершился exitcode 1
|
||||
|
||||
## Answer
|
||||
|
||||
CI дошёл до сканирования опубликованного backend-образа, поэтому передача исходников в контейнеры уже исправлена. HIGH/CRITICAL находились в устаревших пакетах Alpine и Java-зависимостях Spring Boot 3.2.5. Backend обновлён до Spring Boot 3.5.16, PostgreSQL JDBC закреплён на 42.7.12, для Flyway 11 добавлен flyway-database-postgresql, runtime устанавливает исправления Alpine. Локальный образ прошёл тот же Trivy gate: Alpine 0, app.jar 0, OpenTelemetry agent 0. Полный backend-набор: 300 тестов без ошибок; обязательные CI, frontend, Compose и supply-chain проверки также прошли.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Signal: useful
|
||||
|
||||
## Source Nodes
|
||||
|
||||
- Workflow сборки и публикации Docker-образов
|
||||
- Pipeline backend-образа
|
||||
- CI/CD Gitea Actions в инфраструктуре
|
||||
- SpringBootApplication
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
type: "query"
|
||||
date: "2026-07-29T21:50:22.465033+00:00"
|
||||
question: "Проверить backend на HIGH и CRITICAL уязвимости: context deadline exceeded после загрузки Trivy DB"
|
||||
contributor: "graphify"
|
||||
outcome: "useful"
|
||||
source_nodes: ["Workflow сборки и публикации Docker-образов", "Pipeline backend-образа", "Pipeline frontend-образа", "CI/CD Gitea Actions в инфраструктуре"]
|
||||
---
|
||||
|
||||
# Q: Проверить backend на HIGH и CRITICAL уязвимости: context deadline exceeded после загрузки Trivy DB
|
||||
|
||||
## Answer
|
||||
|
||||
Expanded from original query via graph vocab: [backend, build, cache, container, docker, gitea, pipeline, registry, workflow]. Первый traversal по cache попал в frontend cache и был отброшен; уточнённый traversal вывел Workflow сборки и публикации Docker-образов, Pipeline backend-образа и Pipeline frontend-образа. Прямой разбор CI-лога подтвердил: Trivy не сформировал vulnerability report и завершился по стандартному timeout 5m во время Java layer analysis после загрузки vulnerability/Java DB. В workflow обоим scan добавлен --timeout 15m, security-scan job увеличен до 35 минут; severity HIGH,CRITICAL, exit-code 1, ignore-unfixed, Java DB и digest pinning сохранены. Локальный Trivy 0.63.0 с новой командой завершился успешно: Alpine 0, app.jar 0, OpenTelemetry agent 0. Regression gates и YAML validation прошли.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Signal: useful
|
||||
|
||||
## Source Nodes
|
||||
|
||||
- Workflow сборки и публикации Docker-образов
|
||||
- Pipeline backend-образа
|
||||
- Pipeline frontend-образа
|
||||
- CI/CD Gitea Actions в инфраструктуре
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
type: "query"
|
||||
date: "2026-07-29T22:22:22.155742+00:00"
|
||||
question: "Проверить frontend на HIGH и CRITICAL уязвимости: 8 HIGH в Alpine runtime"
|
||||
contributor: "graphify"
|
||||
outcome: "useful"
|
||||
source_nodes: ["Workflow сборки и публикации Docker-образов", "Pipeline frontend-образа", "CI/CD Gitea Actions в инфраструктуре"]
|
||||
---
|
||||
|
||||
# Q: Проверить frontend на HIGH и CRITICAL уязвимости: 8 HIGH в Alpine runtime
|
||||
|
||||
## Answer
|
||||
|
||||
Expanded from original query via graph vocab: [build, container, docker, frontend, gitea, pipeline, registry, workflow]. Traversal вывел Workflow сборки и публикации Docker-образов и Pipeline frontend-образа; фактическая причина проверена по frontend/Dockerfile и Trivy-логу. В финальном httpd:alpine 3.23.4 найдены 8 исправимых HIGH: c-ares, libcrypto3/libssl3, libcurl и libexpat. В runtime-stage добавлен RUN apk upgrade --no-cache. Локальная Docker-сборка подтвердила обновление ровно до fixed versions из отчёта: c-ares 1.34.8-r0, OpenSSL 3.5.7-r0, libcurl 8.20.0-r0, libexpat 2.8.2-r0, Alpine 3.23.5. Supply-chain regression gates и frontend 11/11 прошли. Отдельный локальный Trivy-процесс не стартовал из-за таймаута механизма разрешений Docker socket; окончательный scan выполняет новый CI-run.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Signal: useful
|
||||
|
||||
## Source Nodes
|
||||
|
||||
- Workflow сборки и публикации Docker-образов
|
||||
- Pipeline frontend-образа
|
||||
- CI/CD Gitea Actions в инфраструктуре
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
type: "query"
|
||||
date: "2026-07-30T00:01:24.718235+00:00"
|
||||
question: "кажется бэкенд уходит в рестарт, можешь сам всё посмотреть и проверить"
|
||||
contributor: "graphify"
|
||||
outcome: "useful"
|
||||
source_nodes: ["Workflow сборки и публикации Docker-образов", "Production-инфраструктура Kubernetes", "Job деплоя в Kubernetes", "Политика JWT-секрета"]
|
||||
---
|
||||
|
||||
# Q: кажется бэкенд уходит в рестарт, можешь сам всё посмотреть и проверить
|
||||
|
||||
## Answer
|
||||
|
||||
Expanded from original query via graph vocab: [backend, config, job, jwt, kubernetes, production, secret, workflow]. Live-проверка подтвердила CrashLoopBackOff: JwtTokenService завершает startup с ошибкой JWT_SECRET обязателен и не может быть пустым. Secret app-secret содержит непустой JWT_SECRET, но live Deployment не имеет env JWT_SECRET: CI workflow применил только image digest поверх старого pod template и не применил актуальные production-манифесты. Frontend отвечает 200, backend health 503. Server-side dry-run актуальных манифестов успешен при подстановке текущих digest. Дополнительно ../k8s/deploy.sh неисправен для multi-document backend.yaml/frontend.yaml: kubectl set image --local падает на Service.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Signal: useful
|
||||
|
||||
## Source Nodes
|
||||
|
||||
- Workflow сборки и публикации Docker-образов
|
||||
- Production-инфраструктура Kubernetes
|
||||
- Job деплоя в Kubernetes
|
||||
- Политика JWT-секрета
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
type: "query"
|
||||
date: "2026-07-30T00:15:03.724071+00:00"
|
||||
question: "Ты неправильно меня понял. Мне не нужно, чтобы K8S находился в репозитории. Мне нужно просто, чтобы ты вернул назад, как было, чтобы cicd срабатывала без ручного вмешательства. И всё."
|
||||
contributor: "graphify"
|
||||
outcome: "corrected"
|
||||
correction: "Kubernetes manifests остаются вне репозитория; CI автоматически выполняет только image digest rollout после push в main."
|
||||
source_nodes: ["Workflow сборки и публикации Docker-образов", "Job деплоя в Kubernetes", "Production-инфраструктура Kubernetes"]
|
||||
---
|
||||
|
||||
# Q: Ты неправильно меня понял. Мне не нужно, чтобы K8S находился в репозитории. Мне нужно просто, чтобы ты вернул назад, как было, чтобы cicd срабатывала без ручного вмешательства. И всё.
|
||||
|
||||
## Answer
|
||||
|
||||
Expanded from original query via graph vocab: [backend, config, frontend, gitea, job, kubernetes, pipeline, production, script, workflow]. Каталог ../k8s оставлен вне Git. В workflow удалён manual-only if по workflow_dispatch, поэтому успешный push в main после checks, build и Trivy автоматически запускает deploy-to-k8s с проверенными digest. workflow_dispatch сохранён только как дополнительный повторный запуск. Внешний ../k8s/deploy.sh исправлен для multi-document Deployment+Service через точную замену одного digest-sentinel. Redesign fast-forward до main и является текущей веткой. Shell, artifact, production-secret, immutable rollout/rollback, kustomize, mock полного deploy и git diff проверки прошли.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Signal: corrected
|
||||
- Correction: Kubernetes manifests остаются вне репозитория; CI автоматически выполняет только image digest rollout после push в main.
|
||||
|
||||
## Source Nodes
|
||||
|
||||
- Workflow сборки и публикации Docker-образов
|
||||
- Job деплоя в Kubernetes
|
||||
- Production-инфраструктура Kubernetes
|
||||
@@ -1,30 +1,44 @@
|
||||
# Lessons
|
||||
|
||||
_Auto-generated by `graphify reflect` from 11 session memories in graphify-out/memory/. Deterministic; no LLM. Use for orientation — verify before relying, and revisit dead ends if the code has changed since._
|
||||
_Auto-generated by `graphify reflect` from 20 session memories in graphify-out/memory/. Deterministic; no LLM. Use for orientation — verify before relying, and revisit dead ends if the code has changed since._
|
||||
|
||||
## Summary
|
||||
|
||||
- 10 useful · 1 dead ends · 0 corrected · 0 unmarked
|
||||
- 19 useful · 1 dead ends · 0 corrected · 0 unmarked
|
||||
|
||||
## Lessons
|
||||
|
||||
**Preferred sources** — corroborated by ≥2 useful results; start here.
|
||||
|
||||
- `Workflow сборки и публикации Docker-образов` (8× useful)
|
||||
- `CI/CD Gitea Actions в инфраструктуре` (4× useful)
|
||||
- `Production-инфраструктура Kubernetes` (4× useful)
|
||||
- `Pipeline frontend-образа` (3× useful)
|
||||
- `Pipeline backend-образа` (3× useful)
|
||||
- `Политика JWT-секрета` (3× useful)
|
||||
- `ScheduleGeneratorService` (3× useful)
|
||||
- `TenantRoutingDataSource` (3× useful)
|
||||
- `Job деплоя в Kubernetes` (2× useful)
|
||||
- `TenantConfigWatcher` (3× useful)
|
||||
- `AuthorizationInterceptor` (3× useful)
|
||||
- `Semester` (2× useful)
|
||||
- `ScheduleQueryService` (2× useful)
|
||||
- `ScheduleController.java` (2× useful)
|
||||
- `DatabaseController` (2× useful)
|
||||
- `ScheduleGeneratorService` (2× useful)
|
||||
- `ScheduleRuleAdminController` (2× useful)
|
||||
- `ScheduleRule` (2× useful)
|
||||
|
||||
**Tentative** — useful in fewer than 2 results; verify before relying.
|
||||
|
||||
- `SpringBootApplication` (1× useful)
|
||||
- `PostgreSQL-сервис Docker Compose` (1× useful)
|
||||
- `Flyway-миграции` (1× useful)
|
||||
- `Tenants ConfigMap` (1× useful)
|
||||
- `AcademicCalendar` (1× useful)
|
||||
- `Department` (1× useful)
|
||||
- `StudentGroup` (1× useful)
|
||||
- `Classroom` (1× useful)
|
||||
- `ClassroomController.java` (1× useful)
|
||||
- `Semester` (1× useful)
|
||||
- `DataInitializer` (1× useful)
|
||||
- `TenantConfig` (1× useful)
|
||||
- `ScheduleRuleSlot` (1× useful)
|
||||
|
||||
@@ -4,6 +4,8 @@ set -euo pipefail
|
||||
|
||||
PROJECT_ROOT=${PROJECT_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}
|
||||
WORKFLOW="$PROJECT_ROOT/.gitea/workflows/docker-build.yaml"
|
||||
CI_CONTAINER_SCRIPT="$PROJECT_ROOT/scripts/run-ci-container.sh"
|
||||
CI_CONTAINER_TEST="$PROJECT_ROOT/scripts/test-run-ci-container.sh"
|
||||
|
||||
failures=0
|
||||
|
||||
@@ -60,31 +62,70 @@ require_count() {
|
||||
local description=$3
|
||||
local actual
|
||||
|
||||
actual=$(grep -Ec "$pattern" "$WORKFLOW" || true)
|
||||
actual=$(grep -Ec -- "$pattern" "$WORKFLOW" || true)
|
||||
if (( actual < expected )); then
|
||||
fail "$description: ожидается не менее $expected, найдено $actual"
|
||||
fi
|
||||
}
|
||||
|
||||
check_workflow_gates() {
|
||||
local security_timeout
|
||||
|
||||
require_file "$WORKFLOW" || return
|
||||
require_file "$CI_CONTAINER_SCRIPT" || return
|
||||
require_file "$CI_CONTAINER_TEST" || return
|
||||
|
||||
require_count '^[[:space:]]+sbom:[[:space:]]+true$' 2 \
|
||||
"для обоих образов должна публиковаться SBOM attestation"
|
||||
require_count '^[[:space:]]+provenance:[[:space:]]+mode=max$' 2 \
|
||||
"для обоих образов должна публиковаться provenance attestation"
|
||||
require_count 'image --scanners vuln --exit-code 1 --severity HIGH,CRITICAL' 2 \
|
||||
"оба образа должны проходить блокирующее HIGH/CRITICAL-сканирование"
|
||||
require_count 'image --timeout 15m --scanners vuln --exit-code 1 --severity HIGH,CRITICAL' 2 \
|
||||
"оба образа должны проходить блокирующее HIGH/CRITICAL-сканирование с достаточным таймаутом"
|
||||
require_count 'TRIVY_IMAGE: [^[:space:]]+@sha256:[0-9a-f]{64}$' 1 \
|
||||
"образ Trivy должен быть закреплён digest"
|
||||
require_count 'MAVEN_IMAGE: [^[:space:]]+@sha256:[0-9a-f]{64}$' 1 \
|
||||
"образ Maven для CI должен быть закреплён digest"
|
||||
require_count 'NODE_IMAGE: [^[:space:]]+@sha256:[0-9a-f]{64}$' 1 \
|
||||
"образ Node.js для CI должен быть закреплён digest"
|
||||
require_count 'bash scripts/run-ci-container\.sh' 4 \
|
||||
"тесты и Trivy должны передавать файлы через Docker API"
|
||||
require_count 'bash scripts/test-run-ci-container\.sh' 1 \
|
||||
"workflow должен проверять helper запуска CI-контейнеров"
|
||||
require_count '--env DOCKER_CONFIG=/workspace' 2 \
|
||||
"оба Trivy scan должны получать registry credentials через Docker API"
|
||||
|
||||
security_timeout=$(awk '
|
||||
$0 == " security-scan:" {
|
||||
in_job = 1
|
||||
next
|
||||
}
|
||||
in_job && $0 ~ /^ [[:alnum:]_-]+:$/ {
|
||||
exit
|
||||
}
|
||||
in_job && $1 == "timeout-minutes:" {
|
||||
print $2
|
||||
exit
|
||||
}
|
||||
' "$WORKFLOW")
|
||||
if [[ "$security_timeout" != "35" ]]; then
|
||||
fail "security-scan job должен иметь таймаут 35 минут"
|
||||
fi
|
||||
|
||||
if grep -Fq '${{ github.workspace }}' "$WORKFLOW" \
|
||||
|| grep -Fq -- '-v "$HOME' "$WORKFLOW"; then
|
||||
fail "нельзя использовать пути job-контейнера в bind mount внешнего Docker daemon"
|
||||
fi
|
||||
|
||||
if ! grep -Eq '^ needs: \[build-and-push-backend, build-and-push-frontend, security-scan\]$' "$WORKFLOW"; then
|
||||
fail "deploy обязан зависеть от успешного security-scan"
|
||||
fi
|
||||
if ! grep -Eq '^[[:space:]]{2}push:[[:space:]]*$' "$WORKFLOW" \
|
||||
|| ! grep -Eq '^[[:space:]]{6}- main[[:space:]]*$' "$WORKFLOW"; then
|
||||
fail "production deploy должен автоматически запускаться после push в main"
|
||||
fi
|
||||
if grep -Fq "if: \${{ gitea.event_name == 'workflow_dispatch' }}" "$WORKFLOW"; then
|
||||
fail "production deploy не должен требовать ручной workflow_dispatch"
|
||||
fi
|
||||
|
||||
if ! grep -Eq '^ KUBECTL_VERSION: v[0-9]+\.[0-9]+\.[0-9]+$' "$WORKFLOW" \
|
||||
|| ! grep -Eq '^ KUBECTL_SHA256: [0-9a-f]{64}$' "$WORKFLOW" \
|
||||
@@ -104,6 +145,19 @@ check_downloaded_backend_artifact() {
|
||||
fi
|
||||
}
|
||||
|
||||
check_runtime_updates() {
|
||||
local component
|
||||
local dockerfile
|
||||
|
||||
for component in backend frontend; do
|
||||
dockerfile="$PROJECT_ROOT/$component/Dockerfile"
|
||||
require_file "$dockerfile" || continue
|
||||
if ! grep -Fq 'RUN apk upgrade --no-cache' "$dockerfile"; then
|
||||
fail "runtime-образ $component должен устанавливать доступные исправления Alpine"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
check_testcontainer_images() {
|
||||
local image_catalog="$PROJECT_ROOT/backend/src/test/java/com/magistr/app/testing/TestContainerImages.java"
|
||||
|
||||
@@ -141,6 +195,7 @@ check_compose_images
|
||||
check_actions
|
||||
check_workflow_gates
|
||||
check_downloaded_backend_artifact
|
||||
check_runtime_updates
|
||||
check_testcontainer_images
|
||||
check_optional_kubernetes_images
|
||||
|
||||
|
||||
87
scripts/run-ci-container.sh
Executable file
87
scripts/run-ci-container.sh
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if (( $# < 4 )); then
|
||||
printf 'Использование: %s <каталог-исходников> <образ> [--copy <каталог> <путь>] [--env <ИМЯ=значение>] -- <команда> [аргументы...]\n' \
|
||||
"${0##*/}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
SOURCE_DIR=$1
|
||||
IMAGE=$2
|
||||
shift 2
|
||||
EXTRA_SOURCE_DIRS=()
|
||||
EXTRA_DESTINATIONS=()
|
||||
CREATE_ARGS=()
|
||||
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
--copy)
|
||||
if (( $# < 3 )); then
|
||||
printf '[CI-КОНТЕЙНЕР] Ошибка: для --copy нужны каталог и путь в контейнере\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
EXTRA_SOURCE_DIRS+=("$2")
|
||||
EXTRA_DESTINATIONS+=("$3")
|
||||
shift 3
|
||||
;;
|
||||
--env)
|
||||
if (( $# < 2 )) || [[ ! "$2" =~ ^[A-Za-z_][A-Za-z0-9_]*=.*$ ]]; then
|
||||
printf '[CI-КОНТЕЙНЕР] Ошибка: для --env ожидается ИМЯ=значение\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
CREATE_ARGS+=(--env "$2")
|
||||
shift 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
printf '[CI-КОНТЕЙНЕР] Ошибка: неизвестный аргумент до команды: %s\n' "$1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if (( $# == 0 )); then
|
||||
printf '[CI-КОНТЕЙНЕР] Ошибка: не указана команда контейнера\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -d "$SOURCE_DIR" ]]; then
|
||||
printf '[CI-КОНТЕЙНЕР] Ошибка: каталог исходников не найден: %s\n' "$SOURCE_DIR" >&2
|
||||
exit 2
|
||||
fi
|
||||
for index in "${!EXTRA_SOURCE_DIRS[@]}"; do
|
||||
extra_source=${EXTRA_SOURCE_DIRS[$index]}
|
||||
extra_destination=${EXTRA_DESTINATIONS[$index]}
|
||||
if [[ ! -d "$extra_source" ]]; then
|
||||
printf '[CI-КОНТЕЙНЕР] Ошибка: дополнительный каталог не найден: %s\n' \
|
||||
"$extra_source" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ "$extra_destination" != /* || "$extra_destination" == *..* ]]; then
|
||||
printf '[CI-КОНТЕЙНЕР] Ошибка: путь назначения должен быть абсолютным без ..: %s\n' \
|
||||
"$extra_destination" >&2
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
|
||||
CONTAINER_ID=
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "$CONTAINER_ID" ]]; then
|
||||
docker rm --force "$CONTAINER_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
CONTAINER_ID=$(docker create --workdir /workspace "${CREATE_ARGS[@]}" "$IMAGE" "$@")
|
||||
docker cp "$SOURCE_DIR/." "$CONTAINER_ID:/workspace"
|
||||
for index in "${!EXTRA_SOURCE_DIRS[@]}"; do
|
||||
docker cp "${EXTRA_SOURCE_DIRS[$index]}/." \
|
||||
"$CONTAINER_ID:${EXTRA_DESTINATIONS[$index]}"
|
||||
done
|
||||
docker start --attach "$CONTAINER_ID"
|
||||
@@ -7,12 +7,20 @@ CHECK_SCRIPT="$ROOT_DIR/scripts/check-artifact-pinning.sh"
|
||||
FIXTURE_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$FIXTURE_DIR"' EXIT
|
||||
|
||||
mkdir -p "$FIXTURE_DIR/backend" "$FIXTURE_DIR/frontend" "$FIXTURE_DIR/.gitea/workflows"
|
||||
mkdir -p \
|
||||
"$FIXTURE_DIR/backend" \
|
||||
"$FIXTURE_DIR/frontend" \
|
||||
"$FIXTURE_DIR/.gitea/workflows" \
|
||||
"$FIXTURE_DIR/scripts"
|
||||
cp "$ROOT_DIR/backend/Dockerfile" "$FIXTURE_DIR/backend/Dockerfile"
|
||||
cp "$ROOT_DIR/frontend/Dockerfile" "$FIXTURE_DIR/frontend/Dockerfile"
|
||||
cp "$ROOT_DIR/compose.yaml" "$FIXTURE_DIR/compose.yaml"
|
||||
cp "$ROOT_DIR/.gitea/workflows/docker-build.yaml" \
|
||||
"$FIXTURE_DIR/.gitea/workflows/docker-build.yaml"
|
||||
cp "$ROOT_DIR/scripts/run-ci-container.sh" \
|
||||
"$FIXTURE_DIR/scripts/run-ci-container.sh"
|
||||
cp "$ROOT_DIR/scripts/test-run-ci-container.sh" \
|
||||
"$FIXTURE_DIR/scripts/test-run-ci-container.sh"
|
||||
|
||||
PROJECT_ROOT="$FIXTURE_DIR" bash "$CHECK_SCRIPT" >/dev/null
|
||||
|
||||
@@ -29,4 +37,58 @@ if PROJECT_ROOT="$FIXTURE_DIR" bash "$CHECK_SCRIPT" >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$ROOT_DIR/backend/Dockerfile" "$FIXTURE_DIR/backend/Dockerfile"
|
||||
cp "$ROOT_DIR/.gitea/workflows/docker-build.yaml" \
|
||||
"$FIXTURE_DIR/.gitea/workflows/docker-build.yaml"
|
||||
sed -i '/RUN apk upgrade --no-cache/d' "$FIXTURE_DIR/backend/Dockerfile"
|
||||
if PROJECT_ROOT="$FIXTURE_DIR" bash "$CHECK_SCRIPT" >/dev/null 2>&1; then
|
||||
printf 'Ошибка теста: отсутствие Alpine security updates в backend не было отклонено\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$ROOT_DIR/backend/Dockerfile" "$FIXTURE_DIR/backend/Dockerfile"
|
||||
sed -i '/RUN apk upgrade --no-cache/d' "$FIXTURE_DIR/frontend/Dockerfile"
|
||||
if PROJECT_ROOT="$FIXTURE_DIR" bash "$CHECK_SCRIPT" >/dev/null 2>&1; then
|
||||
printf 'Ошибка теста: отсутствие Alpine security updates во frontend не было отклонено\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$ROOT_DIR/frontend/Dockerfile" "$FIXTURE_DIR/frontend/Dockerfile"
|
||||
cp "$ROOT_DIR/.gitea/workflows/docker-build.yaml" \
|
||||
"$FIXTURE_DIR/.gitea/workflows/docker-build.yaml"
|
||||
sed -i '0,/image --timeout 15m --scanners vuln/{s/image --timeout 15m --scanners vuln/image --scanners vuln/;}' \
|
||||
"$FIXTURE_DIR/.gitea/workflows/docker-build.yaml"
|
||||
if PROJECT_ROOT="$FIXTURE_DIR" bash "$CHECK_SCRIPT" >/dev/null 2>&1; then
|
||||
printf 'Ошибка теста: недостаточный таймаут Trivy не был отклонён\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$ROOT_DIR/.gitea/workflows/docker-build.yaml" \
|
||||
"$FIXTURE_DIR/.gitea/workflows/docker-build.yaml"
|
||||
sed -i '0,/timeout-minutes: 35/s//timeout-minutes: 20/' \
|
||||
"$FIXTURE_DIR/.gitea/workflows/docker-build.yaml"
|
||||
if PROJECT_ROOT="$FIXTURE_DIR" bash "$CHECK_SCRIPT" >/dev/null 2>&1; then
|
||||
printf 'Ошибка теста: недостаточный таймаут security-scan job не был отклонён\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$ROOT_DIR/.gitea/workflows/docker-build.yaml" \
|
||||
"$FIXTURE_DIR/.gitea/workflows/docker-build.yaml"
|
||||
MANUAL_DEPLOY_GATE=" if: \${{ gitea.event_name == 'workflow_dispatch' }}"
|
||||
sed -i "/^ needs: \\[build-and-push-backend, build-and-push-frontend, security-scan\\]$/a\\$MANUAL_DEPLOY_GATE" \
|
||||
"$FIXTURE_DIR/.gitea/workflows/docker-build.yaml"
|
||||
if PROJECT_ROOT="$FIXTURE_DIR" bash "$CHECK_SCRIPT" >/dev/null 2>&1; then
|
||||
printf 'Ошибка теста: обязательный ручной production deploy не был отклонён\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$ROOT_DIR/.gitea/workflows/docker-build.yaml" \
|
||||
"$FIXTURE_DIR/.gitea/workflows/docker-build.yaml"
|
||||
sed -i '$a\# docker run --rm -v "${{ github.workspace }}/backend:/workspace"' \
|
||||
"$FIXTURE_DIR/.gitea/workflows/docker-build.yaml"
|
||||
if PROJECT_ROOT="$FIXTURE_DIR" bash "$CHECK_SCRIPT" >/dev/null 2>&1; then
|
||||
printf 'Ошибка теста: bind mount github.workspace не был отклонён\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Проверка закрепления артефактов завершена успешно\n'
|
||||
|
||||
75
scripts/test-run-ci-container.sh
Executable file
75
scripts/test-run-ci-container.sh
Executable file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
RUN_SCRIPT="$ROOT_DIR/scripts/run-ci-container.sh"
|
||||
FIXTURE_DIR=$(mktemp -d)
|
||||
CALL_LOG="$FIXTURE_DIR/docker-calls.log"
|
||||
FAKE_BIN="$FIXTURE_DIR/bin"
|
||||
SOURCE_DIR="$FIXTURE_DIR/source"
|
||||
EXTRA_SOURCE_DIR="$FIXTURE_DIR/extra-source"
|
||||
trap 'rm -rf "$FIXTURE_DIR"' EXIT
|
||||
|
||||
mkdir -p "$FAKE_BIN" "$SOURCE_DIR" "$EXTRA_SOURCE_DIR"
|
||||
printf 'fixture\n' > "$SOURCE_DIR/pom.xml"
|
||||
printf 'extra fixture\n' > "$EXTRA_SOURCE_DIR/backend.java"
|
||||
|
||||
cat > "$FAKE_BIN/docker" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf '%s\n' "$*" >> "$DOCKER_CALL_LOG"
|
||||
case "$1" in
|
||||
create)
|
||||
printf 'ci-container-id\n'
|
||||
;;
|
||||
start)
|
||||
exit "${DOCKER_START_EXIT_CODE:-0}"
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$FAKE_BIN/docker"
|
||||
|
||||
DOCKER_CALL_LOG="$CALL_LOG" PATH="$FAKE_BIN:$PATH" \
|
||||
bash "$RUN_SCRIPT" "$SOURCE_DIR" 'registry.local/test@sha256:abc' \
|
||||
--copy "$EXTRA_SOURCE_DIR" /backend \
|
||||
--env DOCKER_CONFIG=/workspace \
|
||||
-- sh -c 'exit 0'
|
||||
|
||||
grep --fixed-strings \
|
||||
'create --workdir /workspace --env DOCKER_CONFIG=/workspace registry.local/test@sha256:abc sh -c exit 0' \
|
||||
"$CALL_LOG" >/dev/null
|
||||
grep --fixed-strings \
|
||||
"cp $SOURCE_DIR/. ci-container-id:/workspace" \
|
||||
"$CALL_LOG" >/dev/null
|
||||
grep --fixed-strings \
|
||||
"cp $EXTRA_SOURCE_DIR/. ci-container-id:/backend" \
|
||||
"$CALL_LOG" >/dev/null
|
||||
grep --fixed-strings 'start --attach ci-container-id' "$CALL_LOG" >/dev/null
|
||||
grep --fixed-strings 'rm --force ci-container-id' "$CALL_LOG" >/dev/null
|
||||
if grep -Eq '(^|[[:space:]])(-v|--volume)([[:space:]]|=)' "$CALL_LOG"; then
|
||||
printf 'Ошибка теста: helper использовал bind mount\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
: > "$CALL_LOG"
|
||||
set +e
|
||||
DOCKER_CALL_LOG="$CALL_LOG" DOCKER_START_EXIT_CODE=17 PATH="$FAKE_BIN:$PATH" \
|
||||
bash "$RUN_SCRIPT" "$SOURCE_DIR" 'registry.local/test@sha256:abc' \
|
||||
-- sh -c 'exit 17' >/dev/null 2>&1
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
if (( status != 17 )); then
|
||||
printf 'Ошибка теста: код контейнера 17 превратился в %d\n' "$status" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep --fixed-strings 'rm --force ci-container-id' "$CALL_LOG" >/dev/null
|
||||
|
||||
if PATH="$FAKE_BIN:$PATH" bash "$RUN_SCRIPT" "$FIXTURE_DIR/missing" \
|
||||
'registry.local/test@sha256:abc' -- true >/dev/null 2>&1; then
|
||||
printf 'Ошибка теста: отсутствующий каталог исходников не был отклонён\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Проверка запуска CI-контейнеров без bind mount завершена успешно\n'
|
||||
Reference in New Issue
Block a user