@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 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`**, **`imagePullPolicy: Always`** (перевіряє **`npx @nitra/cursor check k8s`**).
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.23'
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
- У кожному контейнері **`Deployment`** має бути **`imagePullPolicy: Always`** (див. **`check k8s`**).
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`** — критерії збігаються з розділом **«Що закодовано в `check-k8s.mjs`»** і з **«Визначення схеми YAML»** вище. Якщо під `k8s` немає `*.yaml` — перевірку пропущено. Інший зміст маніфесту вручну / **`lint-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
- При зміні правил синхронно оновлюй **`YANNH_PIN`**, **`YANNH_REF`** (якщо зміниться гілка за замовчуванням у репо yannh), **`YANNH_GROUPS`**, **`DATREE_CRD_BASE`** (GitHub Pages каталогу CRD), а в **`run-k8s.mjs`** константу **`KUBERNETES_VERSION`** і **`DATREE_CRD_SCHEMA_LOCATION`** (узгоджено з базою datree у цьому правилі).
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
- - Обхід з пропуском `node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next`.
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`**); додай **`imagePullPolicy: Always`** для кожного контейнера.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.56",
3
+ "version": "1.8.61",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -7,7 +7,10 @@
7
7
  *
8
8
  * Додатково: у кожному YAML-документі з **`kind: Deployment`** у кожного контейнера
9
9
  * **`spec.template.spec.containers[]`** має бути ключ **`resources`** (значення — об'єкт, допускається
10
- * порожній **`{}`**) та **`imagePullPolicy: Always`**.
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
- * Чи контейнери **Deployment** мають **`imagePullPolicy: Always`** (k8s.mdc).
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 deploymentImagePullPolicyViolation(manifest) {
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 podSpec = /** @type {Record<string, unknown>} */ (template).spec
553
- if (podSpec === null || podSpec === undefined || typeof podSpec !== 'object' || Array.isArray(podSpec)) return null
554
- const containers = /** @type {Record<string, unknown>} */ (podSpec).containers
555
- if (!Array.isArray(containers)) return null
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
- for (const [i, c] of containers.entries()) {
558
- const label =
559
- typeof c === 'object' && c !== null && !Array.isArray(c) && typeof c.name === 'string' && c.name !== ''
560
- ? c.name
561
- : `#${i + 1}`
562
- if (c !== null && c !== undefined && typeof c === 'object' && !Array.isArray(c)) {
563
- const cont = /** @type {Record<string, unknown>} */ (c)
564
- if (cont.imagePullPolicy !== 'Always') {
565
- return `контейнер "${label}": imagePullPolicy має бути Always (див. k8s.mdc)`
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 null
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**, **imagePullPolicy**.
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 pullV = deploymentImagePullPolicyViolation(obj)
690
- if (pullV !== null) {
691
- fail(`${rel}: Deployment (документ ${di + 1}): ${pullV}`)
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/`, якщо ці правила увімкнені в проєкті.