@nitra/cursor 1.8.56 → 1.8.61
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 +19 -12
- package/package.json +1 -1
- package/scripts/check-k8s.mjs +210 -25
- 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,18 @@ resources: {}
|
|
|
104
104
|
|
|
105
105
|
Так маніфест явно резервує місце під **`requests` / `limits`** і уникає випадкового пропуску секції. **`check k8s`** перевіряє це для кожного YAML-документа **`Deployment`** у файлах під **`k8s`**.
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
**`imagePullPolicy`:** у маніфестах не вимагається; Kubernetes за замовчуванням: образ **без тега** або з **`:latest`** → **Always**, з іншим тегом → **IfNotPresent**. **`check k8s`** це поле не перевіряє.
|
|
108
|
+
|
|
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
|
+
|
|
111
|
+
## Service: заборонені анотації GKE
|
|
112
|
+
|
|
113
|
+
У **`kind: Service`** не використовуй у **`metadata.annotations`** ключі:
|
|
114
|
+
|
|
115
|
+
- **`cloud.google.com/neg`**
|
|
116
|
+
- **`cloud.google.com/backend-config`**
|
|
117
|
+
|
|
118
|
+
Вони потрібні для інтеграції з **Ingress** / класичним балансуванням GKE; після переходу на **Gateway API** їх слід **прибрати** з маніфестів. **`check k8s`** завершиться помилкою, якщо хоча б один із цих ключів присутній.
|
|
108
119
|
|
|
109
120
|
## Kustomize: структура каталогів (`base` / overlays)
|
|
110
121
|
|
|
@@ -165,27 +176,23 @@ resources: {}
|
|
|
165
176
|
|
|
166
177
|
## Перевірка
|
|
167
178
|
|
|
168
|
-
**`npx @nitra/cursor check k8s`** —
|
|
179
|
+
**`npx @nitra/cursor check k8s`** — деталі умов у **JSDoc на початку** **`npm/scripts/check-k8s.mjs`**; канон URL **`$schema`** для редактору — розділ **«Визначення схеми YAML»** нижче. Якщо під `k8s` немає `*.yaml` — перевірку пропущено. Решту політик кластера / compliance закриває **`lint-k8s`**.
|
|
169
180
|
|
|
170
181
|
Після змін у маніфестах: **`bun run lint-k8s`** (kubeconform + kubescape).
|
|
171
182
|
|
|
172
183
|
## Що закодовано в `check-k8s.mjs`
|
|
173
184
|
|
|
174
|
-
|
|
185
|
+
Не дублюй тут повний перелік — він у **верхньому JSDoc** **`npm/scripts/check-k8s.mjs`** і в константах (**`YANNH_PIN`**, **`YANNH_GROUPS`**, **`EXPLICIT_K8S_SCHEMAS`**, **`CLUSTER_SCOPED_KINDS`**, **`HASURA_GRAPHQL_ENGINE_IMAGE`** тощо).
|
|
186
|
+
|
|
187
|
+
Коротко: **`$schema`** (один modeline, без **`.yml`** під **`k8s`**), заборона **Ingress**, **Deployment.resources**, пін **hasura/graphql-engine**, **Service** без анотацій NEG/backend-config, **`metadata.namespace`** залежно від **base** і графа Kustomize, заборона **`…/k8s/dev/…`**, непорожній **`namespace:`** у **`k8s/base/kustomization.yaml`**, видалення файлів, де всі документи — лише **BackendConfig** (змішані файли — помилка).
|
|
175
188
|
|
|
176
|
-
-
|
|
177
|
-
- **`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`).
|
|
178
|
-
- У кожному YAML-документі з **`kind: Deployment`** — парсинг через **`yaml`**: у кожного елемента **`spec.template.spec.containers[]`** має існувати ключ **`resources`** зі значенням-об'єктом (допускається порожній **`{}`**); у кожного контейнера — **`imagePullPolicy: Always`**.
|
|
179
|
-
- **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`**).
|
|
180
|
-
- Документи з **`kind: Ingress`** — заборонені (потрібен перехід на Gateway API, див. розділ **Ingress → Gateway API**).
|
|
181
|
-
- Заборона шляхів **`…/k8s/dev/…`** (окремої директорії **`dev`** під **`k8s`** не має бути).
|
|
182
|
-
- Якщо в дереві є **`k8s/base/kustomization.yaml`**, у першому документі **завжди** має бути непорожнє поле **`namespace`** (типово **`dev`**; див. розділ **Namespace**).
|
|
189
|
+
При зміні **PIN** версії Kubernetes оновлюй узгоджено **`check-k8s.mjs`**, **`run-k8s.mjs`** (**`KUBERNETES_VERSION`**, **`DATREE_CRD_SCHEMA_LOCATION`**) і розділи **lint-k8s** / **Визначення схеми YAML** у цьому файлі (**`YANNH_REF`**, **`YANNH_GROUPS`**, **`DATREE_CRD_BASE`** — за потреби в скрипті).
|
|
183
190
|
|
|
184
191
|
## Коли застосовувати (агентам)
|
|
185
192
|
|
|
186
193
|
- Зміни в k8s YAML — після правок **`check k8s`**.
|
|
187
194
|
- Якщо перший рядок уже коректний і URL відповідає `apiVersion` / `kind` — не дублюй; змінився ресурс — онови лише `$schema`.
|
|
188
|
-
- У **`Deployment`** без поля **`resources`** у контейнері — додай **`resources: {}`** (див. розділ **Deployment: `resources`**)
|
|
195
|
+
- У **`Deployment`** без поля **`resources`** у контейнері — додай **`resources: {}`** (див. розділ **Deployment: `resources`**). **`imagePullPolicy`** за потреби не дублюй — достатньо політики Kubernetes за тегом образу.
|
|
189
196
|
- Дотримуйся структури **Kustomize** (`base` = dev, overlays без дублювання `base`, коментарі для рядків, що змінюються в overlay). У **`base/kustomization.yaml`** завжди задавай непорожній **`namespace:`**. У **`k8s/base`** у кожному ресурсному YAML має бути явний **`metadata.namespace`**. Поза **base**, якщо не хочеш **`metadata.namespace`** у файлі — підключи його до kustomization (**`resources`** / **`patches`** тощо); інакше додай явний **`metadata.namespace`**.
|
|
190
197
|
- Після міграції на нову структуру **видали** застарілі файли та каталоги, які вже замінені (див. **Міграція зі старої структури**).
|
|
191
198
|
|
package/package.json
CHANGED
package/scripts/check-k8s.mjs
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Додатково: у кожному YAML-документі з **`kind: Deployment`** у кожного контейнера
|
|
9
9
|
* **`spec.template.spec.containers[]`** має бути ключ **`resources`** (значення — об'єкт, допускається
|
|
10
|
-
* порожній **`{}`**)
|
|
10
|
+
* порожній **`{}`**). Поле **`imagePullPolicy`** не перевіряється — діють типові правила Kubernetes
|
|
11
|
+
* (`:latest` або без тега → **Always**, інші теги → **IfNotPresent**). Якщо серед **`containers`** /
|
|
12
|
+
* **`initContainers`** є образ **`hasura/graphql-engine`**, дозволено лише пін **`HASURA_GRAPHQL_ENGINE_IMAGE`**
|
|
13
|
+
* (див. k8s.mdc).
|
|
11
14
|
*
|
|
12
15
|
* **Namespace і Kustomize:** YAML у **`…/k8s/base/`** (окрім імені **`kustomization.yaml`**)
|
|
13
16
|
* завжди має **непорожній** **`metadata.namespace`** у відповідних документах (узгоджено з dev у репозиторії),
|
|
@@ -18,6 +21,12 @@
|
|
|
18
21
|
*
|
|
19
22
|
* **`kind: Ingress`** заборонено (потрібен перехід на Gateway API).
|
|
20
23
|
*
|
|
24
|
+
* Файли під **`k8s`**, де всі YAML-документи — лише **`kind: BackendConfig`**, **видаляються** автоматично.
|
|
25
|
+
* Якщо **BackendConfig** змішано з іншими ресурсами в одному файлі — перевірка завершується помилкою (розділи маніфести).
|
|
26
|
+
*
|
|
27
|
+
* У **`kind: Service`** у **`metadata.annotations`** не повинно бути ключів **`cloud.google.com/neg`**
|
|
28
|
+
* та **`cloud.google.com/backend-config`** (див. k8s.mdc).
|
|
29
|
+
*
|
|
21
30
|
* Структура **Kustomize** (див. k8s.mdc): заборона шляхів **`…/k8s/dev/…`**; у **`k8s/base/kustomization.yaml`**
|
|
22
31
|
* завжди має бути непорожнє поле **`namespace:`** (перевірка, якщо файл існує).
|
|
23
32
|
*
|
|
@@ -27,7 +36,7 @@
|
|
|
27
36
|
* Dockerfile — правило docker.mdc, скрипт check-docker.mjs.
|
|
28
37
|
*/
|
|
29
38
|
import { existsSync } from 'node:fs'
|
|
30
|
-
import { readFile, stat } from 'node:fs/promises'
|
|
39
|
+
import { readFile, stat, unlink } from 'node:fs/promises'
|
|
31
40
|
import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
32
41
|
|
|
33
42
|
import { parseAllDocuments } from 'yaml'
|
|
@@ -38,6 +47,27 @@ import { walkDir } from './utils/walkDir.mjs'
|
|
|
38
47
|
/** Версія набору схем yannh — узгоджено з k8s.mdc */
|
|
39
48
|
const YANNH_PIN = 'v1.33.9-standalone-strict'
|
|
40
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Дозволений образ **hasura/graphql-engine** у Deployment (узгоджено з k8s.mdc).
|
|
52
|
+
* Еквівалент **`docker.io/…`** також приймається.
|
|
53
|
+
*/
|
|
54
|
+
export const HASURA_GRAPHQL_ENGINE_IMAGE = 'hasura/graphql-engine:v2.48.15.ubi.amd64'
|
|
55
|
+
|
|
56
|
+
/** Набір прийнятних рядків `image` без digest (`@sha256:…`). */
|
|
57
|
+
const HASURA_GRAPHQL_ENGINE_ALLOWED_IMAGES = new Set([
|
|
58
|
+
HASURA_GRAPHQL_ENGINE_IMAGE,
|
|
59
|
+
`docker.io/${HASURA_GRAPHQL_ENGINE_IMAGE}`
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Ключі анотацій GKE (NEG / BackendConfig) у **Service** — заборонені (узгоджено з k8s.mdc).
|
|
64
|
+
* @type {readonly string[]}
|
|
65
|
+
*/
|
|
66
|
+
export const SERVICE_FORBIDDEN_GCP_ANNOTATION_KEYS = Object.freeze([
|
|
67
|
+
'cloud.google.com/neg',
|
|
68
|
+
'cloud.google.com/backend-config'
|
|
69
|
+
])
|
|
70
|
+
|
|
41
71
|
/** Гілка репозиторію yannh/kubernetes-json-schema для raw.githubusercontent.com (каталог набору в URL одразу після ref). */
|
|
42
72
|
const YANNH_REF = 'master'
|
|
43
73
|
|
|
@@ -395,6 +425,90 @@ async function findK8sYamlFiles(root) {
|
|
|
395
425
|
return [...out].sort((a, b) => a.localeCompare(b))
|
|
396
426
|
}
|
|
397
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Тіло YAML для політик (Ingress, BackendConfig тощо): якщо перший рядок — modeline `$schema`, береться вміст після нього.
|
|
430
|
+
* @param {string[]} lines рядки файлу
|
|
431
|
+
* @returns {string} фрагмент для `parseAllDocuments`
|
|
432
|
+
*/
|
|
433
|
+
function k8sYamlBodyForDocumentParse(lines) {
|
|
434
|
+
if (lines.length > 0 && MODELINE_RE.test(lines[0])) {
|
|
435
|
+
return yamlBodyAfterModeline(lines)
|
|
436
|
+
}
|
|
437
|
+
return lines.join('\n')
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Чи всі нетривіальні документи у тілі — **`kind: BackendConfig`**, чи є змішування з іншими kind.
|
|
442
|
+
*
|
|
443
|
+
* @param {string} body YAML без обов’язкового modeline (див. `k8sYamlBodyForDocumentParse`)
|
|
444
|
+
* @returns {'none' | 'only' | 'mixed' | 'unparsed'} unparsed — не вдалося розпарсити YAML
|
|
445
|
+
*/
|
|
446
|
+
export function classifyBackendConfigManifestPresence(body) {
|
|
447
|
+
/** @type {import('yaml').Document[]} */
|
|
448
|
+
let docs
|
|
449
|
+
try {
|
|
450
|
+
docs = parseAllDocuments(body)
|
|
451
|
+
} catch {
|
|
452
|
+
return 'unparsed'
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
let hasBc = false
|
|
456
|
+
let hasOther = false
|
|
457
|
+
for (const doc of docs) {
|
|
458
|
+
if (doc.errors.length === 0) {
|
|
459
|
+
const obj = doc.toJSON()
|
|
460
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
461
|
+
const kind = obj.kind
|
|
462
|
+
if (kind === 'BackendConfig') {
|
|
463
|
+
hasBc = true
|
|
464
|
+
} else if (kind !== undefined && kind !== null && String(kind).trim() !== '') {
|
|
465
|
+
hasOther = true
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!hasBc) return 'none'
|
|
472
|
+
if (hasOther) return 'mixed'
|
|
473
|
+
return 'only'
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Видаляє під **`k8s`** YAML-файли, що містять **лише** ресурси **BackendConfig**; змішані файли — `fail`.
|
|
478
|
+
*
|
|
479
|
+
* @param {string} root корінь репозиторію
|
|
480
|
+
* @param {(msg: string) => void} fail реєстрація порушення
|
|
481
|
+
* @param {(msg: string) => void} pass реєстрація успіху
|
|
482
|
+
* @returns {Promise<void>}
|
|
483
|
+
*/
|
|
484
|
+
async function removeBackendConfigOnlyK8sYamlFiles(root, fail, pass) {
|
|
485
|
+
const yamlFiles = await findK8sYamlFiles(root)
|
|
486
|
+
for (const abs of yamlFiles) {
|
|
487
|
+
const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
|
|
488
|
+
try {
|
|
489
|
+
const raw = await readFile(abs, 'utf8')
|
|
490
|
+
const lines = toLines(raw)
|
|
491
|
+
const body = k8sYamlBodyForDocumentParse(lines)
|
|
492
|
+
const bcPresence = classifyBackendConfigManifestPresence(body)
|
|
493
|
+
|
|
494
|
+
if (bcPresence === 'mixed') {
|
|
495
|
+
fail(
|
|
496
|
+
`${rel}: у файлі разом BackendConfig та інші kind — винеси BackendConfig окремо або прибери вручну; автоматичне видалення не застосовується (див. k8s.mdc)`
|
|
497
|
+
)
|
|
498
|
+
} else if (bcPresence === 'only') {
|
|
499
|
+
try {
|
|
500
|
+
await unlink(abs)
|
|
501
|
+
pass(`${rel}: видалено (лише kind: BackendConfig; див. k8s.mdc)`)
|
|
502
|
+
} catch (error) {
|
|
503
|
+
fail(`${rel}: не вдалося видалити BackendConfig-файл (${error.message})`)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
} catch (error) {
|
|
507
|
+
fail(`${rel}: не вдалося прочитати для перевірки BackendConfig (${error.message})`)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
398
512
|
/**
|
|
399
513
|
* Прибирає BOM і ділить на рядки.
|
|
400
514
|
* @param {string} content вміст файлу
|
|
@@ -535,11 +649,58 @@ export function deploymentResourcesViolation(manifest) {
|
|
|
535
649
|
}
|
|
536
650
|
|
|
537
651
|
/**
|
|
538
|
-
*
|
|
652
|
+
* Прибирає digest з посилання на образ (`@sha256:…`) для порівняння тега.
|
|
653
|
+
* @param {string} image значення поля `image`
|
|
654
|
+
* @returns {string} той самий рядок без суфікса `@…` (digest), з `.trim()`
|
|
655
|
+
*/
|
|
656
|
+
function stripImageDigest(image) {
|
|
657
|
+
const at = image.indexOf('@')
|
|
658
|
+
return (at === -1 ? image : image.slice(0, at)).trim()
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Чи рядок `image` вказує на репозиторій **hasura/graphql-engine** (будь-який тег / без тега).
|
|
663
|
+
* @param {string} image значення поля `image`
|
|
664
|
+
* @returns {boolean} true, якщо шлях образу закінчується на `hasura/graphql-engine` з тегом або без
|
|
665
|
+
*/
|
|
666
|
+
function isHasuraGraphqlEngineImageRef(image) {
|
|
667
|
+
const s = stripImageDigest(image)
|
|
668
|
+
return /(^|\/)hasura\/graphql-engine(?:[:]|$)/u.test(s)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Перевіряє пін образу Hasura у одному списку контейнерів Pod spec.
|
|
673
|
+
* @param {string} list ім’я поля для повідомлення (`containers` / `initContainers`)
|
|
674
|
+
* @param {unknown} containers значення з маніфесту
|
|
675
|
+
* @returns {string | null} текст порушення або null
|
|
676
|
+
*/
|
|
677
|
+
function hasuraGraphqlEngineViolationInContainerList(list, containers) {
|
|
678
|
+
if (!Array.isArray(containers)) return null
|
|
679
|
+
for (const [i, c] of containers.entries()) {
|
|
680
|
+
const label =
|
|
681
|
+
typeof c === 'object' && c !== null && !Array.isArray(c) && typeof c.name === 'string' && c.name !== ''
|
|
682
|
+
? c.name
|
|
683
|
+
: `#${i + 1}`
|
|
684
|
+
if (c !== null && c !== undefined && typeof c === 'object' && !Array.isArray(c)) {
|
|
685
|
+
const cont = /** @type {Record<string, unknown>} */ (c)
|
|
686
|
+
const image = cont.image
|
|
687
|
+
if (typeof image === 'string' && image.trim() !== '' && isHasuraGraphqlEngineImageRef(image)) {
|
|
688
|
+
const normalized = stripImageDigest(image)
|
|
689
|
+
if (!HASURA_GRAPHQL_ENGINE_ALLOWED_IMAGES.has(normalized)) {
|
|
690
|
+
return `${list} "${label}": образ hasura/graphql-engine має бути ${HASURA_GRAPHQL_ENGINE_IMAGE} (зараз: ${image}) (див. k8s.mdc)`
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return null
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Чи порушує **Deployment** вимогу пінованого образу **hasura/graphql-engine** (k8s.mdc).
|
|
539
700
|
* @param {unknown} manifest корінь YAML-документа
|
|
540
|
-
* @returns {string | null} текст порушення або null, якщо не Deployment / ок
|
|
701
|
+
* @returns {string | null} текст порушення або null, якщо не Deployment / образу немає / ок
|
|
541
702
|
*/
|
|
542
|
-
export function
|
|
703
|
+
export function deploymentHasuraGraphqlEngineImageViolation(manifest) {
|
|
543
704
|
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
544
705
|
return null
|
|
545
706
|
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
@@ -549,25 +710,41 @@ export function deploymentImagePullPolicyViolation(manifest) {
|
|
|
549
710
|
const template = /** @type {Record<string, unknown>} */ (spec).template
|
|
550
711
|
if (template === null || template === undefined || typeof template !== 'object' || Array.isArray(template))
|
|
551
712
|
return null
|
|
552
|
-
const
|
|
553
|
-
if (
|
|
554
|
-
|
|
555
|
-
|
|
713
|
+
const podSpecRaw = /** @type {Record<string, unknown>} */ (template).spec
|
|
714
|
+
if (podSpecRaw === null || podSpecRaw === undefined || typeof podSpecRaw !== 'object' || Array.isArray(podSpecRaw))
|
|
715
|
+
return null
|
|
716
|
+
const podSpec = /** @type {Record<string, unknown>} */ (podSpecRaw)
|
|
556
717
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
718
|
+
const main = hasuraGraphqlEngineViolationInContainerList('containers', podSpec.containers)
|
|
719
|
+
if (main !== null) return main
|
|
720
|
+
return hasuraGraphqlEngineViolationInContainerList('initContainers', podSpec.initContainers)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Чи **Service** містить заборонені анотації GKE у **`metadata.annotations`** (k8s.mdc).
|
|
725
|
+
* @param {unknown} manifest корінь YAML-документа
|
|
726
|
+
* @returns {string | null} текст порушення або null, якщо не Service / анотацій немає / ок
|
|
727
|
+
*/
|
|
728
|
+
export function serviceForbiddenGcpAnnotationsViolation(manifest) {
|
|
729
|
+
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
730
|
+
return null
|
|
731
|
+
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
732
|
+
if (rec.kind !== 'Service') return null
|
|
733
|
+
const meta = rec.metadata
|
|
734
|
+
if (meta === null || meta === undefined || typeof meta !== 'object' || Array.isArray(meta)) return null
|
|
735
|
+
const m = /** @type {Record<string, unknown>} */ (meta)
|
|
736
|
+
const ann = m.annotations
|
|
737
|
+
if (ann === null || ann === undefined || typeof ann !== 'object' || Array.isArray(ann)) return null
|
|
738
|
+
const a = /** @type {Record<string, unknown>} */ (ann)
|
|
739
|
+
/** @type {string[]} */
|
|
740
|
+
const found = []
|
|
741
|
+
for (const key of SERVICE_FORBIDDEN_GCP_ANNOTATION_KEYS) {
|
|
742
|
+
if (Object.hasOwn(a, key)) {
|
|
743
|
+
found.push(key)
|
|
567
744
|
}
|
|
568
745
|
}
|
|
569
|
-
|
|
570
|
-
return
|
|
746
|
+
if (found.length === 0) return null
|
|
747
|
+
return `metadata.annotations: прибери заборонені ключі GKE: ${found.join(', ')} (див. k8s.mdc)`
|
|
571
748
|
}
|
|
572
749
|
|
|
573
750
|
/**
|
|
@@ -638,7 +815,8 @@ export function isK8sBaseManifestYamlPath(rel, baseLower) {
|
|
|
638
815
|
}
|
|
639
816
|
|
|
640
817
|
/**
|
|
641
|
-
* Парсить усі YAML-документи: **metadata.namespace**, **Deployment.resources**, **
|
|
818
|
+
* Парсить усі YAML-документи: **metadata.namespace**, **Deployment.resources**, **Hasura image pin**,
|
|
819
|
+
* **Service — заборонені GKE-анотації**.
|
|
642
820
|
* @param {string} rel відносний шлях
|
|
643
821
|
* @param {string} baseLower basename файлу (нижній регістр)
|
|
644
822
|
* @param {string} body вміст після modeline
|
|
@@ -686,9 +864,13 @@ function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeMan
|
|
|
686
864
|
if (resV !== null) {
|
|
687
865
|
fail(`${rel}: Deployment (документ ${di + 1}): ${resV}`)
|
|
688
866
|
}
|
|
689
|
-
const
|
|
690
|
-
if (
|
|
691
|
-
fail(`${rel}: Deployment (документ ${di + 1}): ${
|
|
867
|
+
const hasuraV = deploymentHasuraGraphqlEngineImageViolation(obj)
|
|
868
|
+
if (hasuraV !== null) {
|
|
869
|
+
fail(`${rel}: Deployment (документ ${di + 1}): ${hasuraV}`)
|
|
870
|
+
}
|
|
871
|
+
const svcGcpV = serviceForbiddenGcpAnnotationsViolation(obj)
|
|
872
|
+
if (svcGcpV !== null) {
|
|
873
|
+
fail(`${rel}: Service (документ ${di + 1}): ${svcGcpV}`)
|
|
692
874
|
}
|
|
693
875
|
}
|
|
694
876
|
}
|
|
@@ -909,6 +1091,9 @@ export async function check() {
|
|
|
909
1091
|
}
|
|
910
1092
|
|
|
911
1093
|
const root = process.cwd()
|
|
1094
|
+
|
|
1095
|
+
await removeBackendConfigOnlyK8sYamlFiles(root, fail, pass)
|
|
1096
|
+
|
|
912
1097
|
const yamlFiles = await findK8sYamlFiles(root)
|
|
913
1098
|
|
|
914
1099
|
if (yamlFiles.length === 0) {
|
|
@@ -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/`, якщо ці правила увімкнені в проєкті.
|