docs: сортировка History по темам (sonnet, opus, llm, sse, ux-frontend, upload, infra, plans, tests, base, code-fixes)
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
# Диагностика ERR_CONNECTION_RESET в браузере
|
||||
|
||||
**Дата:** 2026-07-14
|
||||
|
||||
---
|
||||
|
||||
## Хронология
|
||||
|
||||
### Фаза 1: MTU (решено)
|
||||
|
||||
Проблема: 63KB POST из браузера → `ERR_CONNECTION_RESET`. Причина: MTU 1500 + Geneve 50 = 1550 > underlay 1450.
|
||||
Решение: Cilium MTU = 1400. Проверено curl с ВМ (30/30).
|
||||
|
||||
### Фаза 2: HTTP/2
|
||||
|
||||
HTTP/2 → `ERR_HTTP2_PROTOCOL_ERROR`. HTTP/1.1 → `ERR_CONNECTION_RESET`.
|
||||
Решение: `use-http2: "false"`.
|
||||
|
||||
### Фаза 3: Поиск корневой причины RST
|
||||
|
||||
Баг: браузер → свежая страница → POST 63KB → `ERR_CONNECTION_RESET`.
|
||||
|
||||
## Полная матрица тестов
|
||||
|
||||
| # | Сценарий | Результат |
|
||||
|---|----------|-----------|
|
||||
| 1 | Браузер: свежая страница → POST 63KB | ❌ RST / `⏳ 100%` hang |
|
||||
| 2 | Браузер: свежая страница → GET (до Flask) → POST 63KB | ✅ 200 OK |
|
||||
| 3 | Браузер: свежая страница → POST 10B → POST 63KB | ✅ 200 OK |
|
||||
| 4 | Браузер: свежая страница → GET (nginx сам, 405) → POST 63KB | ❌ RST |
|
||||
| 5 | `curl` с ВМ (5.172.178.213) → POST 63KB | ✅ всегда <100ms |
|
||||
| 6 | Сразу после `kubectl rollout restart` ingress → браузер | ✅ работает |
|
||||
| 7 | Через 5-10 мин после рестарта → браузер | ❌ снова RST |
|
||||
|
||||
## Что исключено
|
||||
|
||||
| Гипотеза | Статус | Почему |
|
||||
|----------|--------|--------|
|
||||
| MTU | ❌ исключено | 1400, curl с ВМ всегда работает |
|
||||
| HTTP/2 | ❌ исключено | `use-http2: false`, та же проблема |
|
||||
| XHR vs fetch | ❌ исключено | оба метода падают одинаково |
|
||||
| `proxy_request_buffering: off` | ❌ исключено | `on` для `location /` |
|
||||
| `client-body-timeout: 10` | ❌ исключено | 63KB < 1 сек |
|
||||
| FD/worker-лимиты | ❌ исключено | 1M FD, 16K connections, 62MB mem |
|
||||
| OOM/Restart | ❌ исключено | 0 рестартов, нет OOMKilled |
|
||||
| Stale upstream keepalive | ❌ исключено | `keepalive=0` не помогло |
|
||||
| nginx→Flask (upstream) | ❌ исключено | curl с ВМ доказывает что работает |
|
||||
|
||||
## Эксперименты
|
||||
|
||||
### Перезапуск ingress временно чинит
|
||||
|
||||
После `kubectl rollout restart` — браузер работает. Через 5-10 мин — снова RST. Накопление состояния.
|
||||
|
||||
### Ресурсы в норме
|
||||
|
||||
```
|
||||
worker_rlimit_nofile: 1047552 (1M FD)
|
||||
worker_connections: 16384
|
||||
worker_processes: 4
|
||||
Memory: 62 MiB, CPU: 3-7m
|
||||
Restarts: 0, OOMKilled: нет
|
||||
```
|
||||
|
||||
### `upstream-keepalive-connections: 0` — НЕ фикс
|
||||
|
||||
Баг вернулся через 5-10 минут. Keepalive pool не при чём.
|
||||
|
||||
## Критическое противоречие
|
||||
|
||||
- `curl` с ВМ (тот же датацентр) → POST 63KB → **всегда 200 OK <100ms**
|
||||
- Браузер через 5-10 мин после рестарта ingress → **ERR_CONNECTION_RESET**
|
||||
|
||||
curl и браузер идут через один VIP (185.247.187.151), на одни ingress-поды.
|
||||
Разница: клиент (curl vs Chrome) и сетевой путь (локальная сеть ДЦ vs интернет/VPN).
|
||||
|
||||
## Текущая конфигурация (2026-07-14)
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| `use-http2` | `"false"` |
|
||||
| `upstream-keepalive-connections` | `"0"` |
|
||||
| `error-log-level` | `debug` |
|
||||
| `proxy-body-size` | `1024m` |
|
||||
| `client-body-timeout` | `"10"` |
|
||||
| `keep-alive` | `"10"` |
|
||||
| Cilium MTU | `1400` |
|
||||
| `externalTrafficPolicy` | `Cluster` |
|
||||
| Ingress controller | `1.12.6` (Штурвал) |
|
||||
|
||||
---
|
||||
|
||||
## Вопрос к Claude (Sonnet/Opus)
|
||||
|
||||
### Симптом
|
||||
|
||||
Браузер (Chrome/Electron, Windows) → POST 63KB multipart/form-data → `ERR_CONNECTION_RESET` или зависание на `⏳ 100%`. Тело уходит полностью, ответ не приходит — TCP RST.
|
||||
|
||||
### Окружение
|
||||
|
||||
```
|
||||
Кластер: bare-metal K8s 1.34.1, 4 ноды (3 worker + 1 control-plane)
|
||||
CNI: Cilium Geneve, MTU подов = 1400
|
||||
VIP: kube-vip 185.247.187.151 (ARP на все 4 ноды)
|
||||
Ingress: nginx-ingress (Штурвал Helm), контроллер 1.12.6
|
||||
2 реплики, externalTrafficPolicy: Cluster
|
||||
Приложение: Flask/Waitress, 1 реплика (Штурвал managed)
|
||||
```
|
||||
|
||||
Конфиг ingress:
|
||||
```yaml
|
||||
use-http2: "false"
|
||||
upstream-keepalive-connections: "0"
|
||||
client-body-timeout: "10"
|
||||
keep-alive: "10"
|
||||
worker-processes: 4
|
||||
worker-rlimit-nofile: 1047552
|
||||
worker-connections: 16384
|
||||
proxy-body-size: 1024m
|
||||
proxy-request-buffering: on # для location /
|
||||
error-log-level: debug
|
||||
```
|
||||
|
||||
### Критическое противоречие
|
||||
|
||||
- `curl` с ВМ (5.172.178.213, тот же датацентр) → POST 63KB → **всегда 200 OK <100ms**
|
||||
- Браузер через 5-10 мин после рестарта ingress → **ERR_CONNECTION_RESET**
|
||||
- Сразу после рестарта ingress → браузер работает
|
||||
|
||||
curl и браузер идут через один VIP (185.247.187.151), на одни ingress-поды. Разница: клиент (curl vs Chrome) и сетевой путь до VIP (локальная сеть ДЦ vs интернет/VPN).
|
||||
|
||||
### Вопросы
|
||||
|
||||
1. Почему рестарт ingress помогает на 5-10 минут, а потом браузерные запросы снова получают RST, при том что curl с ВМ продолжает работать?
|
||||
|
||||
2. Может ли `externalTrafficPolicy: Cluster` + kube-vip создавать conntrack-записи, которые со временем ломают соединения из интернета (с другими TCP options/MSS) но не из локальной сети?
|
||||
|
||||
3. Стоит ли попробовать `externalTrafficPolicy: Local`? Какие риски?
|
||||
|
||||
4. Какие ещё эксперименты можно провести без tcpdump (нет доступа к нодам по SSH, нет tcpdump в подах)?
|
||||
|
||||
---
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Ответ от Claude
|
||||
- [ ] `externalTrafficPolicy: Local` — эксперимент
|
||||
- [ ] Воспроизвести с `curl` из WSL на машине пользователя
|
||||
- [ ] tcpdump через `kubectl debug` (требует координации)
|
||||
- [ ] Обновить ingress controller 1.12.6 → актуальный
|
||||
- [ ] Проверить `client-body-timeout: 60` + `keep-alive: 75`
|
||||
|
||||
---
|
||||
|
||||
## Ответ Claude (Sonnet) — 2026-07-14
|
||||
|
||||
### Два независимых механизма
|
||||
|
||||
Симптом объясняется наложением двух механизмов:
|
||||
|
||||
### Механизм 1: kube-vip ARP flapping + conntrack asymmetry (основной)
|
||||
|
||||
Именно это объясняет 5-10-минутное окно и разницу между browser и curl.
|
||||
|
||||
**Как работает с `externalTrafficPolicy: Cluster`:**
|
||||
|
||||
```
|
||||
Browser → VIP → Node N1 (kube-vip leader) → iptables DNAT+SNAT → nginx pod
|
||||
│
|
||||
└── conntrack entry создаётся на N1:
|
||||
(browser_ip:srcport → 185.247.187.151:443)
|
||||
↔ (N1_ip:eph → nginx_pod:443)
|
||||
```
|
||||
|
||||
Проблема возникает когда kube-vip меняет лидера:
|
||||
|
||||
```
|
||||
kube-vip: N1 → N2 (истёк lease или GARP от N2)
|
||||
Коммутатор: обновляет ARP-кэш, VIP теперь на N2
|
||||
|
||||
Browser (старое HTTP keep-alive соединение):
|
||||
→ PSH/ACK 63KB → попадает на N2
|
||||
N2: нет conntrack записи → TCP RST
|
||||
|
||||
curl (новое соединение):
|
||||
→ SYN → попадает на N2
|
||||
N2: создаёт новую conntrack → всё ОК
|
||||
```
|
||||
|
||||
**Почему 5-10 минут:** время жизни ARP-записи на коммутаторе или интервал переизбрания kube-vip лидера. "ARP на все 4 ноды" — периодические GARP-анонсы могут уходить с разных нод.
|
||||
|
||||
**Почему GET-to-Flask чинит последующий POST (тесты #2 vs #4):**
|
||||
- GET → Flask: создаётся *новое* TCP-соединение (новый SYN) → правильный conntrack на текущей ноде. POST идёт по *тому же* соединению → conntrack жив → OK.
|
||||
- GET → 405: nginx отвечает сам и закрывает соединение с `Connection: close`. POST создаёт *новое* соединение, но VIP уже на другой ноде → RST.
|
||||
|
||||
### Механизм 2: `keepalive_timeout: 10s` — слишком агрессивно
|
||||
|
||||
`keep-alive: "10"` в ConfigMap = `keepalive_timeout 10s` в nginx.
|
||||
|
||||
```
|
||||
T=0: Browser загружает страницу (GET /)
|
||||
T=10: nginx закрывает idle соединение (отправляет FIN)
|
||||
T=?: Пользователь заполняет форму (>10 сек → почти всегда)
|
||||
T=X: Browser отправляет POST по "живому" с его точки зрения соединению
|
||||
→ nginx уже закрыл socket → RST
|
||||
```
|
||||
|
||||
Браузер не всегда замечает FIN немедленно (TCP буферизация ОС). curl — каждый раз новое соединение.
|
||||
|
||||
### Почему curl всегда работает
|
||||
|
||||
| | Browser | curl |
|
||||
|---|---|---|
|
||||
| Соединение | HTTP keep-alive, долгоживущее | Новое для каждого запроса |
|
||||
| conntrack | Создано при SYN, может устареть | Новый SYN на каждый вызов |
|
||||
| keepalive | Держит idle >10s | Сразу POST → не ждёт |
|
||||
| VIP flap | Пакет идёт на новую ноду без conntrack | SYN на новой ноде создаёт conntrack |
|
||||
|
||||
### Ключевой инсайт
|
||||
|
||||
> Разница не в TCP-опциях (MSS, window size). Разница в **типе пакета**: curl всегда начинает с SYN → conntrack создаётся на правильной ноде. Browser держит соединение между запросами → когда VIP мигрирует, браузерные пакеты (ACK/PSH) приходят на новую ноду без предшествующего SYN → нет conntrack-записи → RST.
|
||||
|
||||
### Итоговая таблица причин
|
||||
|
||||
| Причина | Механизм | Почему не curl |
|
||||
|---|---|---|
|
||||
| kube-vip ARP lease renewal | Смена ноды → conntrack orphan | Новый SYN всегда |
|
||||
| `keepalive_timeout: 10s` | nginx закрывает idle conn → browser не замечает | Нет idle |
|
||||
|
||||
Оба фактора вместе: рестарт сбрасывает состояние, 5-10 минут → kube-vip переизбирается / браузер держит соединение дольше 10s → RST.
|
||||
|
||||
### Рекомендации
|
||||
|
||||
1. **`keep-alive: "75"`** — исключить механизм #2 (самое простое, безопасное)
|
||||
2. **`externalTrafficPolicy: Local`** — исключить механизм #1 (риск: потеря source IP для других сервисов)
|
||||
3. **JS: Connection warmer** — GET на Flask при загрузке страницы (уже доказано тестом #2)
|
||||
4. **JS: пересоздавать соединение** — использовать `fetch()` без keep-alive (credentials: 'omit' или отдельный subdomain)
|
||||
@@ -0,0 +1,158 @@
|
||||
# Cluster Dump — 2026-07-15 17:00 MSK
|
||||
|
||||
## 1. НОДЫ (4 шт)
|
||||
|
||||
| Нода | Роль | Taints |
|
||||
|------|------|--------|
|
||||
| iot-naeel-control-plane-xb699 | control-plane | `NoSchedule:control-plane` |
|
||||
| iot-naeel-workers-vqphm-6f74n | workers | нет |
|
||||
| iot-naeel-workers-vqphm-bhbvs | workers | нет |
|
||||
| iot-naeel-workers-vqphm-v8zq4 | workers | нет |
|
||||
|
||||
## 2. INGRESS (shturval-ingress-controller)
|
||||
|
||||
### Поды (2 реплики)
|
||||
|
||||
| Под | Нода | Возраст |
|
||||
|-----|------|---------|
|
||||
| ...86jsp | control-plane-xb699 | ~6ч |
|
||||
| ...jm4kl | workers-vqphm-v8zq4 | ~6ч |
|
||||
|
||||
### Сервис
|
||||
- Тип: LoadBalancer
|
||||
- External IP: 185.247.187.151
|
||||
- `externalTrafficPolicy: Cluster`
|
||||
- NodePorts: 80:30739, 443:32391
|
||||
|
||||
### Ingress ConfigMap
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| keep-alive | **10** |
|
||||
| client-body-timeout | **10** |
|
||||
| client-header-timeout | **10** |
|
||||
| proxy-body-size | **8m** |
|
||||
| upstream-keepalive-connections | **0** |
|
||||
| use-http2 | false |
|
||||
| worker-processes | 4 |
|
||||
| error-log-level | info |
|
||||
|
||||
### Helm
|
||||
- Chart: shturval-ingress-controller-2.12.1
|
||||
- Ревизия 9 (сегодня 11:53) — `replicaCount: 2`, hostPort enabled
|
||||
- Ревизия 8 (сегодня 02:00) — то же самое
|
||||
- Более старых ревизий нет (почищены)
|
||||
|
||||
### Affinity (ingress deploy)
|
||||
```yaml
|
||||
preferredDuringScheduling:
|
||||
- weight: 10 → избегать control-plane
|
||||
- weight: 80 → предпочитать node-role: ingress
|
||||
- weight: 50 → предпочитать node-role: infra
|
||||
```
|
||||
|
||||
### События (каждые 51 сек!)
|
||||
```
|
||||
Warning SyncLoadBalancerFailed failed to ensure load balancer: no address pools could be found
|
||||
Normal EnsuringLoadBalancer Ensuring load balancer
|
||||
```
|
||||
|
||||
## 3. KUBE-VIP
|
||||
|
||||
### DaemonSet: shturval-vip (4 пода, на ВСЕХ 4 нодах)
|
||||
- Image: r.shturval.tech/kube-vip:v1.0.0
|
||||
- ARP mode: `vip_arp: true`
|
||||
- Election: `svc_election: true`
|
||||
- Leaderelection: `vip_leaderelection: false`
|
||||
- Lease: `sht-uservip-cp-lock`
|
||||
|
||||
### Cloud Provider: shturval-vip-provider (1 под, на bhbvs)
|
||||
- Image: r.shturval.tech/kubevip/kube-vip-cloud-provider:v0.0.12
|
||||
- Ищет ConfigMap: `kubevip` в неймспейсе `kube-system`
|
||||
|
||||
### ❌ ConfigMap `kubevip` в `kube-system` — **ПУСТОЙ!**
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: kubevip
|
||||
namespace: kube-system
|
||||
data: {} # <-- НЕТ ДАННЫХ
|
||||
```
|
||||
|
||||
**Это причина ошибки «no address pools could be found»!**
|
||||
|
||||
## 4. DRHIDER (pythonk8s)
|
||||
|
||||
### Деплой
|
||||
- 1 реплика
|
||||
- Ревизия: 49
|
||||
- Инит-контейнер: клонирует из Git, потом pip install + python app.py
|
||||
- Ресурсы: 500m CPU, 1Gi RAM
|
||||
- Проба: TCP :5000
|
||||
|
||||
### ❌ PodAffinity (добавлен сегодня 16:46, непрошеный)
|
||||
```yaml
|
||||
requiredDuringScheduling:
|
||||
podAffinity → ingress pod на той же ноде
|
||||
```
|
||||
|
||||
### Сервис
|
||||
- ClusterIP: 10.102.171.63
|
||||
- Port 80 → targetPort 5000
|
||||
- `internalTrafficPolicy: Cluster`
|
||||
|
||||
### Ingress (drhider)
|
||||
- Host: drhider.pythonk8s.dev.nubes.ru
|
||||
- Аннотации: proxy-body-size=1024m, connect=120s, read/send=600s
|
||||
- TLS: letsencrypt-prod
|
||||
|
||||
### Поды
|
||||
| Под | Нода | Возраст |
|
||||
|-----|------|---------|
|
||||
| pythonk8s-6698c6c78-dkwsq | workers-vqphm-v8zq4 | ~15 мин |
|
||||
|
||||
Drhider на одной ноде с ingress-подом jm4kl.
|
||||
|
||||
## 5. ВСЕ INGRESS-РЕСУРСЫ
|
||||
|
||||
| Хост | Неймспейс | Возраст |
|
||||
|------|-----------|---------|
|
||||
| drhider.pythonk8s.dev.nubes.ru | 20a75175-... | 4д |
|
||||
| loadtest.pythonk8s.dev.nubes.ru | 9039a501-... | 5д |
|
||||
| contractor.pythonk8s.dev.nubes.ru | b4523aba-... | 7ч |
|
||||
| capire.kube5s.ru | default | 54д |
|
||||
| grafana.kube5s.ru | grafana | 66д |
|
||||
| keycloak.k8c.ru | keycloak | 96д |
|
||||
| qu.kube5s.ru | shared-sqs | 93д |
|
||||
| iot.kube5s.ru | sless | 94д |
|
||||
| terra.k8c.ru | terra | 92д |
|
||||
|
||||
## 6. CILIUM
|
||||
|
||||
4 пода (по одному на ноду), все Running. MTU был изменён на 1400 (по STATE).
|
||||
|
||||
## 7. КЛЮЧЕВЫЕ НАХОДКИ
|
||||
|
||||
### 🔴 ConfigMap `kubevip` пустой
|
||||
kube-vip-cloud-provider не может найти address pools → каждые 51 сек ошибка SyncLoadBalancerFailed. При этом `185.247.187.151` работает через ARP-mode kube-vip DaemonSet (не через cloud provider). Вероятно, это не fatal, но указывает на недонастроенную интеграцию.
|
||||
|
||||
### 🔴 Ingress ConfigMap отличается от ожидаемого
|
||||
- keep-alive: 10 (должен быть 75)
|
||||
- client-body-timeout: 10 (должен быть 60)
|
||||
- client-header-timeout: 10 (должен быть 30)
|
||||
- proxy-body-size: 8m (должен быть 1024m)
|
||||
|
||||
Но для drhider это переопределено в аннотациях ingress-ресурса.
|
||||
|
||||
### 🟡 Ingress 2 реплики на 4 нодах с kube-vip
|
||||
kube-vip анонсирует 185.247.187.151 на ВСЕХ 4 нодах, но ingress-поды только на 2. При `externalTrafficPolicy: Cluster` трафик на ноды без ingress: SNAT → кросс-нода → риск conntrack RST.
|
||||
|
||||
### 🟡 Helm: 2 апгрейда сегодня
|
||||
Ревизии 8 (02:00) и 9 (11:53) сегодня. Кто-то обновлял ingress. `replicaCount: 2` в обеих.
|
||||
|
||||
### 🟢 Аннотации drhider ingress — хорошие
|
||||
proxy-body-size=1024m, connect=120s, read/send=600s. Переопределяют дефолты ConfigMap.
|
||||
|
||||
### 🟢 Cilium — стабильный
|
||||
4 пода, все Running, MTU 1400.
|
||||
@@ -0,0 +1,204 @@
|
||||
# DrHider — итоговая диагностика ERR_CONNECTION_RESET
|
||||
|
||||
**Дата:** 2026-07-15 (финал)
|
||||
**Версия:** v0.0.29
|
||||
|
||||
---
|
||||
|
||||
## Резюме (моё мнение)
|
||||
|
||||
**`keep-alive: 10` — единственная причина ERR_CONNECTION_RESET.** Всё остальное (conntrack, hostPort, ARP flapping, svc_election) — шум, который мы искали 2 дня.
|
||||
|
||||
### Почему это так
|
||||
|
||||
VIP `185.247.187.151` жёстко привязан к control-plane-xb699 через аннотацию `kube-vip.io/vipHost`. Никакого flapping нет — трафик всегда приходит на одну ноду. Второй ingress-под на v8zq4 — избыточен, но не мешает.
|
||||
|
||||
Сценарий:
|
||||
1. Браузер загружает страницу — opens TCP
|
||||
2. Пользователь выбирает файлы — проходит >10 сек
|
||||
3. Nginx (keep-alive=10) закрывает idle соединение
|
||||
4. Браузер не замечает FIN (буферизация ОС)
|
||||
5. POST 63KB летит в мёртвый сокет → RST
|
||||
|
||||
### Почему предыдущие гипотезы неверны
|
||||
|
||||
| Гипотеза | Опровержение |
|
||||
|----------|-------------|
|
||||
| kube-vip ARP flapping | `vipHost` фиксирует VIP на одной ноде. Lease мёртв — плевать |
|
||||
| hostPort на 2 из 4 нод | Трафик не на все 4 ноды, а только на control-plane |
|
||||
| upstream keepalive stale | `keepalive=0` не помог |
|
||||
| MTU | Решено ещё 14-го, не при чём |
|
||||
| conntrack | ETPolicy Cluster не при чём — трафик на одну ноду |
|
||||
|
||||
### Что подтверждает keep-alive гипотезу
|
||||
|
||||
| Тест | Объяснение |
|
||||
|------|-----------|
|
||||
| Сразу после рестарта ingress → OK | Все соединения свежие |
|
||||
| Через 5-10 мин → RST | Соединения постарели >10с |
|
||||
| GET/health → POST → OK | Новое живое соединение |
|
||||
| curl с ВМ → всегда OK | curl открывает новый SYN каждый раз |
|
||||
| `keep-alive: 75` + JS warmup → OK на 10+ мин | Увеличили окно, warmup греет |
|
||||
|
||||
## Хронология
|
||||
|
||||
| Дата | Событие |
|
||||
|------|---------|
|
||||
| 2026-07-11 | Исходный деплой |
|
||||
| 2026-07-14 (день) | MTU fix (Cilium 1400) — решило 51с задержку |
|
||||
| 2026-07-14 (вечер) | RST диагностика: keep-alive 75 + JS warmup — частично |
|
||||
| 2026-07-15 (11:53) | Helm ревизия 9 — сброс ConfigMap на дефолты (keep-alive:10) |
|
||||
| 2026-07-15 | podAffinity `required` на drhider (ручной kubectl edit) |
|
||||
| 2026-07-15 (16:00) | Полная диагностика: hostPort vs cloud LB |
|
||||
|
||||
---
|
||||
|
||||
## Две независимые проблемы
|
||||
|
||||
### Проблема #1: MTU (решена 2026-07-14)
|
||||
|
||||
**Симптом:** 51с задержка при POST любого размера через внешний VIP.
|
||||
**Причина:** Geneve +50, underlay 1450, дефолт 1500 → фрагментация/дроп.
|
||||
**Решение:** Cilium `mtu: 1400`.
|
||||
|
||||
### Проблема #2: ERR_CONNECTION_RESET (хост порт, решена 2026-07-15)
|
||||
|
||||
**Симптом:** Браузер получает TCP RST при POST 63KB через 5-10 мин после рестарта ingress.
|
||||
**Ранее считалось:** keep-alive 10с или kube-vip conntrack.
|
||||
**Реальная причина:** Штурвал балансирует трафик на **все 4 ноды**, но ingress стоит `replicas: 2` с **hostPort** — порт 80/443 открыт только на 2 нодах.
|
||||
|
||||
```
|
||||
Browser → cloud LB → любая из 4 нод
|
||||
├── нода с ingress-подом → hostPort 80 → nginx → ✅
|
||||
└── нода БЕЗ ingress-пода → порт 80 не слушается → RST ❌
|
||||
```
|
||||
|
||||
**50% запросов попадает на пустую ноду → RST.**
|
||||
|
||||
---
|
||||
|
||||
## Подтверждающие данные
|
||||
|
||||
### Helm ревизии (дамп)
|
||||
|
||||
| Ревизия | Время | replicaCount | Другие изменения |
|
||||
|---------|-------|-------------|------------------|
|
||||
| 1 (деплой) | 2026-07-11 | 2 | — |
|
||||
| 8 | 2026-07-15 02:00 | **2** | — |
|
||||
| 9 | 2026-07-15 11:53 | **2** | — |
|
||||
|
||||
Helm **не менял** replicaCount. Всегда 2. Но ConfigMap сбрасывал на дефолты (keep-alive: 10, proxy-body-size: 8m и т.д.).
|
||||
|
||||
### Ingress ConfigMap (последствия Helm upgrade)
|
||||
|
||||
```yaml
|
||||
# Было (ручные правки 2026-07-14) → Стало (после ревизии 9)
|
||||
keep-alive: "75" → "10" # вернулось на дефолт Штурвала
|
||||
proxy-body-size: "1024m" → "8m" # тоже сброшено
|
||||
error-log-level: debug → info # сброшено
|
||||
upstream-keepalive-connections: "0" → сохранилось
|
||||
```
|
||||
|
||||
### kube-vip svc_election — мёртв
|
||||
|
||||
```yaml
|
||||
# Lease ingress/kubevip-shturval-ingress-controller-controller
|
||||
holderIdentity: "" # пусто — никто не держит
|
||||
leaseDurationSeconds: 1 # аномально короткий (норма 15-30)
|
||||
renewTime: 2026-07-11T14:55:28Z # 4 дня назад не обновлялся
|
||||
leaseTransitions: 17 # но было 17 переходов
|
||||
```
|
||||
|
||||
svc_election никогда не работал. VIP назначен через `ipMode: VIP` в статусе сервиса, работает на уровне облака (BGP/маршрутизация).
|
||||
|
||||
### hostPort
|
||||
|
||||
```yaml
|
||||
# В шаблоне пода ingress (Helm template)
|
||||
hostPort: 80
|
||||
hostPort: 443
|
||||
```
|
||||
|
||||
Включён в ревизиях 8 и 9. hostPort = порт слушается ТОЛЬКО на нодах где стоит под.
|
||||
|
||||
### podAffinity на drhider — временный костыль
|
||||
|
||||
```json
|
||||
{
|
||||
"podAffinity": {
|
||||
"requiredDuringSchedulingIgnoredDuringExecution": [{
|
||||
"labelSelector": {"matchLabels": {"app.kubernetes.io/name": "shturval-ingress-controller"}},
|
||||
"namespaceSelector": {"matchLabels": {"name": "ingress"}},
|
||||
"topologyKey": "kubernetes.io/hostname"
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Добавлен через `kubectl edit`, НЕ через Helm. Привязывает drhider к ноде с ingress-подом. Это НЕ лечит RST (RST на уровне VIP→нода, не на уровне нода→drhider). При следующем `helm upgrade` — исчезнет.
|
||||
|
||||
### Kube-vip DaemonSet — не участвует
|
||||
|
||||
```yaml
|
||||
vip_arp: true # ARP включён
|
||||
vip_leaderelection: false # нет CP election
|
||||
svc_election: true # должен быть — но lease мёртв
|
||||
```
|
||||
|
||||
Даемоны не логгируют VIP `185.247.187.151`. Трафик распределяет облако, не kube-vip.
|
||||
|
||||
---
|
||||
|
||||
## Решение
|
||||
|
||||
**Единственное полное решение:** ingress-под на каждой ноде куда приходит трафик.
|
||||
|
||||
### Вариант 1 (рекомендуемый): увеличить replicas
|
||||
|
||||
```yaml
|
||||
# Helm values
|
||||
shturval-ingress-controller:
|
||||
replicaCount: 4
|
||||
```
|
||||
|
||||
Или выяснить у DevOps сколько нод в LB-пуле Штурвала и поставить `replicaCount = число нод`.
|
||||
|
||||
### Вариант 2 (если балансировка на все worker'ы): DaemonSet
|
||||
|
||||
```yaml
|
||||
# Helm values
|
||||
shturval-ingress-controller:
|
||||
kind: DaemonSet
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Что делать сейчас (ручной воркараунд)
|
||||
|
||||
### 1. Починить ConfigMap (срочно)
|
||||
|
||||
```bash
|
||||
kubectl patch configmap -n ingress shturval-ingress-controller-controller --type merge \
|
||||
-p '{"data":{"keep-alive":"75","proxy-body-size":"1024m","client-body-timeout":"60","client-header-timeout":"30"}}'
|
||||
```
|
||||
|
||||
### 2. Масштабировать ingress (временный фикс)
|
||||
|
||||
```bash
|
||||
kubectl scale deploy -n ingress shturval-ingress-controller-controller --replicas=4
|
||||
```
|
||||
|
||||
Поды раскидаются по всем 4 нодам → hostPort на всех → 0% RST.
|
||||
|
||||
⚠️ **Предупреждение:** после следующего `helm upgrade`:
|
||||
- ConfigMap сбросится (нужно править Helm values)
|
||||
- replicas вернётся на 2
|
||||
- podAffinity на drhider исчезнет
|
||||
|
||||
---
|
||||
|
||||
## Вопросы к DevOps
|
||||
|
||||
1. **Сколько нод в LB-пуле Штурвала?** (на какие ноды облако направляет трафик ingress?)
|
||||
2. **Можно ли увеличить `replicaCount` до числа нод?**
|
||||
3. **Как правильно изменить Helm values для ingress?** (чтобы ConfigMap не сбрасывался при upgrade)
|
||||
@@ -0,0 +1,37 @@
|
||||
# v0.0.46 — удалён process_names + git push HTTP/2 проблема — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.45 → 0.0.46
|
||||
|
||||
---
|
||||
|
||||
## Удалён тестовый API process_names
|
||||
|
||||
- `process_names` (обфускация файлов по именам из TEST_INPUT_DIR) **удалён**.
|
||||
- Причина: сервер не может читать локальные файлы клиента (нет доступа к его
|
||||
машине). Файлы в поде — неудобно. API бесполезен.
|
||||
- Удалены: endpoint, `TEST_INPUT_DIR`/`_TEST_API_ENABLED`, `import os`, History-файл.
|
||||
- Версия 0.0.46.
|
||||
|
||||
## Находка: git push к gitea зависал (HTTP/2)
|
||||
|
||||
**Симптом:** `git push origin master` висел бесконечно (таймауты), хотя
|
||||
`git ls-remote` и `curl` к gitea работали (200, быстро).
|
||||
|
||||
**Причина:** git использовал **HTTP/2** для отправки pack; шлюз
|
||||
`gitea.services.ngcloud.ru` блокировал передачу данных по HTTP/2
|
||||
(та же проблема, что была в drhider с HTTP/2 — ERR_HTTP2_PROTOCOL_ERROR).
|
||||
|
||||
**Решение:** push через HTTP/1.1 мгновенно прошёл:
|
||||
```bash
|
||||
git -c http.version=HTTP/1.1 push origin master
|
||||
# и закреплено в конфиге:
|
||||
git config http.version HTTP/1.1
|
||||
```
|
||||
|
||||
**Проверка:** `8d7a7df..d2f2bd3 master -> master`, синхронизировано.
|
||||
|
||||
## Полезно
|
||||
- На этой машине/шлюзе для git (и возможно других HTTP/2 клиентов) использовать
|
||||
HTTP/1.1.
|
||||
- git config `http.version HTTP/1.1` уже установлен.
|
||||
@@ -0,0 +1,124 @@
|
||||
# ПЛАН: перенос паттерна «ВМ-буфер + pull» в drhider
|
||||
|
||||
_Дата: 2026-08-21. Основа: `contracts/loadtest/History/2026-08-21-pattern-vm-buffer-pull.md`._
|
||||
_Статус: РЕАЛИЗОВАНО 2026-08-21 (v0.0.58), см. `2026-08-21-vm-upload-implemented.md`._
|
||||
|
||||
---
|
||||
|
||||
## Цель
|
||||
|
||||
drhider (Managed Flask, Штурвал, `drhider.pythonk8s.dev.nubes.ru`) получает файлы через
|
||||
`POST /api/upload` (multipart). Входной шлюз managed-кластера обрывает тела >~64КБ
|
||||
(HTTP 000 ~10с). Решение — загружать файл на ВМ напрямую, Flask тянет сам (egress).
|
||||
|
||||
## Директива (обязательно)
|
||||
|
||||
**ВСЕ файловые операции загрузки в Flask (из браузера или вообще снаружи) — ТОЛЬКО через ВМ.**
|
||||
Никаких прямых `POST /api/upload` с телом файла во Flask.
|
||||
|
||||
## Среда (уточнено 2026-08-21)
|
||||
|
||||
- `iot-naeel` — СОБСТВЕННЫЙ кластер, параметры/аннотации правились через kubectl. Пока
|
||||
тестируем/смотрим поведение на нём (лимит 64КБ там может не воспроизводиться).
|
||||
- В дальнейшем деплой — на ОБЫЧНЫЙ managed-кластер (лимит 64КБ будет реальным).
|
||||
- ВМ — ОДНА (`5.172.178.213`, `contracts.kube5s.ru`), та же, что для loadtest. Location
|
||||
`/drhider-upload/` добавляется в существующий nginx рядом с `/lt-serve/`.
|
||||
- Вывод: VM-путь строим СЕЙЧАС, независимо от поведения iot-naeel — прод всё равно на обычном.
|
||||
|
||||
## Текущий поток
|
||||
|
||||
`uploadFiles()` (index.html ~413): по одному файлу в цикле → `FormData` → `POST /api/upload`.
|
||||
Бэк: `MAX_CONTENT_LENGTH=200МБ`, сессия 500МБ. HTTP-клиент — **httpx** (не requests).
|
||||
|
||||
## Целевой поток
|
||||
|
||||
```
|
||||
Браузер ──(PUT, файл целиком)──▶ ВМ nginx (без лимита 64КБ)
|
||||
│ └─ получил URL + токен
|
||||
│
|
||||
└──▶ Flask POST /api/upload_refs (JSON: [{name,size,url}], <64КБ)
|
||||
│
|
||||
▼
|
||||
Flask egress GET с ВМ ──▶ читает файл ──▶ add_file() ──▶ дальше как сейчас
|
||||
```
|
||||
|
||||
## Изменения (4 места)
|
||||
|
||||
### 1. ВМ `5.172.178.213`, `nginx-contracts.conf` — приём загрузки
|
||||
Новый location (рядом с `/lt-serve/`), приём через PUT + статическая отдача:
|
||||
```nginx
|
||||
location /drhider-upload/ {
|
||||
alias /var/www/drhider-upload/;
|
||||
dav_methods PUT;
|
||||
create_full_put_path on;
|
||||
client_max_body_size 1024m;
|
||||
# CORS: браузер с drhider.pythonk8s.dev.nubes.ru шлёт PUT (не simple → preflight)
|
||||
add_header Access-Control-Allow-Origin https://drhider.pythonk8s.dev.nubes.ru always;
|
||||
add_header Access-Control-Allow-Methods 'PUT, GET, OPTIONS, DELETE' always;
|
||||
add_header Access-Control-Allow-Headers 'Content-Type' always;
|
||||
if ($request_method = OPTIONS) { return 204; }
|
||||
}
|
||||
```
|
||||
- Каталог `/var/www/drhider-upload/` (владелец `www-data`), НЕ `/home/naeel` (403).
|
||||
- Порядок: backup → правка → `nginx -t` → `systemctl reload nginx`.
|
||||
|
||||
### 2. Flask — новый endpoint pull (`site/routes/api_bp.py`)
|
||||
```python
|
||||
@api_bp.route("/upload_refs", methods=["POST"])
|
||||
def upload_refs():
|
||||
data = request.get_json(silent=True) or {}
|
||||
sid = data.get("session") or create_session()
|
||||
refs = data.get("files") or []
|
||||
if not refs:
|
||||
return jsonify({"ok": False, "error": "No files"}), 400
|
||||
added = 0
|
||||
with httpx.Client(timeout=120, follow_redirects=True) as client:
|
||||
for ref in refs:
|
||||
name, url = ref.get("name"), ref.get("url")
|
||||
if not name or not url:
|
||||
continue
|
||||
content = b"".join(client.stream("GET", url).iter_bytes()) # egress, по частям
|
||||
if not add_file(sid, name, content):
|
||||
return jsonify({"ok": False, "error": "Session not found"}), 404
|
||||
added += 1
|
||||
return jsonify({"ok": True, "session": sid, "count": file_count(sid)})
|
||||
```
|
||||
- Импорт `httpx` (уже в requirements.txt) и `file_count` из session.
|
||||
- `stream(...).iter_bytes()` — НЕ `content` целиком (OOM при 100МБ+).
|
||||
- Обработка ошибок egress (таймаут/403/404) → вернуть 502 с именем файла.
|
||||
|
||||
### 3. Фронт (`site/templates/index.html`, `uploadFiles()`)
|
||||
Разбить «Фазу 1» на два шага:
|
||||
- **Шаг 1a:** каждый файл → `fetch(<ВМ>/drhider-upload/<token>/<name>, {method:'PUT', body:file})`,
|
||||
собирать `[{name, size, url}]`. Прогресс — `xhr.upload.onprogress` заменить на `fetch` +
|
||||
`ReadableStream` (или оставить XHR, но на PUT к ВМ).
|
||||
- **Шаг 1b:** один `POST /api/upload_refs` с JSON `{session, files:[...]}` (<64КБ).
|
||||
- Токен: `crypto.randomUUID()`. URL ВМ — константа/`data-атрибут`.
|
||||
- Показывать прогресс «загрузка на ВМ» отдельно от «загрузка в Flask».
|
||||
|
||||
### 4. `requirements.txt` — без изменений (httpx уже есть).
|
||||
|
||||
## Безопасность и жизненный цикл
|
||||
- Токен в пути URL — только `crypto.randomUUID()`, не переиспользуется.
|
||||
- Flask после `add_file` делает `DELETE` на URL ВМ (или ВМ чистит по TTL — cron/systemd-timer
|
||||
`find /var/www/drhider-upload -mmin +30 -delete`).
|
||||
- ВМ отдаёт файл только по валидному URL с токеном (нет открытого листинга: `autoindex off`).
|
||||
|
||||
## Грабли (из pattern-дока + drhider)
|
||||
- CORS: PUT — не simple-метод → браузер шлёт OPTIONS preflight; nginx обязан отвечать 204.
|
||||
- `site/` конфликтует со stdlib `site.py` (для loadtest; у drhider — Штурвал, без gunicorn).
|
||||
- egress через `httpx` с `stream`, timeout 120; большие ОТВЕТЫ проходят (проверено 50МБ).
|
||||
- OOM: не читать файл в память целиком; сессия 500МБ уже ограничивает.
|
||||
- Локальные curl на Krupski идут через прокси `172.17.192.1:10808` → для теста ВМ `--noproxy '*'`.
|
||||
|
||||
## Порядок работ + проверка
|
||||
1. ВМ nginx (location + каталог + CORS) → проверить `curl --noproxy '*' -X PUT`.
|
||||
2. Flask `/api/upload_refs` → проверить `python3 -c "import py_compile; py_compile.compile(...)"`.
|
||||
3. Фронт `uploadFiles()` → `node -c` нет (это EJS/HTML+JS внутри шаблона) — ручная проверка.
|
||||
4. Сквозной тест: файл 10МБ → ВМ → Flask → обфускация → ZIP.
|
||||
5. Commit + push (правило: после правки + bump версии `VERSION` в `site/app.py`).
|
||||
|
||||
## Решения (закрыты)
|
||||
|
||||
- Кластер: сейчас `iot-naeel` (тест), прод — обычный managed. VM-путь обязателен.
|
||||
- ВМ: одна, `contracts.kube5s.ru` (`5.172.178.213`), общая с loadtest — добавляем location.
|
||||
@@ -0,0 +1,75 @@
|
||||
# ПЛАН: универсальный файловый сервис на ВМ (API)
|
||||
|
||||
_Дата: 2026-08-21. Статус: РЕАЛИЗОВАНО И ЗАДЕПЛОЕНО (v1.0.0), см. `README.md` сервиса `/home/naeel/nubes/vmfiles/` и `2026-08-21-vm-upload-implemented.md`._
|
||||
|
||||
## Цель
|
||||
|
||||
Один внутренний (НЕ публичный) сервис на ВМ `5.172.178.213` (`contracts.kube5s.ru`)
|
||||
для приёма/временного хранения/выдачи файлов. Потребители: **drhider**, **contracts**,
|
||||
**SQS IoT**. Заменяет сырой nginx-dav `/drhider-upload/` на задокументированный,
|
||||
версионированный API, чтобы любому сервису было ясно, как им пользоваться.
|
||||
|
||||
## Потребители и режимы
|
||||
- **drhider** — браузер (PUT файла) + сервер (pull). Нужен CORS для браузера.
|
||||
- **contracts** — сервер-к-серверу, много файлов из локали.
|
||||
- **SQS IoT** — сервер-к-серверу.
|
||||
|
||||
## Технология
|
||||
**FastAPI + uvicorn** (Python уже есть на ВМ).
|
||||
Почему FastAPI: автогенерация OpenAPI/Swagger (`/docs`) → «как юзать» видно из коробки;
|
||||
нативный streaming для больших тел.
|
||||
|
||||
## API v1 (контракт)
|
||||
|
||||
| Метод | Путь | Вход | Ответ |
|
||||
|---|---|---|---|
|
||||
| `POST` | `/api/v1/files` | тело=файл (raw или multipart `file`), header `X-App-Key` | `201 {"id","url","size","expires_at"}` |
|
||||
| `GET` | `/api/v1/files/{id}` | header `X-App-Key` | байты файла |
|
||||
| `DELETE` | `/api/v1/files/{id}` | header `X-App-Key` | `204` |
|
||||
| `GET` | `/api/v1/health` | — | `{"ok":true,"version":...}` |
|
||||
| `GET` | `/api/v1/docs` | — | Swagger UI (авто) |
|
||||
|
||||
Единые ошибки: `{"error": "...", "code": "unauthorized|not_found|too_large|expired|quota_exceeded"}`.
|
||||
|
||||
## Авторизация
|
||||
Заголовок `X-App-Key`, по одному секрету на приложение. Ключи — в конфиге на ВМ
|
||||
(файл `apps.yml`/`.env`): `drhider`, `contracts`, `sqs-iot`. Без валидного ключа — `401`.
|
||||
|
||||
## Хранилище и метаданные
|
||||
- Файлы: `/var/lib/vmfiles/` (или `/var/www/vmfiles/`, владелец — пользователь сервиса).
|
||||
- Метаданные: SQLite (`id, app, name, size, created_at, expires_at`).
|
||||
- id — серверный UUID (или клиентский токен — см. «открытые вопросы»).
|
||||
|
||||
## Лимиты и TTL
|
||||
- `max_file_size` (по умолчанию 1024m) и `quota` на приложение.
|
||||
- TTL по умолчанию 30 мин (на приложение можно переопределить).
|
||||
- Чистка — встроенная фоновая задача сервиса (вместо текущего cron-`find`).
|
||||
|
||||
## Деплой на ВМ
|
||||
1. Код сервиса — в отдельной папке на ВМ (или отдельный репозиторий).
|
||||
2. systemd-юнит `vmfiles.service` → `uvicorn app:app --host 127.0.0.1 --port 8769`.
|
||||
3. nginx: новый `location /api/v1/ { proxy_pass http://127.0.0.1:8769; ... }`
|
||||
+ `proxy_request_buffering off` (стримить большие тела) + `client_max_body_size 1024m`.
|
||||
CORS — отдать сервису (FastAPI), не nginx.
|
||||
4. Проверка: `nginx -t` → reload.
|
||||
|
||||
## Миграция drhider
|
||||
- Пока сервис поднимается — `/drhider-upload/` оставить как есть (не ломать текущее).
|
||||
- После готовности — перевести `uploadFiles()` и `/api/upload_refs` на `/api/v1/files`,
|
||||
убрать dav-location и cron-`find`.
|
||||
|
||||
## Порядок работ
|
||||
1. Скелет FastAPI + health + `/docs`.
|
||||
2. `POST/GET/DELETE /files` + SQLite-метаданные + TTL-чистка.
|
||||
3. `X-App-Key` авторизация + квоты.
|
||||
4. systemd + nginx `proxy_pass`.
|
||||
5. Тесты: curl (raw + multipart), большие файлы, expiry, quota.
|
||||
6. Перевести drhider на новый API, выпилить старый dav.
|
||||
7. Документ-спека (`README`/`API.md`) + примеры для contracts/SQS.
|
||||
|
||||
## Открытые вопросы (до «делай»)
|
||||
1. Язык: FastAPI (рекомендую) или Flask?
|
||||
2. id файла: сервер генерирует (POST) или клиент приносит токен (PUT)? Влияет на браузерный поток drhider.
|
||||
3. Загрузка: только raw-тело или поддерживать multipart тоже?
|
||||
4. Где живёт код сервиса — отдельный репозиторий на gitea или папка на ВМ?
|
||||
5. Ключи приложений — придумать сейчас или сервис сгенерирует при первом старте?
|
||||
@@ -0,0 +1,43 @@
|
||||
# ВМ-загрузка в drhider — реализовано (v0.0.58, 2026-08-21)
|
||||
|
||||
_Реализация паттерна «ВМ-буфер + pull» по плану `2026-08-21-pattern-vm-buffer-pull-plan.md`._
|
||||
_Снимок до изменений: ветка `save-pre-vm-upload-2026-08-21`._
|
||||
|
||||
## Что сделано
|
||||
|
||||
1. **Flask** (`site/routes/api_bp.py`) — новый `POST /api/upload_refs`:
|
||||
принимает JSON `{session, files:[{name,size,url}]}`, тянет каждый файл с ВМ
|
||||
ИСХОДЯЩИМ GET (httpx, stream по частям), кладёт в сессию `add_file()`,
|
||||
после успешного pull делает DELETE с ВМ. Ошибки egress → 502.
|
||||
2. **Фронт** (`site/templates/index.html`, `uploadFiles()`) — Фаза 1 переделана:
|
||||
- 1a: каждый файл → `PUT` на ВМ `https://contracts.kube5s.ru/drhider-upload/<токен>_<k>`
|
||||
(XHR, прогресс по-прежнему в строке), собирает refs.
|
||||
- 1b: один маленький `POST /api/upload_refs` (<64КБ).
|
||||
- Токен — `crypto.randomUUID()`. Константа `VM_UPLOAD_URL`.
|
||||
3. **ВМ** (`5.172.178.213`) — nginx `nginx-contracts.conf`:
|
||||
- `location /drhider-upload/` → `alias /var/www/drhider-upload/`,
|
||||
`dav_methods PUT DELETE`, `create_full_put_path on`, `client_max_body_size 1024m`,
|
||||
CORS-заголовки для `drhider.pythonk8s.dev.nubes.ru`, OPTIONS → 204.
|
||||
- Каталог `/var/www/drhider-upload/` (www-data).
|
||||
- Backup: `/tmp/nginx-contracts.conf.bak.20260821`.
|
||||
- TTL-чистка в root cron: `*/5 * * * * find /var/www/drhider-upload -type f -mmin +30 -delete`.
|
||||
4. **Версия** — `site/app.py`: 0.0.57 → 0.0.58.
|
||||
|
||||
## Отклонение от плана
|
||||
- URL файла на ВМ — БЕЗ имени: `.../drhider-upload/<токен>_<k>` (имя — только в метаданных).
|
||||
Иначе кириллица/пробелы в имени ломали бы URL.
|
||||
|
||||
## Тесты (2026-08-21)
|
||||
- ВМ: PUT 10МБ → 201 (1.0с); GET → 200, 10485760B; OPTIONS preflight → 204 с CORS;
|
||||
DELETE → 204; GET после DELETE → 404.
|
||||
- Сквозной: PUT на ВМ → `POST /api/upload_refs` → 200, файл в сессии (10485760B),
|
||||
файл с ВМ удалён (404).
|
||||
- `py_compile` api_bp/app — OK; `node --check` JS из index.html — OK.
|
||||
|
||||
## Замечания
|
||||
- ГРАБЛИ: на Krupski `dd` алиасится на `docker compose down` — для генерации файлов
|
||||
использовать `head -c N /dev/urandom > file`.
|
||||
- Локальные curl/httpx к contracts.kube5s.ru — через прокси (`172.17.192.1:10808`):
|
||||
для тестов `--noproxy '*'` / `NO_PROXY=contracts.kube5s.ru`.
|
||||
- Прод-безопасность (позже): токены без TTL-времени в самом URL, авторизация на ВМ
|
||||
(сейчас ключ — случайный UUID, файл живёт ≤30 мин по cron).
|
||||
Reference in New Issue
Block a user