@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 +1 -1
- package/mdc/k8s.mdc +8 -12
- package/package.json +1 -1
- package/scripts/check-k8s.mjs +96 -44
- 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
|
|
|
@@ -176,27 +176,23 @@ resources: {}
|
|
|
176
176
|
|
|
177
177
|
## Перевірка
|
|
178
178
|
|
|
179
|
-
**`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`**.
|
|
180
180
|
|
|
181
181
|
Після змін у маніфестах: **`bun run lint-k8s`** (kubeconform + kubescape).
|
|
182
182
|
|
|
183
183
|
## Що закодовано в `check-k8s.mjs`
|
|
184
184
|
|
|
185
|
-
|
|
185
|
+
Не дублюй тут повний перелік — він у **верхньому JSDoc** **`npm/scripts/check-k8s.mjs`** і в константах (**`YANNH_PIN`**, **`YANNH_GROUPS`**, **`EXPLICIT_K8S_SCHEMAS`**, **`CLUSTER_SCOPED_KINDS`**, **`HASURA_GRAPHQL_ENGINE_IMAGE`** тощо).
|
|
186
186
|
|
|
187
|
-
-
|
|
188
|
-
|
|
189
|
-
|
|
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`**)
|
|
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
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,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**, **
|
|
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/`, якщо ці правила увімкнені в проєкті.
|