@nitra/cursor 1.9.17 → 1.9.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/bin/auto-skills.md +2 -0
- package/mdc/abie.mdc +21 -249
- package/mdc/docker.mdc +2 -19
- package/mdc/hasura.mdc +1 -1
- package/mdc/k8s.mdc +2 -2
- package/package.json +1 -1
- package/policy/abie/base_deployment_preem/base_deployment_preem.rego +2 -2
- package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches.rego +2 -2
- package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches_test.rego +5 -5
- package/policy/docker/lint_docker_yml/lint_docker_yml.rego +91 -0
- package/policy/docker/lint_docker_yml/lint_docker_yml_test.rego +104 -0
- package/policy/docker/package_json/package_json.rego +35 -0
- package/policy/docker/package_json/package_json_test.rego +42 -0
- package/scripts/check-abie.mjs +73 -933
- package/scripts/check-hasura.mjs +8 -14
- package/scripts/check-k8s.mjs +1 -18
- package/scripts/lint-conftest.mjs +10 -1
- package/skills/abie-clean/SKILL.md +138 -0
- package/skills/abie-kustomize/SKILL.md +1 -1
package/scripts/check-abie.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* **Гілки:** у **`.github/workflows/clean-merged-branch.yml`** у кроці з
|
|
8
8
|
* **`phpdocker-io/github-actions-delete-abandoned-branches`** у **`with.ignore_branches`** мають бути
|
|
9
|
-
* **dev
|
|
9
|
+
* **dev** та **ua** (разом з іншими гілками, якщо потрібно).
|
|
10
10
|
*
|
|
11
11
|
* **Firebase Hosting:** у **підкаталогах першого рівня** (безпосередні діти кореня репозиторію; `node_modules` / `.git` пропускаються) не має бути
|
|
12
12
|
* **`.firebaserc`**, **`firebase.json`** та каталогу **`.firebase/`**; у **самому** корені репозиторію ці імена не перевіряються.
|
|
@@ -15,35 +15,29 @@
|
|
|
15
15
|
* має існувати **`hc.yaml`** із **`HealthCheckPolicy`** (**`networking.gke.io/v1`**), modeline **`$schema`**
|
|
16
16
|
* як у abie.mdc, **`requestPath`** — непорожній шлях від кореня (рядок, що починається з **`/`**: **`/healthz`**, **`/IsAlive`**, **`/api/live`** тощо), порт **8080**, **`targetRef`** на **headless Service** (ім'я з суфіксом **`-hl`**):
|
|
17
17
|
* якщо **`metadata.name`** уже закінчується на **`-hl`**, **`targetRef.name`** має збігатися з ним; інакше **`targetRef.name`** = **`${metadata.name}-hl`**.
|
|
18
|
-
* Загальні вимоги до **`# yaml-language-server: $schema`** для інших YAML під **`k8s`** — у **check-k8s.mjs** / **k8s.mdc
|
|
19
|
-
* Якщо в дереві **k8s** є **HealthCheckPolicy**, перевіряється **`ru/kustomization.yaml`** з patch **`$patch: delete`**
|
|
20
|
-
* (логіка вмісту — **`ruKustomizationHasHealthCheckDeletePatch`** у **check-k8s.mjs**, узгоджено з **k8s.mdc**).
|
|
18
|
+
* Загальні вимоги до **`# yaml-language-server: $schema`** для інших YAML під **`k8s`** — у **check-k8s.mjs** / **k8s.mdc**.
|
|
21
19
|
*
|
|
22
20
|
* **nodeSelector (base):** якщо **Deployment** лежить у шляху з сегментом **`base`** (наприклад **`…/k8s/base/deploy.yaml`**),
|
|
23
|
-
* у **`spec.template.spec.nodeSelector`** має бути **`preem`** з булевим значенням **true** або рядком **`'true'`** — overlay **ua**
|
|
21
|
+
* у **`spec.template.spec.nodeSelector`** має бути **`preem`** з булевим значенням **true** або рядком **`'true'`** — overlay **ua** далі підміняє селектор.
|
|
24
22
|
*
|
|
25
|
-
* **nodeSelector (overlay):** якщо в дереві **k8s** пакета є **Deployment**, у **`ua
|
|
26
|
-
* з **`path: /spec/template/spec/nodeSelector
|
|
23
|
+
* **nodeSelector (overlay ua):** якщо в дереві **k8s** пакета є **Deployment**, у **`ua/kustomization.yaml`** цього пакета — inline patch на **`kind: Deployment`**
|
|
24
|
+
* з **`path: /spec/template/spec/nodeSelector`** та **`preem: false`**.
|
|
27
25
|
* Узагальнені вимоги **k8s.mdc** до JSON6902 (зокрема заборона **remove** + **add** на той самий **path**) перевіряє **check-k8s.mjs**; **check-abie** — лише abie-специфічний вміст (без дублювання цього правила).
|
|
28
26
|
*
|
|
29
|
-
* **HTTPRoute (overlay):** лише якщо в каталозі пакета (батько **`k8s`**) є **`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`**
|
|
30
|
-
* — тоді в **`ua
|
|
31
|
-
* (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua
|
|
32
|
-
* якщо в тому ж **`kustomization.yaml`** згадується **`HASURA_GRAPHQL_JWT_SECRET`** (Hasura + JWT).
|
|
27
|
+
* **HTTPRoute (overlay ua):** лише якщо в каталозі пакета (батько **`k8s`**) є **`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`**
|
|
28
|
+
* — тоді в **`ua/kustomization.yaml`** потрібен patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
|
|
29
|
+
* (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua**, також дозволені префікси **`ua-*`**).
|
|
33
30
|
* **HTTPRoute (base / dev):** у маніфесті **HTTPRoute** у шляху з сегментом **`base`** (наприклад **`…/k8s/base/hr.yaml`**) у **`spec.hostnames`** дозволені лише **`aiml.live`**, **`*.aiml.live`** та інші піддомени **aiml.live** (канонічно порівняння без урахування регістру).
|
|
34
|
-
* **Спільні бекенди (`auth-run-hl`, `file-link-hl`):** у **HTTPRoute** під **`k8s`** поза overlay **ua**
|
|
35
|
-
* у patch overlay **ua**
|
|
31
|
+
* **Спільні бекенди (`auth-run-hl`, `file-link-hl`):** у **HTTPRoute** під **`k8s`** поза overlay **ua** (шлях не містить **`k8s/ua/`**) кожен такий **`backendRefs`** має **`namespace: dev`** і порт **8080**;
|
|
32
|
+
* у patch overlay **ua** — по одному **JSON6902** на **`/spec/rules/…/backendRefs/…/namespace`** з **`value`**: **ua** (кількість patch-ів = кількість таких **`backendRefs`** у пакеті).
|
|
36
33
|
* Вибір **`op`** — **k8s.mdc**.
|
|
37
34
|
*
|
|
38
|
-
* **
|
|
39
|
-
* у файлі **`k8s/ru/kustomization.yaml`** того ж пакета (overlay середовища **ru**) — inline **JSON6902** на **`kind: Service`** з тим самим **`target.name`**: **`path: /spec/type`**, **`value: NodePort`**; якщо в base було **`spec.clusterIP: None`** — **`op: remove`** для **`/spec/clusterIP`**; якщо в base **явно** задано **`spec.clusterIPs`** — також **`remove`** для **`/spec/clusterIPs`** (інакше **API** може залишити **`None`** для **NodePort**; без ключа **`clusterIPs`** у base **`remove`** на **`/spec/clusterIPs`** ламає **`kubectl kustomize`**).
|
|
40
|
-
*
|
|
41
|
-
* **env→cluster DNS:** abie живе у трьох різних кластерах (GKE dev/ua + YC ru), тож DNS-суфікс і namespace-префікс у будь-якому
|
|
35
|
+
* **env→cluster DNS:** abie живе у двох GKE-кластерах (dev / ua), тож DNS-суфікс і namespace-префікс у будь-якому
|
|
42
36
|
* **внутрішньокластерному** URL виду `http://<svc>.<ns>.svc.<dns>` мають відповідати імені env-файла. Скануються всі `*.env` файли,
|
|
43
|
-
* basename яких збігається з `dev.env` / `ua.env`
|
|
37
|
+
* basename яких збігається з `dev.env` / `ua.env` (опційно з провідною крапкою — `.dev.env` тощо). Для кожного знайденого
|
|
44
38
|
* internal URL у файлі (не лише `HASURA_GRAPHQL_ENDPOINT`, а й KVCMS, auth-run, file-link тощо) валідатор `validateAbieEnvInternalUrls`
|
|
45
|
-
* вимагає: для `dev.env` — DNS `abie-dev.internal` і namespace починається з `dev-`; для `ua.env` — `abie-ua.internal` + `ua
|
|
46
|
-
*
|
|
39
|
+
* вимагає: для `dev.env` — DNS `abie-dev.internal` і namespace починається з `dev-`; для `ua.env` — `abie-ua.internal` + `ua-`.
|
|
40
|
+
* Файл `.env` без імені (локальний для розробника) виключено зі сканування — як і у `check-hasura.mjs`.
|
|
47
41
|
*/
|
|
48
42
|
import { existsSync } from 'node:fs'
|
|
49
43
|
import { readdir, readFile } from 'node:fs/promises'
|
|
@@ -51,7 +45,7 @@ import { basename, dirname, join, relative } from 'node:path'
|
|
|
51
45
|
|
|
52
46
|
import { parseAllDocuments } from 'yaml'
|
|
53
47
|
|
|
54
|
-
import { pathHasK8sSegment
|
|
48
|
+
import { pathHasK8sSegment } from './check-k8s.mjs'
|
|
55
49
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
56
50
|
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
57
51
|
import { runConftestBatch } from './utils/run-conftest-batch.mjs'
|
|
@@ -62,9 +56,6 @@ const CONFIG_FILE = '.n-cursor.json'
|
|
|
62
56
|
/** Каталоги-діти в корені, які пропускаються при скануванні на артефакти Firebase Hosting (abie). */
|
|
63
57
|
const ABIE_FIREBASE_HOSTING_SCAN_SKIP_TOP_DIR_NAMES = new Set(['.git', 'node_modules'])
|
|
64
58
|
|
|
65
|
-
/** Маркер у kustomization.yaml: якщо зустрічається у файлі — для overlay ru у patch HTTPRoute потрібна анотація gwin…websocket. */
|
|
66
|
-
const HASURA_JWT_SECRET_IN_KUSTOMIZATION = 'HASURA_GRAPHQL_JWT_SECRET'
|
|
67
|
-
|
|
68
59
|
/**
|
|
69
60
|
* Спільні **Service** (**`-hl`**) у **dev**: у base-**HTTPRoute** обов'язково **`namespace: dev`**, у overlay — patch **`…/backendRefs/…/namespace`** (abie.mdc).
|
|
70
61
|
* Експорт для споживачів / тестів.
|
|
@@ -81,83 +72,53 @@ export const ABIE_BASE_DEV_HTTPROUTE_HOST_ROOT = 'aiml.live'
|
|
|
81
72
|
|
|
82
73
|
const MODELINE_RE = /^#\s*yaml-language-server:\s*\$schema=(\S+)\s*$/
|
|
83
74
|
const LINE_SPLIT_RE = /\r?\n/u
|
|
84
|
-
const RU_KUSTOMIZATION_PATH_RE = /(^|\/)ru\/kustomization\.yaml$/u
|
|
85
75
|
const UA_KUSTOMIZATION_PATH_RE = /(^|\/)ua\/kustomization\.yaml$/u
|
|
86
|
-
const OVERLAY_PACKAGE_DIR_RE = /^(.+)\/k8s\/
|
|
76
|
+
const OVERLAY_PACKAGE_DIR_RE = /^(.+)\/k8s\/ua\/kustomization\.yaml$/u
|
|
87
77
|
const BASE_SEGMENT_RE = /(^|\/)base\//u
|
|
88
78
|
const YAML_EXTENSION_RE = /\.ya?ml$/iu
|
|
89
79
|
const K8S_PACKAGE_DIR_RE = /^(.+)\/k8s\//u
|
|
90
|
-
const PATCH_PATH_TYPE_RE = /path:\s*\/spec\/type\b/u
|
|
91
|
-
const PATCH_VALUE_NODE_PORT_RE = /value:\s*['"]?NodePort['"]?(?:\s|$)/iu
|
|
92
80
|
const PATCH_NODE_SELECTOR_PATH_RE = /path:\s*\/spec\/template\/spec\/nodeSelector\b/u
|
|
93
81
|
const PATCH_PREEM_FALSE_RE = /\bpreem:\s*['"]?false['"]?\b/u
|
|
94
|
-
const PATCH_YANDEX_PREEMPTIBLE_FALSE_RE = /yandex\.cloud\/preemptible:\s*['"]?false['"]?/u
|
|
95
82
|
const TRAILING_SLASH_RE = /\/$/u
|
|
96
83
|
const PATCH_HOSTNAMES_PATH_RE = /path:\s*\/spec\/hostnames\b/mu
|
|
97
|
-
// Overlay namespaces: allow ua
|
|
84
|
+
// Overlay namespaces: allow `ua` and `ua-*` (e.g. ua-b2b).
|
|
98
85
|
const PATCH_PARENT_REF_NS_UA_RE =
|
|
99
86
|
/path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/imu
|
|
100
|
-
const PATCH_PARENT_REF_NS_RU_RE =
|
|
101
|
-
/path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/imu
|
|
102
|
-
const WEBSOCKET_ANNOTATION_RE = /gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/mu
|
|
103
|
-
const REMOVE_CLUSTER_IP_AFTER_OP_RE = /op:\s*remove\b[\s\S]{0,200}?path:\s*\/spec\/clusterIP\b/mu
|
|
104
|
-
const REMOVE_CLUSTER_IP_BEFORE_OP_RE = /path:\s*\/spec\/clusterIP\b[\s\S]{0,200}?op:\s*remove\b/mu
|
|
105
|
-
const REMOVE_CLUSTER_IPS_AFTER_OP_RE = /op:\s*remove\b[\s\S]{0,200}?path:\s*\/spec\/clusterIPs\b/mu
|
|
106
|
-
const REMOVE_CLUSTER_IPS_BEFORE_OP_RE = /path:\s*\/spec\/clusterIPs\b[\s\S]{0,200}?op:\s*remove\b/mu
|
|
107
|
-
|
|
108
|
-
/** Підрядок образу Hasura у контейнері Deployment (abie.mdc nginx-sidecar). */
|
|
109
|
-
const HASURA_IMAGE_MARKER = 'hasura/graphql-engine'
|
|
110
|
-
/** Nginx-sidecar image (abie.mdc): nginx:*-alpine. */
|
|
111
|
-
const NGINX_SIDECAR_IMAGE_RE = /image:\s*nginx:\S*-alpine/u
|
|
112
|
-
/** containerPort: 8081 у patch Deployment (abie.mdc). */
|
|
113
|
-
const NGINX_SIDECAR_CONTAINER_PORT_RE = /containerPort:\s*8081\b/u
|
|
114
|
-
/** port: 8081 у patch Service -hl (proxy порт, abie.mdc). */
|
|
115
|
-
const PATCH_PROXY_PORT_8081_RE = /\bport:\s*8081\b/u
|
|
116
|
-
/** configmap-nginx.yaml у resources kustomization (abie.mdc). */
|
|
117
|
-
const RESOURCES_CONFIGMAP_NGINX_RE = /configmap-nginx\.yaml/u
|
|
118
|
-
/** path /spec/rules/{i}/backendRefs/{j}/port … value: 8081 у patch HTTPRoute (path→value, abie.mdc). */
|
|
119
|
-
const HTTPROUTE_BACKENDREF_PORT_8081_RE =
|
|
120
|
-
/path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/port\b[\s\S]{0,200}?value:\s*8081\b/mu
|
|
121
|
-
/** Те саме, value→path. */
|
|
122
|
-
const HTTPROUTE_BACKENDREF_PORT_8081_VALUE_FIRST_RE =
|
|
123
|
-
/value:\s*8081\b[\s\S]{0,200}?path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/port\b/mu
|
|
124
87
|
|
|
125
88
|
/**
|
|
126
|
-
* Регекс basename env-файлу abie: `dev.env` / `ua.env
|
|
89
|
+
* Регекс basename env-файлу abie: `dev.env` / `ua.env`, опційно з провідною крапкою (`.dev.env` тощо).
|
|
127
90
|
* Файл рівно `.env` (без імені) — виключення з правила: локальний файл розробника, `check-abie` його не сканує
|
|
128
91
|
* (так само як `check-hasura`, див. `isEnvFile`).
|
|
129
92
|
*/
|
|
130
|
-
const ABIE_ENV_FILE_BASENAME_RE = /^\.?(dev|ua
|
|
93
|
+
const ABIE_ENV_FILE_BASENAME_RE = /^\.?(dev|ua)\.env$/u
|
|
131
94
|
|
|
132
95
|
/**
|
|
133
96
|
* Глобальний регекс кластерного internal URL у тексті env-файлу.
|
|
134
97
|
* Використовується з `String.prototype.matchAll`, тому має флаг `g`.
|
|
135
|
-
*
|
|
98
|
+
* Допустимий DNS-формат — `<cluster>.internal` (GKE).
|
|
136
99
|
* Порт необов'язковий — у KVCMS-конфігах інколи лежить URL без порту (8080 додається сервісом за замовчуванням).
|
|
137
100
|
*/
|
|
138
101
|
const ABIE_INTERNAL_URL_GLOBAL_RE =
|
|
139
|
-
/\bhttp:\/\/([a-z0-9][a-z0-9-]*)\.([a-z0-9][a-z0-9-]*)\.svc\.(
|
|
102
|
+
/\bhttp:\/\/([a-z0-9][a-z0-9-]*)\.([a-z0-9][a-z0-9-]*)\.svc\.([a-z0-9][a-z0-9-]*\.internal)(?::\d+)?(?:\/[^\s"'`]*)?/giu
|
|
140
103
|
|
|
141
104
|
/**
|
|
142
105
|
* Очікуваний кластерний DNS-суфікс і namespace-префікс для кожного env-файлу abie.
|
|
143
|
-
* `dev` / `ua` живуть у GKE з власним `<cluster>.internal
|
|
144
|
-
* стандартний `cluster.local` (без імені кластера).
|
|
106
|
+
* `dev` / `ua` живуть у двох GKE-кластерах з власним `<cluster>.internal`.
|
|
145
107
|
*/
|
|
146
108
|
const ABIE_ENV_CLUSTER_DNS_MAP = Object.freeze({
|
|
147
109
|
dev: Object.freeze({ clusterDns: 'abie-dev.internal', namespacePrefix: 'dev-' }),
|
|
148
|
-
ua: Object.freeze({ clusterDns: 'abie-ua.internal', namespacePrefix: 'ua-' })
|
|
149
|
-
ru: Object.freeze({ clusterDns: 'cluster.local', namespacePrefix: 'ru-' })
|
|
110
|
+
ua: Object.freeze({ clusterDns: 'abie-ua.internal', namespacePrefix: 'ua-' })
|
|
150
111
|
})
|
|
151
112
|
|
|
152
113
|
/**
|
|
153
|
-
* Дістає ім'я env (`dev` / `ua`
|
|
114
|
+
* Дістає ім'я env (`dev` / `ua`) з basename env-файлу abie.
|
|
154
115
|
* Для не-abie env-файлів (наприклад `production.env`, `.env` без імені) повертає `null`.
|
|
155
116
|
* @param {string} basenameOfEnvFile basename файла (без шляху)
|
|
156
|
-
* @returns {('dev' | 'ua'
|
|
117
|
+
* @returns {('dev' | 'ua') | null} ім'я env або `null`
|
|
157
118
|
*/
|
|
158
119
|
export function abieEnvNameFromBasename(basenameOfEnvFile) {
|
|
159
120
|
const m = basenameOfEnvFile.match(ABIE_ENV_FILE_BASENAME_RE)
|
|
160
|
-
return m ? /** @type {'dev' | 'ua'
|
|
121
|
+
return m ? /** @type {'dev' | 'ua'} */ (m[1]) : null
|
|
161
122
|
}
|
|
162
123
|
|
|
163
124
|
/**
|
|
@@ -165,7 +126,7 @@ export function abieEnvNameFromBasename(basenameOfEnvFile) {
|
|
|
165
126
|
* для кожного знайденого internal URL. URL шукається глобально (`matchAll`), тож одне й те саме
|
|
166
127
|
* порушення в кількох змінних дасть стільки ж окремих помилок.
|
|
167
128
|
* @param {string} content вміст env-файлу (UTF-8)
|
|
168
|
-
* @param {'dev' | 'ua'
|
|
129
|
+
* @param {'dev' | 'ua'} envName ім'я env, отримане з `abieEnvNameFromBasename`
|
|
169
130
|
* @returns {string[]} порожній масив, якщо все OK; інакше — список повідомлень про порушення
|
|
170
131
|
*/
|
|
171
132
|
export function validateAbieEnvInternalUrls(content, envName) {
|
|
@@ -189,16 +150,6 @@ export function validateAbieEnvInternalUrls(content, envName) {
|
|
|
189
150
|
return errors
|
|
190
151
|
}
|
|
191
152
|
|
|
192
|
-
/**
|
|
193
|
-
* Чи відносний шлях вказує на **`ru/kustomization.yaml`** (сегмент **`ru`** перед ім'ям файлу) — специфіка abie overlay.
|
|
194
|
-
* @param {string} rel шлях від кореня репозиторію
|
|
195
|
-
* @returns {boolean} true, якщо це `…/ru/kustomization.yaml`
|
|
196
|
-
*/
|
|
197
|
-
export function isRuKustomizationPath(rel) {
|
|
198
|
-
const norm = rel.replaceAll('\\', '/')
|
|
199
|
-
return RU_KUSTOMIZATION_PATH_RE.test(norm)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
153
|
/**
|
|
203
154
|
* Чи відносний шлях вказує на **`ua/kustomization.yaml`** (сегмент **`ua`** перед ім'ям файлу) — специфіка abie overlay.
|
|
204
155
|
* @param {string} rel шлях від кореня репозиторію
|
|
@@ -210,10 +161,10 @@ export function isUaKustomizationPath(rel) {
|
|
|
210
161
|
}
|
|
211
162
|
|
|
212
163
|
/**
|
|
213
|
-
* Каталог пакета: шлях перед сегментом **`/k8s/`** для overlay **`…/k8s/
|
|
164
|
+
* Каталог пакета: шлях перед сегментом **`/k8s/`** для overlay **`…/k8s/ua/kustomization.yaml`**.
|
|
214
165
|
* @param {string} root корінь репозиторію
|
|
215
|
-
* @param {string} kustomizationAbs абсолютний шлях до **ua**
|
|
216
|
-
* @returns {string | null} абсолютний шлях до каталогу пакета або null, якщо шлях не overlay ua
|
|
166
|
+
* @param {string} kustomizationAbs абсолютний шлях до **ua** kustomization.yaml
|
|
167
|
+
* @returns {string | null} абсолютний шлях до каталогу пакета або null, якщо шлях не overlay ua
|
|
217
168
|
*/
|
|
218
169
|
export function abiePackageDirFromK8sOverlay(root, kustomizationAbs) {
|
|
219
170
|
const rel = relative(root, kustomizationAbs).replaceAll('\\', '/') || kustomizationAbs
|
|
@@ -224,7 +175,7 @@ export function abiePackageDirFromK8sOverlay(root, kustomizationAbs) {
|
|
|
224
175
|
/**
|
|
225
176
|
* Чи для цього overlay застосовувати вимоги **HTTPRoute** (лише Vite-пакети).
|
|
226
177
|
* @param {string} root корінь репозиторію
|
|
227
|
-
* @param {string} kustomizationAbs абсолютний шлях до **ua**
|
|
178
|
+
* @param {string} kustomizationAbs абсолютний шлях до **ua** kustomization.yaml
|
|
228
179
|
* @returns {boolean} **true**, якщо поруч із **k8s** є **vite.config** (**js** / **mjs** / **ts**)
|
|
229
180
|
*/
|
|
230
181
|
export function abieOverlayRequiresHttpRouteByVite(root, kustomizationAbs) {
|
|
@@ -240,10 +191,10 @@ export function abieOverlayRequiresHttpRouteByVite(root, kustomizationAbs) {
|
|
|
240
191
|
}
|
|
241
192
|
|
|
242
193
|
/**
|
|
243
|
-
* Чи в дереві **k8s** того ж пакета, що й overlay **ua
|
|
194
|
+
* Чи в дереві **k8s** того ж пакета, що й overlay **ua**, є **Deployment** (за каталогами з **collectDeploymentDirs**).
|
|
244
195
|
* @param {Set<string>} deploymentDirs абсолютні каталоги YAML-файлів із **Deployment**
|
|
245
196
|
* @param {string} root корінь репозиторію
|
|
246
|
-
* @param {string} kustomizationAbs абсолютний шлях до **ua**
|
|
197
|
+
* @param {string} kustomizationAbs абсолютний шлях до **ua** kustomization.yaml
|
|
247
198
|
* @returns {boolean} **true**, якщо хоч один каталог із **deploymentDirs** лежить під **`…/k8s/`** цього пакета
|
|
248
199
|
*/
|
|
249
200
|
export function abieOverlayK8sTreeHasDeployment(deploymentDirs, root, kustomizationAbs) {
|
|
@@ -307,7 +258,7 @@ export async function isAbieRuleEnabled(root) {
|
|
|
307
258
|
}
|
|
308
259
|
|
|
309
260
|
// Per-document валідація `clean-merged-branch.yml` (with.ignore_branches з
|
|
310
|
-
// dev/ua
|
|
261
|
+
// dev/ua) делегована rego-пакету `abie.clean_merged_ignore_branches`
|
|
311
262
|
// (`npm/policy/abie/clean_merged_ignore_branches/`). JS викликає
|
|
312
263
|
// `runConftestBatch` у `checkCleanMergedBranch`.
|
|
313
264
|
|
|
@@ -355,383 +306,6 @@ function isDeploymentDoc(obj) {
|
|
|
355
306
|
)
|
|
356
307
|
}
|
|
357
308
|
|
|
358
|
-
/**
|
|
359
|
-
* Чи документ — **Service**.
|
|
360
|
-
* @param {unknown} obj корінь YAML-документа
|
|
361
|
-
* @returns {boolean} true, якщо **kind** — **Service**
|
|
362
|
-
*/
|
|
363
|
-
function isServiceDoc(obj) {
|
|
364
|
-
return (
|
|
365
|
-
obj !== null &&
|
|
366
|
-
typeof obj === 'object' &&
|
|
367
|
-
!Array.isArray(obj) &&
|
|
368
|
-
/** @type {Record<string, unknown>} */ (obj).kind === 'Service'
|
|
369
|
-
)
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Чи відносний шлях до YAML під **`…/k8s/…`** не в каталозі overlay **`k8s/ua/`** чи **`k8s/ru/`** у репозиторії (після **`k8s/`** одразу не йде **`ua/`** чи **`ru/`**).
|
|
374
|
-
* @param {string} relFromRoot шлях від кореня
|
|
375
|
-
* @returns {boolean} true для base / спільних маніфестів; **false** для файлів усередині **`k8s/ua/…`** або **`k8s/ru/…`**
|
|
376
|
-
*/
|
|
377
|
-
function k8sYamlRelOutsideUaRuOverlays(relFromRoot) {
|
|
378
|
-
const norm = relFromRoot.replaceAll('\\', '/')
|
|
379
|
-
const idx = norm.indexOf('/k8s/')
|
|
380
|
-
if (idx === -1) {
|
|
381
|
-
return false
|
|
382
|
-
}
|
|
383
|
-
const after = norm.slice(idx + '/k8s/'.length)
|
|
384
|
-
return after.length > 0 && !after.startsWith('ua/') && !after.startsWith('ru/')
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Каталог пакета з відносного шляху **`…/k8s/…`** (частина до **`/k8s/`**).
|
|
389
|
-
* @param {string} root корінь репозиторію
|
|
390
|
-
* @param {string} relFromRoot відносний шлях
|
|
391
|
-
* @returns {string | null} абсолютний шлях до каталогу пакета або **null**
|
|
392
|
-
*/
|
|
393
|
-
function abiePackageDirFromK8sYamlRel(root, relFromRoot) {
|
|
394
|
-
const norm = relFromRoot.replaceAll('\\', '/')
|
|
395
|
-
const m = norm.match(K8S_PACKAGE_DIR_RE)
|
|
396
|
-
return m ? join(root, m[1]) : null
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Чи **Service** у base-шарі abie потребує в **ru** patch **`spec.type: NodePort`** (у т. ч. **headless**; не вже **NodePort** / **LoadBalancer** / **ExternalName**).
|
|
401
|
-
* @param {unknown} obj корінь YAML (**Service**)
|
|
402
|
-
* @returns {boolean} true, якщо для overlay **ru** очікується **NodePort**
|
|
403
|
-
*/
|
|
404
|
-
export function serviceDocumentRequiresAbieRuNodePortOverlay(obj) {
|
|
405
|
-
if (!isServiceDoc(obj)) {
|
|
406
|
-
return false
|
|
407
|
-
}
|
|
408
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
409
|
-
const meta = rec.metadata
|
|
410
|
-
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
|
|
411
|
-
return false
|
|
412
|
-
}
|
|
413
|
-
const name = /** @type {Record<string, unknown>} */ (meta).name
|
|
414
|
-
if (typeof name !== 'string' || name.trim() === '') {
|
|
415
|
-
return false
|
|
416
|
-
}
|
|
417
|
-
const spec = rec.spec
|
|
418
|
-
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
419
|
-
return true
|
|
420
|
-
}
|
|
421
|
-
const sp = /** @type {Record<string, unknown>} */ (spec)
|
|
422
|
-
const t = sp.type
|
|
423
|
-
if (t === 'NodePort' || t === 'LoadBalancer' || t === 'ExternalName') {
|
|
424
|
-
return false
|
|
425
|
-
}
|
|
426
|
-
return true
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Чи в base-**Service** задано **headless** через **`spec.clusterIP: None`**, який треба прибрати в **ru** перед **NodePort**.
|
|
431
|
-
* @param {unknown} obj корінь YAML (**Service**)
|
|
432
|
-
* @returns {boolean} **true**, якщо **`spec.clusterIP === 'None'`**
|
|
433
|
-
*/
|
|
434
|
-
export function serviceDocumentRequiresRuClusterIPNoneRemoval(obj) {
|
|
435
|
-
if (!isServiceDoc(obj)) {
|
|
436
|
-
return false
|
|
437
|
-
}
|
|
438
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
439
|
-
const spec = rec.spec
|
|
440
|
-
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
441
|
-
return false
|
|
442
|
-
}
|
|
443
|
-
const sp = /** @type {Record<string, unknown>} */ (spec)
|
|
444
|
-
return sp.clusterIP === 'None'
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Чи в base-**Service** у **`spec`** явно задано поле **`clusterIPs`** (тоді **`remove`** на **`/spec/clusterIPs`** безпечний для **`kubectl kustomize`**).
|
|
449
|
-
* @param {unknown} obj корінь YAML (**Service**)
|
|
450
|
-
* @returns {boolean} **true**, якщо **`Object.hasOwn(spec, 'clusterIPs')`**
|
|
451
|
-
*/
|
|
452
|
-
export function serviceDocumentBaseDeclaresClusterIPsField(obj) {
|
|
453
|
-
if (!isServiceDoc(obj)) {
|
|
454
|
-
return false
|
|
455
|
-
}
|
|
456
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
457
|
-
const spec = rec.spec
|
|
458
|
-
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
459
|
-
return false
|
|
460
|
-
}
|
|
461
|
-
const sp = /** @type {Record<string, unknown>} */ (spec)
|
|
462
|
-
return Object.hasOwn(sp, 'clusterIPs')
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Чи **JSON6902**-текст містить **`op: remove`** для заданого **`path`** (порядок ключів **op** / **path** неважливий).
|
|
467
|
-
* @param {string} patchText поле **patch** у kustomization
|
|
468
|
-
* @param {string} posixPath **`/spec/clusterIP`** або **`/spec/clusterIPs`**
|
|
469
|
-
* @returns {boolean} true, якщо знайдено пару **remove** + **path**
|
|
470
|
-
*/
|
|
471
|
-
export function jsonPatchRemovesPath(patchText, posixPath) {
|
|
472
|
-
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
473
|
-
return false
|
|
474
|
-
}
|
|
475
|
-
if (posixPath !== '/spec/clusterIP' && posixPath !== '/spec/clusterIPs') {
|
|
476
|
-
return false
|
|
477
|
-
}
|
|
478
|
-
if (posixPath === '/spec/clusterIP') {
|
|
479
|
-
return REMOVE_CLUSTER_IP_AFTER_OP_RE.test(patchText) || REMOVE_CLUSTER_IP_BEFORE_OP_RE.test(patchText)
|
|
480
|
-
}
|
|
481
|
-
return REMOVE_CLUSTER_IPS_AFTER_OP_RE.test(patchText) || REMOVE_CLUSTER_IPS_BEFORE_OP_RE.test(patchText)
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
/**
|
|
485
|
-
* Чи patch містить **`op: remove`** для **`/spec/clusterIP`**, щоб прибрати **headless** перед **NodePort**.
|
|
486
|
-
* @param {string} patchText поле **patch** у kustomization
|
|
487
|
-
* @returns {boolean} true, якщо є **remove** для **`/spec/clusterIP`**
|
|
488
|
-
*/
|
|
489
|
-
export function jsonPatchTextClearsHeadlessServiceClusterIPNone(patchText) {
|
|
490
|
-
return jsonPatchRemovesPath(patchText, '/spec/clusterIP')
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Чи фрагмент **JSON6902** у **`patch`** задає **`/spec/type`** зі значенням **NodePort** (abie overlay **ru**).
|
|
495
|
-
* @param {string} patchText поле **patch** у kustomization
|
|
496
|
-
* @returns {boolean} true, якщо знайдено **path** і **value**
|
|
497
|
-
*/
|
|
498
|
-
export function jsonPatchTextSetsServiceTypeNodePort(patchText) {
|
|
499
|
-
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
500
|
-
return false
|
|
501
|
-
}
|
|
502
|
-
if (!PATCH_PATH_TYPE_RE.test(patchText)) {
|
|
503
|
-
return false
|
|
504
|
-
}
|
|
505
|
-
if (!PATCH_VALUE_NODE_PORT_RE.test(patchText)) {
|
|
506
|
-
return false
|
|
507
|
-
}
|
|
508
|
-
return true
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Витягує ім'я та текст patch для Service з елемента patches.
|
|
513
|
-
* @param {unknown} p елемент масиву patches
|
|
514
|
-
* @returns {{ name: string, patchStr: string } | null} ім'я та текст patch або null
|
|
515
|
-
*/
|
|
516
|
-
function extractServicePatchEntry(p) {
|
|
517
|
-
if (p === null || typeof p !== 'object' || Array.isArray(p)) return null
|
|
518
|
-
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
519
|
-
const target = pr.target
|
|
520
|
-
if (target === null || typeof target !== 'object' || Array.isArray(target)) return null
|
|
521
|
-
const tg = /** @type {Record<string, unknown>} */ (target)
|
|
522
|
-
if (tg.kind !== 'Service' || typeof tg.name !== 'string' || tg.name.trim() === '') return null
|
|
523
|
-
const patchStr = pr.patch
|
|
524
|
-
if (typeof patchStr !== 'string' || patchStr.trim() === '') return null
|
|
525
|
-
return { name: tg.name, patchStr }
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* З одного документа **Kustomization** збирає пари **Service name → patch text** для **inline patches** з **target.kind: Service**.
|
|
530
|
-
* @param {import('yaml').Document} doc документ після **parseAllDocuments**
|
|
531
|
-
* @returns {Map<string, string>} ім'я сервісу → текст **patch**
|
|
532
|
-
*/
|
|
533
|
-
function collectAbieServicePatchTextsByNameFromKustomizationDoc(doc) {
|
|
534
|
-
/** @type {Map<string, string>} */
|
|
535
|
-
const out = new Map()
|
|
536
|
-
if (doc.errors.length > 0) return out
|
|
537
|
-
const root = doc.toJSON()
|
|
538
|
-
if (root === null || typeof root !== 'object' || Array.isArray(root)) return out
|
|
539
|
-
const rec = /** @type {Record<string, unknown>} */ (root)
|
|
540
|
-
if (rec.kind !== 'Kustomization' || !Array.isArray(rec.patches)) return out
|
|
541
|
-
for (const p of rec.patches) {
|
|
542
|
-
const entry = extractServicePatchEntry(p)
|
|
543
|
-
if (entry) {
|
|
544
|
-
const prev = out.get(entry.name)
|
|
545
|
-
out.set(entry.name, prev === undefined ? entry.patchStr : `${prev}\n${entry.patchStr}`)
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
return out
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Збирає тексти **patch** на **Service** з **kustomization.yaml** (усі документи).
|
|
553
|
-
* @param {string} raw повний текст **kustomization.yaml**
|
|
554
|
-
* @returns {Map<string, string>} **target.name** → об'єднаний текст **patch**
|
|
555
|
-
*/
|
|
556
|
-
function collectAbieRuServicePatchTextByTargetNameFromRaw(raw) {
|
|
557
|
-
const body = stripBom(raw)
|
|
558
|
-
const lines = body.split(LINE_SPLIT_RE)
|
|
559
|
-
const first = lines[0] ?? ''
|
|
560
|
-
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
561
|
-
/** @type {Map<string, string>} */
|
|
562
|
-
const byName = new Map()
|
|
563
|
-
/** @type {import('yaml').Document[]} */
|
|
564
|
-
let docs
|
|
565
|
-
try {
|
|
566
|
-
docs = parseAllDocuments(rest)
|
|
567
|
-
} catch {
|
|
568
|
-
return byName
|
|
569
|
-
}
|
|
570
|
-
for (const doc of docs) {
|
|
571
|
-
const chunk = collectAbieServicePatchTextsByNameFromKustomizationDoc(doc)
|
|
572
|
-
for (const [k, v] of chunk) {
|
|
573
|
-
const prev = byName.get(k)
|
|
574
|
-
byName.set(k, prev === undefined ? v : `${prev}\n${v}`)
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
return byName
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/**
|
|
581
|
-
* Збирає помилки patch для одного Service за ім'ям.
|
|
582
|
-
* @param {string} name ім'я Service
|
|
583
|
-
* @param {string | undefined} pt текст patch або undefined
|
|
584
|
-
* @param {{ requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove?: boolean } | undefined} flags прапорці
|
|
585
|
-
* @param {string[]} errors масив для запису помилок
|
|
586
|
-
*/
|
|
587
|
-
function collectServicePatchErrors(name, pt, flags, errors) {
|
|
588
|
-
if (pt === undefined || String(pt).trim() === '') {
|
|
589
|
-
errors.push(`${name}: немає inline patch для kind: Service`)
|
|
590
|
-
return
|
|
591
|
-
}
|
|
592
|
-
if (!jsonPatchTextSetsServiceTypeNodePort(pt)) {
|
|
593
|
-
errors.push(`${name}: потрібен JSON6902 path /spec/type та value NodePort`)
|
|
594
|
-
}
|
|
595
|
-
if (flags?.requiresClusterIPNoneClear === true && !jsonPatchTextClearsHeadlessServiceClusterIPNone(pt)) {
|
|
596
|
-
errors.push(
|
|
597
|
-
`${name}: для spec.clusterIP: None додай у той самий patch op: remove для path /spec/clusterIP (abie.mdc)`
|
|
598
|
-
)
|
|
599
|
-
}
|
|
600
|
-
if (flags?.requiresClusterIPsRemove === true && !jsonPatchRemovesPath(pt, '/spec/clusterIPs')) {
|
|
601
|
-
errors.push(
|
|
602
|
-
`${name}: у base задано spec.clusterIPs — додай op: remove для path /spec/clusterIPs (інакше NodePort з None у clusterIPs; abie.mdc)`
|
|
603
|
-
)
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/**
|
|
608
|
-
* Повідомлення про порушення patch **Service** у **ru/kustomization.yaml** (abie.mdc).
|
|
609
|
-
* @param {string} raw повний текст **kustomization.yaml**
|
|
610
|
-
* @param {Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove?: boolean }>} targetsByName ім'я **Service** → прапорці patch
|
|
611
|
-
* @returns {string[]} порожньо, якщо все OK
|
|
612
|
-
*/
|
|
613
|
-
export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
|
|
614
|
-
if (targetsByName.size === 0) return []
|
|
615
|
-
const byName = collectAbieRuServicePatchTextByTargetNameFromRaw(raw)
|
|
616
|
-
/** @type {string[]} */
|
|
617
|
-
const errors = []
|
|
618
|
-
for (const name of [...targetsByName.keys()].toSorted((a, b) => a.localeCompare(b))) {
|
|
619
|
-
collectServicePatchErrors(name, byName.get(name), targetsByName.get(name), errors)
|
|
620
|
-
}
|
|
621
|
-
return errors
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Для кожного пакета збирає **Service**, які в overlay **ru** мають стати **NodePort** (abie.mdc).
|
|
626
|
-
* @param {string} root корінь репозиторію
|
|
627
|
-
* @param {string[]} yamlAbs абсолютні шляхи yaml під **k8s**
|
|
628
|
-
* @param {(msg: string) => void} fail реєстрація помилки читання/парсингу
|
|
629
|
-
* @returns {Promise<Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>>} **pkgAbs** → (**ім'я** → прапорці)
|
|
630
|
-
*/
|
|
631
|
-
/**
|
|
632
|
-
* Обробляє один Service-документ для збору NodePort-патч цілей.
|
|
633
|
-
* @param {unknown} obj YAML-документ (toJSON)
|
|
634
|
-
* @param {string} pkgAbs абсолютний шлях до пакета
|
|
635
|
-
* @param {Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>} map результуючий Map
|
|
636
|
-
*/
|
|
637
|
-
function processServiceDocForNodePortTargets(obj, pkgAbs, map) {
|
|
638
|
-
if (!serviceDocumentRequiresAbieRuNodePortOverlay(obj)) return
|
|
639
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
640
|
-
const meta = /** @type {Record<string, unknown>} */ (rec.metadata)
|
|
641
|
-
const n = meta.name
|
|
642
|
-
if (typeof n !== 'string' || n.trim() === '') return
|
|
643
|
-
let inner = map.get(pkgAbs)
|
|
644
|
-
if (!inner) {
|
|
645
|
-
inner = new Map()
|
|
646
|
-
map.set(pkgAbs, inner)
|
|
647
|
-
}
|
|
648
|
-
const needClear = serviceDocumentRequiresRuClusterIPNoneRemoval(obj)
|
|
649
|
-
const needClusterIPsRemove = serviceDocumentBaseDeclaresClusterIPsField(obj)
|
|
650
|
-
const prev = inner.get(n)
|
|
651
|
-
inner.set(n, {
|
|
652
|
-
requiresClusterIPNoneClear: prev?.requiresClusterIPNoneClear === true || needClear,
|
|
653
|
-
requiresClusterIPsRemove: prev?.requiresClusterIPsRemove === true || needClusterIPsRemove
|
|
654
|
-
})
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* Обробляє YAML-документи з одного файлу для збору NodePort-патч цілей.
|
|
659
|
-
* @param {import('yaml').Document[]} docs документи з файлу
|
|
660
|
-
* @param {string} pkgAbs абсолютний шлях пакета
|
|
661
|
-
* @param {Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>} map результуючий Map
|
|
662
|
-
*/
|
|
663
|
-
function collectNodePortTargetsFromDocs(docs, pkgAbs, map) {
|
|
664
|
-
for (const doc of docs) {
|
|
665
|
-
if (doc.errors.length === 0) {
|
|
666
|
-
processServiceDocForNodePortTargets(doc.toJSON(), pkgAbs, map)
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
/**
|
|
672
|
-
* Для кожного пакета збирає **Service**, які в overlay **ru** мають стати **NodePort** (abie.mdc).
|
|
673
|
-
* @param {string} root корінь репозиторію
|
|
674
|
-
* @param {string[]} yamlAbs абсолютні шляхи yaml під **k8s**
|
|
675
|
-
* @param {(msg: string) => void} fail реєстрація помилки читання/парсингу
|
|
676
|
-
* @returns {Promise<Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>>} пакет → назва сервісу → прапори NodePort
|
|
677
|
-
*/
|
|
678
|
-
async function collectAbieRuNodePortServiceTargetsByPackage(root, yamlAbs, fail) {
|
|
679
|
-
/** @type {Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>} */
|
|
680
|
-
const map = new Map()
|
|
681
|
-
for (const abs of yamlAbs) {
|
|
682
|
-
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
683
|
-
const pkgAbs = k8sYamlRelOutsideUaRuOverlays(rel) ? abiePackageDirFromK8sYamlRel(root, rel) : null
|
|
684
|
-
if (pkgAbs) {
|
|
685
|
-
const docs = await readAndParseYamlDocs(abs, rel, fail)
|
|
686
|
-
if (docs) collectNodePortTargetsFromDocs(docs, pkgAbs, map)
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
return map
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/**
|
|
693
|
-
* У **`k8s/ru/kustomization.yaml`** для кожного **Service** з YAML **`k8s`**, шлях якого без сегментів **`k8s/ua/`** та **`k8s/ru/`** (у т. ч. **headless** / **`-hl`**) — **JSON6902** **`/spec/type` → NodePort**; при **`clusterIP: None`** — **`op: remove`** на **`/spec/clusterIP`**; якщо в base є **`spec.clusterIPs`** — ще **`remove`** на **`/spec/clusterIPs`** (abie.mdc).
|
|
694
|
-
* @param {string} root корінь
|
|
695
|
-
* @param {string[]} yamlFilesAbs yaml під **k8s**
|
|
696
|
-
* @param {(msg: string) => void} fail callback
|
|
697
|
-
* @param {(msg: string) => void} passFn успішне повідомлення
|
|
698
|
-
* @returns {Promise<void>}
|
|
699
|
-
*/
|
|
700
|
-
async function ensureRuAbieServiceNodePortPatches(root, yamlFilesAbs, fail, passFn) {
|
|
701
|
-
const byPkg = await collectAbieRuNodePortServiceTargetsByPackage(root, yamlFilesAbs, fail)
|
|
702
|
-
const entries = [...byPkg.entries()].filter(([, m]) => m.size > 0)
|
|
703
|
-
if (entries.length === 0) {
|
|
704
|
-
passFn('Немає Service у шарі k8s без k8s/ua/ та k8s/ru/ — patch NodePort у k8s/ru/ не вимагається (abie.mdc)')
|
|
705
|
-
return
|
|
706
|
-
}
|
|
707
|
-
for (const [pkgAbs, targetsByName] of entries.toSorted((a, b) => a[0].localeCompare(b[0]))) {
|
|
708
|
-
const relPkg = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
|
|
709
|
-
const ruAbs = join(pkgAbs, 'k8s', 'ru', 'kustomization.yaml')
|
|
710
|
-
const nameList = [...targetsByName.keys()].toSorted((a, b) => a.localeCompare(b))
|
|
711
|
-
if (!existsSync(ruAbs)) {
|
|
712
|
-
fail(
|
|
713
|
-
`${relPkg}/k8s: є Service, для overlay ru потрібен patch Service (NodePort; для headless — ще remove /spec/clusterIP): ${nameList.join(', ')} — додай ru/kustomization.yaml (abie.mdc)`
|
|
714
|
-
)
|
|
715
|
-
return
|
|
716
|
-
}
|
|
717
|
-
const relRu = relative(root, ruAbs).replaceAll('\\', '/') || ruAbs
|
|
718
|
-
let raw
|
|
719
|
-
try {
|
|
720
|
-
raw = await readFile(ruAbs, 'utf8')
|
|
721
|
-
} catch (error) {
|
|
722
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
723
|
-
fail(`${relRu}: не вдалося прочитати (${msg})`)
|
|
724
|
-
return
|
|
725
|
-
}
|
|
726
|
-
const patchErrors = getAbieRuServiceNodePortPatchErrors(raw, targetsByName)
|
|
727
|
-
if (patchErrors.length > 0) {
|
|
728
|
-
fail(`${relRu}: ${patchErrors.join('; ')}`)
|
|
729
|
-
return
|
|
730
|
-
}
|
|
731
|
-
passFn(`${relRu}: patch Service → NodePort (ru) відповідає abie.mdc`)
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
309
|
/**
|
|
736
310
|
* Директорії, де є хоча б один **Deployment** у файлах **k8s**.
|
|
737
311
|
* @param {string} root корінь cwd
|
|
@@ -793,7 +367,7 @@ function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fail, pas
|
|
|
793
367
|
* @returns {string} той самий рядок без BOM (U+FEFF) на початку
|
|
794
368
|
*/
|
|
795
369
|
function stripBom(s) {
|
|
796
|
-
return s.startsWith('
|
|
370
|
+
return s.startsWith('') ? s.slice(1) : s
|
|
797
371
|
}
|
|
798
372
|
|
|
799
373
|
/**
|
|
@@ -855,29 +429,10 @@ function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
|
|
|
855
429
|
return true
|
|
856
430
|
}
|
|
857
431
|
|
|
858
|
-
/**
|
|
859
|
-
* Чи рядок inline JSON6902 patch містить очікуваний **ru** nodeSelector (**yandex.cloud/preemptible: false** на **`/spec/template/spec/nodeSelector`**).
|
|
860
|
-
* Конкретний **`op`** не перевіряється — див. **k8s.mdc**.
|
|
861
|
-
* @param {string} patchText поле **patch** у kustomization
|
|
862
|
-
* @returns {boolean} true, якщо критерії abie.mdc виконано
|
|
863
|
-
*/
|
|
864
|
-
function jsonPatchTextHasRuDeploymentNodeSelector(patchText) {
|
|
865
|
-
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
866
|
-
return false
|
|
867
|
-
}
|
|
868
|
-
if (!PATCH_NODE_SELECTOR_PATH_RE.test(patchText)) {
|
|
869
|
-
return false
|
|
870
|
-
}
|
|
871
|
-
if (!PATCH_YANDEX_PREEMPTIBLE_FALSE_RE.test(patchText)) {
|
|
872
|
-
return false
|
|
873
|
-
}
|
|
874
|
-
return true
|
|
875
|
-
}
|
|
876
|
-
|
|
877
432
|
/**
|
|
878
433
|
* Чи один елемент **patches** у kustomization відповідає abie nodeSelector для заданого **mode**.
|
|
879
434
|
* @param {unknown} p елемент масиву **patches**
|
|
880
|
-
* @param {'ua'
|
|
435
|
+
* @param {'ua'} mode який overlay перевіряти
|
|
881
436
|
* @returns {boolean} true, якщо patch відповідає abie для **mode**
|
|
882
437
|
*/
|
|
883
438
|
function inlineKustomizationPatchMatchesAbieMode(p, mode) {
|
|
@@ -900,16 +455,13 @@ function inlineKustomizationPatchMatchesAbieMode(p, mode) {
|
|
|
900
455
|
if (mode === 'ua' && jsonPatchTextHasUaDeploymentNodeSelector(patchStr)) {
|
|
901
456
|
return true
|
|
902
457
|
}
|
|
903
|
-
if (mode === 'ru' && jsonPatchTextHasRuDeploymentNodeSelector(patchStr)) {
|
|
904
|
-
return true
|
|
905
|
-
}
|
|
906
458
|
return false
|
|
907
459
|
}
|
|
908
460
|
|
|
909
461
|
/**
|
|
910
462
|
* Чи один YAML-документ kustomization містить відповідний inline patch на Deployment.
|
|
911
463
|
* @param {import('yaml').Document} doc документ після **parseAllDocuments**
|
|
912
|
-
* @param {'ua'
|
|
464
|
+
* @param {'ua'} mode який overlay перевіряти
|
|
913
465
|
* @returns {boolean} true, якщо знайдено відповідний patch
|
|
914
466
|
*/
|
|
915
467
|
function kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode) {
|
|
@@ -937,9 +489,9 @@ function kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode) {
|
|
|
937
489
|
}
|
|
938
490
|
|
|
939
491
|
/**
|
|
940
|
-
* Чи **kustomization.yaml** містить inline **patches** на **Deployment** з nodeSelector за abie.mdc (**ua**
|
|
492
|
+
* Чи **kustomization.yaml** містить inline **patches** на **Deployment** з nodeSelector за abie.mdc (overlay **ua**).
|
|
941
493
|
* @param {string} raw повний текст файлу
|
|
942
|
-
* @param {'ua'
|
|
494
|
+
* @param {'ua'} mode який overlay перевіряти
|
|
943
495
|
* @returns {boolean} true, якщо знайдено відповідний patch
|
|
944
496
|
*/
|
|
945
497
|
export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
|
|
@@ -963,12 +515,12 @@ export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
|
|
|
963
515
|
}
|
|
964
516
|
|
|
965
517
|
/**
|
|
966
|
-
* Чи YAML відносно кореня належить до **`${pkgRel}/k8s/**`** поза
|
|
518
|
+
* Чи YAML відносно кореня належить до **`${pkgRel}/k8s/**`** поза піддеревом **`ua/`** (base-шар abie).
|
|
967
519
|
* @param {string} relFromRoot відносний шлях від кореня
|
|
968
520
|
* @param {string} pkgRelFromRoot каталог пакета відносно кореня (без завершального слеша після імені пакета)
|
|
969
521
|
* @returns {boolean} `true`, якщо шлях належить до base-шару abie
|
|
970
522
|
*/
|
|
971
|
-
export function
|
|
523
|
+
export function isK8sYamlInAbiePackageExcludingUaOverlay(relFromRoot, pkgRelFromRoot) {
|
|
972
524
|
const normRel = relFromRoot.replaceAll('\\', '/')
|
|
973
525
|
const pkg = pkgRelFromRoot.replaceAll('\\', '/').replace(TRAILING_SLASH_RE, '')
|
|
974
526
|
const prefix = `${pkg}/k8s/`
|
|
@@ -976,7 +528,7 @@ export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelF
|
|
|
976
528
|
return false
|
|
977
529
|
}
|
|
978
530
|
const after = normRel.slice(prefix.length)
|
|
979
|
-
return !after.startsWith('ua/')
|
|
531
|
+
return !after.startsWith('ua/')
|
|
980
532
|
}
|
|
981
533
|
|
|
982
534
|
/**
|
|
@@ -1028,7 +580,7 @@ function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
|
|
|
1028
580
|
}
|
|
1029
581
|
|
|
1030
582
|
/**
|
|
1031
|
-
* З YAML під **k8s** пакета (без overlay **ua**
|
|
583
|
+
* З YAML під **k8s** пакета (без overlay **ua**) збирає кількість **`backendRefs`** до **`auth-run-hl`** і **`file-link-hl`** і порушення **`namespace: dev`**.
|
|
1032
584
|
* @param {string} root корінь репозиторію
|
|
1033
585
|
* @param {string} pkgAbs абсолютний шлях до каталогу пакета
|
|
1034
586
|
* @param {string[]} yamlFilesAbs усі **yaml** під **k8s** (як **findK8sYamlFiles**)
|
|
@@ -1041,7 +593,7 @@ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yam
|
|
|
1041
593
|
const baseErrors = []
|
|
1042
594
|
for (const abs of yamlFilesAbs) {
|
|
1043
595
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1044
|
-
if (
|
|
596
|
+
if (isK8sYamlInAbiePackageExcludingUaOverlay(rel, pkgRel)) {
|
|
1045
597
|
const docs = await readAndParseYamlDocs(abs, rel, silentFail)
|
|
1046
598
|
if (docs) {
|
|
1047
599
|
for (const doc of docs) {
|
|
@@ -1061,28 +613,19 @@ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yam
|
|
|
1061
613
|
/**
|
|
1062
614
|
* Рахує операції JSON6902 з **`path`**: **`/spec/rules/…/backendRefs/…/namespace`** та **`value`** overlay.
|
|
1063
615
|
* @param {string} combined сукупний текст patch **HTTPRoute**
|
|
1064
|
-
* @param {'ua'
|
|
616
|
+
* @param {'ua'} mode overlay
|
|
1065
617
|
* @returns {number} кількість знайдених патчів namespace
|
|
1066
618
|
*/
|
|
1067
619
|
function countAbieHttpRouteBackendRefNamespacePatchesInCombined(combined, mode) {
|
|
620
|
+
if (mode !== 'ua') return 0
|
|
1068
621
|
const re =
|
|
1069
|
-
|
|
1070
|
-
? /path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/gimu
|
|
1071
|
-
: /path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/gimu
|
|
622
|
+
/path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/gimu
|
|
1072
623
|
return [...combined.matchAll(re)].length
|
|
1073
624
|
}
|
|
1074
625
|
|
|
1075
626
|
/** Домени **hostnames** для overlay **ua** (підрядки у JSON6902-тексті patch), abie.mdc. */
|
|
1076
627
|
const ABIE_UA_HTTPROUTE_HOST_MARKERS = ['abie.app', 'vybeerai.com.ua', '*.abie.app', '*.vybeerai.com.ua']
|
|
1077
628
|
|
|
1078
|
-
/** Домени **hostnames** для overlay **ru** (підрядки), abie.mdc. */
|
|
1079
|
-
const ABIE_RU_HTTPROUTE_HOST_MARKERS = [
|
|
1080
|
-
'napitkivmeste.tech',
|
|
1081
|
-
'выбирайонлайн.рф',
|
|
1082
|
-
'*.napitkivmeste.tech',
|
|
1083
|
-
'*.выбирайонлайн.рф'
|
|
1084
|
-
]
|
|
1085
|
-
|
|
1086
629
|
/**
|
|
1087
630
|
* Витягує текст patch HTTPRoute з елемента patches.
|
|
1088
631
|
* @param {unknown} p елемент масиву patches
|
|
@@ -1147,39 +690,30 @@ export function getCombinedNginxRunPatchTextFromKustomization(raw) {
|
|
|
1147
690
|
/**
|
|
1148
691
|
* Перевіряє сукупний текст patch(ів) **HTTPRoute** (будь-яке **target.name**) на відповідність abie.mdc.
|
|
1149
692
|
* @param {string} combined текст одного або кількох inline **patch**, розділених символом нового рядка
|
|
1150
|
-
* @param {'ua'
|
|
1151
|
-
* @param {string} [
|
|
693
|
+
* @param {'ua'} mode overlay (наразі лише **ua**)
|
|
694
|
+
* @param {string} [_fullKustomizationRaw] збережено для зворотної сумісності API (не використовується)
|
|
1152
695
|
* @param {number} [sharedCrossNsBackendRefCount] скільки **`backendRefs`** до **`auth-run-hl`** і **`file-link-hl`** у base **HTTPRoute** пакета — стільки ж patch-ів **`…/backendRefs/…/namespace`** з **`value`** overlay
|
|
1153
696
|
* @returns {string | null} повідомлення про помилку або **null**
|
|
1154
697
|
*/
|
|
1155
698
|
export function validateAbieNginxRunHttpRoutePatches(
|
|
1156
699
|
combined,
|
|
1157
700
|
mode,
|
|
1158
|
-
|
|
701
|
+
_fullKustomizationRaw,
|
|
1159
702
|
sharedCrossNsBackendRefCount = 0
|
|
1160
703
|
) {
|
|
1161
704
|
if (typeof combined !== 'string' || combined.trim() === '') {
|
|
1162
|
-
return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}
|
|
705
|
+
return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}) — abie.mdc`
|
|
1163
706
|
}
|
|
1164
707
|
if (!PATCH_HOSTNAMES_PATH_RE.test(combined)) {
|
|
1165
708
|
return 'HTTPRoute: потрібен path /spec/hostnames у patch (abie.mdc)'
|
|
1166
709
|
}
|
|
1167
|
-
const markers =
|
|
710
|
+
const markers = ABIE_UA_HTTPROUTE_HOST_MARKERS
|
|
1168
711
|
if (!markers.some(m => combined.includes(m))) {
|
|
1169
712
|
return `HTTPRoute: у value для /spec/hostnames має бути один із доменів abie (${markers.join(', ')}) — abie.mdc`
|
|
1170
713
|
}
|
|
1171
|
-
|
|
1172
|
-
mode === 'ua' ? PATCH_PARENT_REF_NS_UA_RE.test(combined) : PATCH_PARENT_REF_NS_RU_RE.test(combined)
|
|
1173
|
-
if (!namespaceOk) {
|
|
714
|
+
if (!PATCH_PARENT_REF_NS_UA_RE.test(combined)) {
|
|
1174
715
|
return `HTTPRoute: потрібен path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
|
|
1175
716
|
}
|
|
1176
|
-
const ruNeedsWebsocket =
|
|
1177
|
-
mode === 'ru' &&
|
|
1178
|
-
typeof fullKustomizationRaw === 'string' &&
|
|
1179
|
-
fullKustomizationRaw.includes(HASURA_JWT_SECRET_IN_KUSTOMIZATION)
|
|
1180
|
-
if (ruNeedsWebsocket && !WEBSOCKET_ANNOTATION_RE.test(combined)) {
|
|
1181
|
-
return 'HTTPRoute (ru): за наявності HASURA_GRAPHQL_JWT_SECRET у kustomization потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
|
|
1182
|
-
}
|
|
1183
717
|
const sharedCount =
|
|
1184
718
|
typeof sharedCrossNsBackendRefCount === 'number' && Number.isFinite(sharedCrossNsBackendRefCount)
|
|
1185
719
|
? Math.max(0, Math.floor(sharedCrossNsBackendRefCount))
|
|
@@ -1194,9 +728,9 @@ export function validateAbieNginxRunHttpRoutePatches(
|
|
|
1194
728
|
}
|
|
1195
729
|
|
|
1196
730
|
/**
|
|
1197
|
-
* Чи **kustomization** містить валідні для abie **patch** для **HTTPRoute** з непорожнім **target.name** (**ua**
|
|
731
|
+
* Чи **kustomization** містить валідні для abie **patch** для **HTTPRoute** з непорожнім **target.name** (**ua**).
|
|
1198
732
|
* @param {string} raw повний текст **kustomization.yaml**
|
|
1199
|
-
* @param {'ua'
|
|
733
|
+
* @param {'ua'} mode overlay
|
|
1200
734
|
* @returns {boolean} true, якщо **`validateAbieNginxRunHttpRoutePatches`** повертає **null**
|
|
1201
735
|
*/
|
|
1202
736
|
export function kustomizationHasAbieNginxRunHttpRoutePatch(raw, mode) {
|
|
@@ -1231,85 +765,11 @@ export function validateAbieHcModeline(raw, relPath) {
|
|
|
1231
765
|
return null
|
|
1232
766
|
}
|
|
1233
767
|
|
|
1234
|
-
/**
|
|
1235
|
-
* Збирає відносний шлях із документів, що містять HealthCheckPolicy.
|
|
1236
|
-
* @param {import('yaml').Document[]} docs документи з файлу
|
|
1237
|
-
* @param {string} rel відносний шлях файлу
|
|
1238
|
-
* @param {string[]} out масив для запису шляхів
|
|
1239
|
-
*/
|
|
1240
|
-
function collectHealthCheckPolicyRelFromDocs(docs, rel, out) {
|
|
1241
|
-
for (const doc of docs) {
|
|
1242
|
-
if (doc.errors.length === 0) {
|
|
1243
|
-
const obj = doc.toJSON()
|
|
1244
|
-
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
1245
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
1246
|
-
if (rec.kind === 'HealthCheckPolicy' && !out.includes(rel)) {
|
|
1247
|
-
out.push(rel)
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
/**
|
|
1255
|
-
* Збирає відносні шляхи файлів із **HealthCheckPolicy** у дереві k8s.
|
|
1256
|
-
* @param {string} root корінь
|
|
1257
|
-
* @param {string[]} yamlAbs абсолютні шляхи
|
|
1258
|
-
* @returns {Promise<string[]>} унікальні відносні шляхи yaml із **HealthCheckPolicy**
|
|
1259
|
-
*/
|
|
1260
|
-
async function collectHealthCheckPolicyRelPaths(root, yamlAbs) {
|
|
1261
|
-
/** @type {string[]} */
|
|
1262
|
-
const out = []
|
|
1263
|
-
for (const abs of yamlAbs) {
|
|
1264
|
-
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1265
|
-
const docs = await readAndParseYamlDocs(abs, rel, silentFail)
|
|
1266
|
-
if (docs) collectHealthCheckPolicyRelFromDocs(docs, rel, out)
|
|
1267
|
-
}
|
|
1268
|
-
return out
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
/**
|
|
1272
|
-
* Якщо є **HealthCheckPolicy**, вимагає **ru/kustomization.yaml** з patch видалення (**ruKustomizationHasHealthCheckDeletePatch** у **check-k8s**).
|
|
1273
|
-
* @param {string} root корінь
|
|
1274
|
-
* @param {string[]} yamlFilesAbs абсолютні шляхи yaml k8s
|
|
1275
|
-
* @param {string[]} healthCheckPolicyRelativePaths відносні шляхи
|
|
1276
|
-
* @param {(msg: string) => void} fail callback
|
|
1277
|
-
* @returns {Promise<void>}
|
|
1278
|
-
*/
|
|
1279
|
-
async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, healthCheckPolicyRelativePaths, fail) {
|
|
1280
|
-
if (healthCheckPolicyRelativePaths.length === 0) {
|
|
1281
|
-
return
|
|
1282
|
-
}
|
|
1283
|
-
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
1284
|
-
if (ruAbsList.length === 0) {
|
|
1285
|
-
fail(
|
|
1286
|
-
`Знайдено HealthCheckPolicy у ${healthCheckPolicyRelativePaths.join(', ')} — додай ru/kustomization.yaml з patch видалення (abie.mdc)`
|
|
1287
|
-
)
|
|
1288
|
-
return
|
|
1289
|
-
}
|
|
1290
|
-
for (const abs of ruAbsList) {
|
|
1291
|
-
let raw
|
|
1292
|
-
try {
|
|
1293
|
-
raw = await readFile(abs, 'utf8')
|
|
1294
|
-
} catch (error) {
|
|
1295
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1296
|
-
fail(`${relative(root, abs) || abs}: не вдалося прочитати (${msg})`)
|
|
1297
|
-
return
|
|
1298
|
-
}
|
|
1299
|
-
if (ruKustomizationHasHealthCheckDeletePatch(raw)) {
|
|
1300
|
-
return
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
fail(
|
|
1304
|
-
'Є HealthCheckPolicy, але жоден ru/kustomization.yaml не містить очікуваного patch видалення (kind: HealthCheckPolicy, metadata.name, $patch: delete) — abie.mdc'
|
|
1305
|
-
)
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
768
|
/**
|
|
1309
769
|
* Перевіряє одну kustomization.yaml на nodeSelector patch для заданого overlay.
|
|
1310
770
|
* @param {string} abs абсолютний шлях до файлу
|
|
1311
771
|
* @param {string} rel відносний шлях (для повідомлень)
|
|
1312
|
-
* @param {'ua'
|
|
772
|
+
* @param {'ua'} mode параметр mode
|
|
1313
773
|
* @param {Set<string>} deploymentDirs директорії з Deployment (Set)
|
|
1314
774
|
* @param {string} root корінь репозиторію
|
|
1315
775
|
* @param {(msg: string) => void} fail callback при помилці
|
|
@@ -1330,8 +790,7 @@ async function checkNodeSelectorKustomization(abs, rel, mode, deploymentDirs, ro
|
|
|
1330
790
|
return false
|
|
1331
791
|
}
|
|
1332
792
|
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode)) {
|
|
1333
|
-
|
|
1334
|
-
fail(`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та ${detail} (abie.mdc)`)
|
|
793
|
+
fail(`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`)
|
|
1335
794
|
return false
|
|
1336
795
|
}
|
|
1337
796
|
passFn(`${rel}: nodeSelector patch (${mode}) відповідає abie.mdc`)
|
|
@@ -1339,14 +798,14 @@ async function checkNodeSelectorKustomization(abs, rel, mode, deploymentDirs, ro
|
|
|
1339
798
|
}
|
|
1340
799
|
|
|
1341
800
|
/**
|
|
1342
|
-
* Перевіряє наявність патчів nodeSelector для ua
|
|
801
|
+
* Перевіряє наявність патчів nodeSelector для ua overlay у k8s.
|
|
1343
802
|
* @param {string} root корінь репозиторію
|
|
1344
803
|
* @param {string[]} yamlFilesAbs абсолютні шляхи yaml-файлів під k8s
|
|
1345
804
|
* @param {Set<string>} deploymentDirs директорії з Deployment (Set)
|
|
1346
805
|
* @param {(msg: string) => void} fail callback при помилці
|
|
1347
806
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1348
807
|
*/
|
|
1349
|
-
async function
|
|
808
|
+
async function ensureUaAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentDirs, fail, passFn) {
|
|
1350
809
|
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
1351
810
|
if (uaAbsList.length === 0) {
|
|
1352
811
|
fail(
|
|
@@ -1359,26 +818,13 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentD
|
|
|
1359
818
|
const ok = await checkNodeSelectorKustomization(abs, rel, 'ua', deploymentDirs, root, fail, passFn)
|
|
1360
819
|
if (!ok) return
|
|
1361
820
|
}
|
|
1362
|
-
|
|
1363
|
-
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
1364
|
-
if (ruAbsList.length === 0) {
|
|
1365
|
-
fail(
|
|
1366
|
-
'Є Deployment у k8s — додай ru/kustomization.yaml з patch на Deployment: path /spec/template/spec/nodeSelector, yandex.cloud/preemptible false (abie.mdc)'
|
|
1367
|
-
)
|
|
1368
|
-
return
|
|
1369
|
-
}
|
|
1370
|
-
for (const abs of ruAbsList) {
|
|
1371
|
-
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1372
|
-
const ok = await checkNodeSelectorKustomization(abs, rel, 'ru', deploymentDirs, root, fail, passFn)
|
|
1373
|
-
if (!ok) return
|
|
1374
|
-
}
|
|
1375
821
|
}
|
|
1376
822
|
|
|
1377
823
|
/**
|
|
1378
|
-
* Перевіряє HTTPRoute patch для одного overlay (ua
|
|
824
|
+
* Перевіряє HTTPRoute patch для одного overlay (ua).
|
|
1379
825
|
* @param {string} abs абсолютний шлях до kustomization.yaml
|
|
1380
826
|
* @param {string} rel відносний шлях (для повідомлень)
|
|
1381
|
-
* @param {'ua'
|
|
827
|
+
* @param {'ua'} mode overlay
|
|
1382
828
|
* @param {string} root корінь репозиторію
|
|
1383
829
|
* @param {string[]} yamlFilesAbs абсолютні шляхи yaml-файлів під k8s
|
|
1384
830
|
* @param {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} cache кеш аналізу shared backend refs
|
|
@@ -1457,14 +903,14 @@ function ensureAbieBaseHttpRouteHostnames(root, yamlFilesAbs, fail, passFn) {
|
|
|
1457
903
|
}
|
|
1458
904
|
|
|
1459
905
|
/**
|
|
1460
|
-
* Якщо є **Deployment** під **k8s**, вимагає в overlay **ua**
|
|
906
|
+
* Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** patch **HTTPRoute** (непорожній **target.name**) за abie.mdc
|
|
1461
907
|
* лише для пакетів з **vite.config.{js,mjs,ts}** у каталозі пакета (батько **k8s**).
|
|
1462
908
|
* @param {string} root корінь репозиторію
|
|
1463
909
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
1464
910
|
* @param {(msg: string) => void} fail callback при помилці
|
|
1465
911
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1466
912
|
*/
|
|
1467
|
-
async function
|
|
913
|
+
async function ensureUaAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
|
|
1468
914
|
/** @type {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} */
|
|
1469
915
|
const cache = new Map()
|
|
1470
916
|
|
|
@@ -1479,18 +925,6 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
|
|
|
1479
925
|
const ok = await checkHttpRouteKustomization(abs, rel, 'ua', root, yamlFilesAbs, cache, fail, passFn)
|
|
1480
926
|
if (!ok) return
|
|
1481
927
|
}
|
|
1482
|
-
|
|
1483
|
-
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
1484
|
-
if (ruAbsList.length === 0) {
|
|
1485
|
-
passFn(
|
|
1486
|
-
'Немає ru/kustomization.yaml у дереві k8s — patch HTTPRoute (ru) не вимагається (abie.mdc, лише Vite-пакети)'
|
|
1487
|
-
)
|
|
1488
|
-
}
|
|
1489
|
-
for (const abs of ruAbsList) {
|
|
1490
|
-
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1491
|
-
const ok = await checkHttpRouteKustomization(abs, rel, 'ru', root, yamlFilesAbs, cache, fail, passFn)
|
|
1492
|
-
if (!ok) return
|
|
1493
|
-
}
|
|
1494
928
|
}
|
|
1495
929
|
|
|
1496
930
|
/**
|
|
@@ -1553,17 +987,10 @@ function checkCleanMergedBranch(root, pass, fail) {
|
|
|
1553
987
|
})
|
|
1554
988
|
for (const v of violations) fail(v.message)
|
|
1555
989
|
if (violations.length === 0) {
|
|
1556
|
-
pass('clean-merged-branch.yml: ignore_branches містить dev, ua
|
|
990
|
+
pass('clean-merged-branch.yml: ignore_branches містить dev, ua (rego)')
|
|
1557
991
|
}
|
|
1558
992
|
}
|
|
1559
993
|
|
|
1560
|
-
/**
|
|
1561
|
-
* Перевіряє один файл hc.yaml на відповідність abie.mdc.
|
|
1562
|
-
* @param {string} root корінь репозиторію
|
|
1563
|
-
* @param {string} dir директорія з Deployment
|
|
1564
|
-
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
1565
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
1566
|
-
*/
|
|
1567
994
|
/**
|
|
1568
995
|
* Перевіряє hc.yaml у директоріях з Deployment. JS перевіряє modeline, далі
|
|
1569
996
|
* один батч conftest для усіх знайдених hc.yaml — структурна валідація HCP
|
|
@@ -1613,287 +1040,9 @@ async function checkHcYamlFiles(root, deploymentDirs, pass, fail) {
|
|
|
1613
1040
|
}
|
|
1614
1041
|
}
|
|
1615
1042
|
|
|
1616
|
-
/**
|
|
1617
|
-
* Чи Deployment-документ містить контейнер із образом **`hasura/graphql-engine`** (abie.mdc nginx-sidecar).
|
|
1618
|
-
* @param {unknown} obj корінь YAML-документа
|
|
1619
|
-
* @returns {boolean} true якщо документ є Deployment із hasura/graphql-engine
|
|
1620
|
-
*/
|
|
1621
|
-
function deploymentDocHasHasuraImage(obj) {
|
|
1622
|
-
if (!isDeploymentDoc(obj)) return false
|
|
1623
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
1624
|
-
const spec = rec.spec
|
|
1625
|
-
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return false
|
|
1626
|
-
const template = /** @type {Record<string, unknown>} */ (spec).template
|
|
1627
|
-
if (template === null || typeof template !== 'object' || Array.isArray(template)) return false
|
|
1628
|
-
const podSpec = /** @type {Record<string, unknown>} */ (template).spec
|
|
1629
|
-
if (podSpec === null || typeof podSpec !== 'object' || Array.isArray(podSpec)) return false
|
|
1630
|
-
const containers = /** @type {Record<string, unknown>} */ (podSpec).containers
|
|
1631
|
-
if (!Array.isArray(containers)) return false
|
|
1632
|
-
for (const c of containers) {
|
|
1633
|
-
if (c !== null && typeof c === 'object' && !Array.isArray(c)) {
|
|
1634
|
-
const img = /** @type {Record<string, unknown>} */ (c).image
|
|
1635
|
-
if (typeof img === 'string' && img.includes(HASURA_IMAGE_MARKER)) return true
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
return false
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
/**
|
|
1642
|
-
* Чи Kustomization-документ містить у **`images[*].newName`** рядок **`hasura/graphql-engine`** (abie.mdc nginx-sidecar).
|
|
1643
|
-
* @param {unknown} obj корінь YAML-документа
|
|
1644
|
-
* @returns {boolean} true якщо є images[*].newName із hasura/graphql-engine
|
|
1645
|
-
*/
|
|
1646
|
-
function kustomizationDocHasHasuraImageNewName(obj) {
|
|
1647
|
-
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return false
|
|
1648
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
1649
|
-
if (!Array.isArray(rec.images)) return false
|
|
1650
|
-
for (const img of rec.images) {
|
|
1651
|
-
if (img !== null && typeof img === 'object' && !Array.isArray(img)) {
|
|
1652
|
-
const newName = /** @type {Record<string, unknown>} */ (img).newName
|
|
1653
|
-
if (typeof newName === 'string' && newName.includes(HASURA_IMAGE_MARKER)) return true
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
return false
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
/**
|
|
1660
|
-
* Чи patch-запис у Kustomization має **target.kind === 'Deployment'**.
|
|
1661
|
-
* @param {unknown} patchEntry елемент масиву **patches**
|
|
1662
|
-
* @returns {boolean} true, якщо patch цілить на Deployment
|
|
1663
|
-
*/
|
|
1664
|
-
function isPatchTargetingDeployment(patchEntry) {
|
|
1665
|
-
if (patchEntry === null || typeof patchEntry !== 'object' || Array.isArray(patchEntry)) return false
|
|
1666
|
-
const pr = /** @type {Record<string, unknown>} */ (patchEntry)
|
|
1667
|
-
const target = pr.target
|
|
1668
|
-
if (target === null || typeof target !== 'object' || Array.isArray(target)) return false
|
|
1669
|
-
return /** @type {Record<string, unknown>} */ (target).kind === 'Deployment'
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
/**
|
|
1673
|
-
* Витягує текст **patch** із запису Kustomization, якщо він непорожній рядок.
|
|
1674
|
-
* @param {unknown} patchEntry елемент масиву **patches**
|
|
1675
|
-
* @returns {string | null} текст patch або null
|
|
1676
|
-
*/
|
|
1677
|
-
function extractPatchString(patchEntry) {
|
|
1678
|
-
const pr = /** @type {Record<string, unknown>} */ (patchEntry)
|
|
1679
|
-
const patchStr = pr.patch
|
|
1680
|
-
if (typeof patchStr === 'string' && patchStr.trim() !== '') return patchStr
|
|
1681
|
-
return null
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
/**
|
|
1685
|
-
* Збирає тексти inline **patch** для **Deployment** з одного YAML-документа Kustomization.
|
|
1686
|
-
* @param {import('yaml').Document} doc YAML-документ
|
|
1687
|
-
* @param {string[]} out масив для збору результатів
|
|
1688
|
-
*/
|
|
1689
|
-
function collectDeploymentPatchTextsFromDoc(doc, out) {
|
|
1690
|
-
if (doc.errors.length > 0) return
|
|
1691
|
-
const root = doc.toJSON()
|
|
1692
|
-
if (root === null || typeof root !== 'object' || Array.isArray(root)) return
|
|
1693
|
-
const rec = /** @type {Record<string, unknown>} */ (root)
|
|
1694
|
-
if (!Array.isArray(rec.patches)) return
|
|
1695
|
-
for (const p of rec.patches) {
|
|
1696
|
-
if (isPatchTargetingDeployment(p)) {
|
|
1697
|
-
const text = extractPatchString(p)
|
|
1698
|
-
if (text !== null) out.push(text)
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
/**
|
|
1704
|
-
* Збирає тексти inline **patch** для **Deployment** з **kustomization.yaml** (усі документи).
|
|
1705
|
-
* @param {string} raw повний текст файлу
|
|
1706
|
-
* @returns {string[]} рядки patch
|
|
1707
|
-
*/
|
|
1708
|
-
function collectDeploymentPatchTextsFromKustomization(raw) {
|
|
1709
|
-
const body = stripBom(raw)
|
|
1710
|
-
const lines = body.split(LINE_SPLIT_RE)
|
|
1711
|
-
const first = lines[0] ?? ''
|
|
1712
|
-
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
1713
|
-
/** @type {import('yaml').Document[]} */
|
|
1714
|
-
let docs
|
|
1715
|
-
try {
|
|
1716
|
-
docs = parseAllDocuments(rest)
|
|
1717
|
-
} catch {
|
|
1718
|
-
return []
|
|
1719
|
-
}
|
|
1720
|
-
/** @type {string[]} */
|
|
1721
|
-
const out = []
|
|
1722
|
-
for (const doc of docs) {
|
|
1723
|
-
collectDeploymentPatchTextsFromDoc(doc, out)
|
|
1724
|
-
}
|
|
1725
|
-
return out
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
/**
|
|
1729
|
-
* Чи YAML-документ містить образ Hasura (Deployment або Kustomization images).
|
|
1730
|
-
* @param {import('yaml').Document} doc YAML-документ
|
|
1731
|
-
* @returns {boolean} true, якщо документ посилається на hasura/graphql-engine
|
|
1732
|
-
*/
|
|
1733
|
-
function yamlDocReferencesHasuraImage(doc) {
|
|
1734
|
-
if (doc.errors.length > 0) return false
|
|
1735
|
-
const obj = doc.toJSON()
|
|
1736
|
-
return deploymentDocHasHasuraImage(obj) || kustomizationDocHasHasuraImageNewName(obj)
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
/**
|
|
1740
|
-
* Каталоги пакетів, де в дереві **k8s** є **Deployment** з образом **`hasura/graphql-engine`** або
|
|
1741
|
-
* **Kustomization** з **`images[*].newName`** на нього (abie.mdc nginx-sidecar).
|
|
1742
|
-
* @param {string} root корінь репозиторію
|
|
1743
|
-
* @param {string[]} yamlAbs абсолютні шляхи yaml під k8s
|
|
1744
|
-
* @returns {Promise<Set<string>>} абсолютні шляхи каталогів пакетів
|
|
1745
|
-
*/
|
|
1746
|
-
async function collectHasuraK8sPackageDirs(root, yamlAbs) {
|
|
1747
|
-
/** @type {Set<string>} */
|
|
1748
|
-
const dirs = new Set()
|
|
1749
|
-
for (const abs of yamlAbs) {
|
|
1750
|
-
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1751
|
-
const docs = await readAndParseYamlDocs(abs, rel, silentFail)
|
|
1752
|
-
if (docs && docs.some(doc => yamlDocReferencesHasuraImage(doc))) {
|
|
1753
|
-
const pkgDir = abiePackageDirFromK8sYamlRel(root, rel)
|
|
1754
|
-
if (pkgDir) dirs.add(pkgDir)
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
return dirs
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
/**
|
|
1761
|
-
* Якщо в дереві **k8s** є Deployment з **`hasura/graphql-engine`** і **`ru/kustomization.yaml`** містить
|
|
1762
|
-
* **`HASURA_GRAPHQL_JWT_SECRET`** — вимагає **nginx-sidecar** (abie.mdc):
|
|
1763
|
-
* **`ru/configmap-nginx.yaml`**, **`resources`** у kustomization, patch **Service -hl** (port 8081),
|
|
1764
|
-
* patch **Deployment** (nginx-sidecar image + containerPort 8081), patch **HTTPRoute** (port 8081).
|
|
1765
|
-
* @param {string} root корінь репозиторію
|
|
1766
|
-
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
1767
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
1768
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1769
|
-
* @returns {Promise<void>}
|
|
1770
|
-
*/
|
|
1771
|
-
/**
|
|
1772
|
-
* Перевіряє nginx-sidecar вимоги у **ru/kustomization.yaml** для одного Hasura-пакета.
|
|
1773
|
-
* @param {string} relPkg відносний шлях пакета
|
|
1774
|
-
* @param {string} relRu відносний шлях до ru/kustomization.yaml
|
|
1775
|
-
* @param {string} pkgAbs абсолютний шлях до пакета
|
|
1776
|
-
* @param {string} ruRaw вміст ru/kustomization.yaml
|
|
1777
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
1778
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1779
|
-
*/
|
|
1780
|
-
function validateNginxSidecarInRuKustomization(relPkg, relRu, pkgAbs, ruRaw, fail, passFn) {
|
|
1781
|
-
// configmap-nginx.yaml must exist
|
|
1782
|
-
const configmapNginxAbs = join(pkgAbs, 'k8s', 'ru', 'configmap-nginx.yaml')
|
|
1783
|
-
if (!existsSync(configmapNginxAbs)) {
|
|
1784
|
-
fail(`${relPkg}/k8s/ru: потрібен configmap-nginx.yaml з nginx.conf (nginx-sidecar для Hasura WebSocket, abie.mdc)`)
|
|
1785
|
-
return
|
|
1786
|
-
}
|
|
1787
|
-
passFn(`${relPkg}/k8s/ru/configmap-nginx.yaml: існує`)
|
|
1788
|
-
// kustomization resources must include configmap-nginx.yaml
|
|
1789
|
-
if (!RESOURCES_CONFIGMAP_NGINX_RE.test(ruRaw)) {
|
|
1790
|
-
fail(`${relRu}: у resources потрібен configmap-nginx.yaml (nginx-sidecar, abie.mdc)`)
|
|
1791
|
-
return
|
|
1792
|
-
}
|
|
1793
|
-
passFn(`${relRu}: resources містить configmap-nginx.yaml`)
|
|
1794
|
-
validateNginxSidecarPatches(relRu, ruRaw, fail, passFn)
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
/**
|
|
1798
|
-
* Перевіряє наявність nginx-sidecar patch (Service -hl, Deployment, HTTPRoute) у **ru/kustomization.yaml**.
|
|
1799
|
-
* @param {string} relRu відносний шлях до ru/kustomization.yaml
|
|
1800
|
-
* @param {string} ruRaw вміст ru/kustomization.yaml
|
|
1801
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
1802
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1803
|
-
*/
|
|
1804
|
-
function validateNginxSidecarPatches(relRu, ruRaw, fail, passFn) {
|
|
1805
|
-
// Service -hl patch must include port: 8081 (proxy)
|
|
1806
|
-
const svcPatchByName = collectAbieRuServicePatchTextByTargetNameFromRaw(ruRaw)
|
|
1807
|
-
const hasHlWith8081 = [...svcPatchByName.entries()].some(
|
|
1808
|
-
([name, pt]) => name.endsWith('-hl') && PATCH_PROXY_PORT_8081_RE.test(pt)
|
|
1809
|
-
)
|
|
1810
|
-
if (!hasHlWith8081) {
|
|
1811
|
-
fail(`${relRu}: у patch Service -hl потрібен port: 8081 (proxy) для nginx-sidecar (abie.mdc)`)
|
|
1812
|
-
return
|
|
1813
|
-
}
|
|
1814
|
-
passFn(`${relRu}: Service -hl patch містить port 8081 (nginx-sidecar)`)
|
|
1815
|
-
// Deployment patch must include nginx-sidecar (image nginx:*-alpine + containerPort: 8081)
|
|
1816
|
-
const deployPatches = collectDeploymentPatchTextsFromKustomization(ruRaw)
|
|
1817
|
-
const hasNginxSidecar = deployPatches.some(
|
|
1818
|
-
pt => NGINX_SIDECAR_IMAGE_RE.test(pt) && NGINX_SIDECAR_CONTAINER_PORT_RE.test(pt)
|
|
1819
|
-
)
|
|
1820
|
-
if (!hasNginxSidecar) {
|
|
1821
|
-
fail(`${relRu}: у patch Deployment потрібен nginx-sidecar (image nginx:…-alpine, containerPort: 8081) — abie.mdc`)
|
|
1822
|
-
return
|
|
1823
|
-
}
|
|
1824
|
-
passFn(`${relRu}: Deployment patch містить nginx-sidecar (image + containerPort 8081)`)
|
|
1825
|
-
// HTTPRoute patch must replace a backendRef port to 8081
|
|
1826
|
-
const combined = getCombinedNginxRunPatchTextFromKustomization(ruRaw)
|
|
1827
|
-
if (
|
|
1828
|
-
!HTTPROUTE_BACKENDREF_PORT_8081_RE.test(combined) &&
|
|
1829
|
-
!HTTPROUTE_BACKENDREF_PORT_8081_VALUE_FIRST_RE.test(combined)
|
|
1830
|
-
) {
|
|
1831
|
-
fail(
|
|
1832
|
-
`${relRu}: у patch HTTPRoute потрібен JSON6902 з path /spec/rules/…/backendRefs/…/port та value: 8081 (nginx-sidecar, abie.mdc)`
|
|
1833
|
-
)
|
|
1834
|
-
return
|
|
1835
|
-
}
|
|
1836
|
-
passFn(`${relRu}: HTTPRoute patch замінює порт на 8081 (nginx-sidecar)`)
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
/**
|
|
1840
|
-
* Обробляє один Hasura-пакет: читає **ru/kustomization.yaml** та перевіряє nginx-sidecar вимоги.
|
|
1841
|
-
* @param {string} root корінь репозиторію
|
|
1842
|
-
* @param {string} pkgAbs абсолютний шлях до пакета
|
|
1843
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
1844
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1845
|
-
* @returns {Promise<void>}
|
|
1846
|
-
*/
|
|
1847
|
-
async function checkNginxSidecarForHasuraPackage(root, pkgAbs, fail, passFn) {
|
|
1848
|
-
const relPkg = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
|
|
1849
|
-
const ruAbs = join(pkgAbs, 'k8s', 'ru', 'kustomization.yaml')
|
|
1850
|
-
if (!existsSync(ruAbs)) {
|
|
1851
|
-
passFn(`${relPkg}/k8s: є Hasura Deployment, але немає ru/kustomization.yaml — nginx-sidecar не перевіряється`)
|
|
1852
|
-
return
|
|
1853
|
-
}
|
|
1854
|
-
let ruRaw
|
|
1855
|
-
try {
|
|
1856
|
-
ruRaw = await readFile(ruAbs, 'utf8')
|
|
1857
|
-
} catch (error) {
|
|
1858
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1859
|
-
fail(`${relPkg}/k8s/ru/kustomization.yaml: не вдалося прочитати (${msg})`)
|
|
1860
|
-
return
|
|
1861
|
-
}
|
|
1862
|
-
if (!ruRaw.includes(HASURA_JWT_SECRET_IN_KUSTOMIZATION)) {
|
|
1863
|
-
passFn(
|
|
1864
|
-
`${relPkg}/k8s/ru/kustomization.yaml: немає ${HASURA_JWT_SECRET_IN_KUSTOMIZATION} — nginx-sidecar не вимагається (abie.mdc)`
|
|
1865
|
-
)
|
|
1866
|
-
return
|
|
1867
|
-
}
|
|
1868
|
-
const relRu = relative(root, ruAbs).replaceAll('\\', '/') || ruAbs
|
|
1869
|
-
validateNginxSidecarInRuKustomization(relPkg, relRu, pkgAbs, ruRaw, fail, passFn)
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
/**
|
|
1873
|
-
* Якщо в дереві **k8s** є Deployment з **`hasura/graphql-engine`** і **`ru/kustomization.yaml`** містить
|
|
1874
|
-
* **`HASURA_GRAPHQL_JWT_SECRET`** — вимагає **nginx-sidecar** (abie.mdc):
|
|
1875
|
-
* **`ru/configmap-nginx.yaml`**, **`resources`** у kustomization, patch **Service -hl** (port 8081),
|
|
1876
|
-
* patch **Deployment** (nginx-sidecar image + containerPort 8081), patch **HTTPRoute** (port 8081).
|
|
1877
|
-
* @param {string} root корінь репозиторію
|
|
1878
|
-
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
1879
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
1880
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1881
|
-
* @returns {Promise<void>}
|
|
1882
|
-
*/
|
|
1883
|
-
async function ensureAbieNginxSidecarForHasura(root, yamlFilesAbs, fail, passFn) {
|
|
1884
|
-
const hasuraPkgDirs = await collectHasuraK8sPackageDirs(root, yamlFilesAbs)
|
|
1885
|
-
if (hasuraPkgDirs.size === 0) {
|
|
1886
|
-
passFn('Немає Deployment із hasura/graphql-engine у дереві k8s — nginx-sidecar не вимагається (abie.mdc)')
|
|
1887
|
-
return
|
|
1888
|
-
}
|
|
1889
|
-
for (const pkgAbs of [...hasuraPkgDirs].toSorted((a, b) => a.localeCompare(b))) {
|
|
1890
|
-
await checkNginxSidecarForHasuraPackage(root, pkgAbs, fail, passFn)
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
1043
|
/**
|
|
1895
1044
|
* Збирає всі `*.env` файли в дереві (за виключенням `node_modules`, `.git` та інших службових каталогів),
|
|
1896
|
-
* basename яких — abie env-файл (`dev.env` / `ua.env`
|
|
1045
|
+
* basename яких — abie env-файл (`dev.env` / `ua.env` опційно з провідною крапкою). Файл `.env`
|
|
1897
1046
|
* без імені виключається — як і у `check-hasura.mjs`.
|
|
1898
1047
|
* @param {string} root корінь репозиторію
|
|
1899
1048
|
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
@@ -1915,11 +1064,11 @@ async function collectAbieEnvFiles(root, ignorePaths) {
|
|
|
1915
1064
|
}
|
|
1916
1065
|
|
|
1917
1066
|
/**
|
|
1918
|
-
* Сканує всі `*.env` файли abie (`.dev.env` / `.ua.env`
|
|
1067
|
+
* Сканує всі `*.env` файли abie (`.dev.env` / `.ua.env`) і для кожного знайденого
|
|
1919
1068
|
* **внутрішньокластерного** URL (`http://<svc>.<ns>.svc.<dns>`) перевіряє, що DNS-суфікс і namespace-префікс
|
|
1920
1069
|
* відповідають середовищу env-файла. Не лише `HASURA_GRAPHQL_ENDPOINT`, а й будь-який сервіс у env (KVCMS,
|
|
1921
1070
|
* `auth-run-hl`, `file-link-hl` тощо) мусить мати кластер, що відповідає env: dev → `abie-dev.internal`,
|
|
1922
|
-
* ua → `abie-ua.internal
|
|
1071
|
+
* ua → `abie-ua.internal`.
|
|
1923
1072
|
* @param {string} root корінь репозиторію
|
|
1924
1073
|
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
1925
1074
|
* @param {(msg: string) => void} pass успішне повідомлення
|
|
@@ -1929,7 +1078,7 @@ async function collectAbieEnvFiles(root, ignorePaths) {
|
|
|
1929
1078
|
async function ensureAbieEnvFilesMatchClusterDns(root, ignorePaths, pass, fail) {
|
|
1930
1079
|
const envFiles = await collectAbieEnvFiles(root, ignorePaths)
|
|
1931
1080
|
if (envFiles.length === 0) {
|
|
1932
|
-
pass('Не знайдено dev.env / ua.env
|
|
1081
|
+
pass('Не знайдено dev.env / ua.env у репозиторії — перевірку env→cluster DNS пропущено (abie.mdc)')
|
|
1933
1082
|
return
|
|
1934
1083
|
}
|
|
1935
1084
|
for (const abs of envFiles) {
|
|
@@ -1987,26 +1136,17 @@ export async function check() {
|
|
|
1987
1136
|
pass('Немає Deployment у дереві k8s — перевірку hc.yaml пропущено')
|
|
1988
1137
|
}
|
|
1989
1138
|
|
|
1990
|
-
const healthCheckPolicyRelativePaths = await collectHealthCheckPolicyRelPaths(root, yamlFiles)
|
|
1991
|
-
await ensureRuKustomizationHealthCheckDelete(root, yamlFiles, healthCheckPolicyRelativePaths, fail)
|
|
1992
|
-
|
|
1993
|
-
pass('Перевіряємо Service → NodePort у ru/kustomization (abie.mdc)')
|
|
1994
|
-
await ensureRuAbieServiceNodePortPatches(root, yamlFiles, fail, pass)
|
|
1995
|
-
|
|
1996
1139
|
pass('Перевіряємо HTTPRoute spec.hostnames у …/k8s/base/… (aiml.live, abie.mdc)')
|
|
1997
1140
|
await ensureAbieBaseHttpRouteHostnames(root, yamlFiles, fail, pass)
|
|
1998
1141
|
|
|
1999
1142
|
if (deploymentDirs.size > 0) {
|
|
2000
|
-
pass('Є Deployment — перевіряємо nodeSelector у ua/
|
|
2001
|
-
await
|
|
2002
|
-
pass('Є Deployment — перевіряємо HTTPRoute у ua/
|
|
2003
|
-
await
|
|
1143
|
+
pass('Є Deployment — перевіряємо nodeSelector у ua/kustomization (abie.mdc)')
|
|
1144
|
+
await ensureUaAbieNodeSelectorPatches(root, yamlFiles, deploymentDirs, fail, pass)
|
|
1145
|
+
pass('Є Deployment — перевіряємо HTTPRoute у ua/kustomization (abie.mdc)')
|
|
1146
|
+
await ensureUaAbieHttpRoutePatches(root, yamlFiles, fail, pass)
|
|
2004
1147
|
}
|
|
2005
1148
|
|
|
2006
|
-
pass('Перевіряємо
|
|
2007
|
-
await ensureAbieNginxSidecarForHasura(root, yamlFiles, fail, pass)
|
|
2008
|
-
|
|
2009
|
-
pass('Перевіряємо env→cluster DNS у dev.env / ua.env / ru.env (abie.mdc)')
|
|
1149
|
+
pass('Перевіряємо env→cluster DNS у dev.env / ua.env (abie.mdc)')
|
|
2010
1150
|
await ensureAbieEnvFilesMatchClusterDns(root, ignorePaths, pass, fail)
|
|
2011
1151
|
|
|
2012
1152
|
return reporter.getExitCode()
|