"""Собирает .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//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 и какие технологические сервисы это даёт. " "Приложения-потребители вынесены в отдельные представления T2–T9.") 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: , затем bounds, # sourceConnection, вложенные . Имена EClass в метамодели # (DiagramModelArchimateObject) в XML не используются. s = f'{p}\n' s += f'{p} \n' for c in o["src"]: s += (f'{p} \n') for c in o["children"]: s += emit_obj(c, ind + 2) s += f'{p}\n' return s out = ['', '', ' Технологический слой контура d8-ugmk-prod, выведенный из репозитория infra/iac. ' 'Прикладной слой присутствует только как потребители технологических сервисов — ' 'межсервисные связи приложений моделируются отдельно.'] 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' \n' f' {escape(doc)}\n' f' \n') else: inner += f' \n' elif ftype == "relations": for rid, rtype, src, tgt in relations: inner += (f' \n') elif ftype == "diagrams": for v in views: inner += f' \n' if v["doc"]: inner += f' {escape(v["doc"])}\n' for o in v["objects"]: inner += emit_obj(o, 6) inner += ' \n' if inner: out.append(f' \n{inner} ') else: out.append(f' ') out.append('') 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} объектов)")