@nitra/cursor 1.8.73 → 1.8.75
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/mdc/abie.mdc +20 -16
- package/mdc/k8s.mdc +2 -16
- package/package.json +1 -1
- package/scripts/check-abie.mjs +33 -38
- package/scripts/check-k8s.mjs +223 -0
- package/scripts/utils/check-reporter.mjs +1 -1
package/mdc/abie.mdc
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Правила для проєктів AbInBev Efes
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.7'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
Правило **abie** для споживачів **@nitra/cursor**: **k8s** (
|
|
7
|
+
Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** / **ru** — **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**), видалення **HealthCheckPolicy** у **ru**), а також гілки **dev**, **ua**, **ru** у **clean-merged-branch**.
|
|
8
|
+
|
|
9
|
+
**`npx @nitra/cursor check abie`** виконується лише якщо в **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень.
|
|
10
|
+
|
|
11
|
+
**Канон перевірки** — **`npm/scripts/check-abie.mjs`**: верхній JSDoc і реалізація задають точні умови, допустимі домени для hostnames, тексти помилок. Нижче — зміст правила й орієнтовні фрагменти YAML; не дублюй тут покроковий алгоритм зі скрипта.
|
|
8
12
|
|
|
9
13
|
## k8s: `hc.yaml` поруч із Deployment
|
|
10
14
|
|
|
11
|
-
Якщо під **`k8s`** є
|
|
15
|
+
Якщо під **`k8s`** є **Deployment**, у **тій самій директорії** має бути **`hc.yaml`** з **HealthCheckPolicy** (**`networking.gke.io/v1`**): коректний modeline **`$schema`**, **`/healthz`**, порт **8080**, **`targetRef.name`** = **`metadata.name`**.
|
|
12
16
|
|
|
13
17
|
```yaml title="hc.yaml"
|
|
14
18
|
# yaml-language-server: $schema=https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json
|
|
@@ -30,19 +34,19 @@ spec:
|
|
|
30
34
|
name: СЕРВІС
|
|
31
35
|
```
|
|
32
36
|
|
|
33
|
-
## k8s: overlay **HTTPRoute**
|
|
37
|
+
## k8s: overlay **HTTPRoute** (**ua** / **ru**)
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
За наявності **Deployment** під **k8s** у **кожному** **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`** потрібні **inline JSON6902** у **`patches`**: **target** **`kind: HTTPRoute`**, **непорожній `name`** (як у маніфесті маршруту). Мають бути зміни **`/spec/hostnames`** (домени abie — у скрипті) та **`/spec/parentRefs/0/namespace`** (**`ua`** / **`ru`**). Для **ru** — анотація **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**. Як обирати **`op`** (**add** / **replace** тощо) у patch — **k8s.mdc** (розділ про JSON patch у kustomization).
|
|
36
40
|
|
|
37
41
|
```yaml title="…/ua/kustomization.yaml (фрагмент)"
|
|
38
42
|
- target:
|
|
39
43
|
kind: HTTPRoute
|
|
40
|
-
name:
|
|
44
|
+
name: my-httproute
|
|
41
45
|
patch: |-
|
|
42
46
|
- op: replace
|
|
43
47
|
path: /spec/hostnames
|
|
44
48
|
value:
|
|
45
|
-
- "abie.app" #
|
|
49
|
+
- "abie.app" # зокрема vybeerai.com.ua, *.vybeerai.com.ua, *.abie.app
|
|
46
50
|
- op: replace
|
|
47
51
|
path: /spec/parentRefs/0/namespace
|
|
48
52
|
value: ua
|
|
@@ -51,12 +55,12 @@ spec:
|
|
|
51
55
|
```yaml title="…/ru/kustomization.yaml (фрагмент)"
|
|
52
56
|
- target:
|
|
53
57
|
kind: HTTPRoute
|
|
54
|
-
name:
|
|
58
|
+
name: my-httproute
|
|
55
59
|
patch: |-
|
|
56
60
|
- op: replace
|
|
57
61
|
path: /spec/hostnames
|
|
58
62
|
value:
|
|
59
|
-
- "napitkivmeste.tech" #
|
|
63
|
+
- "napitkivmeste.tech" # зокрема выбирайонлайн.рф, *.napitkivmeste.tech, *.выбирайонлайн.рф
|
|
60
64
|
- op: replace
|
|
61
65
|
path: /spec/parentRefs/0/namespace
|
|
62
66
|
value: ru
|
|
@@ -65,7 +69,7 @@ spec:
|
|
|
65
69
|
```yaml title="…/ru/kustomization.yaml (фрагмент)"
|
|
66
70
|
- target:
|
|
67
71
|
kind: HTTPRoute
|
|
68
|
-
name:
|
|
72
|
+
name: my-httproute
|
|
69
73
|
patch: |-
|
|
70
74
|
- op: add
|
|
71
75
|
path: /metadata/annotations
|
|
@@ -73,9 +77,9 @@ spec:
|
|
|
73
77
|
gwin.yandex.cloud/rules.http.upgradeTypes: "websocket"
|
|
74
78
|
```
|
|
75
79
|
|
|
76
|
-
## k8s: overlay
|
|
80
|
+
## k8s: overlay **ru** і HealthCheckPolicy
|
|
77
81
|
|
|
78
|
-
Якщо в дереві **k8s** є **HealthCheckPolicy**,
|
|
82
|
+
Якщо в дереві **k8s** є **HealthCheckPolicy**, у **`ru/kustomization.yaml`** має бути patch **`$patch: delete`** для політики (узгоджено з **k8s.mdc**; перевірка в **`check-k8s.mjs`**, **`ruKustomizationHasHealthCheckDeletePatch`**). Підстав реальне ім’я замість **`СЕРВІС`**:
|
|
79
83
|
|
|
80
84
|
```yaml title="…/ru/kustomization.yaml (фрагмент)"
|
|
81
85
|
patches:
|
|
@@ -89,9 +93,9 @@ patches:
|
|
|
89
93
|
$patch: delete
|
|
90
94
|
```
|
|
91
95
|
|
|
92
|
-
## k8s: overlay
|
|
96
|
+
## k8s: overlay **ua** / **ru** і nodeSelector
|
|
93
97
|
|
|
94
|
-
|
|
98
|
+
За наявності **Deployment** під **k8s** у **кожному** **`…/ua/kustomization.yaml`** та **`…/ru/kustomization.yaml`** — patch на **`kind: Deployment`**: **ua** — **`spec.template.spec.nodeSelector`** з **`preem: false`**; **ru** — **`spec.template.spec.nodeSelector`** з **`yandex.cloud/preemptible: false`**. Форму **JSON6902** (шлях **`/spec/template/spec/nodeSelector`**, **`op`**) див. **k8s.mdc**.
|
|
95
99
|
|
|
96
100
|
```yaml title="…/ua/kustomization.yaml (фрагмент)"
|
|
97
101
|
patches:
|
|
@@ -119,7 +123,7 @@ patches:
|
|
|
119
123
|
|
|
120
124
|
### Базовий Deployment (`…/base/`)
|
|
121
125
|
|
|
122
|
-
Якщо **Deployment** у YAML під **`k8s`** лежить у шляху з сегментом **`base
|
|
126
|
+
Якщо **Deployment** у YAML під **`k8s`** лежить у шляху з сегментом **`base`**, у **`spec.template.spec.nodeSelector`** має бути **`preem`** зі значенням **істинно** (**`true`** або рядок **`'true'`**); overlay **ua** / **ru** підміняє селектор.
|
|
123
127
|
|
|
124
128
|
```yaml title="…/base/deploy.yaml (фрагмент)"
|
|
125
129
|
spec:
|
|
@@ -131,7 +135,7 @@ spec:
|
|
|
131
135
|
|
|
132
136
|
## Git branches
|
|
133
137
|
|
|
134
|
-
У **`.github/workflows/clean-merged-branch.yml`** у кроці **`phpdocker-io/github-actions-delete-abandoned-branches`** значення **`with.ignore_branches`** має містити **dev**, **ua** та **ru** (разом з іншими гілками, якщо потрібно)
|
|
138
|
+
У **`.github/workflows/clean-merged-branch.yml`** у кроці **`phpdocker-io/github-actions-delete-abandoned-branches`** значення **`with.ignore_branches`** має містити **dev**, **ua** та **ru** (разом з іншими гілками, якщо потрібно):
|
|
135
139
|
|
|
136
140
|
```yaml title=".github/workflows/clean-merged-branch.yml (фрагмент)"
|
|
137
141
|
with:
|
package/mdc/k8s.mdc
CHANGED
|
@@ -206,20 +206,10 @@ patches:
|
|
|
206
206
|
value: dev
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
-
4.
|
|
210
|
-
|
|
211
|
-
```yaml title="overlay/kustomization.yaml (фрагмент)"
|
|
212
|
-
- op: remove
|
|
213
|
-
path: /spec/template/spec/nodeSelector
|
|
214
|
-
- op: add
|
|
215
|
-
path: /spec/template/spec/nodeSelector
|
|
216
|
-
value:
|
|
217
|
-
preem: "false"
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
заміняй на replace:
|
|
209
|
+
4. **JSON patch у kustomization:** де можливо, змінюй ресурс через **`op: replace`** (одна операція на `path`), а не пару **`remove` + `add`** на той самий шлях. **`add`** / **`remove`** лишай лише коли **`replace`** не підходить (наприклад додати новий ключ або прибрати поле без заміни).
|
|
221
210
|
|
|
222
211
|
```yaml title="overlay/kustomization.yaml (фрагмент)"
|
|
212
|
+
patches:
|
|
223
213
|
- target:
|
|
224
214
|
kind: Deployment
|
|
225
215
|
name: x
|
|
@@ -236,11 +226,7 @@ patches:
|
|
|
236
226
|
|
|
237
227
|
**Не входить у check k8s:** наприклад **ReferenceGrant** і доступ між **namespace** (лише рекомендації тут), **kubeconform** / **kubescape** — це **`bun run lint-k8s`**.
|
|
238
228
|
|
|
239
|
-
## Що саме в скрипті `check-k8s.mjs`
|
|
240
|
-
|
|
241
|
-
Повний перелік умов, константи (**`YANNH_PIN`**, **`CLUSTER_SCOPED_KINDS`**, **`HASURA_GRAPHQL_ENGINE_IMAGE`** тощо) і допоміжні функції — у файлі скрипта; змінив вимогу для **check** — онови **JSDoc** і за потреби тести в **`npm/tests/check-k8s-schema.test.mjs`**.
|
|
242
229
|
|
|
243
|
-
При зміні **PIN** версії Kubernetes узгодь **`check-k8s.mjs`**, **`run-k8s.mjs`** (**`KUBERNETES_VERSION`**, **`DATREE_CRD_SCHEMA_LOCATION`**) і цей файл (**lint-k8s**, **Визначення схеми YAML**).
|
|
244
230
|
|
|
245
231
|
## Коли застосовувати (агентам)
|
|
246
232
|
|
package/package.json
CHANGED
package/scripts/check-abie.mjs
CHANGED
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
* **nodeSelector (base):** якщо **Deployment** лежить у шляху з сегментом **`base`** (наприклад **`…/k8s/base/deploy.yaml`**),
|
|
18
18
|
* у **`spec.template.spec.nodeSelector`** має бути **`preem`** з булевим значенням **true** або рядком **`'true'`** — overlay **ua** та **ru** далі підміняють селектор.
|
|
19
19
|
*
|
|
20
|
-
* **nodeSelector (overlay):** якщо є **Deployment** під **k8s**, у
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* **nodeSelector (overlay):** якщо є **Deployment** під **k8s**, у **`ua`/`ru` kustomization** — inline patch на **`kind: Deployment`**
|
|
21
|
+
* з **`path: /spec/template/spec/nodeSelector`**: **ua** — **`preem: false`**; **ru** — **`yandex.cloud/preemptible: false`**.
|
|
22
|
+
* Узагальнені вимоги **k8s.mdc** до JSON6902 (зокрема заборона **remove** + **add** на той самий **path**) перевіряє **check-k8s.mjs**; **check-abie** — лише abie-специфічний вміст (без дублювання цього правила).
|
|
23
23
|
*
|
|
24
|
-
* **HTTPRoute
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* **HTTPRoute (overlay):** тієї ж умови — patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
|
|
25
|
+
* (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** — **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**.
|
|
26
|
+
* Вибір **`op`** — **k8s.mdc**.
|
|
27
27
|
*/
|
|
28
28
|
import { existsSync } from 'node:fs'
|
|
29
29
|
import { readFile } from 'node:fs/promises'
|
|
@@ -343,7 +343,8 @@ function stripBom(s) {
|
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
/**
|
|
346
|
-
* Чи рядок inline JSON6902 patch містить очікуваний **ua** nodeSelector (**
|
|
346
|
+
* Чи рядок inline JSON6902 patch містить очікуваний **ua** nodeSelector (**preem: false** на **`/spec/template/spec/nodeSelector`**).
|
|
347
|
+
* Конкретний **`op`** не перевіряється — див. **k8s.mdc**.
|
|
347
348
|
* @param {string} patchText поле **patch** у kustomization
|
|
348
349
|
* @returns {boolean} true, якщо критерії abie.mdc виконано
|
|
349
350
|
*/
|
|
@@ -351,9 +352,6 @@ function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
|
|
|
351
352
|
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
352
353
|
return false
|
|
353
354
|
}
|
|
354
|
-
if (!/op:\s*add\b/u.test(patchText)) {
|
|
355
|
-
return false
|
|
356
|
-
}
|
|
357
355
|
if (!/path:\s*\/spec\/template\/spec\/nodeSelector\b/u.test(patchText)) {
|
|
358
356
|
return false
|
|
359
357
|
}
|
|
@@ -364,7 +362,8 @@ function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
|
|
|
364
362
|
}
|
|
365
363
|
|
|
366
364
|
/**
|
|
367
|
-
* Чи рядок inline JSON6902 patch містить очікуваний **ru** nodeSelector (**
|
|
365
|
+
* Чи рядок inline JSON6902 patch містить очікуваний **ru** nodeSelector (**yandex.cloud/preemptible: false** на **`/spec/template/spec/nodeSelector`**).
|
|
366
|
+
* Конкретний **`op`** не перевіряється — див. **k8s.mdc**.
|
|
368
367
|
* @param {string} patchText поле **patch** у kustomization
|
|
369
368
|
* @returns {boolean} true, якщо критерії abie.mdc виконано
|
|
370
369
|
*/
|
|
@@ -372,9 +371,6 @@ function jsonPatchTextHasRuDeploymentNodeSelector(patchText) {
|
|
|
372
371
|
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
373
372
|
return false
|
|
374
373
|
}
|
|
375
|
-
if (!/op:\s*replace\b/u.test(patchText)) {
|
|
376
|
-
return false
|
|
377
|
-
}
|
|
378
374
|
if (!/path:\s*\/spec\/template\/spec\/nodeSelector\b/u.test(patchText)) {
|
|
379
375
|
return false
|
|
380
376
|
}
|
|
@@ -484,11 +480,11 @@ const ABIE_RU_HTTPROUTE_HOST_MARKERS = [
|
|
|
484
480
|
]
|
|
485
481
|
|
|
486
482
|
/**
|
|
487
|
-
* Збирає тексти inline **patch** для **HTTPRoute
|
|
483
|
+
* Збирає тексти inline **patch** для **HTTPRoute** (будь-який непорожній **target.name**) з одного документа **Kustomization**.
|
|
488
484
|
* @param {import('yaml').Document} doc документ після **parseAllDocuments**
|
|
489
485
|
* @returns {string[]} непорожні рядки **patch**
|
|
490
486
|
*/
|
|
491
|
-
function
|
|
487
|
+
function collectAbieHttpRoutePatchStringsFromKustomizationDoc(doc) {
|
|
492
488
|
if (doc.errors.length > 0) {
|
|
493
489
|
return []
|
|
494
490
|
}
|
|
@@ -512,7 +508,7 @@ function collectNginxRunPatchStringsFromKustomizationDoc(doc) {
|
|
|
512
508
|
const target = pr.target
|
|
513
509
|
if (target !== null && typeof target === 'object' && !Array.isArray(target)) {
|
|
514
510
|
const tg = /** @type {Record<string, unknown>} */ (target)
|
|
515
|
-
if (tg.kind === 'HTTPRoute' && tg.name === '
|
|
511
|
+
if (tg.kind === 'HTTPRoute' && typeof tg.name === 'string' && tg.name.trim() !== '') {
|
|
516
512
|
const patchStr = pr.patch
|
|
517
513
|
if (typeof patchStr === 'string' && patchStr.trim() !== '') {
|
|
518
514
|
out.push(patchStr)
|
|
@@ -525,7 +521,7 @@ function collectNginxRunPatchStringsFromKustomizationDoc(doc) {
|
|
|
525
521
|
}
|
|
526
522
|
|
|
527
523
|
/**
|
|
528
|
-
* Збирає всі inline **JSON6902**-фрагменти для **HTTPRoute
|
|
524
|
+
* Збирає всі inline **JSON6902**-фрагменти для **HTTPRoute** (непорожній **target.name**) у **kustomization.yaml** (усі документи у файлі).
|
|
529
525
|
* @param {string} raw повний текст файлу
|
|
530
526
|
* @returns {string} текст для **`validateAbieNginxRunHttpRoutePatches`** (може бути порожнім)
|
|
531
527
|
*/
|
|
@@ -544,44 +540,43 @@ export function getCombinedNginxRunPatchTextFromKustomization(raw) {
|
|
|
544
540
|
/** @type {string[]} */
|
|
545
541
|
const chunks = []
|
|
546
542
|
for (const doc of docs) {
|
|
547
|
-
chunks.push(...
|
|
543
|
+
chunks.push(...collectAbieHttpRoutePatchStringsFromKustomizationDoc(doc))
|
|
548
544
|
}
|
|
549
545
|
return chunks.join('\n')
|
|
550
546
|
}
|
|
551
547
|
|
|
552
548
|
/**
|
|
553
|
-
* Перевіряє сукупний текст patch(ів) **HTTPRoute
|
|
549
|
+
* Перевіряє сукупний текст patch(ів) **HTTPRoute** (будь-яке **target.name**) на відповідність abie.mdc.
|
|
554
550
|
* @param {string} combined текст одного або кількох inline **patch**, розділених символом нового рядка
|
|
555
551
|
* @param {'ua' | 'ru'} mode **ua** або **ru**
|
|
556
552
|
* @returns {string | null} повідомлення про помилку або **null**
|
|
557
553
|
*/
|
|
558
554
|
export function validateAbieNginxRunHttpRoutePatches(combined, mode) {
|
|
559
555
|
if (typeof combined !== 'string' || combined.trim() === '') {
|
|
560
|
-
return `очікується patch target kind HTTPRoute name
|
|
556
|
+
return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}; для ru — також gwin… websocket) — abie.mdc`
|
|
561
557
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
return 'HTTPRoute nginx-run: потрібен блок op replace з path /spec/hostnames (abie.mdc)'
|
|
558
|
+
if (!/path:\s*\/spec\/hostnames\b/m.test(combined)) {
|
|
559
|
+
return 'HTTPRoute: потрібен path /spec/hostnames у patch (abie.mdc)'
|
|
565
560
|
}
|
|
566
561
|
const markers = mode === 'ua' ? ABIE_UA_HTTPROUTE_HOST_MARKERS : ABIE_RU_HTTPROUTE_HOST_MARKERS
|
|
567
562
|
if (!markers.some(m => combined.includes(m))) {
|
|
568
|
-
return `HTTPRoute
|
|
563
|
+
return `HTTPRoute: у value для /spec/hostnames має бути один із доменів abie (${markers.join(', ')}) — abie.mdc`
|
|
569
564
|
}
|
|
570
565
|
const namespaceOk =
|
|
571
566
|
mode === 'ua'
|
|
572
567
|
? /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua['"]?(?:\s|$)/mu.test(combined)
|
|
573
568
|
: /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru['"]?(?:\s|$)/mu.test(combined)
|
|
574
569
|
if (!namespaceOk) {
|
|
575
|
-
return `HTTPRoute
|
|
570
|
+
return `HTTPRoute: потрібен path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
|
|
576
571
|
}
|
|
577
572
|
if (mode === 'ru' && !/gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/m.test(combined)) {
|
|
578
|
-
return 'HTTPRoute
|
|
573
|
+
return 'HTTPRoute (ru): потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
|
|
579
574
|
}
|
|
580
575
|
return null
|
|
581
576
|
}
|
|
582
577
|
|
|
583
578
|
/**
|
|
584
|
-
* Чи **kustomization** містить валідні для abie
|
|
579
|
+
* Чи **kustomization** містить валідні для abie **patch** для **HTTPRoute** з непорожнім **target.name** (**ua** або **ru**).
|
|
585
580
|
* @param {string} raw повний текст **kustomization.yaml**
|
|
586
581
|
* @param {'ua' | 'ru'} mode overlay
|
|
587
582
|
* @returns {boolean} true, якщо **`validateAbieNginxRunHttpRoutePatches`** повертає **null**
|
|
@@ -782,7 +777,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
782
777
|
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
783
778
|
if (uaAbsList.length === 0) {
|
|
784
779
|
fail(
|
|
785
|
-
'Є Deployment у k8s — додай ua/kustomization.yaml з
|
|
780
|
+
'Є Deployment у k8s — додай ua/kustomization.yaml з patch на Deployment: path /spec/template/spec/nodeSelector, preem false (abie.mdc)'
|
|
786
781
|
)
|
|
787
782
|
return
|
|
788
783
|
}
|
|
@@ -798,7 +793,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
798
793
|
}
|
|
799
794
|
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
|
|
800
795
|
fail(
|
|
801
|
-
`${rel}: потрібен patch target kind Deployment
|
|
796
|
+
`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`
|
|
802
797
|
)
|
|
803
798
|
return
|
|
804
799
|
}
|
|
@@ -808,7 +803,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
808
803
|
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
809
804
|
if (ruAbsList.length === 0) {
|
|
810
805
|
fail(
|
|
811
|
-
'Є Deployment у k8s — додай ru/kustomization.yaml з
|
|
806
|
+
'Є Deployment у k8s — додай ru/kustomization.yaml з patch на Deployment: path /spec/template/spec/nodeSelector, yandex.cloud/preemptible false (abie.mdc)'
|
|
812
807
|
)
|
|
813
808
|
return
|
|
814
809
|
}
|
|
@@ -824,7 +819,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
824
819
|
}
|
|
825
820
|
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ru')) {
|
|
826
821
|
fail(
|
|
827
|
-
`${rel}: потрібен patch target kind Deployment
|
|
822
|
+
`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та yandex.cloud/preemptible: false (abie.mdc)`
|
|
828
823
|
)
|
|
829
824
|
return
|
|
830
825
|
}
|
|
@@ -833,7 +828,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
833
828
|
}
|
|
834
829
|
|
|
835
830
|
/**
|
|
836
|
-
* Якщо є **Deployment** під **k8s**, вимагає в кожному overlay **ua** та **ru** patch **HTTPRoute
|
|
831
|
+
* Якщо є **Deployment** під **k8s**, вимагає в кожному overlay **ua** та **ru** patch **HTTPRoute** (непорожній **target.name**) за abie.mdc.
|
|
837
832
|
* @param {string} root корінь репозиторію
|
|
838
833
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
839
834
|
* @param {(msg: string) => void} fail callback
|
|
@@ -844,7 +839,7 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
|
|
|
844
839
|
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
845
840
|
if (uaAbsList.length === 0) {
|
|
846
841
|
fail(
|
|
847
|
-
'Є Deployment у k8s — додай ua/kustomization.yaml з patch HTTPRoute
|
|
842
|
+
'Є Deployment у k8s — додай ua/kustomization.yaml з patch HTTPRoute (будь-який target.name: hostnames, parentRefs namespace ua) — abie.mdc'
|
|
848
843
|
)
|
|
849
844
|
return
|
|
850
845
|
}
|
|
@@ -864,13 +859,13 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
|
|
|
864
859
|
fail(`${rel}: ${v}`)
|
|
865
860
|
return
|
|
866
861
|
}
|
|
867
|
-
passFn(`${rel}: HTTPRoute
|
|
862
|
+
passFn(`${rel}: HTTPRoute patch (ua) відповідає abie.mdc`)
|
|
868
863
|
}
|
|
869
864
|
|
|
870
865
|
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
871
866
|
if (ruAbsList.length === 0) {
|
|
872
867
|
fail(
|
|
873
|
-
'Є Deployment у k8s — додай ru/kustomization.yaml з patch HTTPRoute
|
|
868
|
+
'Є Deployment у k8s — додай ru/kustomization.yaml з patch HTTPRoute (будь-який target.name: hostnames, namespace ru, gwin websocket) — abie.mdc'
|
|
874
869
|
)
|
|
875
870
|
return
|
|
876
871
|
}
|
|
@@ -890,7 +885,7 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
|
|
|
890
885
|
fail(`${rel}: ${v}`)
|
|
891
886
|
return
|
|
892
887
|
}
|
|
893
|
-
passFn(`${rel}: HTTPRoute
|
|
888
|
+
passFn(`${rel}: HTTPRoute patch (ru) відповідає abie.mdc`)
|
|
894
889
|
}
|
|
895
890
|
}
|
|
896
891
|
|
|
@@ -983,7 +978,7 @@ export async function check() {
|
|
|
983
978
|
if (deploymentDirs.size > 0) {
|
|
984
979
|
pass('Є Deployment — перевіряємо nodeSelector у ua/ru kustomization (abie.mdc)')
|
|
985
980
|
await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, fail, pass)
|
|
986
|
-
pass('Є Deployment — перевіряємо HTTPRoute
|
|
981
|
+
pass('Є Deployment — перевіряємо HTTPRoute у ua/ru kustomization (abie.mdc)')
|
|
987
982
|
await ensureUaRuAbieHttpRoutePatches(root, yamlFiles, fail, pass)
|
|
988
983
|
}
|
|
989
984
|
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
* Структура **Kustomize** (див. k8s.mdc): заборона шляхів **`…/k8s/dev/…`**; у **`k8s/base/kustomization.yaml`**
|
|
41
41
|
* завжди має бути непорожнє поле **`namespace:`** (перевірка, якщо файл існує).
|
|
42
42
|
*
|
|
43
|
+
* **Inline JSON6902** у **`patches`** (і зовнішні файли з **`patches[].path`** під **`k8s`**, якщо вміст — масив JSON Patch): не допускається пара **`remove`** і **`add`**
|
|
44
|
+
* на один і той самий **`path`** у межах одного фрагмента — потрібен **`op: replace`** (k8s.mdc). **check-k8s** це перевіряє.
|
|
45
|
+
*
|
|
43
46
|
* Явні винятки до загальної логіки yannh/datree — таблиця **`EXPLICIT_K8S_SCHEMAS`** (`Map`): ключ
|
|
44
47
|
* **`apiVersion`, `kind`, `type`** (для CRD без поля `type` у маніфесті — зірочка **`*`** як третій
|
|
45
48
|
* компонент). Спочатку шукається збіг за фактичним `type`, потім за **`*`**.
|
|
@@ -679,6 +682,224 @@ export function ruKustomizationHasHealthCheckDeletePatch(raw) {
|
|
|
679
682
|
return true
|
|
680
683
|
}
|
|
681
684
|
|
|
685
|
+
/**
|
|
686
|
+
* Чи абсолютний шлях лежить усередині кореня репозиторію (без виходу через `..`).
|
|
687
|
+
* @param {string} rootAbs абсолютний корінь
|
|
688
|
+
* @param {string} fileAbs абсолютний шлях до файлу
|
|
689
|
+
* @returns {boolean} true, якщо `fileAbs` усередині `rootAbs`
|
|
690
|
+
*/
|
|
691
|
+
function resolvedFilePathIsUnderRoot(rootAbs, fileAbs) {
|
|
692
|
+
const r = resolve(rootAbs)
|
|
693
|
+
const f = resolve(fileAbs)
|
|
694
|
+
const rel = relative(r, f).replaceAll('\\', '/')
|
|
695
|
+
if (rel === '') {
|
|
696
|
+
return true
|
|
697
|
+
}
|
|
698
|
+
return !rel.startsWith('../') && rel !== '..'
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Нормалізує **`path`** з операції JSON Patch (RFC 6902).
|
|
703
|
+
* @param {string} p значення поля **path**
|
|
704
|
+
* @returns {string} обрізаний рядок
|
|
705
|
+
*/
|
|
706
|
+
function normalizeJsonPatchPath(p) {
|
|
707
|
+
return typeof p === 'string' ? p.trim() : ''
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Витягує пари **op** / **path** з масиву операцій JSON6902.
|
|
712
|
+
* @param {unknown[]} arr корінь-масив з YAML/JSON
|
|
713
|
+
* @returns {Array<{ op: string, path: string }>} **op** у нижньому регістрі
|
|
714
|
+
*/
|
|
715
|
+
function extractJson6902OpsFromArray(arr) {
|
|
716
|
+
/** @type {Array<{ op: string, path: string }>} */
|
|
717
|
+
const out = []
|
|
718
|
+
for (const item of arr) {
|
|
719
|
+
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
|
|
720
|
+
const rec = /** @type {Record<string, unknown>} */ (item)
|
|
721
|
+
const op = rec.op
|
|
722
|
+
const path = rec.path
|
|
723
|
+
if (typeof op === 'string' && typeof path === 'string') {
|
|
724
|
+
const p = normalizeJsonPatchPath(path)
|
|
725
|
+
if (p !== '') {
|
|
726
|
+
out.push({ op: op.trim().toLowerCase(), path: p })
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return out
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Витягує операції JSON6902 з тексту inline **patch** або окремого файлу patch (YAML-масив або JSON-масив).
|
|
736
|
+
* Інший вміст (strategic merge, `$patch: delete` тощо) дає порожній масив.
|
|
737
|
+
* @param {string} patchText вміст поля **patch** або файлу
|
|
738
|
+
* @returns {Array<{ op: string, path: string }>}
|
|
739
|
+
*/
|
|
740
|
+
export function collectJson6902OperationsFromPatchText(patchText) {
|
|
741
|
+
const t = typeof patchText === 'string' ? patchText.trim() : ''
|
|
742
|
+
if (t === '') {
|
|
743
|
+
return []
|
|
744
|
+
}
|
|
745
|
+
try {
|
|
746
|
+
const docs = parseAllDocuments(t)
|
|
747
|
+
for (const d of docs) {
|
|
748
|
+
if (d.errors.length > 0) {
|
|
749
|
+
continue
|
|
750
|
+
}
|
|
751
|
+
const j = d.toJSON()
|
|
752
|
+
if (Array.isArray(j)) {
|
|
753
|
+
return extractJson6902OpsFromArray(j)
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
} catch {
|
|
757
|
+
/* пробуємо JSON */
|
|
758
|
+
}
|
|
759
|
+
if (t.startsWith('[')) {
|
|
760
|
+
try {
|
|
761
|
+
const j = JSON.parse(t)
|
|
762
|
+
if (Array.isArray(j)) {
|
|
763
|
+
return extractJson6902OpsFromArray(j)
|
|
764
|
+
}
|
|
765
|
+
} catch {
|
|
766
|
+
/* ignore */
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return []
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Шляхи JSON Patch, де в одному наборі операцій є і **remove**, і **add** (k8s.mdc: краще **replace**).
|
|
774
|
+
* @param {Array<{ op: string, path: string }>} ops нормалізовані **op**
|
|
775
|
+
* @returns {string[]} унікальні **path** з порушенням (відсортовано)
|
|
776
|
+
*/
|
|
777
|
+
export function json6902PathsWithRemoveAndAddOnSamePath(ops) {
|
|
778
|
+
/** @type {Map<string, Set<string>>} */
|
|
779
|
+
const byPath = new Map()
|
|
780
|
+
for (const { op, path } of ops) {
|
|
781
|
+
if (!path) {
|
|
782
|
+
continue
|
|
783
|
+
}
|
|
784
|
+
if (!byPath.has(path)) {
|
|
785
|
+
byPath.set(path, new Set())
|
|
786
|
+
}
|
|
787
|
+
byPath.get(path).add(op)
|
|
788
|
+
}
|
|
789
|
+
/** @type {string[]} */
|
|
790
|
+
const out = []
|
|
791
|
+
for (const [path, set] of byPath) {
|
|
792
|
+
if (set.has('remove') && set.has('add')) {
|
|
793
|
+
out.push(path)
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return out.toSorted((a, b) => a.localeCompare(b))
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Перевіряє всі **`kustomization.yaml`** під **`k8s`**: у inline **`patch`** і у зовнішніх patch-файлах не має бути **remove** і **add** на той самий **path**.
|
|
801
|
+
* @param {string} root корінь репозиторію
|
|
802
|
+
* @param {string[]} yamlFilesAbs абсолютні шляхи до yaml під k8s
|
|
803
|
+
* @param {(msg: string) => void} fail реєстрація порушення
|
|
804
|
+
* @returns {Promise<void>}
|
|
805
|
+
*/
|
|
806
|
+
async function validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFilesAbs, fail) {
|
|
807
|
+
const rootNorm = resolve(root)
|
|
808
|
+
for (const kustAbs of yamlFilesAbs) {
|
|
809
|
+
if (basename(kustAbs).toLowerCase() !== 'kustomization.yaml') {
|
|
810
|
+
continue
|
|
811
|
+
}
|
|
812
|
+
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
813
|
+
let raw
|
|
814
|
+
try {
|
|
815
|
+
raw = await readFile(kustAbs, 'utf8')
|
|
816
|
+
} catch (error) {
|
|
817
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
818
|
+
fail(`${rel}: не вдалося прочитати для перевірки JSON6902 (${msg})`)
|
|
819
|
+
continue
|
|
820
|
+
}
|
|
821
|
+
const lines = toLines(raw)
|
|
822
|
+
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
823
|
+
/** @type {import('yaml').Document[]} */
|
|
824
|
+
let docs
|
|
825
|
+
try {
|
|
826
|
+
docs = parseAllDocuments(body)
|
|
827
|
+
} catch {
|
|
828
|
+
continue
|
|
829
|
+
}
|
|
830
|
+
for (const doc of docs) {
|
|
831
|
+
if (doc.errors.length > 0) {
|
|
832
|
+
continue
|
|
833
|
+
}
|
|
834
|
+
const rootObj = doc.toJSON()
|
|
835
|
+
if (rootObj === null || typeof rootObj !== 'object' || Array.isArray(rootObj)) {
|
|
836
|
+
continue
|
|
837
|
+
}
|
|
838
|
+
const rec = /** @type {Record<string, unknown>} */ (rootObj)
|
|
839
|
+
if (rec.kind !== 'Kustomization') {
|
|
840
|
+
continue
|
|
841
|
+
}
|
|
842
|
+
const patches = rec.patches
|
|
843
|
+
if (!Array.isArray(patches)) {
|
|
844
|
+
continue
|
|
845
|
+
}
|
|
846
|
+
let patchIdx = 0
|
|
847
|
+
for (const p of patches) {
|
|
848
|
+
patchIdx++
|
|
849
|
+
if (p === null || typeof p !== 'object' || Array.isArray(p)) {
|
|
850
|
+
continue
|
|
851
|
+
}
|
|
852
|
+
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
853
|
+
if (typeof pr.patch === 'string' && pr.patch.trim() !== '') {
|
|
854
|
+
const ops = collectJson6902OperationsFromPatchText(pr.patch)
|
|
855
|
+
const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
|
|
856
|
+
if (bad.length > 0) {
|
|
857
|
+
fail(
|
|
858
|
+
`${rel}: patches[${patchIdx}] inline JSON6902: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
|
|
859
|
+
)
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (typeof pr.path === 'string' && pr.path.trim() !== '') {
|
|
863
|
+
const patchRef = pr.path.trim()
|
|
864
|
+
const resolved = resolve(dirname(kustAbs), patchRef)
|
|
865
|
+
if (!resolvedFilePathIsUnderRoot(rootNorm, resolved)) {
|
|
866
|
+
continue
|
|
867
|
+
}
|
|
868
|
+
if (!existsSync(resolved)) {
|
|
869
|
+
continue
|
|
870
|
+
}
|
|
871
|
+
let st
|
|
872
|
+
try {
|
|
873
|
+
st = await stat(resolved)
|
|
874
|
+
} catch {
|
|
875
|
+
continue
|
|
876
|
+
}
|
|
877
|
+
if (!st.isFile()) {
|
|
878
|
+
continue
|
|
879
|
+
}
|
|
880
|
+
let pRaw
|
|
881
|
+
try {
|
|
882
|
+
pRaw = await readFile(resolved, 'utf8')
|
|
883
|
+
} catch {
|
|
884
|
+
continue
|
|
885
|
+
}
|
|
886
|
+
const ops = collectJson6902OperationsFromPatchText(pRaw)
|
|
887
|
+
if (ops.length === 0) {
|
|
888
|
+
continue
|
|
889
|
+
}
|
|
890
|
+
const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
|
|
891
|
+
if (bad.length > 0) {
|
|
892
|
+
const relPatch = (relative(root, resolved) || patchRef).replaceAll('\\', '/')
|
|
893
|
+
fail(
|
|
894
|
+
`${rel}: patch-файл «${relPatch}»: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
|
|
895
|
+
)
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
682
903
|
/**
|
|
683
904
|
* Шукає **Ingress** у розібраних документах; реєструє порушення.
|
|
684
905
|
* @param {string} rel відносний шлях до файлу
|
|
@@ -1510,6 +1731,8 @@ export async function check() {
|
|
|
1510
1731
|
|
|
1511
1732
|
await validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail)
|
|
1512
1733
|
|
|
1734
|
+
await validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFiles, fail)
|
|
1735
|
+
|
|
1513
1736
|
await ensureBaseKustomizationHasNamespace(root, yamlFiles, fail)
|
|
1514
1737
|
|
|
1515
1738
|
return reporter.getExitCode()
|
|
@@ -10,7 +10,7 @@ import { pass } from './pass.mjs'
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Створює пару `pass` / `fail` з накопиченням ненульового коду виходу.
|
|
13
|
-
* @returns {{ pass: typeof pass, fail: (msg: string) => void, getExitCode: () => number }}
|
|
13
|
+
* @returns {{ pass: typeof pass, fail: (msg: string) => void, getExitCode: () => number }} об’єкт з `pass`, `fail` і `getExitCode()` (0 або 1 після будь-якого `fail`)
|
|
14
14
|
*/
|
|
15
15
|
export function createCheckReporter() {
|
|
16
16
|
let exitCode = 0
|