iac/docs/architecture/build_archimate.py

256 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Собирает .archimate (формат Archi 5) для технологического слоя контура УГМК."""
import json, pathlib, sys, io
from xml.sax.saxutils import escape, quoteattr
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
HERE = pathlib.Path(__file__).parent
DATA = json.loads((HERE / "matrix.json").read_text(encoding="utf-8"))
OUT = pathlib.Path(sys.argv[1])
elements = [] # (folder, id, type, name, doc)
relations = [] # (id, type, src, tgt)
_rc = [0]
def el(folder, eid, etype, name, doc=""):
elements.append((folder, eid, etype, name, doc)); return eid
def rel(rtype, src, tgt):
_rc[0] += 1
rid = f"rel-{_rc[0]:03d}"
relations.append((rid, rtype, src, tgt)); return rid
REL_INDEX = {}
def R(rtype, src, tgt):
REL_INDEX[(rtype, src, tgt)] = rel(rtype, src, tgt); return REL_INDEX[(rtype, src, tgt)]
# ---------------------------------------------------------------- технологический слой
T = "technology"
nd_cluster = el(T, "nd-cluster", "Node", "Кластер d8-ugmk-prod (Deckhouse)",
"clusters/d8-ugmk-prod/kustomization.yaml. GitOps через FluxCD v2.")
nd_front = el(T, "nd-frontend", "Node", "um-sarex-k8s-frontend-01",
"Выделенный frontend-узел. К нему прибит ingress-gateway через nodeSelector: "
"clusters/d8-ugmk-prod/istio-ingress/ingress-controller.yaml")
nd_s3 = el(T, "nd-s3", "Node", "s3.uralmine.com — объектное хранилище УГМК",
"Внешнее по отношению к кластеру. MinIO в контуре не разворачивается.")
nd_nfs = el(T, "nd-nfs", "Node", "NFS-хранилище (StorageClass nfs01)",
"Единственный storageClass в контуре, 9 упоминаний в манифестах.")
nd_gitea = el(T, "nd-gitea", "Node", "Gitea 10.222.255.165:3000 (infra/iac)",
"Источник для Flux: clusters/d8-ugmk-prod/flux-system/gotk-sync.yaml")
nd_cr = el(T, "nd-cr", "Node", "cr.yandex/crp3ccidau046kdj8g9q",
"Реестр чартов (OCI) и образов контейнеров.")
SS = [
("ss-istio-gw", "Istio Ingress Gateway", "Deckhouse IngressIstioController «sarex», inlet NodePort 30080/30443. clusters/d8-ugmk-prod/istio-ingress/ingress-controller.yaml"),
("ss-istio-mesh", "Istio Service Mesh", "chart istio-config-contour 0.1.0. Gateway, VirtualService, RequestAuthentication."),
("ss-certmanager", "cert-manager", "ClusterIssuer sarex-selfsigned — сертификаты самоподписанные, не Let's Encrypt. Wildcard *.uralmine.com и *.sarex-k8s.uralmine.com."),
("ss-zitadel", "Zitadel", "chart idp-contour 4.12.13, ns zitadel."),
("ss-vault", "Vault + Agent Injector", "chart vault-contour 0.2.3, ns vault. Единственный механизм доставки секретов, путь secrets/data/apps/<app>/postgres."),
("ss-postgres", "PostgreSQL", "chart postgresql-contour 17.0.7, ns postgresql. PVC 20Gi на nfs01."),
("ss-redis", "Redis", "Разворачивается только в ns pm: apps/pm/d8-ugmk-prod/redis.yaml."),
("ss-kafka", "Kafka", "chart kafka-contour 0.1.15, ns kafka."),
("ss-rabbit", "RabbitMQ", "chart rabbitmq-contour 15.2.4, ns rabbitmq. Публикуется наружу на rabbitmq.sarex-k8s.uralmine.com."),
("ss-camunda", "Camunda Platform", "chart camunda-contour 11.0.14, ns camunda. ВНИМАНИЕ: ни одно приложение контура на неё не сконфигурировано — потребителей нет."),
]
for sid, name, doc in SS:
el(T, sid, "SystemSoftware", name, doc); R("Composition", nd_cluster, sid)
CAMUNDA_SUB = ["Zeebe", "Zeebe Gateway", "Operate", "Tasklist", "Optimize", "Identity", "Connectors"]
cam_ids = []
for n in CAMUNDA_SUB:
cid = "ss-cam-" + n.lower().replace(" ", "-")
el(T, cid, "SystemSoftware", n, "infrastructure/camunda/d8-ugmk-prod/camunda.yaml")
R("Composition", "ss-camunda", cid); cam_ids.append(cid)
ar_charts = el(T, "ar-charts", "Artifact", "Helm-чарты *-contour (OCI)")
ar_images = el(T, "ar-images", "Artifact", "Образы контейнеров")
R("Composition", nd_cr, ar_charts); R("Composition", nd_cr, ar_images)
# технологические сервисы: id -> (имя, кто реализует, ключ в матрице)
TS = [
("ts-ingress", "Маршрутизация HTTPS-трафика", ["ss-istio-gw", "ss-istio-mesh"], "istio"),
("ts-tls", "Выпуск TLS-сертификатов", ["ss-certmanager"], None),
("ts-oidc", "Аутентификация OIDC", ["ss-zitadel"], "oidc"),
("ts-secrets", "Управление секретами", ["ss-vault"], "vault"),
("ts-rdbms", "Реляционное хранилище", ["ss-postgres"], "postgres"),
("ts-cache", "Кэш", ["ss-redis"], "redis"),
("ts-events", "Событийная шина", ["ss-kafka"], "kafka"),
("ts-amqp", "Очередь сообщений AMQP", ["ss-rabbit"], "rabbitmq"),
("ts-bpmn", "Оркестрация процессов BPMN", ["ss-camunda"], "camunda"),
("ts-object", "Объектное хранилище", ["nd-s3"], "s3"),
]
for tid, name, realizers, _ in TS:
el(T, tid, "TechnologyService", name)
for r in realizers:
R("Realization", r, tid)
R("Assignment", nd_front, "ss-istio-gw")
R("Serving", "ts-tls", "ss-istio-gw")
R("Serving", nd_nfs, "ss-postgres")
R("Serving", nd_gitea, nd_cluster)
R("Serving", nd_cr, nd_cluster)
# ---------------------------------------------------------------- прикладной слой
app_id = {a: "ac-" + a for a in DATA["apps"]}
for a in DATA["apps"]:
el("application", app_id[a], "ApplicationComponent", a, f"apps/{a}/d8-ugmk-prod/")
consumers = {}
for tid, _, _, key in TS:
if key is None:
consumers[tid] = []; continue
if key == "istio":
lst = [a for a in DATA["apps"] if a in DATA["exposed"]]
else:
lst = [a for a in DATA["apps"] if DATA["matrix"][a][key]]
consumers[tid] = lst
for a in lst:
R("Serving", tid, app_id[a])
# ---------------------------------------------------------------- представления
views = []
def view(name, doc=""):
v = {"id": f"view-{len(views)+1}", "name": name, "doc": doc, "objects": [], "conns": []}
views.append(v); return v
_oc = [0]
def obj(v, eid, x, y, w, h, parent=None):
_oc[0] += 1
o = {"id": f"do-{_oc[0]:04d}", "el": eid, "b": (x, y, w, h), "children": [], "src": [], "tgt": []}
(parent["children"] if parent else v["objects"]).append(o)
return o
def conn(v, rtype, so, to):
key = (rtype, so["el"], to["el"])
if key not in REL_INDEX:
raise KeyError(f"нет отношения в модели: {key}")
_oc[0] += 1
cid = f"cn-{_oc[0]:04d}"
so["src"].append({"id": cid, "rel": REL_INDEX[key], "tgt": to["id"]})
to["tgt"].append(cid)
# --- T1: инфраструктура без приложений
v1 = view("T1. Технологический слой — контур УГМК",
"Что развёрнуто в кластере d8-ugmk-prod и какие технологические сервисы это даёт. "
"Приложения-потребители вынесены в отдельные представления T2T9.")
o_ts = {}
for i, (tid, _, _, _) in enumerate(TS):
o_ts[tid] = obj(v1, tid, 20 + i * 165, 30, 150, 60)
o_cluster = obj(v1, nd_cluster, 20, 140, 1270, 400)
o_ss = {}
row1 = ["ss-istio-gw", "ss-istio-mesh", "ss-certmanager", "ss-zitadel", "ss-vault"]
for i, sid in enumerate(row1):
o_ss[sid] = obj(v1, sid, 20 + i * 240, 45, 220, 70, o_cluster)
row2 = ["ss-postgres", "ss-redis", "ss-kafka", "ss-rabbit"]
for i, sid in enumerate(row2):
o_ss[sid] = obj(v1, sid, 20 + i * 240, 145, 220, 70, o_cluster)
o_ss["ss-camunda"] = obj(v1, "ss-camunda", 20, 245, 1230, 130, o_cluster)
for i, cid in enumerate(cam_ids):
obj(v1, cid, 10 + i * 174, 50, 162, 60, o_ss["ss-camunda"])
o_s3 = obj(v1, nd_s3, 1320, 140, 330, 70)
o_nfs = obj(v1, nd_nfs, 1320, 230, 330, 70)
o_gitea = obj(v1, nd_gitea, 1320, 320, 330, 70)
o_cr = obj(v1, nd_cr, 1320, 410, 330, 130)
obj(v1, ar_charts, 15, 50, 145, 60, o_cr)
obj(v1, ar_images, 170, 50, 145, 60, o_cr)
o_front = obj(v1, nd_front, 20, 570, 300, 60)
for tid, _, realizers, _ in TS:
for r in realizers:
conn(v1, "Realization", o_ss.get(r) or o_s3, o_ts[tid])
conn(v1, "Assignment", o_front, o_ss["ss-istio-gw"])
conn(v1, "Serving", o_ts["ts-tls"], o_ss["ss-istio-gw"])
conn(v1, "Serving", o_nfs, o_ss["ss-postgres"])
conn(v1, "Serving", o_gitea, o_cluster)
conn(v1, "Serving", o_cr, o_cluster)
# --- T2..T9: потребители каждого сервиса
PER_ROW = 6
for tid, tname, realizers, key in TS:
lst = consumers[tid]
if not lst:
continue
v = view(f"T{len(views)+1}. Потребители: {tname}",
f"{len(lst)} из {len(DATA['apps'])} приложений контура. Связи выведены из манифестов "
f"apps/*/base и apps/*/d8-ugmk-prod.")
ots = obj(v, tid, 20, 20, 260, 60)
oss = [obj(v, r, 320 + i * 280, 20, 260, 60) for i, r in enumerate(realizers)]
for o in oss:
conn(v, "Realization", o, ots)
oapps = []
for i, a in enumerate(lst):
oapps.append(obj(v, app_id[a], 20 + (i % PER_ROW) * 190, 150 + (i // PER_ROW) * 80, 170, 55))
for oa in oapps:
conn(v, "Serving", ots, oa)
# ---------------------------------------------------------------- сериализация
FOLDERS = [("Strategy", "strategy"), ("Business", "business"), ("Application", "application"),
("Technology & Physical", "technology"), ("Motivation", "motivation"),
("Implementation & Migration", "implementation_migration"), ("Other", "other"),
("Relations", "relations"), ("Views", "diagrams")]
def emit_obj(o, ind):
p = " " * ind
tgt = f' targetConnections={quoteattr(" ".join(o["tgt"]))}' if o["tgt"] else ""
x, y, w, h = o["b"]
# Порядок и имена — как их пишет сам Archi: <child>, затем bounds,
# sourceConnection, вложенные <child>. Имена EClass в метамодели
# (DiagramModelArchimateObject) в XML не используются.
s = f'{p}<child xsi:type="archimate:DiagramObject" id="{o["id"]}" archimateElement="{o["el"]}"{tgt}>\n'
s += f'{p} <bounds x="{x}" y="{y}" width="{w}" height="{h}"/>\n'
for c in o["src"]:
s += (f'{p} <sourceConnection xsi:type="archimate:Connection" id="{c["id"]}" '
f'source="{o["id"]}" target="{c["tgt"]}" relationship="{c["rel"]}"/>\n')
for c in o["children"]:
s += emit_obj(c, ind + 2)
s += f'{p}</child>\n'
return s
out = ['<?xml version="1.0" encoding="UTF-8"?>',
'<archimate:model xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
'xmlns:archimate="http://www.archimatetool.com/archimate" '
'name="СДЕ Sarex — контур УГМК" id="model-ugmk" version="5.0.0">',
' <purpose>Технологический слой контура d8-ugmk-prod, выведенный из репозитория infra/iac. '
'Прикладной слой присутствует только как потребители технологических сервисов — '
'межсервисные связи приложений моделируются отдельно.</purpose>']
for fname, ftype in FOLDERS:
inner = ""
if ftype in ("technology", "application"):
for folder, eid, etype, name, doc in elements:
if folder != ftype:
continue
if doc:
inner += (f' <element xsi:type="archimate:{etype}" name={quoteattr(name)} id="{eid}">\n'
f' <documentation>{escape(doc)}</documentation>\n'
f' </element>\n')
else:
inner += f' <element xsi:type="archimate:{etype}" name={quoteattr(name)} id="{eid}"/>\n'
elif ftype == "relations":
for rid, rtype, src, tgt in relations:
inner += (f' <element xsi:type="archimate:{rtype}Relationship" id="{rid}" '
f'source="{src}" target="{tgt}"/>\n')
elif ftype == "diagrams":
for v in views:
inner += f' <element xsi:type="archimate:ArchimateDiagramModel" name={quoteattr(v["name"])} id="{v["id"]}">\n'
if v["doc"]:
inner += f' <documentation>{escape(v["doc"])}</documentation>\n'
for o in v["objects"]:
inner += emit_obj(o, 6)
inner += ' </element>\n'
if inner:
out.append(f' <folder name={quoteattr(fname)} id="folder-{ftype}" type="{ftype}">\n{inner} </folder>')
else:
out.append(f' <folder name={quoteattr(fname)} id="folder-{ftype}" type="{ftype}"/>')
out.append('</archimate:model>')
OUT.write_text("\n".join(out) + "\n", encoding="utf-8")
print(f"записано: {OUT}")
print(f"элементов: {len(elements)} отношений: {len(relations)} представлений: {len(views)}")
for v in views:
n_obj = sum(1 + len(o["children"]) + sum(len(c["children"]) for c in o["children"]) for o in v["objects"])
print(f" {v['name']} ({n_obj} объектов)")