Add inter-service call graph: generate calls.example.json, implement scan_calls.py, and update documentation
This commit is contained in:
parent
047e2e6a91
commit
d077847c00
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Модель технологического слоя типового контура платформы. **Все данные, зависящие от конкретной инсталляции — имя кластера, узлы, домены, реестр — заменены на примеры.** Реальные значения в репозиторий не попадают: они задаются профилем контура, который держится отдельно.
|
Модель технологического слоя типового контура платформы. **Все данные, зависящие от конкретной инсталляции — имя кластера, узлы, домены, реестр — заменены на примеры.** Реальные значения в репозиторий не попадают: они задаются профилем контура, который держится отдельно.
|
||||||
|
|
||||||
Прикладной слой присутствует только как потребители технологических сервисов. Межсервисные REST-связи приложений и бизнес-слой будут добавлены отдельно.
|
Прикладной слой представлен потребителями технологических сервисов и межсервисными вызовами. Бизнес-слой будет добавлен отдельно.
|
||||||
|
|
||||||
## Файлы
|
## Файлы
|
||||||
|
|
||||||
@ -11,24 +11,32 @@
|
|||||||
| `example-technology.archimate` | нативный формат [Archi](https://www.archimatetool.com/) | десктопный Archi: File → Open |
|
| `example-technology.archimate` | нативный формат [Archi](https://www.archimatetool.com/) | десктопный Archi: File → Open |
|
||||||
| `example-technology.xml` | The Open Group ArchiMate Model Exchange File Format 3.1 | любой инструмент с импортом Open Exchange, в том числе archi-online.com: **Import, save, and create models** → Open Exchange |
|
| `example-technology.xml` | The Open Group ArchiMate Model Exchange File Format 3.1 | любой инструмент с импортом Open Exchange, в том числе archi-online.com: **Import, save, and create models** → Open Exchange |
|
||||||
| `preview.html` | SVG-рендер всех представлений | любой браузер, без установки чего-либо |
|
| `preview.html` | SVG-рендер всех представлений | любой браузер, без установки чего-либо |
|
||||||
|
| `matrix.example.json` | снимок матрицы «приложение → технологический сервис» | вход для сборки |
|
||||||
|
| `calls.example.json` | снимок графа межсервисных вызовов | вход для сборки |
|
||||||
|
|
||||||
70 элементов, 153 отношения, 9 представлений.
|
80 элементов, 318 отношений, 23 представления.
|
||||||
|
|
||||||
| Представление | Что показывает |
|
| Представление | Что показывает |
|
||||||
|---|---|
|
|---|---|
|
||||||
| T1 | Кластер, ПО внутри него, внешние узлы, технологические сервисы. Без приложений — обзорная схема |
|
| T1 | Кластер, ПО внутри него, внешние узлы, технологические сервисы. Без приложений — обзорная схема |
|
||||||
| T2–T9 | По одному представлению на технологический сервис: кто им пользуется |
|
| T2 | Развёртывание по узлам: какая группа узлов какие компоненты несёт |
|
||||||
|
| A1 | Ядро прикладного слоя: восемь самых связанных сервисов и вызовы между ними |
|
||||||
|
| A2–A12 | По представлению на сервис с тремя и более потребителями: кто от него зависит |
|
||||||
|
| T15–T23 | По одному представлению на технологический сервис: кто им пользуется |
|
||||||
|
|
||||||
Разбиение по сервисам сделано осознанно: все 118 связей «сервис → приложение» на одной схеме нечитаемы. В модели они лежат одним набором, представления — это срезы.
|
Направление связи — как принято в ArchiMate: стрелка идёт от вызываемого к вызывающему (serving). Читается как «кого лишишься — тот и сломается».
|
||||||
|
|
||||||
|
Разбиение по сервисам сделано осознанно: 148 связей «технологический сервис → приложение» и 86 межсервисных вызовов на одной схеме нечитаемы. В модели они лежат одним набором, представления — это срезы.
|
||||||
|
|
||||||
Метамодель намеренно урезана. Технологический слой: `Node`, `SystemSoftware`, `TechnologyService`, `Artifact`. Прикладной: `ApplicationComponent`. Отношения: `Composition`, `Realization`, `Serving`, `Assignment`. Не используются `Device`, `Path`, `CommunicationNetwork`, `TechnologyFunction` — они не несут информации на этом уровне.
|
Метамодель намеренно урезана. Технологический слой: `Node`, `SystemSoftware`, `TechnologyService`, `Artifact`. Прикладной: `ApplicationComponent`. Отношения: `Composition`, `Realization`, `Serving`, `Assignment`. Не используются `Device`, `Path`, `CommunicationNetwork`, `TechnologyFunction` — они не несут информации на этом уровне.
|
||||||
|
|
||||||
## Как собрать модель для реального контура
|
## Как собрать модель для реального контура
|
||||||
|
|
||||||
1. Снять матрицу связей с нужного кластера:
|
1. Снять с нужного кластера матрицу использования технологических сервисов и граф межсервисных вызовов:
|
||||||
|
|
||||||
```
|
```
|
||||||
py scan_contour.py <имя-кластера> matrix.json
|
py scan_contour.py <имя-кластера> matrix.json
|
||||||
|
py scan_calls.py <имя-кластера> calls.json
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Скопировать `contour-profile.example.json`, подставить настоящие имена кластера, узлов, доменов и реестра. **Копию хранить вне репозитория.**
|
2. Скопировать `contour-profile.example.json`, подставить настоящие имена кластера, узлов, доменов и реестра. **Копию хранить вне репозитория.**
|
||||||
@ -36,11 +44,13 @@
|
|||||||
3. Собрать модель и производные форматы:
|
3. Собрать модель и производные форматы:
|
||||||
|
|
||||||
```
|
```
|
||||||
py build_archimate.py <профиль.json> matrix.json out.archimate
|
py build_archimate.py <профиль.json> matrix.json out.archimate calls.json
|
||||||
py to_open_exchange.py out.archimate out.xml
|
py to_open_exchange.py out.archimate out.xml
|
||||||
py render_preview.py out.archimate out.html
|
py render_preview.py out.archimate out.html
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Последний аргумент `build_archimate.py` необязателен: без него собирается только технологический слой.
|
||||||
|
|
||||||
Результат шага 3 в репозиторий не коммитить — он содержит данные контура.
|
Результат шага 3 в репозиторий не коммитить — он содержит данные контура.
|
||||||
|
|
||||||
## Откуда взяты связи
|
## Откуда взяты связи
|
||||||
@ -58,10 +68,16 @@
|
|||||||
| Приложение → Vault | аннотации `vault.hashicorp.com/agent-inject*` |
|
| Приложение → Vault | аннотации `vault.hashicorp.com/agent-inject*` |
|
||||||
| Приложение → Zitadel | env `ZITADEL*`, `JWKS`, `OIDC`, секрет `jwt-public` |
|
| Приложение → Zitadel | env `ZITADEL*`, `JWKS`, `OIDC`, секрет `jwt-public` |
|
||||||
| Приложение → Istio | `service:` в маршрутах `infrastructure/istio-config/<контур>/istio-config.yaml` (закомментированные не считаются) |
|
| Приложение → Istio | `service:` в маршрутах `infrastructure/istio-config/<контур>/istio-config.yaml` (закомментированные не считаются) |
|
||||||
|
| Приложение → приложение | адрес `<сервис>.<namespace>` в манифестах вызывающего; либо переменная `<APP>_URL` / `_HOST` / `_BASE_URL` / `_ENDPOINT` |
|
||||||
|
|
||||||
Сканируются `*.yaml` в `apps/<app>/base/` и `apps/<app>/<контур>/`; документация `*.md` игнорируется, иначе прозаические упоминания переменных дают ложные срабатывания.
|
Сканируются `*.yaml` в `apps/<app>/base/` и `apps/<app>/<контур>/`. Для матрицы технологических сервисов документация `*.md` служит вторым источником, для графа вызовов — `*ENDPOINTS.md` и `*CONFIGURATION.md`.
|
||||||
|
|
||||||
`matrix.example.json` — снимок такой матрицы. Он содержит только имена приложений и флаги использования сервисов; идентификаторов контура в нём нет.
|
Две поправки, без которых граф вызовов врёт:
|
||||||
|
|
||||||
|
- **суффикс контура в namespace.** Адреса встречаются в формах `documentations`, `documentations-prod`, `bim-api`; без нормализации связь теряется, а короткая форма `<сервис>.<namespace>` без `.svc.cluster.local` не распознаётся вовсе.
|
||||||
|
- **инфраструктурный сегмент в имени переменной.** `DJANGO_POSTGRES_HOST` и `ISSUES_DB_HOST` адресуют базу данных чужого сервиса, а не сам сервис. Это связь другого рода, и как вызов её показывать нельзя.
|
||||||
|
|
||||||
|
`matrix.example.json` и `calls.example.json` — снимки. В них только имена приложений и флаги связей; идентификаторов контура нет.
|
||||||
|
|
||||||
## Про формат файлов
|
## Про формат файлов
|
||||||
|
|
||||||
@ -78,6 +94,7 @@
|
|||||||
| Скрипт | Назначение |
|
| Скрипт | Назначение |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `scan_contour.py` | матрица «приложение → технологический сервис» по кластеру |
|
| `scan_contour.py` | матрица «приложение → технологический сервис» по кластеру |
|
||||||
|
| `scan_calls.py` | граф межсервисных вызовов прикладного слоя |
|
||||||
| `build_archimate.py` | сборка модели из профиля и матрицы |
|
| `build_archimate.py` | сборка модели из профиля и матрицы |
|
||||||
| `to_open_exchange.py` | конвертация в обменный формат |
|
| `to_open_exchange.py` | конвертация в обменный формат |
|
||||||
| `render_preview.py` | рендер представлений в HTML со встроенным SVG |
|
| `render_preview.py` | рендер представлений в HTML со встроенным SVG |
|
||||||
|
|||||||
@ -9,15 +9,22 @@ import json, pathlib, sys, io
|
|||||||
from xml.sax.saxutils import escape, quoteattr
|
from xml.sax.saxutils import escape, quoteattr
|
||||||
|
|
||||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
||||||
if len(sys.argv) != 4:
|
if len(sys.argv) not in (4, 5):
|
||||||
sys.exit(__doc__)
|
sys.exit(__doc__)
|
||||||
PROFILE = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
|
PROFILE = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||||
DATA = json.loads(pathlib.Path(sys.argv[2]).read_text(encoding="utf-8"))
|
DATA = json.loads(pathlib.Path(sys.argv[2]).read_text(encoding="utf-8"))
|
||||||
OUT = pathlib.Path(sys.argv[3])
|
OUT = pathlib.Path(sys.argv[3])
|
||||||
|
CALLS = json.loads(pathlib.Path(sys.argv[4]).read_text(encoding="utf-8")) if len(sys.argv) == 5 else None
|
||||||
|
|
||||||
elements, relations, _rc = [], [], [0]
|
elements, relations, _rc = [], [], [0]
|
||||||
REL_INDEX = {}
|
REL_INDEX = {}
|
||||||
|
|
||||||
|
def plural(n, one, few, many):
|
||||||
|
"""Русское согласование числительного: 1 сервис, 3 сервиса, 5 сервисов."""
|
||||||
|
if n % 100 // 10 == 1:
|
||||||
|
return many
|
||||||
|
return {1: one, 2: few, 3: few, 4: few}.get(n % 10, many)
|
||||||
|
|
||||||
def el(folder, eid, etype, name, doc=""):
|
def el(folder, eid, etype, name, doc=""):
|
||||||
elements.append((folder, eid, etype, name, doc)); return eid
|
elements.append((folder, eid, etype, name, doc)); return eid
|
||||||
|
|
||||||
@ -204,19 +211,61 @@ if GROUPS:
|
|||||||
for i, a in enumerate(big["_apps"]):
|
for i, a in enumerate(big["_apps"]):
|
||||||
obj(vd, app_id[a], 14 + (i % per) * bw, yy + (i // per) * 62, bw - 15, 50, og)
|
obj(vd, app_id[a], 14 + (i % per) * bw, yy + (i // per) * 62, bw - 15, 50, og)
|
||||||
|
|
||||||
|
# --- прикладной слой: межсервисные вызовы
|
||||||
|
if CALLS:
|
||||||
|
import math, collections
|
||||||
|
known = set(DATA["apps"])
|
||||||
|
calls = [(e["from"], e["to"], e["src"]) for e in CALLS["edges"]
|
||||||
|
if e["from"] in known and e["to"] in known]
|
||||||
|
# если A зовёт B, то в ArchiMate B обслуживает A
|
||||||
|
for a, b, _ in calls:
|
||||||
|
R("Serving", app_id[b], app_id[a])
|
||||||
|
inbound = collections.Counter(b for _, b, _ in calls)
|
||||||
|
outbound = collections.Counter(a for a, _, _ in calls)
|
||||||
|
|
||||||
|
# A1: ядро — самые связанные сервисы и связи между ними, по кругу
|
||||||
|
core = [a for a, _ in collections.Counter(
|
||||||
|
{a: inbound[a] + outbound[a] for a in known}).most_common(8)]
|
||||||
|
vc = view("A1. Ядро прикладного слоя",
|
||||||
|
f"Восемь сервисов с наибольшим числом связей и вызовы между ними. "
|
||||||
|
f"Стрелка идёт от вызываемого к вызывающему (serving): кого лишишься — "
|
||||||
|
f"тот и сломается. Всего в контуре {len(calls)} межсервисных связей.")
|
||||||
|
# координаты считаем от нуля, затем сдвигаем: обменный формат не допускает
|
||||||
|
# отрицательных x/y, а круговая раскладка их легко даёт
|
||||||
|
rad, pos = 300, {}
|
||||||
|
for i, a in enumerate(core):
|
||||||
|
ang = 2 * math.pi * i / len(core) - math.pi / 2
|
||||||
|
pos[a] = (int(rad * math.cos(ang) * 1.35) - 85, int(rad * math.sin(ang)) - 27)
|
||||||
|
dx, dy = 20 - min(x for x, _ in pos.values()), 20 - min(y for _, y in pos.values())
|
||||||
|
core_obj = {a: obj(vc, app_id[a], x + dx, y + dy, 170, 55) for a, (x, y) in pos.items()}
|
||||||
|
for a, b, _ in calls:
|
||||||
|
if a in core_obj and b in core_obj:
|
||||||
|
conn("Serving", core_obj[b], core_obj[a])
|
||||||
|
|
||||||
|
# A2…: по представлению на каждый сервис с тремя и более потребителями
|
||||||
|
a_no = 2
|
||||||
|
for hub, n in inbound.most_common():
|
||||||
|
if n < 3:
|
||||||
|
break
|
||||||
|
callers = sorted(a for a, b, _ in calls if b == hub)
|
||||||
|
v = view(f"A{a_no}. Зависят от: {hub}",
|
||||||
|
f"К {hub} обращаются {n} {plural(n, 'сервис', 'сервиса', 'сервисов')}.")
|
||||||
|
a_no += 1
|
||||||
|
oh = obj(v, app_id[hub], 20, 20, 260, 60)
|
||||||
|
for i, a in enumerate(callers):
|
||||||
|
conn("Serving", oh,
|
||||||
|
obj(v, app_id[a], 20 + (i % 6) * 190, 150 + (i // 6) * 80, 170, 55))
|
||||||
|
|
||||||
PER_ROW = 6
|
PER_ROW = 6
|
||||||
for ts in SERVICES:
|
for ts in SERVICES:
|
||||||
lst = consumers[ts["id"]]
|
lst = consumers[ts["id"]]
|
||||||
extra = EXTRA_USES.get(ts["id"], [])
|
extra = EXTRA_USES.get(ts["id"], [])
|
||||||
if not lst and not extra:
|
if not lst and not extra:
|
||||||
continue
|
continue
|
||||||
n_doc = sum(1 for a in lst if DATA["matrix"][a].get(ts.get("key")) == "doc")
|
n = len(lst)
|
||||||
src = ("Связи выведены из манифестов приложений."
|
|
||||||
if not n_doc else
|
|
||||||
f"Связи выведены из манифестов; {n_doc} из них подтверждаются только "
|
|
||||||
f"документацией сервиса — значения приходят из k8s-секретов и в конфигах не видны.")
|
|
||||||
v = view(f"T{len(views)+1}. Потребители: {ts['name']}",
|
v = view(f"T{len(views)+1}. Потребители: {ts['name']}",
|
||||||
f"{len(lst)} из {len(DATA['apps'])} приложений контура. {src}")
|
f"Сервисом {plural(n, 'пользуется', 'пользуются', 'пользуются')} "
|
||||||
|
f"{n} из {len(DATA['apps'])} приложений контура.")
|
||||||
ots = obj(v, ts["id"], 20, 20, 260, 60)
|
ots = obj(v, ts["id"], 20, 20, 260, 60)
|
||||||
for i, r in enumerate(ts["realizers"]):
|
for i, r in enumerate(ts["realizers"]):
|
||||||
conn("Realization", obj(v, r, 320 + i * 280, 20, 260, 60), ots)
|
conn("Realization", obj(v, r, 320 + i * 280, 20, 260, 60), ots)
|
||||||
|
|||||||
473
docs/architecture/calls.example.json
Normal file
473
docs/architecture/calls.example.json
Normal file
@ -0,0 +1,473 @@
|
|||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
"ams-sync",
|
||||||
|
"attachments",
|
||||||
|
"auth-flow",
|
||||||
|
"bim",
|
||||||
|
"cde",
|
||||||
|
"checklists",
|
||||||
|
"comparisons",
|
||||||
|
"contracts",
|
||||||
|
"control-interface",
|
||||||
|
"cross-section",
|
||||||
|
"django",
|
||||||
|
"document-link",
|
||||||
|
"documentations",
|
||||||
|
"drawings",
|
||||||
|
"eav",
|
||||||
|
"faas",
|
||||||
|
"flows",
|
||||||
|
"iam",
|
||||||
|
"inspections",
|
||||||
|
"issues",
|
||||||
|
"mapper",
|
||||||
|
"measurements",
|
||||||
|
"message-hub",
|
||||||
|
"notes",
|
||||||
|
"pm",
|
||||||
|
"prescriptions",
|
||||||
|
"processing",
|
||||||
|
"projects",
|
||||||
|
"remarks",
|
||||||
|
"resources",
|
||||||
|
"reviews",
|
||||||
|
"rfi",
|
||||||
|
"stamp-verification",
|
||||||
|
"subscriptions",
|
||||||
|
"system-log",
|
||||||
|
"transmittal",
|
||||||
|
"workspaces"
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"from": "bim",
|
||||||
|
"to": "django",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "cde",
|
||||||
|
"to": "flows",
|
||||||
|
"src": "doc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "cde",
|
||||||
|
"to": "system-log",
|
||||||
|
"src": "doc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "cde",
|
||||||
|
"to": "workspaces",
|
||||||
|
"src": "doc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "comparisons",
|
||||||
|
"to": "bim",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "comparisons",
|
||||||
|
"to": "django",
|
||||||
|
"src": "doc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "comparisons",
|
||||||
|
"to": "documentations",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "comparisons",
|
||||||
|
"to": "eav",
|
||||||
|
"src": "env"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "comparisons",
|
||||||
|
"to": "processing",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "comparisons",
|
||||||
|
"to": "workspaces",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "django",
|
||||||
|
"to": "bim",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "django",
|
||||||
|
"to": "documentations",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "django",
|
||||||
|
"to": "measurements",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "django",
|
||||||
|
"to": "pm",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "django",
|
||||||
|
"to": "processing",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "django",
|
||||||
|
"to": "workspaces",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "documentations",
|
||||||
|
"to": "attachments",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "documentations",
|
||||||
|
"to": "bim",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "documentations",
|
||||||
|
"to": "django",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "documentations",
|
||||||
|
"to": "drawings",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "documentations",
|
||||||
|
"to": "eav",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "documentations",
|
||||||
|
"to": "flows",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "documentations",
|
||||||
|
"to": "iam",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "documentations",
|
||||||
|
"to": "inspections",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "documentations",
|
||||||
|
"to": "notes",
|
||||||
|
"src": "env"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "documentations",
|
||||||
|
"to": "processing",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "documentations",
|
||||||
|
"to": "remarks",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "documentations",
|
||||||
|
"to": "resources",
|
||||||
|
"src": "env"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "documentations",
|
||||||
|
"to": "subscriptions",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "documentations",
|
||||||
|
"to": "system-log",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "documentations",
|
||||||
|
"to": "workspaces",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "faas",
|
||||||
|
"to": "django",
|
||||||
|
"src": "env"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "faas",
|
||||||
|
"to": "documentations",
|
||||||
|
"src": "env"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "faas",
|
||||||
|
"to": "eav",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "faas",
|
||||||
|
"to": "reviews",
|
||||||
|
"src": "env"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "faas",
|
||||||
|
"to": "transmittal",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "flows",
|
||||||
|
"to": "checklists",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "flows",
|
||||||
|
"to": "django",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "flows",
|
||||||
|
"to": "documentations",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "flows",
|
||||||
|
"to": "eav",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "flows",
|
||||||
|
"to": "iam",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "flows",
|
||||||
|
"to": "pm",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "flows",
|
||||||
|
"to": "processing",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "flows",
|
||||||
|
"to": "resources",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "inspections",
|
||||||
|
"to": "eav",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "inspections",
|
||||||
|
"to": "processing",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "issues",
|
||||||
|
"to": "django",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "issues",
|
||||||
|
"to": "documentations",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "issues",
|
||||||
|
"to": "eav",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "issues",
|
||||||
|
"to": "flows",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "issues",
|
||||||
|
"to": "iam",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "issues",
|
||||||
|
"to": "processing",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "issues",
|
||||||
|
"to": "resources",
|
||||||
|
"src": "env"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "mapper",
|
||||||
|
"to": "django",
|
||||||
|
"src": "env"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "measurements",
|
||||||
|
"to": "django",
|
||||||
|
"src": "doc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "message-hub",
|
||||||
|
"to": "django",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "message-hub",
|
||||||
|
"to": "eav",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "message-hub",
|
||||||
|
"to": "issues",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "message-hub",
|
||||||
|
"to": "pm",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "notes",
|
||||||
|
"to": "attachments",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "notes",
|
||||||
|
"to": "django",
|
||||||
|
"src": "env"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "notes",
|
||||||
|
"to": "documentations",
|
||||||
|
"src": "doc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "pm",
|
||||||
|
"to": "django",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "pm",
|
||||||
|
"to": "eav",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "pm",
|
||||||
|
"to": "iam",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "pm",
|
||||||
|
"to": "resources",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "processing",
|
||||||
|
"to": "django",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "processing",
|
||||||
|
"to": "documentations",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "processing",
|
||||||
|
"to": "iam",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "processing",
|
||||||
|
"to": "resources",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "resources",
|
||||||
|
"to": "django",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "rfi",
|
||||||
|
"to": "django",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "rfi",
|
||||||
|
"to": "documentations",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "rfi",
|
||||||
|
"to": "eav",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "rfi",
|
||||||
|
"to": "resources",
|
||||||
|
"src": "doc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "subscriptions",
|
||||||
|
"to": "django",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "subscriptions",
|
||||||
|
"to": "system-log",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "system-log",
|
||||||
|
"to": "django",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "system-log",
|
||||||
|
"to": "documentations",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "transmittal",
|
||||||
|
"to": "django",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "transmittal",
|
||||||
|
"to": "documentations",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "transmittal",
|
||||||
|
"to": "flows",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "transmittal",
|
||||||
|
"to": "iam",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "transmittal",
|
||||||
|
"to": "resources",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "workspaces",
|
||||||
|
"to": "django",
|
||||||
|
"src": "svc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "workspaces",
|
||||||
|
"to": "documentations",
|
||||||
|
"src": "svc"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -32,7 +32,7 @@
|
|||||||
{
|
{
|
||||||
"id": "ss-vault",
|
"id": "ss-vault",
|
||||||
"name": "Vault + Agent Injector",
|
"name": "Vault + Agent Injector",
|
||||||
"doc": "HashiCorp Vault — централизованная система управления секретами: учётными данными, ключами шифрования и сертификатами, с аудитом всех обращений. Работает по схеме «клиент аутентифицируется — получает токен, связанный с набором политик — обращается к секретам по путям — Vault проверяет права и записывает обращение в журнал независимо от исхода». Модульная архитектура позволяет подключать разные способы аутентификации и разные хранилища секретов, включая выдачу временных учётных данных к базам данных.\n\nЕдинственный механизм доставки секретов приложениям. Секреты не лежат в манифестах и не хранятся в Git: под получает их через sidecar Agent Injector, который аутентифицируется в Vault по service account пода и монтирует значения из пути вида secrets/data/apps/<приложение>/postgres. Ролью и service account служит имя приложения — соглашение единообразно для всех сервисов. Практическое следствие для чтения архитектуры: связи приложения с БД, брокером или S3 часто не видны в YAML, потому что реквизиты приходят из Vault. Трафик к Vault выведен из-под sidecar-прокси Istio отдельной аннотацией, иначе инициализация пода не проходит."
|
"doc": "HashiCorp Vault — централизованная система управления секретами: учётными данными, ключами шифрования и сертификатами, с аудитом всех обращений. Работает по схеме «клиент аутентифицируется — получает токен, связанный с набором политик — обращается к секретам по путям — Vault проверяет права и записывает обращение в журнал независимо от исхода». Модульная архитектура позволяет подключать разные способы аутентификации и разные хранилища секретов, включая выдачу временных учётных данных к базам данных.\n\nЕдинственный механизм доставки секретов приложениям. Секреты не лежат в манифестах и не хранятся в Git: под получает их через sidecar Agent Injector, который аутентифицируется в Vault по service account пода и монтирует значения из пути вида secrets/data/apps/<приложение>/postgres. Ролью и service account служит имя приложения — соглашение единообразно для всех сервисов. Трафик к Vault выведен из-под sidecar-прокси Istio отдельной аннотацией, иначе инициализация пода не проходит."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ss-postgres",
|
"id": "ss-postgres",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"apps": [
|
"apps": [
|
||||||
|
"ams-sync",
|
||||||
"attachments",
|
"attachments",
|
||||||
"auth-flow",
|
"auth-flow",
|
||||||
"bim",
|
"bim",
|
||||||
@ -28,6 +29,7 @@
|
|||||||
"processing",
|
"processing",
|
||||||
"projects",
|
"projects",
|
||||||
"remarks",
|
"remarks",
|
||||||
|
"resources",
|
||||||
"reviews",
|
"reviews",
|
||||||
"rfi",
|
"rfi",
|
||||||
"stamp-verification",
|
"stamp-verification",
|
||||||
@ -37,6 +39,16 @@
|
|||||||
"workspaces"
|
"workspaces"
|
||||||
],
|
],
|
||||||
"matrix": {
|
"matrix": {
|
||||||
|
"ams-sync": {
|
||||||
|
"postgres": false,
|
||||||
|
"rabbitmq": false,
|
||||||
|
"kafka": "manifest",
|
||||||
|
"camunda": false,
|
||||||
|
"s3": false,
|
||||||
|
"redis": false,
|
||||||
|
"vault": false,
|
||||||
|
"oidc": "manifest"
|
||||||
|
},
|
||||||
"attachments": {
|
"attachments": {
|
||||||
"postgres": "manifest",
|
"postgres": "manifest",
|
||||||
"rabbitmq": false,
|
"rabbitmq": false,
|
||||||
@ -317,6 +329,16 @@
|
|||||||
"vault": false,
|
"vault": false,
|
||||||
"oidc": "doc"
|
"oidc": "doc"
|
||||||
},
|
},
|
||||||
|
"resources": {
|
||||||
|
"postgres": "manifest",
|
||||||
|
"rabbitmq": false,
|
||||||
|
"kafka": false,
|
||||||
|
"camunda": false,
|
||||||
|
"s3": "manifest",
|
||||||
|
"redis": false,
|
||||||
|
"vault": "manifest",
|
||||||
|
"oidc": false
|
||||||
|
},
|
||||||
"reviews": {
|
"reviews": {
|
||||||
"postgres": false,
|
"postgres": false,
|
||||||
"rabbitmq": false,
|
"rabbitmq": false,
|
||||||
@ -410,6 +432,7 @@
|
|||||||
"reviews",
|
"reviews",
|
||||||
"rfi",
|
"rfi",
|
||||||
"stamp-verification",
|
"stamp-verification",
|
||||||
|
"superset",
|
||||||
"transmittal",
|
"transmittal",
|
||||||
"workspaces",
|
"workspaces",
|
||||||
"zitadel"
|
"zitadel"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
127
docs/architecture/scan_calls.py
Normal file
127
docs/architecture/scan_calls.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"""Строит граф межсервисных вызовов прикладного слоя.
|
||||||
|
|
||||||
|
py scan_calls.py <имя-кластера> <выход.json>
|
||||||
|
|
||||||
|
Три источника, по убыванию надёжности:
|
||||||
|
svc — внутрикластерный адрес сервиса в манифестах вызывающего;
|
||||||
|
env — переменная окружения вида <APP>_URL / _HOST / _BASE_URL в манифестах;
|
||||||
|
doc — та же переменная, но найденная только в *ENDPOINTS.md / *CONFIGURATION.md.
|
||||||
|
|
||||||
|
Третий источник нужен потому, что значительная часть адресов приезжает из
|
||||||
|
k8s-секретов и в манифестах отсутствует — на технологическом слое такой же
|
||||||
|
приём вскрыл четверть связей.
|
||||||
|
"""
|
||||||
|
import re, json, pathlib, sys, io, collections
|
||||||
|
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
sys.exit(__doc__)
|
||||||
|
CLUSTER, OUT = sys.argv[1], pathlib.Path(sys.argv[2])
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
# Считаем развёрнутыми все приложения репозитория — см. комментарий в scan_contour.py
|
||||||
|
ALL = sorted(d.name for d in (ROOT / "apps").iterdir() if d.is_dir())
|
||||||
|
apps = ALL
|
||||||
|
|
||||||
|
# namespace -> приложение. Совпадают везде, кроме алиасов других контуров.
|
||||||
|
ALIASES = {
|
||||||
|
"proc": "processing", "workflow": "processing", "sarex-processing": "processing",
|
||||||
|
"orchestrator": "cde", "rfi-prod": "rfi",
|
||||||
|
"transmittals": "transmittal", "transmittal-api-stage": "transmittal",
|
||||||
|
"sarex": "django", "sarex-django": "django",
|
||||||
|
"sarex-documentations": "documentations",
|
||||||
|
"sarex-faas": "faas", "sarex-workspaces": "workspaces",
|
||||||
|
"sarex-attachments": "attachments", "planning": "pm",
|
||||||
|
}
|
||||||
|
# namespace технологических компонентов — они уже отражены на технологическом слое
|
||||||
|
INFRA_NS = {"postgresql", "rabbitmq", "minio", "kafka", "signoz", "redis",
|
||||||
|
"vault", "zitadel", "camunda", "istio-system", "insert-db-documentation"}
|
||||||
|
|
||||||
|
# В адресах встречаются суффиксы контура: documentations-prod, bim-api, eav-stage
|
||||||
|
NS_SUFFIX = ("-prod", "-stage", "-preprod", "-test", "-api")
|
||||||
|
|
||||||
|
def resolve(ns):
|
||||||
|
if ns in INFRA_NS:
|
||||||
|
return None
|
||||||
|
if ns in ALL:
|
||||||
|
return ns
|
||||||
|
if ns in ALIASES:
|
||||||
|
return ALIASES[ns]
|
||||||
|
for suf in NS_SUFFIX:
|
||||||
|
if ns.endswith(suf) and ns[:-len(suf)] in ALL:
|
||||||
|
return ns[:-len(suf)]
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Адрес внутри кластера: <сервис>.<namespace> с необязательным .svc.cluster.local
|
||||||
|
# и портом. Короткая форма (backend-svc.django:8000) встречается наравне с полной.
|
||||||
|
HOST = re.compile(r"https?://([a-z0-9][a-z0-9-]*)\.([a-z0-9][a-z0-9-]*)"
|
||||||
|
r"(?:\.svc(?:\.cluster\.local)?)?(?::\d+)?(?![a-z0-9.-])")
|
||||||
|
TLD = {"ru", "com", "tech", "io", "lan", "net", "org", "dev", "local", "svc"}
|
||||||
|
|
||||||
|
def envpat(app):
|
||||||
|
u = app.upper().replace("-", "_")
|
||||||
|
return re.compile(rf"\b{u}(?:_[A-Z0-9]+)*_(?:URL|HOST|BASE_URL|INTERNAL_URL|ENDPOINT)\b")
|
||||||
|
|
||||||
|
PATS = {a: envpat(a) for a in ALL}
|
||||||
|
|
||||||
|
# DJANGO_POSTGRES_HOST, ISSUES_DB_HOST — адрес базы данных сервиса, а не самого
|
||||||
|
# сервиса. Такая переменная означает доступ в чужую БД, а не вызов по HTTP.
|
||||||
|
INFRA_SEG = re.compile(r"_(?:DB|POSTGRES|PG|REDIS|KAFKA|RABBITMQ|AMQP|S3|SMTP"
|
||||||
|
r"|MONGO|CLICKHOUSE|VAULT|BROKER|CACHE)_")
|
||||||
|
|
||||||
|
def env_hit(pat, blob):
|
||||||
|
return any(not INFRA_SEG.search(m.group(0)) for m in pat.finditer(blob))
|
||||||
|
|
||||||
|
def read(paths):
|
||||||
|
return "\n".join(p.read_text(encoding="utf-8", errors="replace") for p in paths)
|
||||||
|
|
||||||
|
edges, unresolved = {}, collections.Counter()
|
||||||
|
for caller in apps:
|
||||||
|
d = ROOT / "apps" / caller
|
||||||
|
yaml_blob = read([f for sub in ("base", CLUSTER) if (d / sub).is_dir()
|
||||||
|
for f in (d / sub).rglob("*.yaml")])
|
||||||
|
doc_blob = read(sorted(d.glob("*ENDPOINTS*.md")) + sorted(d.glob("*CONFIGURATION*.md")))
|
||||||
|
|
||||||
|
for svc, ns in set(HOST.findall(yaml_blob)):
|
||||||
|
if ns in TLD:
|
||||||
|
continue
|
||||||
|
callee = resolve(ns)
|
||||||
|
if callee is None:
|
||||||
|
if ns not in INFRA_NS:
|
||||||
|
unresolved[f"{svc}.{ns}"] += 1
|
||||||
|
continue
|
||||||
|
if callee != caller:
|
||||||
|
edges[(caller, callee)] = "svc"
|
||||||
|
|
||||||
|
for callee, pat in PATS.items():
|
||||||
|
if callee == caller or (caller, callee) in edges:
|
||||||
|
continue
|
||||||
|
if env_hit(pat, yaml_blob):
|
||||||
|
edges[(caller, callee)] = "env"
|
||||||
|
elif env_hit(pat, doc_blob):
|
||||||
|
edges[(caller, callee)] = "doc"
|
||||||
|
|
||||||
|
inbound = collections.Counter(c for _, c in edges)
|
||||||
|
outbound = collections.Counter(c for c, _ in edges)
|
||||||
|
by_src = collections.Counter(edges.values())
|
||||||
|
|
||||||
|
print(f"приложений в контуре: {len(apps)} связей: {len(edges)}")
|
||||||
|
print("по источнику:", dict(by_src))
|
||||||
|
print()
|
||||||
|
print("самые вызываемые (входящие связи):")
|
||||||
|
for a, n in inbound.most_common(12):
|
||||||
|
print(f" {a:18s} {n:2d} исходящих: {outbound[a]}")
|
||||||
|
print()
|
||||||
|
print("самые зависимые (исходящие связи):")
|
||||||
|
for a, n in outbound.most_common(8):
|
||||||
|
print(f" {a:18s} {n:2d}")
|
||||||
|
isolated = [a for a in apps if not inbound[a] and not outbound[a]]
|
||||||
|
print(f"\nбез связей: {', '.join(isolated) if isolated else 'нет'}")
|
||||||
|
if unresolved:
|
||||||
|
print(f"\nне сопоставлены с приложением: {dict(unresolved.most_common(8))}")
|
||||||
|
|
||||||
|
OUT.write_text(json.dumps({
|
||||||
|
"apps": apps,
|
||||||
|
"edges": [{"from": c, "to": t, "src": s} for (c, t), s in sorted(edges.items())],
|
||||||
|
}, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
print(f"\nзаписано: {OUT}")
|
||||||
@ -15,10 +15,13 @@ if len(sys.argv) != 3:
|
|||||||
CLUSTER, OUT = sys.argv[1], pathlib.Path(sys.argv[2])
|
CLUSTER, OUT = sys.argv[1], pathlib.Path(sys.argv[2])
|
||||||
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
kust = (ROOT / "clusters" / CLUSTER / "kustomization.yaml").read_text(encoding="utf-8")
|
# Считаем развёрнутыми все приложения репозитория. Имя кластера определяет,
|
||||||
apps = sorted(set(re.findall(r"\.\./\.\./apps/([a-z0-9-]+)/" + re.escape(CLUSTER), kust)))
|
# какой оверлей читать поверх base, но состав приложений им не ограничивается:
|
||||||
if not apps:
|
# часть сервисов работает в контуре, не будучи заявленной в kustomization.
|
||||||
sys.exit(f"в clusters/{CLUSTER}/kustomization.yaml не найдено ни одного приложения")
|
apps = sorted(d.name for d in (ROOT / "apps").iterdir() if d.is_dir())
|
||||||
|
with_overlay = [a for a in apps if (ROOT / "apps" / a / CLUSTER).is_dir()]
|
||||||
|
print(f"приложений: {len(apps)}, из них с оверлеем {CLUSTER}: {len(with_overlay)} "
|
||||||
|
f"(остальные читаются только из base)\n")
|
||||||
|
|
||||||
MARKERS = {
|
MARKERS = {
|
||||||
"postgres": [r"POSTGRES_[A-Z]", r"DB__HOST", r"postgresql\.postgresql\.svc",
|
"postgres": [r"POSTGRES_[A-Z]", r"DB__HOST", r"postgresql\.postgresql\.svc",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user