bcfb07f547
Чёткое разделение audiences:
- docs/ — consumer-facing only, Diplodoc-built. Public contract для
интеграторов (Альтум, BI, third-party).
- docs-internal/ — operator runbooks, status snapshots, legacy tech pages.
НЕ в Diplodoc build, НЕ публикуются.
Изменения:
- Move docs/{ops,status,tech} → docs-internal/{ops,status,tech}
- docs/toc.yaml: убраны секции Operations, Архитектура (legacy), Status
- docs/index.md: убран tab "оператор / on-call", focus на integrators
- docs/README.md: rewritten — scope только integrator guide, editorial
guidelines (no leak internal architecture, stable API contract)
- .yfm: убран явный ignore status/* (теперь не нужно — папка переехала)
- docs-internal/README.md: index для внутренней документации с rationale
разделения
Scrubbing implementation details из Diplodoc страниц:
- events.md: убраны Strimzi/cluster.142 references, OutboxPoller детали,
internal alert names (OrdinisOutboxLagHigh, OrdinisOutboxDLQNonEmpty),
metric names (nsi_outbox_*). Заменены на consumer-observable contract.
- webhooks.md: убраны metric names (nsi_webhook_ssrf_rejected_total),
alert table (OrdinisWebhookHighFailureRate и т.д.). Retry policy
переписана на user-side perspective.
- errors.md: убран alert name OrdinisReadApiErrorBudgetBurnFast, Tempo
endpoint URL, internal connection details.
- x-references.md: OrphanReferenceScanner → general "Ordinis периодически
детектирует". Убраны metric/alert names. Cascade roadmap без link на
internal design doc.
- auth.md: убран file path ScopeContext.java.
- bundle-cuod.md: убран Maven module path.
- caching.md: cascade design link заменён на text reference.
CI:
- .gitlab-ci.yml: новый job build-docs (stage: build) — Node 20 alpine,
pnpm install --frozen-lockfile + pnpm build, artifact docs/_dist на
неделю. Triggers: auto на docs/**, либо manual web. Готов к follow-up
pages-deploy job когда host решён.
- Новый pattern .docs-changes для rules.
Build verified clean: 13 HTML pages, 6.9M, no broken links/refs.
206 lines
6.6 KiB
Markdown
206 lines
6.6 KiB
Markdown
# Webhooks
|
||
|
||
Альтернатива Kafka events для consumer'ов которые не хотят / не могут
|
||
поддерживать Kafka client. Ordinis отправляет HTTP POST на зарегистрированный
|
||
URL при изменениях справочников.
|
||
|
||
{% note info %}
|
||
|
||
Если у тебя есть Kafka access — используй [события](events.md). Webhooks
|
||
проще в развёртывании, но менее надёжны (HTTP retry vs Kafka durable log)
|
||
и медленнее (HTTP overhead vs Kafka batch).
|
||
|
||
{% endnote %}
|
||
|
||
## Регистрация подписки
|
||
|
||
```bash
|
||
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
https://ordinis.k8s.264.nstart.cloud/api/v1/admin/webhooks/subscriptions \
|
||
-d '{
|
||
"name": "altum-cache-invalidation",
|
||
"url": "https://altum-backend.altum.local/webhooks/ordinis",
|
||
"secret": "<HMAC_SECRET_GENERATED_BY_YOU>",
|
||
"scopes": ["PUBLIC", "INTERNAL"],
|
||
"dictionaries": ["spacecraft", "satellite_type", "instrument"],
|
||
"actions": ["created", "updated", "closed"],
|
||
"active": true
|
||
}'
|
||
```
|
||
|
||
### Поля
|
||
|
||
| Поле | Тип | Назначение |
|
||
|---|---|---|
|
||
| `name` | string | Человекочитаемое имя для admin UI. |
|
||
| `url` | string | HTTPS endpoint. HTTP блокируется SSRF guard'ом. |
|
||
| `secret` | string | HMAC-SHA256 секрет для подписи payload (см. ниже). |
|
||
| `scopes` | array | Фильтр по scope (subset из доступных consumer'у). |
|
||
| `dictionaries` | array | Фильтр по dictionary names. `null`/`[]` = все доступные. |
|
||
| `actions` | array | `created` / `updated` / `closed`. `null` = все. |
|
||
| `active` | bool | Можно временно отключить без удаления. |
|
||
|
||
## SSRF guard
|
||
|
||
Webhook URLs валидируются перед отправкой:
|
||
|
||
- **Запрещено:** private CIDR (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16),
|
||
localhost, link-local (169.254/16), metadata IPs.
|
||
- **Запрещено:** non-HTTPS scheme. Только `https://`.
|
||
- **Запрещено:** URL > 2048 chars.
|
||
|
||
При нарушении registration возвращает 422 + список причин.
|
||
|
||
## Payload
|
||
|
||
POST на зарегистрированный URL с body:
|
||
|
||
```json
|
||
{
|
||
"deliveryId": "wd-abc123-xyz789",
|
||
"subscriptionName": "altum-cache-invalidation",
|
||
"occurredAt": "2026-05-07T15:23:45.123+03:00",
|
||
"events": [
|
||
{
|
||
"schemaVersion": "1.0.0",
|
||
"eventType": "dictionary.changed",
|
||
"dictionary": "spacecraft",
|
||
"businessKey": "RESURS-P-3",
|
||
"action": "updated",
|
||
"version": 3,
|
||
"validFrom": "2026-05-07T00:00:00+03:00",
|
||
"data": { ... }
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
{% note tip %}
|
||
|
||
Payload может содержать **несколько events** (`events` array) если они
|
||
произошли в одной transaction (bulk close, bundle import). Не предполагай
|
||
single-event payload.
|
||
|
||
{% endnote %}
|
||
|
||
## HMAC подпись
|
||
|
||
Каждый POST имеет header:
|
||
|
||
```
|
||
X-Ordinis-Signature: sha256=<HEX>
|
||
X-Ordinis-Delivery: wd-abc123-xyz789
|
||
X-Ordinis-Subscription: altum-cache-invalidation
|
||
```
|
||
|
||
Где `<HEX>` = HMAC-SHA256 от raw body bytes с `secret` который ты задал
|
||
при регистрации.
|
||
|
||
### Verify (Python)
|
||
|
||
```python
|
||
import hmac, hashlib
|
||
|
||
def verify(body: bytes, signature_header: str, secret: str) -> bool:
|
||
expected = "sha256=" + hmac.new(
|
||
secret.encode(), body, hashlib.sha256
|
||
).hexdigest()
|
||
return hmac.compare_digest(expected, signature_header)
|
||
```
|
||
|
||
### Verify (Node.js)
|
||
|
||
```javascript
|
||
import crypto from 'crypto'
|
||
|
||
function verify(body, signatureHeader, secret) {
|
||
const expected = 'sha256=' + crypto
|
||
.createHmac('sha256', secret)
|
||
.update(body)
|
||
.digest('hex')
|
||
return crypto.timingSafeEqual(
|
||
Buffer.from(expected),
|
||
Buffer.from(signatureHeader)
|
||
)
|
||
}
|
||
```
|
||
|
||
{% note warning %}
|
||
|
||
Verify подпись **до** парсинга JSON. Если invalid — `403 Forbidden`,
|
||
не трогай payload. Без verify ты уязвим к подделке events от любого, кто
|
||
знает твой URL.
|
||
|
||
{% endnote %}
|
||
|
||
## Response contract
|
||
|
||
Consumer должен ответить `2xx` в течение 10 секунд:
|
||
|
||
| Status | Семантика |
|
||
|---|---|
|
||
| `200` / `201` / `202` / `204` | Принято. Ordinis помечает delivered. |
|
||
| `4xx` | Permanent failure (твой bug). Ordinis НЕ retry. |
|
||
| `5xx` / timeout | Transient. Ordinis retry с exponential backoff. |
|
||
|
||
## Retry policy
|
||
|
||
| Попытка | Интервал |
|
||
|---|---|
|
||
| 1 | сразу |
|
||
| 2 | через 30s |
|
||
| 3 | через 2 мин |
|
||
| 4 | через 10 мин |
|
||
| 5 | через 1ч |
|
||
| 6-10 | каждые 6ч |
|
||
| После всех попыток | Ordinis team помечает delivery как failed для ручного разбора |
|
||
|
||
Если consumer недоступен > 1000 retry попыток — Ordinis team будет
|
||
расследовать. Долгосрочное unavailability стоит coordinate (planned
|
||
maintenance window) чтобы не накапливать orphan deliveries.
|
||
|
||
## Test ping
|
||
|
||
Endpoint для проверки доступности URL **до** регистрации:
|
||
|
||
```bash
|
||
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
https://ordinis.k8s.264.nstart.cloud/api/v1/admin/webhooks/test-ping \
|
||
-d '{
|
||
"url": "https://altum-backend.altum.local/webhooks/ordinis",
|
||
"secret": "<TEST_SECRET>"
|
||
}'
|
||
```
|
||
|
||
Response:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"statusCode": 200,
|
||
"responseBody": "OK",
|
||
"elapsedMs": 142
|
||
}
|
||
```
|
||
|
||
Либо details про failure (DNS issue, timeout, SSRF reject и т.д.).
|
||
|
||
## Best practices
|
||
|
||
1. **Idempotency** — webhook может прийти **дважды** (network retry, partial
|
||
failure). Используй `deliveryId` как dedup key.
|
||
2. **Async обработка** — отвечай `202 Accepted` сразу, обрабатывай в
|
||
background. 10s timeout жёсткий — если БД медленная, лучше принять и
|
||
сложить в очередь.
|
||
3. **HMAC verify первым** — до парсинга JSON.
|
||
4. **Metrics** — track received / processed / errors для своего side.
|
||
5. **Whitelist Ordinis IP** на firewall если строгие правила. Точный
|
||
адрес запросить у Ordinis team при registration.
|
||
|
||
## Дальше
|
||
|
||
- [События (Kafka)](events.md) — primary delivery channel
|
||
- [Caching](caching.md) — webhook invalidation pattern
|