@nitra/cursor 1.8.18 → 1.8.22
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 +16 -0
- package/mdc/bun.mdc +3 -1
- package/mdc/k8s.mdc +81 -4
- package/package.json +1 -1
- package/scripts/check-bun.mjs +44 -1
- package/scripts/check-k8s.mjs +196 -13
package/README.md
CHANGED
|
@@ -27,9 +27,25 @@
|
|
|
27
27
|
| `js-format` | Правила форматування JavaScript ecosystem (oxfmt) |
|
|
28
28
|
| `npm-module` | Структура репозиторію для npm-модуля (bun mono) |
|
|
29
29
|
| `text` | Текстові файли: cspell, CI |
|
|
30
|
+
| `k8s` | Kubernetes YAML, Kustomize, kubeconform |
|
|
30
31
|
|
|
31
32
|
Щоб використовувати конкретну версію правил, оновіть залежність `@nitra/cursor` у проєкті (`bun add -d @nitra/cursor@<версія>` тощо). Поле `version` у `.n-cursor.json`, якщо воно лишилось у старих конфігах, **ігнорується**.
|
|
32
33
|
|
|
34
|
+
### Правило `k8s` і Kustomize
|
|
35
|
+
|
|
36
|
+
У цільовому репозиторії з маніфестами під **`**/k8s`** дотримуйтесь **`mdc/k8s.mdc`** з пакету (після синку — `.cursor/rules/n-k8s.mdc`або копія з`node_modules/@nitra/cursor/mdc/k8s.mdc`).
|
|
37
|
+
|
|
38
|
+
Коротко:
|
|
39
|
+
|
|
40
|
+
- **Структура Kustomize:** спільне виноситься в **`base`**; вміст **base** відповідає тому, як має виглядати середовище **dev**; окремої директорії **`dev/`** немає — за dev відповідає **`base`**. У інших середовищах — тонкі **overlays** (часто лише **`kustomization.yaml`** і patches / оверрайди).
|
|
41
|
+
- У каталозі **`k8s`** має бути **`README.md`** з описом дерев і застосування.
|
|
42
|
+
- **Namespace** задається в **`kustomization.yaml`** (`namespace:`), а не через **`metadata.namespace`** у кожному ресурсі; окремі patches лише на зміну **namespace** не потрібні.
|
|
43
|
+
- У **Deployment** для кожного контейнера: **`resources`**, **`imagePullPolicy: Always`** (перевіряє **`npx @nitra/cursor check k8s`**).
|
|
44
|
+
- Рядки в **base**, які змінюються в overlays, позначайте коментарем на рядку (узгоджено в команді), наприклад: `# буде замінено через kustomize`.
|
|
45
|
+
- Після перенесення в **`base`** / overlays **видаляйте** застарілі маніфести та каталоги, які більше не потрібні.
|
|
46
|
+
|
|
47
|
+
Повний текст правил — у **`k8s.mdc`**; programmatic перевірки — у **`npm/scripts/check-k8s.mjs`** (у встановленому пакеті — `scripts/check-k8s.mjs`).
|
|
48
|
+
|
|
33
49
|
### v8r і власний каталог схем
|
|
34
50
|
|
|
35
51
|
Скрипт `scripts/run-v8r.mjs` передає в v8r каталог **`schemas/v8r-catalog.json`** пакета автоматично (у репозиторії той самий файл, що й `npm/schemas/v8r-catalog.json` від кореня монорепо). Якщо викликаєш `bunx v8r` напряму, передай `-c`: локально `node_modules/@nitra/cursor/schemas/v8r-catalog.json` або [unpkg](https://unpkg.com/@nitra/cursor/schemas/v8r-catalog.json). JSON Schema конфігурації: [n-cursor.json](https://unpkg.com/@nitra/cursor/schemas/n-cursor.json).
|
package/mdc/bun.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Bun як єдиний package manager у монорепо
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.4'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
Проект використовує тільки Bun для керування залежностями та запуску скриптів.
|
|
@@ -48,6 +48,8 @@ Lockfile у репозиторії: `bun.lock`.
|
|
|
48
48
|
- Якщо залежність потрібна лише одному пакету, додавати її в директорії цього пакета.
|
|
49
49
|
- У CI та локально запускати скрипти через `bun run`.
|
|
50
50
|
|
|
51
|
+
В кореневому в package.json не повинно бути `dependencies`, а в `devDependencies` будуть тільки `@cspell/*` та `@nitra/*` модулі.
|
|
52
|
+
|
|
51
53
|
Якщо в package.json є поля `packageManager`, то прибрати їх, також прибрати всі директорії та файли для yarn
|
|
52
54
|
|
|
53
55
|
Якщо в проекті використовується npx, то не замінювати його на bunx, а використовувати npx.
|
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.16'
|
|
4
4
|
globs: "**/k8s/**/*.{yaml,yml}"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -125,9 +125,81 @@ resources: {}
|
|
|
125
125
|
|
|
126
126
|
Так маніфест явно резервує місце під **`requests` / `limits`** і уникає випадкового пропуску секції. **`check k8s`** перевіряє це для кожного YAML-документа **`Deployment`** у файлах під **`k8s`**.
|
|
127
127
|
|
|
128
|
+
У кожному контейнері **`Deployment`** має бути **`imagePullPolicy: Always`** (див. **`check k8s`**).
|
|
129
|
+
|
|
130
|
+
## Kustomize: структура каталогів (`base` / overlays)
|
|
131
|
+
|
|
132
|
+
Трансформуй дерева **`**/k8s`**, щоб **винести спільне** через [Kustomize](https://kustomize.io/): один канонічний **`base`** і тонкі **overlays** для інших середовищ.
|
|
133
|
+
|
|
134
|
+
### Джерело правди — середовище dev
|
|
135
|
+
|
|
136
|
+
- За основу бери **все, що відповідає середовищу dev** (як воно має виглядати в кластері для dev).
|
|
137
|
+
- У **такому вигляді** цей набір стає каталогом **`base`**: спільні маніфести без окремої директорії **`dev/`**.
|
|
138
|
+
- Окремої директорії **`dev`** **не повинно існувати**: за середовище **dev** відповідає **`base`** (застосування **`kubectl apply -k …/base`** або збірка з overlay, який посилається на `base`).
|
|
139
|
+
|
|
140
|
+
### Overlays (не-dev)
|
|
141
|
+
|
|
142
|
+
- У каталозі кожного іншого середовища (наприклад **`ru/`**, **`prod/`**) має бути **мінімум файлів**: типово лише **`kustomization.yaml`** (посилання на `base`, `patches`, `replacements`, `components` тощо) і ресурси чи додаткові YAML, **необхідні лише для цього overlay**.
|
|
143
|
+
- Відмінності від dev вносяться **оверрайдами** (patches, `images`, `replicas`, `configMapGenerator` тощо), а не копіюванням повного дерева з `base`.
|
|
144
|
+
|
|
145
|
+
### Namespace
|
|
146
|
+
|
|
147
|
+
- У **`base/kustomization.yaml`** задай **`namespace: dev`** (або узгоджене ім’я для dev **namespace**), щоб **усі ресурси base** потрапляли в цей namespace через Kustomize.
|
|
148
|
+
- У **маніфестах ресурсів** (Deployment, Service, …) **не вказуй** **`metadata.namespace`** — **namespace** задається **лише в файлах Kustomize** (`kustomization.yaml` / `kustomization.yml`), полем верхнього рівня **`namespace:`**.
|
|
149
|
+
- **Не додавай** окремі **patches** Kustomize, які лише змінюють **namespace**: **namespace** визначає Kustomize; у overlays додаткові зміни — без дублювання логіки **namespace**.
|
|
150
|
+
|
|
151
|
+
### Рядки, що змінюються між середовищами
|
|
152
|
+
|
|
153
|
+
- У маніфестах у **`base`** для полів (або значень), які **будуть відрізнятися** в інших середовищах, на **тому самому рядку** додай коментар:
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
image: my-app:dev-tag # буде замінено через kustomize
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Текст коментаря узгодь у команді; важливо, щоб було видно, що значення **навіть у base** може бути замінене overlay.
|
|
160
|
+
|
|
161
|
+
### Документація в дереві k8s
|
|
162
|
+
|
|
163
|
+
- У каталозі **`k8s`** (корінь оголошеного дерева маніфестів) має бути **`README.md`**: коротко опиши структуру (`base`, overlays), як зібрати/застосувати середовища, посилання на внутрішні правила команди.
|
|
164
|
+
|
|
165
|
+
### Міграція зі старої структури
|
|
166
|
+
|
|
167
|
+
- Після перенесення маніфестів у **`base`** та overlays і перевірки (**`check k8s`**, **`lint-k8s`**) **видали** застарілі файли та директорії, які замінені новою схемою (дубльовані копії, колишні шляхи без Kustomize), щоб у репозиторії не залишалося зайвих або суперечливих маніфестів.
|
|
168
|
+
|
|
169
|
+
## Ingress → Gateway API (GKE)
|
|
170
|
+
|
|
171
|
+
Якщо в дереві **`k8s`** трапляється маніфест з **`kind: Ingress`**, його потрібно **замінити на Gateway API**, а не залишати Ingress.
|
|
172
|
+
|
|
173
|
+
1. **HTTPRoute** — окремий файл **`hr.yaml`** (або узгоджене ім’я в команді), **`kind: HTTPRoute`**, `apiVersion` з групи **`gateway.networking.k8s.io`** (див. приклад `$schema` для HTTPRoute у розділі «Визначення схеми YAML»).
|
|
174
|
+
2. **HealthCheckPolicy (GKE)** — окремий файл **`hc.yaml`**, наприклад:
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
apiVersion: networking.gke.io/v1
|
|
178
|
+
kind: HealthCheckPolicy
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Для `$schema` у першому рядку див. приклад **HealthCheckPolicy** у тому ж розділі (datree CRDs-catalog).
|
|
182
|
+
|
|
183
|
+
3. **Overlay `ru`:** у **`ru/kustomization.yaml`** (шлях з сегментами **`…/ru/kustomization.yaml`** під **`k8s`**) додай **видалення** ресурсу HealthCheckPolicy для середовища, де політика не потрібна (підстав **реальне ім’я** замість `SERVICE_NAME`):
|
|
184
|
+
|
|
185
|
+
```yaml
|
|
186
|
+
patches:
|
|
187
|
+
- target:
|
|
188
|
+
kind: HealthCheckPolicy
|
|
189
|
+
patch: |-
|
|
190
|
+
kind: HealthCheckPolicy
|
|
191
|
+
metadata:
|
|
192
|
+
name: SERVICE_NAME
|
|
193
|
+
$patch: delete
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
За потреби розшир **`target`** (`name`, `namespace`), щоб однозначно вказати об’єкт.
|
|
197
|
+
|
|
198
|
+
**`check k8s`:** заборонено **`kind: Ingress`**; якщо в проєкті є **`kind: HealthCheckPolicy`**, має існувати хоча б один **`ru/kustomization.yaml`** під **`k8s`** із блоком видалення (**`$patch: delete`** для **HealthCheckPolicy**).
|
|
199
|
+
|
|
128
200
|
## Перевірка
|
|
129
201
|
|
|
130
|
-
**`npx @nitra/cursor check k8s`** — узгодженість першого рядка (`$schema`), один рядок `# yaml-language-server` на файл, правило для `.yml`, відповідність URL першому YAML-документу (до `---`) за логікою нижче; для кожного документа **`Deployment`** у
|
|
202
|
+
**`npx @nitra/cursor check k8s`** — узгодженість першого рядка (`$schema`), один рядок `# yaml-language-server` на файл, правило для `.yml`, відповідність URL першому YAML-документу (до `---`) за логікою нижче; для кожного документа **`Deployment`** — **`containers[].resources`**, **`imagePullPolicy: Always`**; у файлах **не** `kustomization.yaml` / `kustomization.yml` — заборона **`metadata.namespace`** (**namespace** лише в Kustomize, див. **Kustomize: структура каталогів**); заборона **`kind: Ingress`**; наявність **`HealthCheckPolicy`** — вимога до **`ru/kustomization.yaml`** з **`$patch: delete`** (див. **Ingress → Gateway API**). Якщо під `k8s` немає yaml/yml — перевірку пропущено. Інший зміст маніфесту — вручну / **`lint-k8s`**.
|
|
131
203
|
|
|
132
204
|
Після змін у маніфестах: **`bun run lint-k8s`** (kubeconform + kubescape; обов'язково для проєктів з правилом **`k8s`** у **`.n-cursor.json`**).
|
|
133
205
|
|
|
@@ -137,13 +209,18 @@ resources: {}
|
|
|
137
209
|
|
|
138
210
|
- Обхід з пропуском `node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next`.
|
|
139
211
|
- **`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`).
|
|
140
|
-
- У кожному YAML-документі з **`kind: Deployment`** — парсинг через **`yaml`**: у кожного елемента **`spec.template.spec.containers[]`** має існувати ключ **`resources`** зі значенням-об'єктом (допускається порожній **`{}`**)
|
|
212
|
+
- У кожному YAML-документі з **`kind: Deployment`** — парсинг через **`yaml`**: у кожного елемента **`spec.template.spec.containers[]`** має існувати ключ **`resources`** зі значенням-об'єктом (допускається порожній **`{}`**); у кожного контейнера — **`imagePullPolicy: Always`**.
|
|
213
|
+
- У файлах, ім’я яких **не** `kustomization.yaml` / `kustomization.yml`, у кожному документі — заборона поля **`metadata.namespace`** (**namespace** задається в Kustomize).
|
|
214
|
+
- Документи з **`kind: Ingress`** — заборонені (потрібен перехід на Gateway API, див. розділ **Ingress → Gateway API**).
|
|
215
|
+
- Якщо в будь-якому файлі під **`k8s`** є **`kind: HealthCheckPolicy`**, серед файлів має бути **`ru/kustomization.yaml`** (сегмент шляху **`ru`** перед іменем файлу), а його вміст — patch видалення **HealthCheckPolicy** з **`$patch: delete`** (див. той самий розділ).
|
|
141
216
|
|
|
142
217
|
## Коли застосовувати (агентам)
|
|
143
218
|
|
|
144
219
|
- Зміни в k8s YAML — після правок **`check k8s`**.
|
|
145
220
|
- Якщо перший рядок уже коректний і URL відповідає `apiVersion` / `kind` — не дублюй; змінився ресурс — онови лише `$schema`.
|
|
146
|
-
- У **`Deployment`** без поля **`resources`** у контейнері — додай **`resources: {}`** (див. розділ **Deployment: `resources`**)
|
|
221
|
+
- У **`Deployment`** без поля **`resources`** у контейнері — додай **`resources: {}`** (див. розділ **Deployment: `resources`**); додай **`imagePullPolicy: Always`** для кожного контейнера.
|
|
222
|
+
- Дотримуйся структури **Kustomize** (`base` = dev, overlays без дублювання `base`, **`README.md`** у `k8s`, коментарі для рядків, що змінюються в overlay).
|
|
223
|
+
- Після міграції на нову структуру **видали** застарілі файли та каталоги, які вже замінені (див. **Міграція зі старої структури**).
|
|
147
224
|
|
|
148
225
|
## Визначення схеми YAML (канон)
|
|
149
226
|
|
package/package.json
CHANGED
package/scripts/check-bun.mjs
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Перевіряє відповідність репозиторію правилам Bun (
|
|
2
|
+
* Перевіряє відповідність репозиторію правилам Bun (bun.mdc).
|
|
3
3
|
*
|
|
4
4
|
* Очікує наявність `bun.lock`, забороняє lockfile та артефакти yarn/pnpm, директорію `.yarn`
|
|
5
5
|
* і поле `packageManager` у кореневому `package.json`.
|
|
6
6
|
*
|
|
7
|
+
* У кореневому `package.json` не має бути поля **`dependencies`**; у **`devDependencies`** дозволені
|
|
8
|
+
* лише пакети з префіксами **`@cspell/`** та **`@nitra/`** (інші залежності — у workspace-пакетах).
|
|
9
|
+
*
|
|
7
10
|
* Якщо в `.n-cursor.json` у `rules` є `docker` або `k8s`, вимагає у кореневому `package.json`
|
|
8
11
|
* відповідно скриптів `lint-docker` / `lint-k8s` (див. docker.mdc, k8s.mdc).
|
|
9
12
|
*
|
|
@@ -16,6 +19,15 @@ import { readFile } from 'node:fs/promises'
|
|
|
16
19
|
|
|
17
20
|
import { pass } from './utils/pass.mjs'
|
|
18
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Чи ім'я пакета дозволене в кореневих `devDependencies` за bun.mdc (лише `@cspell/*` та `@nitra/*`).
|
|
24
|
+
* @param {string} name ключ з поля `devDependencies`
|
|
25
|
+
* @returns {boolean} true, якщо префікс дозволений
|
|
26
|
+
*/
|
|
27
|
+
export function isAllowedRootDevDependency(name) {
|
|
28
|
+
return name.startsWith('@cspell/') || name.startsWith('@nitra/')
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
/**
|
|
20
32
|
* Зчитує ідентифікатори правил з `.n-cursor.json` (поле `rules`).
|
|
21
33
|
* @returns {Promise<Set<string>>} множина рядків id правил або порожня, якщо файлу/поля немає
|
|
@@ -77,6 +89,37 @@ export async function check() {
|
|
|
77
89
|
pass('package.json не містить packageManager')
|
|
78
90
|
}
|
|
79
91
|
|
|
92
|
+
if (pkg.dependencies === undefined) {
|
|
93
|
+
pass('Кореневий package.json без поля `dependencies`')
|
|
94
|
+
} else {
|
|
95
|
+
fail(
|
|
96
|
+
'Кореневий package.json не повинен містити поле `dependencies` — додай залежності в workspace-пакети (bun.mdc)'
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const dev = pkg.devDependencies
|
|
101
|
+
if (dev === undefined) {
|
|
102
|
+
pass('Кореневий package.json без devDependencies')
|
|
103
|
+
} else if (dev === null || typeof dev !== 'object' || Array.isArray(dev)) {
|
|
104
|
+
fail(
|
|
105
|
+
'Кореневий package.json: `devDependencies` має бути object з ключами пакетів і діапазонами версій (не null, не масив)'
|
|
106
|
+
)
|
|
107
|
+
} else {
|
|
108
|
+
const bad = Object.keys(dev).filter(n => !isAllowedRootDevDependency(n))
|
|
109
|
+
if (bad.length > 0) {
|
|
110
|
+
fail(
|
|
111
|
+
`Кореневі devDependencies дозволені лише @cspell/* та @nitra/* — прибери або перенеси: ${bad.join(', ')} (bun.mdc)`
|
|
112
|
+
)
|
|
113
|
+
} else {
|
|
114
|
+
const n = Object.keys(dev).length
|
|
115
|
+
pass(
|
|
116
|
+
n === 0
|
|
117
|
+
? 'Кореневі devDependencies порожні (дозволені лише @cspell/* та @nitra/*)'
|
|
118
|
+
: `Кореневі devDependencies лише @cspell/* та @nitra/* (${n} пак.)`
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
80
123
|
const scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {}
|
|
81
124
|
|
|
82
125
|
if (cursorRules.has('docker')) {
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Додатково: у кожному YAML-документі з **`kind: Deployment`** у кожного контейнера
|
|
9
9
|
* **`spec.template.spec.containers[]`** має бути ключ **`resources`** (значення — об'єкт, допускається
|
|
10
|
-
* порожній **`{}`**)
|
|
10
|
+
* порожній **`{}`**) та **`imagePullPolicy: Always`**.
|
|
11
|
+
*
|
|
12
|
+
* У файлах **не** `kustomization.yaml` / `kustomization.yml` у документах не має бути **`metadata.namespace`**
|
|
13
|
+
* (namespace лише в Kustomize).
|
|
14
|
+
*
|
|
15
|
+
* **`kind: Ingress`** заборонено (потрібен перехід на Gateway API). Якщо є **`HealthCheckPolicy`**,
|
|
16
|
+
* має існувати **`ru/kustomization.yaml`** з patch видалення цього kind (`$patch: delete`).
|
|
11
17
|
*
|
|
12
18
|
* Явні винятки до загальної логіки yannh/datree — таблиця **`EXPLICIT_K8S_SCHEMAS`** (`Map`): ключ
|
|
13
19
|
* **`apiVersion`, `kind`, `type`** (для CRD без поля `type` у маніфесті — зірочка **`*`** як третій
|
|
@@ -205,6 +211,103 @@ function extractApiVersionAndKind(doc) {
|
|
|
205
211
|
}
|
|
206
212
|
}
|
|
207
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Чи відносний шлях вказує на **`ru/kustomization.yaml`** (сегмент **`ru`** перед ім’ям файлу).
|
|
216
|
+
* @param {string} rel шлях від кореня репозиторію
|
|
217
|
+
* @returns {boolean} true, якщо це `…/ru/kustomization.yaml`
|
|
218
|
+
*/
|
|
219
|
+
export function isRuKustomizationPath(rel) {
|
|
220
|
+
const norm = rel.replaceAll('\\', '/')
|
|
221
|
+
return /(^|\/)ru\/kustomization\.yaml$/u.test(norm)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Чи вміст overlay **`ru/kustomization.yaml`** містить Kustomize patch видалення **HealthCheckPolicy**.
|
|
226
|
+
* @param {string} raw повний текст файлу
|
|
227
|
+
* @returns {boolean} true, якщо є `$patch: delete` і блоки kind/metadata для HealthCheckPolicy
|
|
228
|
+
*/
|
|
229
|
+
export function ruKustomizationHasHealthCheckDeletePatch(raw) {
|
|
230
|
+
if (!/\$patch:\s*delete/u.test(raw)) return false
|
|
231
|
+
if (!/kind:\s*HealthCheckPolicy/u.test(raw)) return false
|
|
232
|
+
if (!/metadata:/u.test(raw)) return false
|
|
233
|
+
if (!/name:\s*\S+/u.test(raw)) return false
|
|
234
|
+
return true
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Шукає **Ingress** / **HealthCheckPolicy** у розібраних документах; реєструє порушення для Ingress.
|
|
239
|
+
* @param {string} rel відносний шлях до файлу
|
|
240
|
+
* @param {string} body YAML після modeline
|
|
241
|
+
* @param {(msg: string) => void} fail callback для помилки (Ingress)
|
|
242
|
+
* @param {string[]} healthCheckPolicyFiles накопичувач шляхів, де зустріли HealthCheckPolicy
|
|
243
|
+
* @returns {void}
|
|
244
|
+
*/
|
|
245
|
+
function scanIngressAndHealthCheckPolicy(rel, body, fail, healthCheckPolicyFiles) {
|
|
246
|
+
/** @type {import('yaml').Document[]} */
|
|
247
|
+
let docs
|
|
248
|
+
try {
|
|
249
|
+
docs = parseAllDocuments(body)
|
|
250
|
+
} catch {
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
for (const [di, doc] of docs.entries()) {
|
|
255
|
+
if (doc.errors.length === 0) {
|
|
256
|
+
const obj = doc.toJSON()
|
|
257
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
258
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
259
|
+
if (rec.kind === 'Ingress') {
|
|
260
|
+
fail(
|
|
261
|
+
`${rel}: знайдено kind: Ingress (документ ${di + 1}) — заміни на Gateway API: HTTPRoute (hr.yaml), HealthCheckPolicy (hc.yaml), patch у ru/kustomization.yaml (див. k8s.mdc)`
|
|
262
|
+
)
|
|
263
|
+
} else if (rec.kind === 'HealthCheckPolicy' && !healthCheckPolicyFiles.includes(rel)) {
|
|
264
|
+
healthCheckPolicyFiles.push(rel)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Якщо у дереві k8s є HealthCheckPolicy, вимагає **ru/kustomization.yaml** з patch видалення.
|
|
273
|
+
* @param {string} root корінь cwd
|
|
274
|
+
* @param {string[]} yamlFiles абсолютні шляхи до yaml під k8s
|
|
275
|
+
* @param {string[]} healthCheckPolicyFiles відносні шляхи з HealthCheckPolicy
|
|
276
|
+
* @param {(msg: string) => void} fail callback для помилки (немає ru або немає patch)
|
|
277
|
+
* @returns {Promise<void>} завершення після перевірки overlay ru
|
|
278
|
+
*/
|
|
279
|
+
async function ensureRuKustomizationHealthCheckDelete(root, yamlFiles, healthCheckPolicyFiles, fail) {
|
|
280
|
+
if (healthCheckPolicyFiles.length === 0) {
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const ruAbsList = yamlFiles.filter(abs => isRuKustomizationPath(relative(root, abs) || abs))
|
|
285
|
+
if (ruAbsList.length === 0) {
|
|
286
|
+
fail(
|
|
287
|
+
`Знайдено HealthCheckPolicy у ${healthCheckPolicyFiles.join(', ')} — додай ru/kustomization.yaml з patch видалення (див. k8s.mdc)`
|
|
288
|
+
)
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
for (const abs of ruAbsList) {
|
|
293
|
+
let raw
|
|
294
|
+
try {
|
|
295
|
+
raw = await readFile(abs, 'utf8')
|
|
296
|
+
} catch (error) {
|
|
297
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
298
|
+
fail(`${relative(root, abs) || abs}: не вдалося прочитати (${msg})`)
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
if (ruKustomizationHasHealthCheckDeletePatch(raw)) {
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
fail(
|
|
307
|
+
'Є HealthCheckPolicy, але жоден ru/kustomization.yaml не містить очікуваного patch видалення (kind: HealthCheckPolicy, metadata.name, $patch: delete) — див. k8s.mdc'
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
208
311
|
/**
|
|
209
312
|
* Чи порушує маніфест вимогу **`Deployment.spec.template.spec.containers[].resources`** (див. k8s.mdc).
|
|
210
313
|
* @param {unknown} manifest корінь YAML-документа як об'єкт JavaScript
|
|
@@ -246,32 +349,104 @@ export function deploymentResourcesViolation(manifest) {
|
|
|
246
349
|
}
|
|
247
350
|
|
|
248
351
|
/**
|
|
249
|
-
*
|
|
250
|
-
* @param {
|
|
352
|
+
* Чи контейнери **Deployment** мають **`imagePullPolicy: Always`** (k8s.mdc).
|
|
353
|
+
* @param {unknown} manifest корінь YAML-документа
|
|
354
|
+
* @returns {string | null} текст порушення або null, якщо не Deployment / ок
|
|
355
|
+
*/
|
|
356
|
+
export function deploymentImagePullPolicyViolation(manifest) {
|
|
357
|
+
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
358
|
+
return null
|
|
359
|
+
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
360
|
+
if (rec.kind !== 'Deployment') return null
|
|
361
|
+
const spec = rec.spec
|
|
362
|
+
if (spec === null || spec === undefined || typeof spec !== 'object' || Array.isArray(spec)) return null
|
|
363
|
+
const template = /** @type {Record<string, unknown>} */ (spec).template
|
|
364
|
+
if (template === null || template === undefined || typeof template !== 'object' || Array.isArray(template))
|
|
365
|
+
return null
|
|
366
|
+
const podSpec = /** @type {Record<string, unknown>} */ (template).spec
|
|
367
|
+
if (podSpec === null || podSpec === undefined || typeof podSpec !== 'object' || Array.isArray(podSpec)) return null
|
|
368
|
+
const containers = /** @type {Record<string, unknown>} */ (podSpec).containers
|
|
369
|
+
if (!Array.isArray(containers)) return null
|
|
370
|
+
|
|
371
|
+
for (const [i, c] of containers.entries()) {
|
|
372
|
+
const label =
|
|
373
|
+
typeof c === 'object' && c !== null && !Array.isArray(c) && typeof c.name === 'string' && c.name !== ''
|
|
374
|
+
? c.name
|
|
375
|
+
: `#${i + 1}`
|
|
376
|
+
if (c !== null && c !== undefined && typeof c === 'object' && !Array.isArray(c)) {
|
|
377
|
+
const cont = /** @type {Record<string, unknown>} */ (c)
|
|
378
|
+
if (cont.imagePullPolicy !== 'Always') {
|
|
379
|
+
return `контейнер "${label}": imagePullPolicy має бути Always (див. k8s.mdc)`
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return null
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* У маніфестах ресурсів не має бути **metadata.namespace** — лише у **kustomization** (k8s.mdc).
|
|
389
|
+
* @param {unknown} manifest корінь YAML-документа
|
|
390
|
+
* @returns {string | null} текст порушення або null, якщо поля немає
|
|
391
|
+
*/
|
|
392
|
+
export function metadataNamespaceForbiddenViolation(manifest) {
|
|
393
|
+
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
394
|
+
return null
|
|
395
|
+
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
396
|
+
const meta = rec.metadata
|
|
397
|
+
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta) && 'namespace' in meta) {
|
|
398
|
+
return 'metadata.namespace заборонено — задай namespace у kustomization.yaml (поле namespace) (див. k8s.mdc)'
|
|
399
|
+
}
|
|
400
|
+
return null
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Чи ім’я файлу — kustomization (дозволяє не застосовувати перевірку metadata.namespace до вмісту).
|
|
405
|
+
* @param {string} baseLower basename у нижньому регістрі
|
|
406
|
+
* @returns {boolean} true для `kustomization.yaml` / `kustomization.yml`
|
|
407
|
+
*/
|
|
408
|
+
function isKustomizationFileName(baseLower) {
|
|
409
|
+
return baseLower === 'kustomization.yaml' || baseLower === 'kustomization.yml'
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Парсить усі YAML-документи: **metadata.namespace**, **Deployment.resources**, **imagePullPolicy**.
|
|
414
|
+
* @param {string} rel відносний шлях
|
|
415
|
+
* @param {string} baseLower basename файлу (нижній регістр)
|
|
251
416
|
* @param {string} body вміст після modeline
|
|
252
417
|
* @param {(msg: string) => void} fail реєстрація помилки
|
|
253
418
|
*/
|
|
254
|
-
function
|
|
419
|
+
function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail) {
|
|
255
420
|
/** @type {import('yaml').Document[]} */
|
|
256
421
|
let docs
|
|
257
422
|
try {
|
|
258
423
|
docs = parseAllDocuments(body)
|
|
259
424
|
} catch (error) {
|
|
260
425
|
const msg = error instanceof Error ? error.message : String(error)
|
|
261
|
-
fail(
|
|
262
|
-
`${rel}: не вдалося розібрати YAML для перевірки Deployment.spec.template.spec.containers[].resources (${msg})`
|
|
263
|
-
)
|
|
426
|
+
fail(`${rel}: не вдалося розібрати YAML для перевірок маніфестів (${msg})`)
|
|
264
427
|
return
|
|
265
428
|
}
|
|
266
429
|
|
|
430
|
+
const skipMetaNs = isKustomizationFileName(baseLower)
|
|
431
|
+
|
|
267
432
|
for (const [di, doc] of docs.entries()) {
|
|
268
433
|
if (doc.errors.length > 0) {
|
|
269
434
|
fail(`${rel}: YAML (документ ${di + 1}): ${doc.errors.map(e => e.message).join('; ')}`)
|
|
270
435
|
} else {
|
|
271
436
|
const obj = doc.toJSON()
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
437
|
+
if (!skipMetaNs) {
|
|
438
|
+
const ns = metadataNamespaceForbiddenViolation(obj)
|
|
439
|
+
if (ns !== null) {
|
|
440
|
+
fail(`${rel}: документ ${di + 1}: ${ns}`)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const resV = deploymentResourcesViolation(obj)
|
|
444
|
+
if (resV !== null) {
|
|
445
|
+
fail(`${rel}: Deployment (документ ${di + 1}): ${resV}`)
|
|
446
|
+
}
|
|
447
|
+
const pullV = deploymentImagePullPolicyViolation(obj)
|
|
448
|
+
if (pullV !== null) {
|
|
449
|
+
fail(`${rel}: Deployment (документ ${di + 1}): ${pullV}`)
|
|
275
450
|
}
|
|
276
451
|
}
|
|
277
452
|
}
|
|
@@ -358,9 +533,10 @@ function countSchemaModelines(lines) {
|
|
|
358
533
|
* @param {string} root корінь репозиторію
|
|
359
534
|
* @param {(msg: string) => void} fail реєстрація помилки
|
|
360
535
|
* @param {(msg: string) => void} pass реєстрація успіху
|
|
536
|
+
* @param {string[]} healthCheckPolicyFiles накопичувач файлів із kind: HealthCheckPolicy
|
|
361
537
|
* @returns {Promise<void>}
|
|
362
538
|
*/
|
|
363
|
-
async function checkK8sYamlFile(abs, root, fail, pass) {
|
|
539
|
+
async function checkK8sYamlFile(abs, root, fail, pass, healthCheckPolicyFiles) {
|
|
364
540
|
const rel = relative(root, abs) || abs
|
|
365
541
|
const base = basename(abs)
|
|
366
542
|
const baseLower = base.toLowerCase()
|
|
@@ -398,6 +574,8 @@ async function checkK8sYamlFile(abs, root, fail, pass) {
|
|
|
398
574
|
|
|
399
575
|
const body = yamlBodyAfterModeline(lines)
|
|
400
576
|
|
|
577
|
+
scanIngressAndHealthCheckPolicy(rel, body, fail, healthCheckPolicyFiles)
|
|
578
|
+
|
|
401
579
|
if (schemaUrl.startsWith('file:')) {
|
|
402
580
|
pass(`${rel}: локальна схема (file:) — перевірка URL за apiVersion/kind пропущена`)
|
|
403
581
|
} else if (/^https:/iu.test(schemaUrl)) {
|
|
@@ -420,7 +598,7 @@ async function checkK8sYamlFile(abs, root, fail, pass) {
|
|
|
420
598
|
return
|
|
421
599
|
}
|
|
422
600
|
|
|
423
|
-
|
|
601
|
+
validateK8sYamlPolicyDocuments(rel, baseLower, body, fail)
|
|
424
602
|
}
|
|
425
603
|
|
|
426
604
|
/**
|
|
@@ -444,9 +622,14 @@ export async function check() {
|
|
|
444
622
|
|
|
445
623
|
pass(`YAML у k8s: ${yamlFiles.length} файл(ів)`)
|
|
446
624
|
|
|
625
|
+
/** @type {string[]} */
|
|
626
|
+
const healthCheckPolicyFiles = []
|
|
627
|
+
|
|
447
628
|
for (const abs of yamlFiles) {
|
|
448
|
-
await checkK8sYamlFile(abs, root, fail, pass)
|
|
629
|
+
await checkK8sYamlFile(abs, root, fail, pass, healthCheckPolicyFiles)
|
|
449
630
|
}
|
|
450
631
|
|
|
632
|
+
await ensureRuKustomizationHealthCheckDelete(root, yamlFiles, healthCheckPolicyFiles, fail)
|
|
633
|
+
|
|
451
634
|
return exitCode
|
|
452
635
|
}
|