@nitra/cursor 1.8.58 → 1.8.63
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/README.md +1 -1
- package/mdc/k8s.mdc +16 -12
- package/package.json +1 -1
- package/scripts/check-k8s.mjs +509 -45
- package/skills/abie-kustomize/SKILL.md +0 -2
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
- **Структура Kustomize:** спільне виноситься в **`base`**; вміст **base** відповідає тому, як має виглядати середовище **dev**; окремої директорії **`dev/`** немає — за dev відповідає **`base`**. У інших середовищах — тонкі **overlays** (часто лише **`kustomization.yaml`** і patches / оверрайди).
|
|
41
41
|
- **Namespace** задається в **`kustomization.yaml`** (`namespace:`), а не через **`metadata.namespace`** у кожному ресурсі; окремі patches лише на зміну **namespace** не потрібні.
|
|
42
|
-
- У **Deployment** для кожного контейнера: **`resources
|
|
42
|
+
- У **Deployment** для кожного контейнера: **`resources`** (перевіряє **`npx @nitra/cursor check k8s`**);
|
|
43
43
|
- Рядки в **base**, які змінюються в overlays, позначайте коментарем на рядку (узгоджено в команді), наприклад: `# буде замінено через kustomize`.
|
|
44
44
|
- Після перенесення в **`base`** / overlays **видаляйте** застарілі маніфести та каталоги, які більше не потрібні.
|
|
45
45
|
|
package/mdc/k8s.mdc
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: K8s YAML — $schema (yaml-language-server); lint-k8s (kubeconform, kubescape); check-k8s
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.24'
|
|
4
4
|
globs: "**/k8s/**/*.yaml"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -104,7 +104,7 @@ resources: {}
|
|
|
104
104
|
|
|
105
105
|
Так маніфест явно резервує місце під **`requests` / `limits`** і уникає випадкового пропуску секції. **`check k8s`** перевіряє це для кожного YAML-документа **`Deployment`** у файлах під **`k8s`**.
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
**`imagePullPolicy`:** у маніфестах не вимагається; Kubernetes за замовчуванням: образ **без тега** або з **`:latest`** → **Always**, з іншим тегом → **IfNotPresent**. **`check k8s`** це поле не перевіряє.
|
|
108
108
|
|
|
109
109
|
Якщо в **`Deployment`** у **`spec.template.spec.containers`** або **`initContainers`** задано образ **`hasura/graphql-engine`**, у полі **`image`** має бути саме **`hasura/graphql-engine:v2.48.15.ubi.amd64`** (для еквівалента Docker Hub допускається префікс **`docker.io/`**). Інші теги або сторонні реєстри з тим самим репозиторієм **`hasura/graphql-engine`** — порушення **`check k8s`**.
|
|
110
110
|
|
|
@@ -117,6 +117,14 @@ resources: {}
|
|
|
117
117
|
|
|
118
118
|
Вони потрібні для інтеграції з **Ingress** / класичним балансуванням GKE; після переходу на **Gateway API** їх слід **прибрати** з маніфестів. **`check k8s`** завершиться помилкою, якщо хоча б один із цих ключів присутній.
|
|
119
119
|
|
|
120
|
+
## Service: `svc.yaml` і `svc-hl.yaml` (Gateway API)
|
|
121
|
+
|
|
122
|
+
- У тому самому каталозі, де лежить **`svc.yaml`**, обов’язково має бути **`svc-hl.yaml`**. Якщо файлу ще немає — створи його як копію **`svc.yaml`**: для кожного **Service** задай **`metadata.name`** з суфіксом **`-hl`**, **`spec.clusterIP: None`** (headless), збережи селектор і порти як у кластерного сервісу; **`check k8s`** не генерує файли автоматично.
|
|
123
|
+
- У **`svc.yaml`** кожен **Service** має мати **`spec.type: ClusterIP`**.
|
|
124
|
+
- У **`svc-hl.yaml`** кожен **Service** має мати **`spec.clusterIP: None`** та **`metadata.name`**, що закінчується на **`-hl`**, узгоджено з відповідним ім’ям у **`svc.yaml`** (наприклад **`app`** → **`app-hl`**).
|
|
125
|
+
- У маршрутах **Gateway API** з групи **`gateway.networking.k8s.io`** (**`HTTPRoute`**, **`GRPCRoute`**, **`TCPRoute`**, **`TLSRoute`**, **`UDPRoute`**) посилання **`backendRefs`** / **`backendRef`** на **Service** мають вказувати лише сервіси з цього headless-файлу (ім’я з суфіксом **`-hl`**).
|
|
126
|
+
- Якщо **`svc.yaml`** підключено через **`kustomization.yaml`** (**`resources`**, **`bases`**, **`components`**, **`crds`**, **`patches[].path`**, **`patchesStrategicMerge`**), у **тому ж** **`kustomization.yaml`** додай посилання на відповідний **`svc-hl.yaml`** (той самий відносний префікс каталогу, що й для **`svc.yaml`**).
|
|
127
|
+
|
|
120
128
|
## Kustomize: структура каталогів (`base` / overlays)
|
|
121
129
|
|
|
122
130
|
Трансформуй дерева **`**/k8s`**, щоб **винести спільне** через [Kustomize](https://kustomize.io/): один канонічний **`base`** і тонкі **overlays** для інших середовищ.
|
|
@@ -176,27 +184,23 @@ resources: {}
|
|
|
176
184
|
|
|
177
185
|
## Перевірка
|
|
178
186
|
|
|
179
|
-
**`npx @nitra/cursor check k8s`** —
|
|
187
|
+
**`npx @nitra/cursor check k8s`** — деталі умов у **JSDoc на початку** **`npm/scripts/check-k8s.mjs`**; канон URL **`$schema`** для редактору — розділ **«Визначення схеми YAML»** нижче. Якщо під `k8s` немає `*.yaml` — перевірку пропущено. Решту політик кластера / compliance закриває **`lint-k8s`**.
|
|
180
188
|
|
|
181
189
|
Після змін у маніфестах: **`bun run lint-k8s`** (kubeconform + kubescape).
|
|
182
190
|
|
|
183
191
|
## Що закодовано в `check-k8s.mjs`
|
|
184
192
|
|
|
185
|
-
|
|
193
|
+
Не дублюй тут повний перелік — він у **верхньому JSDoc** **`npm/scripts/check-k8s.mjs`** і в константах (**`YANNH_PIN`**, **`YANNH_GROUPS`**, **`EXPLICIT_K8S_SCHEMAS`**, **`CLUSTER_SCOPED_KINDS`**, **`HASURA_GRAPHQL_ENGINE_IMAGE`** тощо).
|
|
194
|
+
|
|
195
|
+
Коротко: **`$schema`** (один modeline, без **`.yml`** під **`k8s`**), заборона **Ingress**, **Deployment.resources**, пін **hasura/graphql-engine**, **Service** без анотацій NEG/backend-config, пари **`svc.yaml`** / **`svc-hl.yaml`** (**ClusterIP** / headless **`-hl`**) і їхні записи в **`kustomization.yaml`**, **Gateway API** маршрути — **backendRef** лише на **`-hl`**, **`metadata.namespace`** залежно від **base** і графа Kustomize, заборона **`…/k8s/dev/…`**, непорожній **`namespace:`** у **`k8s/base/kustomization.yaml`**, видалення файлів, де всі документи — лише **BackendConfig** (змішані файли — помилка).
|
|
186
196
|
|
|
187
|
-
-
|
|
188
|
-
- **`file:`** у `$schema` — URL до apiVersion/kind не звіряється; **`https:`** — kustomization за іменем файлу → Schema Store; далі перевіряється **`EXPLICIT_K8S_SCHEMAS`** (`Map`: `apiVersion` + `kind` + `type`, для записів без `type` у маніфесті — третій компонент **`*`**); потім `v1` → yannh; група з `YANNH_GROUPS` → yannh; інакше → datree (GitHub Pages), крім рядків явної таблиці (наприклад **InfisicalSecret** — raw на `main`).
|
|
189
|
-
- У кожному YAML-документі з **`kind: Deployment`** — парсинг через **`yaml`**: у кожного елемента **`spec.template.spec.containers[]`** має існувати ключ **`resources`** зі значенням-об'єктом (допускається порожній **`{}`**); у кожного контейнера — **`imagePullPolicy: Always`**.
|
|
190
|
-
- **Namespace у маніфестах (не ім’я `kustomization`):** ресурсні YAML у **`…/k8s/base/`** — **завжди** непорожній **`metadata.namespace`**. Інакше будується множина шляхів, досяжних з **`kustomization.yaml`** через **`resources`**, **`bases`**, **`components`**, **`crds`**, **`patches[].path`**, **`patchesStrategicMerge`**, з рекурсією в каталоги з дочірнім **`kustomization.yaml`**: для таких файлів **поза** **`k8s/base`** — **заборона** **`metadata.namespace`**; для файлів поза **`k8s/base`** і поза множиною — **вимога** непорожнього **`metadata.namespace`** (крім **`CLUSTER_SCOPED_KINDS`**).
|
|
191
|
-
- Документи з **`kind: Ingress`** — заборонені (потрібен перехід на Gateway API, див. розділ **Ingress → Gateway API**).
|
|
192
|
-
- Заборона шляхів **`…/k8s/dev/…`** (окремої директорії **`dev`** під **`k8s`** не має бути).
|
|
193
|
-
- Якщо в дереві є **`k8s/base/kustomization.yaml`**, у першому документі **завжди** має бути непорожнє поле **`namespace`** (типово **`dev`**; див. розділ **Namespace**).
|
|
197
|
+
При зміні **PIN** версії Kubernetes оновлюй узгоджено **`check-k8s.mjs`**, **`run-k8s.mjs`** (**`KUBERNETES_VERSION`**, **`DATREE_CRD_SCHEMA_LOCATION`**) і розділи **lint-k8s** / **Визначення схеми YAML** у цьому файлі (**`YANNH_REF`**, **`YANNH_GROUPS`**, **`DATREE_CRD_BASE`** — за потреби в скрипті).
|
|
194
198
|
|
|
195
199
|
## Коли застосовувати (агентам)
|
|
196
200
|
|
|
197
201
|
- Зміни в k8s YAML — після правок **`check k8s`**.
|
|
198
202
|
- Якщо перший рядок уже коректний і URL відповідає `apiVersion` / `kind` — не дублюй; змінився ресурс — онови лише `$schema`.
|
|
199
|
-
- У **`Deployment`** без поля **`resources`** у контейнері — додай **`resources: {}`** (див. розділ **Deployment: `resources`**)
|
|
203
|
+
- У **`Deployment`** без поля **`resources`** у контейнері — додай **`resources: {}`** (див. розділ **Deployment: `resources`**). **`imagePullPolicy`** за потреби не дублюй — достатньо політики Kubernetes за тегом образу.
|
|
200
204
|
- Дотримуйся структури **Kustomize** (`base` = dev, overlays без дублювання `base`, коментарі для рядків, що змінюються в overlay). У **`base/kustomization.yaml`** завжди задавай непорожній **`namespace:`**. У **`k8s/base`** у кожному ресурсному YAML має бути явний **`metadata.namespace`**. Поза **base**, якщо не хочеш **`metadata.namespace`** у файлі — підключи його до kustomization (**`resources`** / **`patches`** тощо); інакше додай явний **`metadata.namespace`**.
|
|
201
205
|
- Після міграції на нову структуру **видали** застарілі файли та каталоги, які вже замінені (див. **Міграція зі старої структури**).
|
|
202
206
|
|
package/package.json
CHANGED
package/scripts/check-k8s.mjs
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Додатково: у кожному YAML-документі з **`kind: Deployment`** у кожного контейнера
|
|
9
9
|
* **`spec.template.spec.containers[]`** має бути ключ **`resources`** (значення — об'єкт, допускається
|
|
10
|
-
* порожній **`{}`**)
|
|
11
|
-
*
|
|
10
|
+
* порожній **`{}`**). Поле **`imagePullPolicy`** не перевіряється — діють типові правила Kubernetes
|
|
11
|
+
* (`:latest` або без тега → **Always**, інші теги → **IfNotPresent**). Якщо серед **`containers`** /
|
|
12
|
+
* **`initContainers`** є образ **`hasura/graphql-engine`**, дозволено лише пін **`HASURA_GRAPHQL_ENGINE_IMAGE`**
|
|
13
|
+
* (див. k8s.mdc).
|
|
12
14
|
*
|
|
13
15
|
* **Namespace і Kustomize:** YAML у **`…/k8s/base/`** (окрім імені **`kustomization.yaml`**)
|
|
14
16
|
* завжди має **непорожній** **`metadata.namespace`** у відповідних документах (узгоджено з dev у репозиторії),
|
|
@@ -19,9 +21,22 @@
|
|
|
19
21
|
*
|
|
20
22
|
* **`kind: Ingress`** заборонено (потрібен перехід на Gateway API).
|
|
21
23
|
*
|
|
24
|
+
* Файли під **`k8s`**, де всі YAML-документи — лише **`kind: BackendConfig`**, **видаляються** автоматично.
|
|
25
|
+
* Якщо **BackendConfig** змішано з іншими ресурсами в одному файлі — перевірка завершується помилкою (розділи маніфести).
|
|
26
|
+
*
|
|
22
27
|
* У **`kind: Service`** у **`metadata.annotations`** не повинно бути ключів **`cloud.google.com/neg`**
|
|
23
28
|
* та **`cloud.google.com/backend-config`** (див. k8s.mdc).
|
|
24
29
|
*
|
|
30
|
+
* Файли **`svc.yaml`** / **`svc-hl.yaml`** у **одному каталозі** (див. k8s.mdc): для кожного **`svc.yaml`**
|
|
31
|
+
* поруч обов’язковий **`svc-hl.yaml`** (headless-копія: той самий селектор/порти, **`metadata.name`** з суфіксом **`-hl`**,
|
|
32
|
+
* **`spec.clusterIP: None`**). У **`svc.yaml`** кожен **Service** має **`spec.type: ClusterIP`**. У **`svc-hl.yaml`**
|
|
33
|
+
* кожен **Service** — **`spec.clusterIP: None`** та ім’я на **`-hl`**. У маршрутах **Gateway API**
|
|
34
|
+
* (**`HTTPRoute`**, **`GRPCRoute`**, **`TCPRoute`**, **`TLSRoute`**, **`UDPRoute`**, група **`gateway.networking.k8s.io`**)
|
|
35
|
+
* посилання **`backendRefs` / `backendRef`** на **Service** мають вказувати лише сервіси з суфіксом **`-hl`** у **`name`**.
|
|
36
|
+
* Якщо **`kustomization.yaml`** посилається на **`svc.yaml`** (**`resources`**, **`bases`**, **`components`**, **`crds`**,
|
|
37
|
+
* **`patches[].path`**, **`patchesStrategicMerge`**), у **тому ж** файлі має бути посилання на відповідний **`svc-hl.yaml`**
|
|
38
|
+
* в **тому ж каталозі**, що й **`svc.yaml`** (логіка збігається з **`pathsFromKustomizationObject`**).
|
|
39
|
+
*
|
|
25
40
|
* Структура **Kustomize** (див. k8s.mdc): заборона шляхів **`…/k8s/dev/…`**; у **`k8s/base/kustomization.yaml`**
|
|
26
41
|
* завжди має бути непорожнє поле **`namespace:`** (перевірка, якщо файл існує).
|
|
27
42
|
*
|
|
@@ -31,7 +46,7 @@
|
|
|
31
46
|
* Dockerfile — правило docker.mdc, скрипт check-docker.mjs.
|
|
32
47
|
*/
|
|
33
48
|
import { existsSync } from 'node:fs'
|
|
34
|
-
import { readFile, stat } from 'node:fs/promises'
|
|
49
|
+
import { readFile, stat, unlink } from 'node:fs/promises'
|
|
35
50
|
import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
36
51
|
|
|
37
52
|
import { parseAllDocuments } from 'yaml'
|
|
@@ -291,6 +306,79 @@ function pathsFromKustomizationObject(obj) {
|
|
|
291
306
|
return out
|
|
292
307
|
}
|
|
293
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Чи для кожного посилання kustomization на файл **`svc.yaml`** у списку є посилання на sibling **`svc-hl.yaml`**
|
|
311
|
+
* (той самий каталог після **`resolve`** відносно каталогу **`kustomization.yaml`**).
|
|
312
|
+
* @param {string} kustomizationDir абсолютний шлях до каталогу з **`kustomization.yaml`**
|
|
313
|
+
* @param {string[]} pathRefs рядки з **`pathsFromKustomizationObject`**
|
|
314
|
+
* @returns {string | null} текст порушення або null, якщо ок
|
|
315
|
+
*/
|
|
316
|
+
export function kustomizationSvcYamlMissingSvcHlViolation(kustomizationDir, pathRefs) {
|
|
317
|
+
/** @type {Set<string>} */
|
|
318
|
+
const resolved = new Set()
|
|
319
|
+
for (const ref of pathRefs) {
|
|
320
|
+
if (typeof ref === 'string' && !ref.includes('://')) {
|
|
321
|
+
resolved.add(resolve(kustomizationDir, ref))
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
for (const ref of pathRefs) {
|
|
325
|
+
if (typeof ref === 'string' && !ref.includes('://')) {
|
|
326
|
+
const abs = resolve(kustomizationDir, ref)
|
|
327
|
+
if (basename(abs).toLowerCase() === 'svc.yaml') {
|
|
328
|
+
const hlAbs = resolve(dirname(abs), 'svc-hl.yaml')
|
|
329
|
+
if (!resolved.has(hlAbs)) {
|
|
330
|
+
return `kustomization посилається на «${ref}» — додай у тому ж kustomization.yaml посилання на відповідний svc-hl.yaml (очікуваний шлях поруч, напр. той самий префікс каталогу + svc-hl.yaml; див. k8s.mdc)`
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return null
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Перевіряє всі **`kustomization.yaml`** під **`k8s`**: разом із **`svc.yaml`** має бути **`svc-hl.yaml`** у полях шляхів.
|
|
340
|
+
* @param {string} root корінь репозиторію
|
|
341
|
+
* @param {string[]} yamlFiles абсолютні шляхи до yaml під k8s
|
|
342
|
+
* @param {(msg: string) => void} fail callback помилки
|
|
343
|
+
* @returns {Promise<void>}
|
|
344
|
+
*/
|
|
345
|
+
async function validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail) {
|
|
346
|
+
for (const kustAbs of yamlFiles) {
|
|
347
|
+
if (basename(kustAbs).toLowerCase() === 'kustomization.yaml') {
|
|
348
|
+
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
349
|
+
let raw
|
|
350
|
+
try {
|
|
351
|
+
raw = await readFile(kustAbs, 'utf8')
|
|
352
|
+
} catch (error) {
|
|
353
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
354
|
+
fail(`${rel}: не вдалося прочитати для перевірки svc.yaml/svc-hl.yaml у kustomization (${msg})`)
|
|
355
|
+
}
|
|
356
|
+
if (raw !== undefined) {
|
|
357
|
+
const lines = toLines(raw)
|
|
358
|
+
const body = yamlBodyAfterModeline(lines)
|
|
359
|
+
/** @type {import('yaml').Document[] | undefined} */
|
|
360
|
+
let docs
|
|
361
|
+
try {
|
|
362
|
+
docs = parseAllDocuments(body)
|
|
363
|
+
} catch {
|
|
364
|
+
fail(`${rel}: не вдалося розпарсити YAML для перевірки svc.yaml/svc-hl.yaml у kustomization (див. k8s.mdc)`)
|
|
365
|
+
}
|
|
366
|
+
if (docs !== undefined) {
|
|
367
|
+
const first = docs[0]?.toJSON()
|
|
368
|
+
if (first !== null && first !== undefined && typeof first === 'object' && !Array.isArray(first)) {
|
|
369
|
+
const pathRefs = pathsFromKustomizationObject(first)
|
|
370
|
+
const kustDir = dirname(kustAbs)
|
|
371
|
+
const v = kustomizationSvcYamlMissingSvcHlViolation(kustDir, pathRefs)
|
|
372
|
+
if (v !== null) {
|
|
373
|
+
fail(`${rel}: ${v}`)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
294
382
|
/**
|
|
295
383
|
* Збирає відносні шляхи (posix) до YAML, підключених до Kustomize з будь-якого **`kustomization.yaml`** під `k8s`.
|
|
296
384
|
* Обходить **`resources`**, **`bases`**, **`components`**, **`crds`**, **`patches[].path`**, **`patchesStrategicMerge`**;
|
|
@@ -420,6 +508,88 @@ async function findK8sYamlFiles(root) {
|
|
|
420
508
|
return [...out].sort((a, b) => a.localeCompare(b))
|
|
421
509
|
}
|
|
422
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Тіло YAML для політик (Ingress, BackendConfig тощо): якщо перший рядок — modeline `$schema`, береться вміст після нього.
|
|
513
|
+
* @param {string[]} lines рядки файлу
|
|
514
|
+
* @returns {string} фрагмент для `parseAllDocuments`
|
|
515
|
+
*/
|
|
516
|
+
function k8sYamlBodyForDocumentParse(lines) {
|
|
517
|
+
if (lines.length > 0 && MODELINE_RE.test(lines[0])) {
|
|
518
|
+
return yamlBodyAfterModeline(lines)
|
|
519
|
+
}
|
|
520
|
+
return lines.join('\n')
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Чи всі нетривіальні документи у тілі — **`kind: BackendConfig`**, чи є змішування з іншими kind.
|
|
525
|
+
* @param {string} body YAML без обов’язкового modeline (див. `k8sYamlBodyForDocumentParse`)
|
|
526
|
+
* @returns {'none' | 'only' | 'mixed' | 'unparsed'} unparsed — не вдалося розпарсити YAML
|
|
527
|
+
*/
|
|
528
|
+
export function classifyBackendConfigManifestPresence(body) {
|
|
529
|
+
/** @type {import('yaml').Document[]} */
|
|
530
|
+
let docs
|
|
531
|
+
try {
|
|
532
|
+
docs = parseAllDocuments(body)
|
|
533
|
+
} catch {
|
|
534
|
+
return 'unparsed'
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
let hasBc = false
|
|
538
|
+
let hasOther = false
|
|
539
|
+
for (const doc of docs) {
|
|
540
|
+
if (doc.errors.length === 0) {
|
|
541
|
+
const obj = doc.toJSON()
|
|
542
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
543
|
+
const kind = obj.kind
|
|
544
|
+
if (kind === 'BackendConfig') {
|
|
545
|
+
hasBc = true
|
|
546
|
+
} else if (kind !== undefined && kind !== null && String(kind).trim() !== '') {
|
|
547
|
+
hasOther = true
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!hasBc) return 'none'
|
|
554
|
+
if (hasOther) return 'mixed'
|
|
555
|
+
return 'only'
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Видаляє під **`k8s`** YAML-файли, що містять **лише** ресурси **BackendConfig**; змішані файли — `fail`.
|
|
560
|
+
* @param {string} root корінь репозиторію
|
|
561
|
+
* @param {(msg: string) => void} fail реєстрація порушення
|
|
562
|
+
* @param {(msg: string) => void} pass реєстрація успіху
|
|
563
|
+
* @returns {Promise<void>}
|
|
564
|
+
*/
|
|
565
|
+
async function removeBackendConfigOnlyK8sYamlFiles(root, fail, pass) {
|
|
566
|
+
const yamlFiles = await findK8sYamlFiles(root)
|
|
567
|
+
for (const abs of yamlFiles) {
|
|
568
|
+
const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
|
|
569
|
+
try {
|
|
570
|
+
const raw = await readFile(abs, 'utf8')
|
|
571
|
+
const lines = toLines(raw)
|
|
572
|
+
const body = k8sYamlBodyForDocumentParse(lines)
|
|
573
|
+
const bcPresence = classifyBackendConfigManifestPresence(body)
|
|
574
|
+
|
|
575
|
+
if (bcPresence === 'mixed') {
|
|
576
|
+
fail(
|
|
577
|
+
`${rel}: у файлі разом BackendConfig та інші kind — винеси BackendConfig окремо або прибери вручну; автоматичне видалення не застосовується (див. k8s.mdc)`
|
|
578
|
+
)
|
|
579
|
+
} else if (bcPresence === 'only') {
|
|
580
|
+
try {
|
|
581
|
+
await unlink(abs)
|
|
582
|
+
pass(`${rel}: видалено (лише kind: BackendConfig; див. k8s.mdc)`)
|
|
583
|
+
} catch (error) {
|
|
584
|
+
fail(`${rel}: не вдалося видалити BackendConfig-файл (${error.message})`)
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
} catch (error) {
|
|
588
|
+
fail(`${rel}: не вдалося прочитати для перевірки BackendConfig (${error.message})`)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
423
593
|
/**
|
|
424
594
|
* Прибирає BOM і ділить на рядки.
|
|
425
595
|
* @param {string} content вміст файлу
|
|
@@ -441,6 +611,37 @@ function yamlBodyAfterModeline(lines) {
|
|
|
441
611
|
return lines.slice(i).join('\n')
|
|
442
612
|
}
|
|
443
613
|
|
|
614
|
+
/**
|
|
615
|
+
* Читає k8s YAML і повертає фрагмент після modeline `$schema`, якщо перший рядок — modeline.
|
|
616
|
+
* Потрібно для парної перевірки **`svc.yaml`** / **`svc-hl.yaml`**.
|
|
617
|
+
* @param {string} abs абсолютний шлях до файлу
|
|
618
|
+
* @returns {Promise<string>} тіло для `parseAllDocuments`
|
|
619
|
+
*/
|
|
620
|
+
async function readK8sYamlBodyAfterModelineForSvcPair(abs) {
|
|
621
|
+
const raw = await readFile(abs, 'utf8')
|
|
622
|
+
const lines = toLines(raw)
|
|
623
|
+
if (lines.length > 0 && MODELINE_RE.test(lines[0])) {
|
|
624
|
+
return yamlBodyAfterModeline(lines)
|
|
625
|
+
}
|
|
626
|
+
return lines.join('\n')
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Розбирає YAML на корені документів-об’єктів (ігнорує зламані документи).
|
|
631
|
+
* @param {string} body фрагмент YAML
|
|
632
|
+
* @returns {unknown[]} масив об’єктів-документів
|
|
633
|
+
*/
|
|
634
|
+
function parseK8sYamlDocumentObjectRoots(body) {
|
|
635
|
+
try {
|
|
636
|
+
return parseAllDocuments(body)
|
|
637
|
+
.filter(d => d.errors.length === 0)
|
|
638
|
+
.map(d => d.toJSON())
|
|
639
|
+
.filter(x => x !== null && x !== undefined && typeof x === 'object' && !Array.isArray(x))
|
|
640
|
+
} catch {
|
|
641
|
+
return []
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
444
645
|
/**
|
|
445
646
|
* Перший YAML-документ (до наступного `---` на окремому рядку).
|
|
446
647
|
* @param {string} body фрагмент YAML
|
|
@@ -559,42 +760,6 @@ export function deploymentResourcesViolation(manifest) {
|
|
|
559
760
|
return null
|
|
560
761
|
}
|
|
561
762
|
|
|
562
|
-
/**
|
|
563
|
-
* Чи контейнери **Deployment** мають **`imagePullPolicy: Always`** (k8s.mdc).
|
|
564
|
-
* @param {unknown} manifest корінь YAML-документа
|
|
565
|
-
* @returns {string | null} текст порушення або null, якщо не Deployment / ок
|
|
566
|
-
*/
|
|
567
|
-
export function deploymentImagePullPolicyViolation(manifest) {
|
|
568
|
-
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
569
|
-
return null
|
|
570
|
-
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
571
|
-
if (rec.kind !== 'Deployment') return null
|
|
572
|
-
const spec = rec.spec
|
|
573
|
-
if (spec === null || spec === undefined || typeof spec !== 'object' || Array.isArray(spec)) return null
|
|
574
|
-
const template = /** @type {Record<string, unknown>} */ (spec).template
|
|
575
|
-
if (template === null || template === undefined || typeof template !== 'object' || Array.isArray(template))
|
|
576
|
-
return null
|
|
577
|
-
const podSpec = /** @type {Record<string, unknown>} */ (template).spec
|
|
578
|
-
if (podSpec === null || podSpec === undefined || typeof podSpec !== 'object' || Array.isArray(podSpec)) return null
|
|
579
|
-
const containers = /** @type {Record<string, unknown>} */ (podSpec).containers
|
|
580
|
-
if (!Array.isArray(containers)) return null
|
|
581
|
-
|
|
582
|
-
for (const [i, c] of containers.entries()) {
|
|
583
|
-
const label =
|
|
584
|
-
typeof c === 'object' && c !== null && !Array.isArray(c) && typeof c.name === 'string' && c.name !== ''
|
|
585
|
-
? c.name
|
|
586
|
-
: `#${i + 1}`
|
|
587
|
-
if (c !== null && c !== undefined && typeof c === 'object' && !Array.isArray(c)) {
|
|
588
|
-
const cont = /** @type {Record<string, unknown>} */ (c)
|
|
589
|
-
if (cont.imagePullPolicy !== 'Always') {
|
|
590
|
-
return `контейнер "${label}": imagePullPolicy має бути Always (див. k8s.mdc)`
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
return null
|
|
596
|
-
}
|
|
597
|
-
|
|
598
763
|
/**
|
|
599
764
|
* Прибирає digest з посилання на образ (`@sha256:…`) для порівняння тега.
|
|
600
765
|
* @param {string} image значення поля `image`
|
|
@@ -694,6 +859,287 @@ export function serviceForbiddenGcpAnnotationsViolation(manifest) {
|
|
|
694
859
|
return `metadata.annotations: прибери заборонені ключі GKE: ${found.join(', ')} (див. k8s.mdc)`
|
|
695
860
|
}
|
|
696
861
|
|
|
862
|
+
/** Суфікс **`metadata.name`** headless-сервісу поруч із **`svc.yaml`** (див. k8s.mdc). */
|
|
863
|
+
const SVC_HL_NAME_SUFFIX = '-hl'
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Kind маршрутів Gateway API, у **`spec`** яких шукаємо **`backendRefs`** / **`backendRef`** до **Service**.
|
|
867
|
+
* @type {Set<string>}
|
|
868
|
+
*/
|
|
869
|
+
const GATEWAY_API_ROUTE_KINDS = new Set(['HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute', 'UDPRoute'])
|
|
870
|
+
|
|
871
|
+
/** Префікс **`apiVersion`** стандартних ресурсів Gateway API. */
|
|
872
|
+
const GATEWAY_API_GROUP_PREFIX = 'gateway.networking.k8s.io/'
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Чи **Service** у **`svc.yaml`** має **`spec.type: ClusterIP`** (k8s.mdc).
|
|
876
|
+
* @param {unknown} manifest корінь YAML-документа
|
|
877
|
+
* @returns {string | null} текст порушення або null
|
|
878
|
+
*/
|
|
879
|
+
export function serviceSvcYamlClusterIpTypeViolation(manifest) {
|
|
880
|
+
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
881
|
+
return null
|
|
882
|
+
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
883
|
+
if (rec.kind !== 'Service') return null
|
|
884
|
+
const spec = rec.spec
|
|
885
|
+
if (spec === null || spec === undefined || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
886
|
+
return 'Service: додай spec.type: ClusterIP (svc.yaml, див. k8s.mdc)'
|
|
887
|
+
}
|
|
888
|
+
const s = /** @type {Record<string, unknown>} */ (spec)
|
|
889
|
+
if (s.type !== 'ClusterIP') {
|
|
890
|
+
const cur = s.type === undefined ? 'відсутнє' : String(s.type)
|
|
891
|
+
return `Service spec.type має бути ClusterIP (svc.yaml; зараз: ${cur}; див. k8s.mdc)`
|
|
892
|
+
}
|
|
893
|
+
return null
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Чи **Service** у **`svc-hl.yaml`** headless (**`spec.clusterIP: None`**) з суфіксом **`-hl`** у **`metadata.name`**.
|
|
898
|
+
* @param {unknown} manifest корінь YAML-документа
|
|
899
|
+
* @returns {string | null} текст порушення або null
|
|
900
|
+
*/
|
|
901
|
+
export function serviceSvcHlYamlHeadlessViolation(manifest) {
|
|
902
|
+
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
903
|
+
return null
|
|
904
|
+
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
905
|
+
if (rec.kind !== 'Service') return null
|
|
906
|
+
const meta = rec.metadata
|
|
907
|
+
if (meta === null || meta === undefined || typeof meta !== 'object' || Array.isArray(meta)) {
|
|
908
|
+
return 'Service: потрібні metadata.name з суфіксом -hl (svc-hl.yaml, див. k8s.mdc)'
|
|
909
|
+
}
|
|
910
|
+
const m = /** @type {Record<string, unknown>} */ (meta)
|
|
911
|
+
const n = m.name
|
|
912
|
+
if (typeof n !== 'string' || !n.endsWith(SVC_HL_NAME_SUFFIX)) {
|
|
913
|
+
return `Service metadata.name має закінчуватися на «${SVC_HL_NAME_SUFFIX}» (svc-hl.yaml; див. k8s.mdc)`
|
|
914
|
+
}
|
|
915
|
+
const spec = rec.spec
|
|
916
|
+
if (spec === null || spec === undefined || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
917
|
+
return 'Service: додай spec.clusterIP: None (svc-hl.yaml, див. k8s.mdc)'
|
|
918
|
+
}
|
|
919
|
+
const s = /** @type {Record<string, unknown>} */ (spec)
|
|
920
|
+
if (s.clusterIP !== 'None') {
|
|
921
|
+
const cur = s.clusterIP === undefined ? 'відсутнє' : String(s.clusterIP)
|
|
922
|
+
return `Service spec.clusterIP має бути None (headless, svc-hl.yaml; зараз: ${cur}; див. k8s.mdc)`
|
|
923
|
+
}
|
|
924
|
+
return null
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Чи об’єкт схожий на **backendRef** до **Kubernetes Service** у Gateway API.
|
|
929
|
+
* @param {unknown} obj вузол у дереві **`spec`**
|
|
930
|
+
* @returns {boolean} true, якщо враховуємо поле **`name`** як посилання на Service
|
|
931
|
+
*/
|
|
932
|
+
function isGatewayApiBackendRefToService(obj) {
|
|
933
|
+
if (obj === null || obj === undefined || typeof obj !== 'object' || Array.isArray(obj)) return false
|
|
934
|
+
const o = /** @type {Record<string, unknown>} */ (obj)
|
|
935
|
+
if (typeof o.name !== 'string') return false
|
|
936
|
+
const kind = o.kind
|
|
937
|
+
if (kind !== undefined && kind !== 'Service') return false
|
|
938
|
+
const group = o.group
|
|
939
|
+
if (typeof group === 'string' && group !== '' && group !== 'core') return false
|
|
940
|
+
return true
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Збирає імена **Service** з **`backendRefs`** / **`backendRef`** у піддереві **`spec`** маршруту Gateway API.
|
|
945
|
+
* @param {unknown} spec значення **`spec`** маршруту
|
|
946
|
+
* @returns {string[]} імена backend-сервісів (можливі дублікати)
|
|
947
|
+
*/
|
|
948
|
+
export function collectGatewayApiRouteBackendServiceNames(spec) {
|
|
949
|
+
/** @type {string[]} */
|
|
950
|
+
const out = []
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* @param {unknown} node вузол для обходу
|
|
954
|
+
* @returns {void}
|
|
955
|
+
*/
|
|
956
|
+
function walk(node) {
|
|
957
|
+
if (node === null || node === undefined) return
|
|
958
|
+
if (Array.isArray(node)) {
|
|
959
|
+
for (const x of node) {
|
|
960
|
+
walk(x)
|
|
961
|
+
}
|
|
962
|
+
return
|
|
963
|
+
}
|
|
964
|
+
if (typeof node !== 'object') return
|
|
965
|
+
if (isGatewayApiBackendRefToService(node)) {
|
|
966
|
+
out.push(String(/** @type {Record<string, unknown>} */ (node).name))
|
|
967
|
+
}
|
|
968
|
+
for (const v of Object.values(node)) {
|
|
969
|
+
walk(v)
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
walk(spec)
|
|
974
|
+
return out
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Реєструє порушення: маршрути Gateway API мають посилатися на **Service** з суфіксом **`-hl`**.
|
|
979
|
+
* @param {string} rel відносний шлях до файлу
|
|
980
|
+
* @param {string} body YAML після modeline
|
|
981
|
+
* @param {(msg: string) => void} fail callback помилки
|
|
982
|
+
* @returns {void}
|
|
983
|
+
*/
|
|
984
|
+
function scanGatewayApiRouteBackendRefsInYamlBody(rel, body, fail) {
|
|
985
|
+
/** @type {import('yaml').Document[]} */
|
|
986
|
+
let docs
|
|
987
|
+
try {
|
|
988
|
+
docs = parseAllDocuments(body)
|
|
989
|
+
} catch {
|
|
990
|
+
return
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
for (const [di, doc] of docs.entries()) {
|
|
994
|
+
if (doc.errors.length === 0) {
|
|
995
|
+
const obj = doc.toJSON()
|
|
996
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
997
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
998
|
+
const av = rec.apiVersion
|
|
999
|
+
const kind = rec.kind
|
|
1000
|
+
if (
|
|
1001
|
+
typeof av === 'string' &&
|
|
1002
|
+
av.startsWith(GATEWAY_API_GROUP_PREFIX) &&
|
|
1003
|
+
typeof kind === 'string' &&
|
|
1004
|
+
GATEWAY_API_ROUTE_KINDS.has(kind)
|
|
1005
|
+
) {
|
|
1006
|
+
const names = collectGatewayApiRouteBackendServiceNames(rec.spec)
|
|
1007
|
+
for (const svcName of names) {
|
|
1008
|
+
if (!svcName.endsWith(SVC_HL_NAME_SUFFIX)) {
|
|
1009
|
+
fail(
|
|
1010
|
+
`${rel}: Gateway API ${kind} (документ ${di + 1}): backendRef до Service має вказувати headless-сервіс з суфіксом «${SVC_HL_NAME_SUFFIX}» у name (зараз: «${svcName}»; див. k8s.mdc)`
|
|
1011
|
+
)
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Перевіряє пари **`svc.yaml`** / **`svc-hl.yaml`** у каталозі (наявність, узгоджені імена **Service**).
|
|
1022
|
+
* @param {string} root корінь репозиторію
|
|
1023
|
+
* @param {string[]} yamlFiles абсолютні шляхи до `*.yaml` під `k8s`
|
|
1024
|
+
* @param {(msg: string) => void} fail callback помилки
|
|
1025
|
+
* @returns {Promise<void>}
|
|
1026
|
+
*/
|
|
1027
|
+
async function validateSvcYamlAndSvcHlPairs(root, yamlFiles, fail) {
|
|
1028
|
+
const absSet = new Set(yamlFiles)
|
|
1029
|
+
|
|
1030
|
+
for (const abs of yamlFiles) {
|
|
1031
|
+
if (basename(abs).toLowerCase() === 'svc-hl.yaml') {
|
|
1032
|
+
const svcAbs = join(dirname(abs), 'svc.yaml')
|
|
1033
|
+
if (!absSet.has(svcAbs)) {
|
|
1034
|
+
const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
|
|
1035
|
+
fail(`${rel}: svc-hl.yaml потребує svc.yaml у тому самому каталозі (див. k8s.mdc)`)
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
for (const svcAbs of yamlFiles) {
|
|
1041
|
+
if (basename(svcAbs).toLowerCase() === 'svc.yaml') {
|
|
1042
|
+
const rel = (relative(root, svcAbs) || svcAbs).replaceAll('\\', '/')
|
|
1043
|
+
const hlAbs = join(dirname(svcAbs), 'svc-hl.yaml')
|
|
1044
|
+
if (absSet.has(hlAbs)) {
|
|
1045
|
+
/** @type {string | undefined} */
|
|
1046
|
+
let svcBody
|
|
1047
|
+
/** @type {string | undefined} */
|
|
1048
|
+
let hlBody
|
|
1049
|
+
try {
|
|
1050
|
+
svcBody = await readK8sYamlBodyAfterModelineForSvcPair(svcAbs)
|
|
1051
|
+
hlBody = await readK8sYamlBodyAfterModelineForSvcPair(hlAbs)
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
1054
|
+
fail(`${rel}: не вдалося прочитати svc.yaml / svc-hl.yaml (${msg})`)
|
|
1055
|
+
}
|
|
1056
|
+
if (svcBody !== undefined && hlBody !== undefined) {
|
|
1057
|
+
const svcRoots = parseK8sYamlDocumentObjectRoots(svcBody)
|
|
1058
|
+
const hlRoots = parseK8sYamlDocumentObjectRoots(hlBody)
|
|
1059
|
+
|
|
1060
|
+
/** @type {string[]} */
|
|
1061
|
+
const svcNames = []
|
|
1062
|
+
for (const [i, rootObj] of svcRoots.entries()) {
|
|
1063
|
+
const r = /** @type {Record<string, unknown>} */ (rootObj)
|
|
1064
|
+
if (r.kind === 'Service') {
|
|
1065
|
+
const meta = r.metadata
|
|
1066
|
+
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
|
|
1067
|
+
const nm = /** @type {Record<string, unknown>} */ (meta).name
|
|
1068
|
+
if (typeof nm === 'string') {
|
|
1069
|
+
svcNames.push(nm)
|
|
1070
|
+
} else {
|
|
1071
|
+
fail(`${rel}: svc.yaml (документ ${i + 1}): Service без metadata.name (див. k8s.mdc)`)
|
|
1072
|
+
}
|
|
1073
|
+
} else {
|
|
1074
|
+
fail(`${rel}: svc.yaml (документ ${i + 1}): Service без metadata (див. k8s.mdc)`)
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (svcNames.length === 0) {
|
|
1080
|
+
fail(`${rel}: svc.yaml має містити принаймні один kind: Service (див. k8s.mdc)`)
|
|
1081
|
+
} else {
|
|
1082
|
+
/** @type {string[]} */
|
|
1083
|
+
const hlNames = []
|
|
1084
|
+
for (const [i, rootObj] of hlRoots.entries()) {
|
|
1085
|
+
const r = /** @type {Record<string, unknown>} */ (rootObj)
|
|
1086
|
+
if (r.kind === 'Service') {
|
|
1087
|
+
const meta = r.metadata
|
|
1088
|
+
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
|
|
1089
|
+
const nm = /** @type {Record<string, unknown>} */ (meta).name
|
|
1090
|
+
if (typeof nm === 'string') {
|
|
1091
|
+
hlNames.push(nm)
|
|
1092
|
+
} else {
|
|
1093
|
+
const hlRel = (relative(root, hlAbs) || hlAbs).replaceAll('\\', '/')
|
|
1094
|
+
fail(`${hlRel}: svc-hl.yaml (документ ${i + 1}): Service без metadata.name (див. k8s.mdc)`)
|
|
1095
|
+
}
|
|
1096
|
+
} else {
|
|
1097
|
+
const hlRel = (relative(root, hlAbs) || hlAbs).replaceAll('\\', '/')
|
|
1098
|
+
fail(`${hlRel}: svc-hl.yaml (документ ${i + 1}): Service без metadata (див. k8s.mdc)`)
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
if (hlNames.length === 0) {
|
|
1104
|
+
const hlRel = (relative(root, hlAbs) || hlAbs).replaceAll('\\', '/')
|
|
1105
|
+
fail(`${hlRel}: svc-hl.yaml має містити принаймні один kind: Service (див. k8s.mdc)`)
|
|
1106
|
+
} else {
|
|
1107
|
+
const hlSet = new Set(hlNames)
|
|
1108
|
+
for (const n of svcNames) {
|
|
1109
|
+
const expectHl = `${n}${SVC_HL_NAME_SUFFIX}`
|
|
1110
|
+
if (!hlSet.has(expectHl)) {
|
|
1111
|
+
fail(
|
|
1112
|
+
`${rel}: для Service «${n}» у svc.yaml у svc-hl.yaml має бути Service з metadata.name «${expectHl}» (див. k8s.mdc)`
|
|
1113
|
+
)
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
for (const h of hlNames) {
|
|
1118
|
+
if (h.endsWith(SVC_HL_NAME_SUFFIX)) {
|
|
1119
|
+
const base = h.slice(0, -SVC_HL_NAME_SUFFIX.length)
|
|
1120
|
+
if (!svcNames.includes(base)) {
|
|
1121
|
+
const hlRel = (relative(root, hlAbs) || hlAbs).replaceAll('\\', '/')
|
|
1122
|
+
fail(
|
|
1123
|
+
`${hlRel}: Service «${h}» у svc-hl.yaml не відповідає жодному Service у svc.yaml (очікується базове ім’я «${base}»; див. k8s.mdc)`
|
|
1124
|
+
)
|
|
1125
|
+
}
|
|
1126
|
+
} else {
|
|
1127
|
+
const hlRel = (relative(root, hlAbs) || hlAbs).replaceAll('\\', '/')
|
|
1128
|
+
fail(
|
|
1129
|
+
`${hlRel}: Service «${h}» у svc-hl.yaml: metadata.name має закінчуватися на «${SVC_HL_NAME_SUFFIX}» (див. k8s.mdc)`
|
|
1130
|
+
)
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
} else {
|
|
1137
|
+
fail(`${rel}: поруч обов’язковий svc-hl.yaml (headless-копія з суфіксом -hl у metadata.name; див. k8s.mdc)`)
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
697
1143
|
/**
|
|
698
1144
|
* Для маніфестів, **підключених** до Kustomize (шлях у `resources` / `patches` / …), **metadata.namespace** не додають.
|
|
699
1145
|
* @param {unknown} manifest корінь YAML-документа
|
|
@@ -762,8 +1208,9 @@ export function isK8sBaseManifestYamlPath(rel, baseLower) {
|
|
|
762
1208
|
}
|
|
763
1209
|
|
|
764
1210
|
/**
|
|
765
|
-
* Парсить усі YAML-документи: **metadata.namespace**, **Deployment.resources**, **
|
|
766
|
-
* **Service — заборонені GKE
|
|
1211
|
+
* Парсить усі YAML-документи: **metadata.namespace**, **Deployment.resources**, **Hasura image pin**,
|
|
1212
|
+
* **Service — заборонені GKE-анотації**, **`svc.yaml`** (**`spec.type: ClusterIP`**), **`svc-hl.yaml`**
|
|
1213
|
+
* (**headless**, суфікс **`-hl`** у **`metadata.name`**).
|
|
767
1214
|
* @param {string} rel відносний шлях
|
|
768
1215
|
* @param {string} baseLower basename файлу (нижній регістр)
|
|
769
1216
|
* @param {string} body вміст після modeline
|
|
@@ -811,10 +1258,6 @@ function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeMan
|
|
|
811
1258
|
if (resV !== null) {
|
|
812
1259
|
fail(`${rel}: Deployment (документ ${di + 1}): ${resV}`)
|
|
813
1260
|
}
|
|
814
|
-
const pullV = deploymentImagePullPolicyViolation(obj)
|
|
815
|
-
if (pullV !== null) {
|
|
816
|
-
fail(`${rel}: Deployment (документ ${di + 1}): ${pullV}`)
|
|
817
|
-
}
|
|
818
1261
|
const hasuraV = deploymentHasuraGraphqlEngineImageViolation(obj)
|
|
819
1262
|
if (hasuraV !== null) {
|
|
820
1263
|
fail(`${rel}: Deployment (документ ${di + 1}): ${hasuraV}`)
|
|
@@ -823,6 +1266,18 @@ function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeMan
|
|
|
823
1266
|
if (svcGcpV !== null) {
|
|
824
1267
|
fail(`${rel}: Service (документ ${di + 1}): ${svcGcpV}`)
|
|
825
1268
|
}
|
|
1269
|
+
if (baseLower === 'svc.yaml') {
|
|
1270
|
+
const svcT = serviceSvcYamlClusterIpTypeViolation(obj)
|
|
1271
|
+
if (svcT !== null) {
|
|
1272
|
+
fail(`${rel}: Service (документ ${di + 1}): ${svcT}`)
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (baseLower === 'svc-hl.yaml') {
|
|
1276
|
+
const svcH = serviceSvcHlYamlHeadlessViolation(obj)
|
|
1277
|
+
if (svcH !== null) {
|
|
1278
|
+
fail(`${rel}: Service (документ ${di + 1}): ${svcH}`)
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
826
1281
|
}
|
|
827
1282
|
}
|
|
828
1283
|
}
|
|
@@ -975,6 +1430,8 @@ async function checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel) {
|
|
|
975
1430
|
|
|
976
1431
|
const kustomizeManaged = kustomizeManagedRel.has(rel)
|
|
977
1432
|
validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeManaged)
|
|
1433
|
+
|
|
1434
|
+
scanGatewayApiRouteBackendRefsInYamlBody(rel, body, fail)
|
|
978
1435
|
}
|
|
979
1436
|
|
|
980
1437
|
/**
|
|
@@ -1042,6 +1499,9 @@ export async function check() {
|
|
|
1042
1499
|
}
|
|
1043
1500
|
|
|
1044
1501
|
const root = process.cwd()
|
|
1502
|
+
|
|
1503
|
+
await removeBackendConfigOnlyK8sYamlFiles(root, fail, pass)
|
|
1504
|
+
|
|
1045
1505
|
const yamlFiles = await findK8sYamlFiles(root)
|
|
1046
1506
|
|
|
1047
1507
|
if (yamlFiles.length === 0) {
|
|
@@ -1059,6 +1519,10 @@ export async function check() {
|
|
|
1059
1519
|
await checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel)
|
|
1060
1520
|
}
|
|
1061
1521
|
|
|
1522
|
+
await validateSvcYamlAndSvcHlPairs(root, yamlFiles, fail)
|
|
1523
|
+
|
|
1524
|
+
await validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail)
|
|
1525
|
+
|
|
1062
1526
|
await ensureBaseKustomizationHasNamespace(root, yamlFiles, fail)
|
|
1063
1527
|
|
|
1064
1528
|
return exitCode
|
|
@@ -21,6 +21,4 @@ README має бути в директорії **k8s**.
|
|
|
21
21
|
|
|
22
22
|
Застарілі файли прибирай.
|
|
23
23
|
|
|
24
|
-
У всіх Deployment має бути `imagePullPolicy: Always`.
|
|
25
|
-
|
|
26
24
|
Для overlays **ru** та **ua** `namespace` задавай у `kustomization.yaml` (без окремих patch лише на зміну namespace). Деталі — **n-k8s** / **abie** у `.cursor/rules/`, якщо ці правила увімкнені в проєкті.
|