@nitra/cursor 1.8.58 → 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,7 @@ 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
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
 
@@ -176,27 +176,23 @@ resources: {}
176
176
 
177
177
  ## Перевірка
178
178
 
179
- **`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`**.
180
180
 
181
181
  Після змін у маніфестах: **`bun run lint-k8s`** (kubeconform + kubescape).
182
182
 
183
183
  ## Що закодовано в `check-k8s.mjs`
184
184
 
185
- При зміні правил синхронно оновлюй **`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
186
 
187
- - Обхід з пропуском `node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next`.
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**).
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** (змішані файли — помилка).
188
+
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`** за потреби в скрипті).
194
190
 
195
191
  ## Коли застосовувати (агентам)
196
192
 
197
193
  - Зміни в k8s YAML — після правок **`check k8s`**.
198
194
  - Якщо перший рядок уже коректний і URL відповідає `apiVersion` / `kind` — не дублюй; змінився ресурс — онови лише `$schema`.
199
- - У **`Deployment`** без поля **`resources`** у контейнері — додай **`resources: {}`** (див. розділ **Deployment: `resources`**); додай **`imagePullPolicy: Always`** для кожного контейнера.
195
+ - У **`Deployment`** без поля **`resources`** у контейнері — додай **`resources: {}`** (див. розділ **Deployment: `resources`**). **`imagePullPolicy`** за потреби не дублюй — достатньо політики Kubernetes за тегом образу.
200
196
  - Дотримуйся структури **Kustomize** (`base` = dev, overlays без дублювання `base`, коментарі для рядків, що змінюються в overlay). У **`base/kustomization.yaml`** завжди задавай непорожній **`namespace:`**. У **`k8s/base`** у кожному ресурсному YAML має бути явний **`metadata.namespace`**. Поза **base**, якщо не хочеш **`metadata.namespace`** у файлі — підключи його до kustomization (**`resources`** / **`patches`** тощо); інакше додай явний **`metadata.namespace`**.
201
197
  - Після міграції на нову структуру **видали** застарілі файли та каталоги, які вже замінені (див. **Міграція зі старої структури**).
202
198
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.58",
3
+ "version": "1.8.61",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -7,8 +7,10 @@
7
7
  *
8
8
  * Додатково: у кожному YAML-документі з **`kind: Deployment`** у кожного контейнера
9
9
  * **`spec.template.spec.containers[]`** має бути ключ **`resources`** (значення — об'єкт, допускається
10
- * порожній **`{}`**) та **`imagePullPolicy: Always`**. Якщо серед **`containers`** / **`initContainers`**
11
- * є образ **`hasura/graphql-engine`**, дозволено лише пін **`HASURA_GRAPHQL_ENGINE_IMAGE`** (див. k8s.mdc).
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,6 +21,9 @@
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
  *
@@ -31,7 +36,7 @@
31
36
  * Dockerfile — правило docker.mdc, скрипт check-docker.mjs.
32
37
  */
33
38
  import { existsSync } from 'node:fs'
34
- import { readFile, stat } from 'node:fs/promises'
39
+ import { readFile, stat, unlink } from 'node:fs/promises'
35
40
  import { basename, dirname, join, relative, resolve } from 'node:path'
36
41
 
37
42
  import { parseAllDocuments } from 'yaml'
@@ -420,6 +425,90 @@ async function findK8sYamlFiles(root) {
420
425
  return [...out].sort((a, b) => a.localeCompare(b))
421
426
  }
422
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
+
423
512
  /**
424
513
  * Прибирає BOM і ділить на рядки.
425
514
  * @param {string} content вміст файлу
@@ -559,42 +648,6 @@ export function deploymentResourcesViolation(manifest) {
559
648
  return null
560
649
  }
561
650
 
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
651
  /**
599
652
  * Прибирає digest з посилання на образ (`@sha256:…`) для порівняння тега.
600
653
  * @param {string} image значення поля `image`
@@ -762,7 +815,7 @@ export function isK8sBaseManifestYamlPath(rel, baseLower) {
762
815
  }
763
816
 
764
817
  /**
765
- * Парсить усі YAML-документи: **metadata.namespace**, **Deployment.resources**, **imagePullPolicy**, **Hasura image pin**,
818
+ * Парсить усі YAML-документи: **metadata.namespace**, **Deployment.resources**, **Hasura image pin**,
766
819
  * **Service — заборонені GKE-анотації**.
767
820
  * @param {string} rel відносний шлях
768
821
  * @param {string} baseLower basename файлу (нижній регістр)
@@ -811,10 +864,6 @@ function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeMan
811
864
  if (resV !== null) {
812
865
  fail(`${rel}: Deployment (документ ${di + 1}): ${resV}`)
813
866
  }
814
- const pullV = deploymentImagePullPolicyViolation(obj)
815
- if (pullV !== null) {
816
- fail(`${rel}: Deployment (документ ${di + 1}): ${pullV}`)
817
- }
818
867
  const hasuraV = deploymentHasuraGraphqlEngineImageViolation(obj)
819
868
  if (hasuraV !== null) {
820
869
  fail(`${rel}: Deployment (документ ${di + 1}): ${hasuraV}`)
@@ -1042,6 +1091,9 @@ export async function check() {
1042
1091
  }
1043
1092
 
1044
1093
  const root = process.cwd()
1094
+
1095
+ await removeBackendConfigOnlyK8sYamlFiles(root, fail, pass)
1096
+
1045
1097
  const yamlFiles = await findK8sYamlFiles(root)
1046
1098
 
1047
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/`, якщо ці правила увімкнені в проєкті.