@nitra/cursor 1.8.19 → 1.8.24
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 +15 -0
- package/mdc/k8s.mdc +79 -4
- package/package.json +1 -1
- package/scripts/check-k8s.mjs +295 -14
package/README.md
CHANGED
|
@@ -27,9 +27,24 @@
|
|
|
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
|
+
- **Namespace** задається в **`kustomization.yaml`** (`namespace:`), а не через **`metadata.namespace`** у кожному ресурсі; окремі patches лише на зміну **namespace** не потрібні.
|
|
42
|
+
- У **Deployment** для кожного контейнера: **`resources`**, **`imagePullPolicy: Always`** (перевіряє **`npx @nitra/cursor check k8s`**).
|
|
43
|
+
- Рядки в **base**, які змінюються в overlays, позначайте коментарем на рядку (узгоджено в команді), наприклад: `# буде замінено через kustomize`.
|
|
44
|
+
- Після перенесення в **`base`** / overlays **видаляйте** застарілі маніфести та каталоги, які більше не потрібні.
|
|
45
|
+
|
|
46
|
+
Повний текст правил — у **`k8s.mdc`**; programmatic перевірки — у **`npm/scripts/check-k8s.mjs`** (у встановленому пакеті — `scripts/check-k8s.mjs`).
|
|
47
|
+
|
|
33
48
|
### v8r і власний каталог схем
|
|
34
49
|
|
|
35
50
|
Скрипт `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/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.18'
|
|
4
4
|
globs: "**/k8s/**/*.{yaml,yml}"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -125,9 +125,77 @@ 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
|
+
|
|
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
|
+
### Міграція зі старої структури
|
|
162
|
+
|
|
163
|
+
- Після перенесення маніфестів у **`base`** та overlays і перевірки (**`check k8s`**, **`lint-k8s`**) **видали** застарілі файли та директорії, які замінені новою схемою (дубльовані копії, колишні шляхи без Kustomize), щоб у репозиторії не залишалося зайвих або суперечливих маніфестів.
|
|
164
|
+
|
|
165
|
+
## Ingress → Gateway API (GKE)
|
|
166
|
+
|
|
167
|
+
Якщо в дереві **`k8s`** трапляється маніфест з **`kind: Ingress`**, його потрібно **замінити на Gateway API**, а не залишати Ingress.
|
|
168
|
+
|
|
169
|
+
1. **HTTPRoute** — окремий файл **`hr.yaml`** (або узгоджене ім’я в команді), **`kind: HTTPRoute`**, `apiVersion` з групи **`gateway.networking.k8s.io`** (див. приклад `$schema` для HTTPRoute у розділі «Визначення схеми YAML»).
|
|
170
|
+
2. **HealthCheckPolicy (GKE)** — окремий файл **`hc.yaml`**, наприклад:
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
apiVersion: networking.gke.io/v1
|
|
174
|
+
kind: HealthCheckPolicy
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Для `$schema` у першому рядку див. приклад **HealthCheckPolicy** у тому ж розділі (datree CRDs-catalog).
|
|
178
|
+
|
|
179
|
+
3. **Overlay `ru`:** у **`ru/kustomization.yaml`** (шлях з сегментами **`…/ru/kustomization.yaml`** під **`k8s`**) додай **видалення** ресурсу HealthCheckPolicy для середовища, де політика не потрібна (підстав **реальне ім’я** замість `SERVICE_NAME`):
|
|
180
|
+
|
|
181
|
+
```yaml
|
|
182
|
+
patches:
|
|
183
|
+
- target:
|
|
184
|
+
kind: HealthCheckPolicy
|
|
185
|
+
patch: |-
|
|
186
|
+
kind: HealthCheckPolicy
|
|
187
|
+
metadata:
|
|
188
|
+
name: SERVICE_NAME
|
|
189
|
+
$patch: delete
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
За потреби розшир **`target`** (`name`, `namespace`), щоб однозначно вказати об’єкт.
|
|
193
|
+
|
|
194
|
+
**`check k8s`:** заборонено **`kind: Ingress`**; якщо в проєкті є **`kind: HealthCheckPolicy`**, має існувати хоча б один **`ru/kustomization.yaml`** під **`k8s`** із блоком видалення (**`$patch: delete`** для **HealthCheckPolicy**).
|
|
195
|
+
|
|
128
196
|
## Перевірка
|
|
129
197
|
|
|
130
|
-
**`npx @nitra/cursor check k8s`** — узгодженість першого рядка (`$schema`), один рядок `# yaml-language-server` на файл, правило для `.yml`, відповідність URL першому YAML-документу (до `---`) за логікою нижче; для кожного документа **`Deployment`**
|
|
198
|
+
**`npx @nitra/cursor check k8s`** — узгодженість першого рядка (`$schema`), один рядок `# yaml-language-server` на файл, правило для `.yml`, відповідність URL першому YAML-документу (до `---`) за логікою нижче; для кожного документа **`Deployment`** — **`containers[].resources`**, **`imagePullPolicy: Always`**; заборона **`kind: Ingress`**; наявність **`HealthCheckPolicy`** — вимога до **`ru/kustomization.yaml`** з **`$patch: delete`** (див. **Ingress → Gateway API**); заборона шляхів **`…/k8s/dev/…`**; якщо існує **`k8s/base/kustomization.yaml`** (або **`.yml`**) — непорожній **`namespace`** у першому документі. Якщо під `k8s` немає yaml/yml — перевірку пропущено. Інший зміст маніфесту — вручну / **`lint-k8s`**.
|
|
131
199
|
|
|
132
200
|
Після змін у маніфестах: **`bun run lint-k8s`** (kubeconform + kubescape; обов'язково для проєктів з правилом **`k8s`** у **`.n-cursor.json`**).
|
|
133
201
|
|
|
@@ -137,13 +205,20 @@ resources: {}
|
|
|
137
205
|
|
|
138
206
|
- Обхід з пропуском `node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next`.
|
|
139
207
|
- **`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`** зі значенням-об'єктом (допускається порожній **`{}`**)
|
|
208
|
+
- У кожному YAML-документі з **`kind: Deployment`** — парсинг через **`yaml`**: у кожного елемента **`spec.template.spec.containers[]`** має існувати ключ **`resources`** зі значенням-об'єктом (допускається порожній **`{}`**); у кожного контейнера — **`imagePullPolicy: Always`**.
|
|
209
|
+
- У файлах, ім’я яких **не** `kustomization.yaml` / `kustomization.yml`, у кожному документі — заборона поля **`metadata.namespace`** (**namespace** задається в Kustomize).
|
|
210
|
+
- Документи з **`kind: Ingress`** — заборонені (потрібен перехід на Gateway API, див. розділ **Ingress → Gateway API**).
|
|
211
|
+
- Якщо в будь-якому файлі під **`k8s`** є **`kind: HealthCheckPolicy`**, серед файлів має бути **`ru/kustomization.yaml`** (сегмент шляху **`ru`** перед іменем файлу), а його вміст — patch видалення **HealthCheckPolicy** з **`$patch: delete`** (див. той самий розділ).
|
|
212
|
+
- Заборона шляхів **`…/k8s/dev/…`** (окремої директорії **`dev`** під **`k8s`** не має бути).
|
|
213
|
+
- Якщо існує **`k8s/base/kustomization.yaml`** (або **`.yml`**), у першому документі має бути непорожнє поле **`namespace`** (типово **`dev`**; див. розділ **Namespace**).
|
|
141
214
|
|
|
142
215
|
## Коли застосовувати (агентам)
|
|
143
216
|
|
|
144
217
|
- Зміни в k8s YAML — після правок **`check k8s`**.
|
|
145
218
|
- Якщо перший рядок уже коректний і URL відповідає `apiVersion` / `kind` — не дублюй; змінився ресурс — онови лише `$schema`.
|
|
146
|
-
- У **`Deployment`** без поля **`resources`** у контейнері — додай **`resources: {}`** (див. розділ **Deployment: `resources`**)
|
|
219
|
+
- У **`Deployment`** без поля **`resources`** у контейнері — додай **`resources: {}`** (див. розділ **Deployment: `resources`**); додай **`imagePullPolicy: Always`** для кожного контейнера.
|
|
220
|
+
- Дотримуйся структури **Kustomize** (`base` = dev, overlays без дублювання `base`, коментарі для рядків, що змінюються в overlay).
|
|
221
|
+
- Після міграції на нову структуру **видали** застарілі файли та каталоги, які вже замінені (див. **Міграція зі старої структури**).
|
|
147
222
|
|
|
148
223
|
## Визначення схеми YAML (канон)
|
|
149
224
|
|
package/package.json
CHANGED
package/scripts/check-k8s.mjs
CHANGED
|
@@ -7,7 +7,16 @@
|
|
|
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`).
|
|
17
|
+
*
|
|
18
|
+
* Структура **Kustomize** (див. k8s.mdc): заборона шляхів **`…/k8s/dev/…`**; якщо є **`…/k8s/base/kustomization.yaml`**
|
|
19
|
+
* (або **`.yml`**), у першому документі має бути непорожнє поле **`namespace`**.
|
|
11
20
|
*
|
|
12
21
|
* Явні винятки до загальної логіки yannh/datree — таблиця **`EXPLICIT_K8S_SCHEMAS`** (`Map`): ключ
|
|
13
22
|
* **`apiVersion`, `kind`, `type`** (для CRD без поля `type` у маніфесті — зірочка **`*`** як третій
|
|
@@ -144,6 +153,43 @@ export function pathHasK8sSegment(filePath) {
|
|
|
144
153
|
return parts.includes('k8s')
|
|
145
154
|
}
|
|
146
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Чи заборонений шлях з окремою директорією **`dev`** під **`k8s`** (джерело правди — **`base`**).
|
|
158
|
+
* @param {string} rel шлях від кореня репозиторію
|
|
159
|
+
* @returns {boolean} true для `…/k8s/dev/…`
|
|
160
|
+
*/
|
|
161
|
+
export function isForbiddenK8sDevPath(rel) {
|
|
162
|
+
const n = rel.replaceAll('\\', '/')
|
|
163
|
+
return n.includes('/k8s/dev/')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Чи це **`k8s/base/kustomization.yaml`** або **`kustomization.yml`** (перевірка поля **`namespace`**).
|
|
168
|
+
* @param {string} rel шлях від кореня репозиторію
|
|
169
|
+
* @returns {boolean} true, якщо це `…/k8s/base/kustomization.yaml` або `…/k8s/base/kustomization.yml`
|
|
170
|
+
*/
|
|
171
|
+
export function isBaseKustomizationPath(rel) {
|
|
172
|
+
const n = rel.replaceAll('\\', '/')
|
|
173
|
+
return /(^|\/)k8s\/base\/kustomization\.yaml$/u.test(n) || /(^|\/)k8s\/base\/kustomization\.yml$/u.test(n)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Чи коректне поле **`namespace`** у розібраному Kustomization для **`base`**.
|
|
178
|
+
* @param {unknown} obj перший документ YAML
|
|
179
|
+
* @returns {string | null} текст порушення або null, якщо ок
|
|
180
|
+
*/
|
|
181
|
+
export function baseKustomizationNamespaceViolation(obj) {
|
|
182
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
183
|
+
return 'у base/kustomization.yaml має бути непорожній namespace (див. k8s.mdc)'
|
|
184
|
+
}
|
|
185
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
186
|
+
const ns = rec.namespace
|
|
187
|
+
if (typeof ns === 'string' && ns.trim() !== '') {
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
return 'у base/kustomization.yaml має бути непорожній namespace (наприклад namespace: dev; див. k8s.mdc)'
|
|
191
|
+
}
|
|
192
|
+
|
|
147
193
|
/**
|
|
148
194
|
* Збирає всі yaml/yml під деревом від кореня cwd, якщо шлях містить сегмент `k8s`.
|
|
149
195
|
* @param {string} root корінь репозиторію (cwd)
|
|
@@ -157,7 +203,8 @@ async function findK8sYamlFiles(root) {
|
|
|
157
203
|
if (!/\.ya?ml$/iu.test(p)) return
|
|
158
204
|
out.push(p)
|
|
159
205
|
})
|
|
160
|
-
|
|
206
|
+
// eslint-disable-next-line unicorn/no-array-sort -- toSorted потребує lib ES2023 у перевірці типів IDE
|
|
207
|
+
return [...out].sort((a, b) => a.localeCompare(b))
|
|
161
208
|
}
|
|
162
209
|
|
|
163
210
|
/**
|
|
@@ -205,6 +252,103 @@ function extractApiVersionAndKind(doc) {
|
|
|
205
252
|
}
|
|
206
253
|
}
|
|
207
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Чи відносний шлях вказує на **`ru/kustomization.yaml`** (сегмент **`ru`** перед ім’ям файлу).
|
|
257
|
+
* @param {string} rel шлях від кореня репозиторію
|
|
258
|
+
* @returns {boolean} true, якщо це `…/ru/kustomization.yaml`
|
|
259
|
+
*/
|
|
260
|
+
export function isRuKustomizationPath(rel) {
|
|
261
|
+
const norm = rel.replaceAll('\\', '/')
|
|
262
|
+
return /(^|\/)ru\/kustomization\.yaml$/u.test(norm)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Чи вміст overlay **`ru/kustomization.yaml`** містить Kustomize patch видалення **HealthCheckPolicy**.
|
|
267
|
+
* @param {string} raw повний текст файлу
|
|
268
|
+
* @returns {boolean} true, якщо є `$patch: delete` і блоки kind/metadata для HealthCheckPolicy
|
|
269
|
+
*/
|
|
270
|
+
export function ruKustomizationHasHealthCheckDeletePatch(raw) {
|
|
271
|
+
if (!/\$patch:\s*delete/u.test(raw)) return false
|
|
272
|
+
if (!/kind:\s*HealthCheckPolicy/u.test(raw)) return false
|
|
273
|
+
if (!/metadata:/u.test(raw)) return false
|
|
274
|
+
if (!/name:\s*\S+/u.test(raw)) return false
|
|
275
|
+
return true
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Шукає **Ingress** / **HealthCheckPolicy** у розібраних документах; реєструє порушення для Ingress.
|
|
280
|
+
* @param {string} rel відносний шлях до файлу
|
|
281
|
+
* @param {string} body YAML після modeline
|
|
282
|
+
* @param {(msg: string) => void} fail callback для помилки (Ingress)
|
|
283
|
+
* @param {string[]} healthCheckPolicyFiles накопичувач шляхів, де зустріли HealthCheckPolicy
|
|
284
|
+
* @returns {void}
|
|
285
|
+
*/
|
|
286
|
+
function scanIngressAndHealthCheckPolicy(rel, body, fail, healthCheckPolicyFiles) {
|
|
287
|
+
/** @type {import('yaml').Document[]} */
|
|
288
|
+
let docs
|
|
289
|
+
try {
|
|
290
|
+
docs = parseAllDocuments(body)
|
|
291
|
+
} catch {
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (const [di, doc] of docs.entries()) {
|
|
296
|
+
if (doc.errors.length === 0) {
|
|
297
|
+
const obj = doc.toJSON()
|
|
298
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
299
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
300
|
+
if (rec.kind === 'Ingress') {
|
|
301
|
+
fail(
|
|
302
|
+
`${rel}: знайдено kind: Ingress (документ ${di + 1}) — заміни на Gateway API: HTTPRoute (hr.yaml), HealthCheckPolicy (hc.yaml), patch у ru/kustomization.yaml (див. k8s.mdc)`
|
|
303
|
+
)
|
|
304
|
+
} else if (rec.kind === 'HealthCheckPolicy' && !healthCheckPolicyFiles.includes(rel)) {
|
|
305
|
+
healthCheckPolicyFiles.push(rel)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Якщо у дереві k8s є HealthCheckPolicy, вимагає **ru/kustomization.yaml** з patch видалення.
|
|
314
|
+
* @param {string} root корінь cwd
|
|
315
|
+
* @param {string[]} yamlFiles абсолютні шляхи до yaml під k8s
|
|
316
|
+
* @param {string[]} healthCheckPolicyFiles відносні шляхи з HealthCheckPolicy
|
|
317
|
+
* @param {(msg: string) => void} fail callback для помилки (немає ru або немає patch)
|
|
318
|
+
* @returns {Promise<void>} завершення після перевірки overlay ru
|
|
319
|
+
*/
|
|
320
|
+
async function ensureRuKustomizationHealthCheckDelete(root, yamlFiles, healthCheckPolicyFiles, fail) {
|
|
321
|
+
if (healthCheckPolicyFiles.length === 0) {
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const ruAbsList = yamlFiles.filter(abs => isRuKustomizationPath(relative(root, abs) || abs))
|
|
326
|
+
if (ruAbsList.length === 0) {
|
|
327
|
+
fail(
|
|
328
|
+
`Знайдено HealthCheckPolicy у ${healthCheckPolicyFiles.join(', ')} — додай ru/kustomization.yaml з patch видалення (див. k8s.mdc)`
|
|
329
|
+
)
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
for (const abs of ruAbsList) {
|
|
334
|
+
let raw
|
|
335
|
+
try {
|
|
336
|
+
raw = await readFile(abs, 'utf8')
|
|
337
|
+
} catch (error) {
|
|
338
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
339
|
+
fail(`${relative(root, abs) || abs}: не вдалося прочитати (${msg})`)
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
if (ruKustomizationHasHealthCheckDeletePatch(raw)) {
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
fail(
|
|
348
|
+
'Є HealthCheckPolicy, але жоден ru/kustomization.yaml не містить очікуваного patch видалення (kind: HealthCheckPolicy, metadata.name, $patch: delete) — див. k8s.mdc'
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
|
|
208
352
|
/**
|
|
209
353
|
* Чи порушує маніфест вимогу **`Deployment.spec.template.spec.containers[].resources`** (див. k8s.mdc).
|
|
210
354
|
* @param {unknown} manifest корінь YAML-документа як об'єкт JavaScript
|
|
@@ -246,32 +390,104 @@ export function deploymentResourcesViolation(manifest) {
|
|
|
246
390
|
}
|
|
247
391
|
|
|
248
392
|
/**
|
|
249
|
-
*
|
|
250
|
-
* @param {
|
|
393
|
+
* Чи контейнери **Deployment** мають **`imagePullPolicy: Always`** (k8s.mdc).
|
|
394
|
+
* @param {unknown} manifest корінь YAML-документа
|
|
395
|
+
* @returns {string | null} текст порушення або null, якщо не Deployment / ок
|
|
396
|
+
*/
|
|
397
|
+
export function deploymentImagePullPolicyViolation(manifest) {
|
|
398
|
+
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
399
|
+
return null
|
|
400
|
+
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
401
|
+
if (rec.kind !== 'Deployment') return null
|
|
402
|
+
const spec = rec.spec
|
|
403
|
+
if (spec === null || spec === undefined || typeof spec !== 'object' || Array.isArray(spec)) return null
|
|
404
|
+
const template = /** @type {Record<string, unknown>} */ (spec).template
|
|
405
|
+
if (template === null || template === undefined || typeof template !== 'object' || Array.isArray(template))
|
|
406
|
+
return null
|
|
407
|
+
const podSpec = /** @type {Record<string, unknown>} */ (template).spec
|
|
408
|
+
if (podSpec === null || podSpec === undefined || typeof podSpec !== 'object' || Array.isArray(podSpec)) return null
|
|
409
|
+
const containers = /** @type {Record<string, unknown>} */ (podSpec).containers
|
|
410
|
+
if (!Array.isArray(containers)) return null
|
|
411
|
+
|
|
412
|
+
for (const [i, c] of containers.entries()) {
|
|
413
|
+
const label =
|
|
414
|
+
typeof c === 'object' && c !== null && !Array.isArray(c) && typeof c.name === 'string' && c.name !== ''
|
|
415
|
+
? c.name
|
|
416
|
+
: `#${i + 1}`
|
|
417
|
+
if (c !== null && c !== undefined && typeof c === 'object' && !Array.isArray(c)) {
|
|
418
|
+
const cont = /** @type {Record<string, unknown>} */ (c)
|
|
419
|
+
if (cont.imagePullPolicy !== 'Always') {
|
|
420
|
+
return `контейнер "${label}": imagePullPolicy має бути Always (див. k8s.mdc)`
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return null
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* У маніфестах ресурсів не має бути **metadata.namespace** — лише у **kustomization** (k8s.mdc).
|
|
430
|
+
* @param {unknown} manifest корінь YAML-документа
|
|
431
|
+
* @returns {string | null} текст порушення або null, якщо поля немає
|
|
432
|
+
*/
|
|
433
|
+
export function metadataNamespaceForbiddenViolation(manifest) {
|
|
434
|
+
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
435
|
+
return null
|
|
436
|
+
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
437
|
+
const meta = rec.metadata
|
|
438
|
+
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta) && 'namespace' in meta) {
|
|
439
|
+
return 'metadata.namespace заборонено — задай namespace у kustomization.yaml (поле namespace) (див. k8s.mdc)'
|
|
440
|
+
}
|
|
441
|
+
return null
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Чи ім’я файлу — kustomization (дозволяє не застосовувати перевірку metadata.namespace до вмісту).
|
|
446
|
+
* @param {string} baseLower basename у нижньому регістрі
|
|
447
|
+
* @returns {boolean} true для `kustomization.yaml` / `kustomization.yml`
|
|
448
|
+
*/
|
|
449
|
+
function isKustomizationFileName(baseLower) {
|
|
450
|
+
return baseLower === 'kustomization.yaml' || baseLower === 'kustomization.yml'
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Парсить усі YAML-документи: **metadata.namespace**, **Deployment.resources**, **imagePullPolicy**.
|
|
455
|
+
* @param {string} rel відносний шлях
|
|
456
|
+
* @param {string} baseLower basename файлу (нижній регістр)
|
|
251
457
|
* @param {string} body вміст після modeline
|
|
252
458
|
* @param {(msg: string) => void} fail реєстрація помилки
|
|
253
459
|
*/
|
|
254
|
-
function
|
|
460
|
+
function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail) {
|
|
255
461
|
/** @type {import('yaml').Document[]} */
|
|
256
462
|
let docs
|
|
257
463
|
try {
|
|
258
464
|
docs = parseAllDocuments(body)
|
|
259
465
|
} catch (error) {
|
|
260
466
|
const msg = error instanceof Error ? error.message : String(error)
|
|
261
|
-
fail(
|
|
262
|
-
`${rel}: не вдалося розібрати YAML для перевірки Deployment.spec.template.spec.containers[].resources (${msg})`
|
|
263
|
-
)
|
|
467
|
+
fail(`${rel}: не вдалося розібрати YAML для перевірок маніфестів (${msg})`)
|
|
264
468
|
return
|
|
265
469
|
}
|
|
266
470
|
|
|
471
|
+
const skipMetaNs = isKustomizationFileName(baseLower)
|
|
472
|
+
|
|
267
473
|
for (const [di, doc] of docs.entries()) {
|
|
268
474
|
if (doc.errors.length > 0) {
|
|
269
475
|
fail(`${rel}: YAML (документ ${di + 1}): ${doc.errors.map(e => e.message).join('; ')}`)
|
|
270
476
|
} else {
|
|
271
477
|
const obj = doc.toJSON()
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
478
|
+
if (!skipMetaNs) {
|
|
479
|
+
const ns = metadataNamespaceForbiddenViolation(obj)
|
|
480
|
+
if (ns !== null) {
|
|
481
|
+
fail(`${rel}: документ ${di + 1}: ${ns}`)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const resV = deploymentResourcesViolation(obj)
|
|
485
|
+
if (resV !== null) {
|
|
486
|
+
fail(`${rel}: Deployment (документ ${di + 1}): ${resV}`)
|
|
487
|
+
}
|
|
488
|
+
const pullV = deploymentImagePullPolicyViolation(obj)
|
|
489
|
+
if (pullV !== null) {
|
|
490
|
+
fail(`${rel}: Deployment (документ ${di + 1}): ${pullV}`)
|
|
275
491
|
}
|
|
276
492
|
}
|
|
277
493
|
}
|
|
@@ -358,9 +574,10 @@ function countSchemaModelines(lines) {
|
|
|
358
574
|
* @param {string} root корінь репозиторію
|
|
359
575
|
* @param {(msg: string) => void} fail реєстрація помилки
|
|
360
576
|
* @param {(msg: string) => void} pass реєстрація успіху
|
|
577
|
+
* @param {string[]} healthCheckPolicyFiles накопичувач файлів із kind: HealthCheckPolicy
|
|
361
578
|
* @returns {Promise<void>}
|
|
362
579
|
*/
|
|
363
|
-
async function checkK8sYamlFile(abs, root, fail, pass) {
|
|
580
|
+
async function checkK8sYamlFile(abs, root, fail, pass, healthCheckPolicyFiles) {
|
|
364
581
|
const rel = relative(root, abs) || abs
|
|
365
582
|
const base = basename(abs)
|
|
366
583
|
const baseLower = base.toLowerCase()
|
|
@@ -398,6 +615,8 @@ async function checkK8sYamlFile(abs, root, fail, pass) {
|
|
|
398
615
|
|
|
399
616
|
const body = yamlBodyAfterModeline(lines)
|
|
400
617
|
|
|
618
|
+
scanIngressAndHealthCheckPolicy(rel, body, fail, healthCheckPolicyFiles)
|
|
619
|
+
|
|
401
620
|
if (schemaUrl.startsWith('file:')) {
|
|
402
621
|
pass(`${rel}: локальна схема (file:) — перевірка URL за apiVersion/kind пропущена`)
|
|
403
622
|
} else if (/^https:/iu.test(schemaUrl)) {
|
|
@@ -420,7 +639,60 @@ async function checkK8sYamlFile(abs, root, fail, pass) {
|
|
|
420
639
|
return
|
|
421
640
|
}
|
|
422
641
|
|
|
423
|
-
|
|
642
|
+
validateK8sYamlPolicyDocuments(rel, baseLower, body, fail)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Реєструє порушення для шляхів виду **`…/k8s/dev/…`** (окремої директорії **dev** не має бути).
|
|
647
|
+
* @param {string[]} yamlFiles абсолютні шляхи
|
|
648
|
+
* @param {string} root корінь репозиторію
|
|
649
|
+
* @param {(msg: string) => void} fail callback для реєстрації порушення
|
|
650
|
+
* @returns {void}
|
|
651
|
+
*/
|
|
652
|
+
function assertNoForbiddenK8sDevPaths(yamlFiles, root, fail) {
|
|
653
|
+
for (const abs of yamlFiles) {
|
|
654
|
+
const rel = relative(root, abs).replaceAll('\\', '/')
|
|
655
|
+
if (isForbiddenK8sDevPath(rel)) {
|
|
656
|
+
fail(`${rel}: заборонена директорія k8s/dev/ — середовище dev відповідає base (див. k8s.mdc)`)
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Якщо є **`k8s/base/kustomization.yaml`**, у ньому має бути непорожній **`namespace`**.
|
|
663
|
+
* @param {string} root корінь репозиторію
|
|
664
|
+
* @param {string[]} yamlFiles абсолютні шляхи
|
|
665
|
+
* @param {(msg: string) => void} fail callback для реєстрації порушення
|
|
666
|
+
* @returns {Promise<void>}
|
|
667
|
+
*/
|
|
668
|
+
async function ensureBaseKustomizationHasNamespace(root, yamlFiles, fail) {
|
|
669
|
+
for (const abs of yamlFiles) {
|
|
670
|
+
const rel = relative(root, abs).replaceAll('\\', '/')
|
|
671
|
+
if (isBaseKustomizationPath(rel)) {
|
|
672
|
+
try {
|
|
673
|
+
const raw = await readFile(abs, 'utf8')
|
|
674
|
+
const lines = toLines(raw)
|
|
675
|
+
const body = yamlBodyAfterModeline(lines)
|
|
676
|
+
/** @type {import('yaml').Document[] | undefined} */
|
|
677
|
+
let docs
|
|
678
|
+
try {
|
|
679
|
+
docs = parseAllDocuments(body)
|
|
680
|
+
} catch {
|
|
681
|
+
fail(`${rel}: не вдалося розпарсити YAML для перевірки namespace у base (див. k8s.mdc)`)
|
|
682
|
+
}
|
|
683
|
+
if (docs !== undefined) {
|
|
684
|
+
const first = docs[0]?.toJSON()
|
|
685
|
+
const v = baseKustomizationNamespaceViolation(first)
|
|
686
|
+
if (v) {
|
|
687
|
+
fail(`${rel}: ${v}`)
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
} catch (error) {
|
|
691
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
692
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
424
696
|
}
|
|
425
697
|
|
|
426
698
|
/**
|
|
@@ -444,9 +716,18 @@ export async function check() {
|
|
|
444
716
|
|
|
445
717
|
pass(`YAML у k8s: ${yamlFiles.length} файл(ів)`)
|
|
446
718
|
|
|
719
|
+
assertNoForbiddenK8sDevPaths(yamlFiles, root, fail)
|
|
720
|
+
|
|
721
|
+
/** @type {string[]} */
|
|
722
|
+
const healthCheckPolicyFiles = []
|
|
723
|
+
|
|
447
724
|
for (const abs of yamlFiles) {
|
|
448
|
-
await checkK8sYamlFile(abs, root, fail, pass)
|
|
725
|
+
await checkK8sYamlFile(abs, root, fail, pass, healthCheckPolicyFiles)
|
|
449
726
|
}
|
|
450
727
|
|
|
728
|
+
await ensureRuKustomizationHealthCheckDelete(root, yamlFiles, healthCheckPolicyFiles, fail)
|
|
729
|
+
|
|
730
|
+
await ensureBaseKustomizationHasNamespace(root, yamlFiles, fail)
|
|
731
|
+
|
|
451
732
|
return exitCode
|
|
452
733
|
}
|