@nitra/cursor 1.8.79 → 1.8.81
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 +7 -5
- package/mdc/k8s.mdc +0 -2
- package/package.json +1 -1
- package/scripts/check-abie.mjs +153 -74
- package/scripts/check-k8s.mjs +94 -98
package/mdc/abie.mdc
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Правила для проєктів AbInBev Efes
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.8'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
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
8
|
|
|
9
9
|
**`npx @nitra/cursor check abie`** виконується лише якщо в **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень.
|
|
10
10
|
|
|
11
|
-
**Канон перевірки** — **`npm/scripts/check-abie.mjs`**: верхній JSDoc і реалізація задають точні умови, допустимі домени для hostnames, тексти помилок. Нижче — зміст правила й орієнтовні фрагменти YAML; не дублюй тут покроковий алгоритм зі скрипта.
|
|
11
|
+
**Канон перевірки** — **`npm/scripts/check-abie.mjs`** у пакеті **`@nitra/cursor`**: верхній JSDoc і реалізація задають точні умови, допустимі домени для hostnames, тексти помилок. Нижче — зміст правила й орієнтовні фрагменти YAML; не дублюй тут покроковий алгоритм зі скрипта.
|
|
12
12
|
|
|
13
13
|
## k8s: `hc.yaml` поруч із Deployment
|
|
14
14
|
|
|
@@ -36,7 +36,7 @@ spec:
|
|
|
36
36
|
|
|
37
37
|
## k8s: overlay **HTTPRoute** (**ua** / **ru**)
|
|
38
38
|
|
|
39
|
-
За наявності **Deployment** під **k8s** у
|
|
39
|
+
За наявності **Deployment** під **k8s** і наявності **Vite** (**`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`** у каталозі пакета) у **`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`** лише якщо в **тому ж** **`ru/kustomization.yaml`** є згадка **`HASURA_GRAPHQL_JWT_SECRET`** (типово patch на **ConfigMap** Hasura). Як обирати **`op`** (**add** / **replace** тощо) у patch — **k8s.mdc** (розділ про JSON patch у kustomization).
|
|
40
40
|
|
|
41
41
|
```yaml title="…/ua/kustomization.yaml (фрагмент)"
|
|
42
42
|
- target:
|
|
@@ -66,7 +66,9 @@ spec:
|
|
|
66
66
|
value: ru
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
Якщо в цьому ж файлі є **`HASURA_GRAPHQL_JWT_SECRET`** (Hasura з JWT), додай окремий patch на **HTTPRoute** з анотацією для WebSocket:
|
|
70
|
+
|
|
71
|
+
```yaml title="…/ru/kustomization.yaml (фрагмент, після patch на ConfigMap з HASURA_GRAPHQL_JWT_SECRET)"
|
|
70
72
|
- target:
|
|
71
73
|
kind: HTTPRoute
|
|
72
74
|
name: my-httproute
|
|
@@ -95,7 +97,7 @@ patches:
|
|
|
95
97
|
|
|
96
98
|
## k8s: overlay **ua** / **ru** і nodeSelector
|
|
97
99
|
|
|
98
|
-
|
|
100
|
+
У **`…/ua/kustomization.yaml`** та **`…/ru/kustomization.yaml`** того пакета, у дереві **`k8s`** якого є **Deployment**, потрібен 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**.
|
|
99
101
|
|
|
100
102
|
```yaml title="…/ua/kustomization.yaml (фрагмент)"
|
|
101
103
|
patches:
|
package/mdc/k8s.mdc
CHANGED
|
@@ -226,8 +226,6 @@ patches:
|
|
|
226
226
|
|
|
227
227
|
**Не входить у check k8s:** наприклад **ReferenceGrant** і доступ між **namespace** (лише рекомендації тут), **kubeconform** / **kubescape** — це **`bun run lint-k8s`**.
|
|
228
228
|
|
|
229
|
-
|
|
230
|
-
|
|
231
229
|
## Коли застосовувати (агентам)
|
|
232
230
|
|
|
233
231
|
- Після змін у k8s YAML: **`npx @nitra/cursor check k8s`** і за наявності правила — **`bun run lint-k8s`**.
|
package/package.json
CHANGED
package/scripts/check-abie.mjs
CHANGED
|
@@ -17,12 +17,14 @@
|
|
|
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):** якщо
|
|
20
|
+
* **nodeSelector (overlay):** якщо в дереві **k8s** пакета є **Deployment**, у **`ua`/`ru` kustomization** цього пакета — inline patch на **`kind: Deployment`**
|
|
21
21
|
* з **`path: /spec/template/spec/nodeSelector`**: **ua** — **`preem: false`**; **ru** — **`yandex.cloud/preemptible: false`**.
|
|
22
22
|
* Узагальнені вимоги **k8s.mdc** до JSON6902 (зокрема заборона **remove** + **add** на той самий **path**) перевіряє **check-k8s.mjs**; **check-abie** — лише abie-специфічний вміст (без дублювання цього правила).
|
|
23
23
|
*
|
|
24
|
-
* **HTTPRoute (overlay):**
|
|
25
|
-
*
|
|
24
|
+
* **HTTPRoute (overlay):** лише якщо в каталозі пакета (батько **`k8s`**) є **`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`**
|
|
25
|
+
* — тоді в **`ua`/`ru` kustomization** потрібен patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
|
|
26
|
+
* (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** — **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**,
|
|
27
|
+
* якщо в тому ж **`kustomization.yaml`** згадується **`HASURA_GRAPHQL_JWT_SECRET`** (Hasura + JWT).
|
|
26
28
|
* Вибір **`op`** — **k8s.mdc**.
|
|
27
29
|
*/
|
|
28
30
|
import { existsSync } from 'node:fs'
|
|
@@ -38,6 +40,9 @@ import { walkDir } from './utils/walkDir.mjs'
|
|
|
38
40
|
|
|
39
41
|
const CONFIG_FILE = '.n-cursor.json'
|
|
40
42
|
|
|
43
|
+
/** Маркер у kustomization.yaml: якщо зустрічається у файлі — для overlay ru у patch HTTPRoute потрібна анотація gwin…websocket. */
|
|
44
|
+
const HASURA_JWT_SECRET_IN_KUSTOMIZATION = 'HASURA_GRAPHQL_JWT_SECRET'
|
|
45
|
+
|
|
41
46
|
/** Очікуваний URL **`$schema`** для **hc.yaml** (abie.mdc). */
|
|
42
47
|
export const ABIE_HC_SCHEMA_URL = 'https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json'
|
|
43
48
|
|
|
@@ -66,6 +71,58 @@ export function isUaKustomizationPath(rel) {
|
|
|
66
71
|
return /(^|\/)ua\/kustomization\.yaml$/u.test(norm)
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Каталог пакета: шлях перед сегментом **`/k8s/`** для overlay **`…/k8s/(ua|ru)/kustomization.yaml`**.
|
|
76
|
+
* @param {string} root корінь репозиторію
|
|
77
|
+
* @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
|
|
78
|
+
* @returns {string | null} абсолютний шлях до каталогу пакета або null, якщо шлях не overlay ua чи ru
|
|
79
|
+
*/
|
|
80
|
+
export function abiePackageDirFromK8sOverlay(root, kustomizationAbs) {
|
|
81
|
+
const rel = relative(root, kustomizationAbs).replaceAll('\\', '/') || kustomizationAbs
|
|
82
|
+
const m = rel.match(/^(.+)\/k8s\/(?:ua|ru)\/kustomization\.yaml$/u)
|
|
83
|
+
return m ? join(root, m[1]) : null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Чи для цього overlay застосовувати вимоги **HTTPRoute** (лише Vite-пакети).
|
|
88
|
+
* @param {string} root корінь репозиторію
|
|
89
|
+
* @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
|
|
90
|
+
* @returns {boolean} **true**, якщо поруч із **k8s** є **vite.config** (**js** / **mjs** / **ts**)
|
|
91
|
+
*/
|
|
92
|
+
export function abieOverlayRequiresHttpRouteByVite(root, kustomizationAbs) {
|
|
93
|
+
const pkg = abiePackageDirFromK8sOverlay(root, kustomizationAbs)
|
|
94
|
+
if (!pkg) {
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
return (
|
|
98
|
+
existsSync(join(pkg, 'vite.config.js')) ||
|
|
99
|
+
existsSync(join(pkg, 'vite.config.mjs')) ||
|
|
100
|
+
existsSync(join(pkg, 'vite.config.ts'))
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Чи в дереві **k8s** того ж пакета, що й overlay **ua** або **ru**, є **Deployment** (за каталогами з **collectDeploymentDirs**).
|
|
106
|
+
* @param {Set<string>} deploymentDirs абсолютні каталоги YAML-файлів із **Deployment**
|
|
107
|
+
* @param {string} root корінь репозиторію
|
|
108
|
+
* @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
|
|
109
|
+
* @returns {boolean} **true**, якщо хоч один каталог із **deploymentDirs** лежить під **`…/k8s/`** цього пакета
|
|
110
|
+
*/
|
|
111
|
+
export function abieOverlayK8sTreeHasDeployment(deploymentDirs, root, kustomizationAbs) {
|
|
112
|
+
const pkg = abiePackageDirFromK8sOverlay(root, kustomizationAbs)
|
|
113
|
+
if (!pkg) {
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
const k8sRoot = join(pkg, 'k8s').replaceAll('\\', '/')
|
|
117
|
+
for (const dir of deploymentDirs) {
|
|
118
|
+
const norm = dir.replaceAll('\\', '/')
|
|
119
|
+
if (norm === k8sRoot || norm.startsWith(`${k8sRoot}/`)) {
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
|
|
69
126
|
/**
|
|
70
127
|
* Чи відносний шлях до YAML під **k8s** вказує на файл у каталозі **`base`** (сегмент **`base`** у шляху), abie.mdc.
|
|
71
128
|
* @param {string} rel шлях від кореня репозиторію
|
|
@@ -549,11 +606,12 @@ export function getCombinedNginxRunPatchTextFromKustomization(raw) {
|
|
|
549
606
|
* Перевіряє сукупний текст patch(ів) **HTTPRoute** (будь-яке **target.name**) на відповідність abie.mdc.
|
|
550
607
|
* @param {string} combined текст одного або кількох inline **patch**, розділених символом нового рядка
|
|
551
608
|
* @param {'ua' | 'ru'} mode **ua** або **ru**
|
|
609
|
+
* @param {string} [fullKustomizationRaw] повний текст **kustomization.yaml** — для **ru** визначає, чи потрібна анотація **gwin…websocket** (лише якщо є **`HASURA_GRAPHQL_JWT_SECRET`**)
|
|
552
610
|
* @returns {string | null} повідомлення про помилку або **null**
|
|
553
611
|
*/
|
|
554
|
-
export function validateAbieNginxRunHttpRoutePatches(combined, mode) {
|
|
612
|
+
export function validateAbieNginxRunHttpRoutePatches(combined, mode, fullKustomizationRaw) {
|
|
555
613
|
if (typeof combined !== 'string' || combined.trim() === '') {
|
|
556
|
-
return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}; для ru —
|
|
614
|
+
return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}; для ru — gwin… websocket лише за наявності HASURA_GRAPHQL_JWT_SECRET у файлі) — abie.mdc`
|
|
557
615
|
}
|
|
558
616
|
if (!/path:\s*\/spec\/hostnames\b/m.test(combined)) {
|
|
559
617
|
return 'HTTPRoute: потрібен path /spec/hostnames у patch (abie.mdc)'
|
|
@@ -569,8 +627,12 @@ export function validateAbieNginxRunHttpRoutePatches(combined, mode) {
|
|
|
569
627
|
if (!namespaceOk) {
|
|
570
628
|
return `HTTPRoute: потрібен path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
|
|
571
629
|
}
|
|
572
|
-
|
|
573
|
-
|
|
630
|
+
const ruNeedsWebsocket =
|
|
631
|
+
mode === 'ru' &&
|
|
632
|
+
typeof fullKustomizationRaw === 'string' &&
|
|
633
|
+
fullKustomizationRaw.includes(HASURA_JWT_SECRET_IN_KUSTOMIZATION)
|
|
634
|
+
if (ruNeedsWebsocket && !/gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/m.test(combined)) {
|
|
635
|
+
return 'HTTPRoute (ru): за наявності HASURA_GRAPHQL_JWT_SECRET у kustomization потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
|
|
574
636
|
}
|
|
575
637
|
return null
|
|
576
638
|
}
|
|
@@ -583,7 +645,7 @@ export function validateAbieNginxRunHttpRoutePatches(combined, mode) {
|
|
|
583
645
|
*/
|
|
584
646
|
export function kustomizationHasAbieNginxRunHttpRoutePatch(raw, mode) {
|
|
585
647
|
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
586
|
-
return validateAbieNginxRunHttpRoutePatches(combined, mode) === null
|
|
648
|
+
return validateAbieNginxRunHttpRoutePatches(combined, mode, raw) === null
|
|
587
649
|
}
|
|
588
650
|
|
|
589
651
|
/**
|
|
@@ -766,14 +828,16 @@ async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, health
|
|
|
766
828
|
}
|
|
767
829
|
|
|
768
830
|
/**
|
|
769
|
-
* Якщо є **Deployment** під **k8s**, вимагає в
|
|
831
|
+
* Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** та **ru** (**kustomization.yaml**) JSON6902 patch nodeSelector (abie.mdc)
|
|
832
|
+
* лише для kustomization того пакета, у дереві **k8s** якого є **Deployment**.
|
|
770
833
|
* @param {string} root корінь репозиторію
|
|
771
834
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
835
|
+
* @param {Set<string>} deploymentDirs абсолютні каталоги YAML-файлів із **Deployment**
|
|
772
836
|
* @param {(msg: string) => void} fail callback
|
|
773
837
|
* @param {(msg: string) => void} passFn успішне повідомлення
|
|
774
838
|
* @returns {Promise<void>}
|
|
775
839
|
*/
|
|
776
|
-
async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passFn) {
|
|
840
|
+
async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentDirs, fail, passFn) {
|
|
777
841
|
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
778
842
|
if (uaAbsList.length === 0) {
|
|
779
843
|
fail(
|
|
@@ -783,21 +847,25 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
783
847
|
}
|
|
784
848
|
for (const abs of uaAbsList) {
|
|
785
849
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
850
|
+
if (abieOverlayK8sTreeHasDeployment(deploymentDirs, root, abs)) {
|
|
851
|
+
let raw
|
|
852
|
+
try {
|
|
853
|
+
raw = await readFile(abs, 'utf8')
|
|
854
|
+
} catch (error) {
|
|
855
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
856
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
857
|
+
return
|
|
858
|
+
}
|
|
859
|
+
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
|
|
860
|
+
fail(
|
|
861
|
+
`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`
|
|
862
|
+
)
|
|
863
|
+
return
|
|
864
|
+
}
|
|
865
|
+
passFn(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
|
|
866
|
+
} else {
|
|
867
|
+
passFn(`${rel}: nodeSelector patch (ua) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
|
|
799
868
|
}
|
|
800
|
-
passFn(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
|
|
801
869
|
}
|
|
802
870
|
|
|
803
871
|
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
@@ -809,26 +877,31 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
809
877
|
}
|
|
810
878
|
for (const abs of ruAbsList) {
|
|
811
879
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
880
|
+
if (abieOverlayK8sTreeHasDeployment(deploymentDirs, root, abs)) {
|
|
881
|
+
let raw
|
|
882
|
+
try {
|
|
883
|
+
raw = await readFile(abs, 'utf8')
|
|
884
|
+
} catch (error) {
|
|
885
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
886
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
887
|
+
return
|
|
888
|
+
}
|
|
889
|
+
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ru')) {
|
|
890
|
+
fail(
|
|
891
|
+
`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та yandex.cloud/preemptible: false (abie.mdc)`
|
|
892
|
+
)
|
|
893
|
+
return
|
|
894
|
+
}
|
|
895
|
+
passFn(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
|
|
896
|
+
} else {
|
|
897
|
+
passFn(`${rel}: nodeSelector patch (ru) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
|
|
825
898
|
}
|
|
826
|
-
passFn(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
|
|
827
899
|
}
|
|
828
900
|
}
|
|
829
901
|
|
|
830
902
|
/**
|
|
831
|
-
* Якщо є **Deployment** під **k8s**, вимагає в
|
|
903
|
+
* Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** та **ru** patch **HTTPRoute** (непорожній **target.name**) за abie.mdc
|
|
904
|
+
* лише для пакетів з **vite.config.{js,mjs,ts}** у каталозі пакета (батько **k8s**).
|
|
832
905
|
* @param {string} root корінь репозиторію
|
|
833
906
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
834
907
|
* @param {(msg: string) => void} fail callback
|
|
@@ -838,54 +911,60 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
838
911
|
async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
|
|
839
912
|
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
840
913
|
if (uaAbsList.length === 0) {
|
|
841
|
-
|
|
842
|
-
'
|
|
914
|
+
passFn(
|
|
915
|
+
'Немає ua/kustomization.yaml у дереві k8s — patch HTTPRoute (ua) не вимагається (abie.mdc, лише Vite-пакети)'
|
|
843
916
|
)
|
|
844
|
-
return
|
|
845
917
|
}
|
|
846
918
|
for (const abs of uaAbsList) {
|
|
847
919
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
920
|
+
if (abieOverlayRequiresHttpRouteByVite(root, abs)) {
|
|
921
|
+
let raw
|
|
922
|
+
try {
|
|
923
|
+
raw = await readFile(abs, 'utf8')
|
|
924
|
+
} catch (error) {
|
|
925
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
926
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
927
|
+
return
|
|
928
|
+
}
|
|
929
|
+
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
930
|
+
const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua')
|
|
931
|
+
if (v !== null) {
|
|
932
|
+
fail(`${rel}: ${v}`)
|
|
933
|
+
return
|
|
934
|
+
}
|
|
935
|
+
passFn(`${rel}: HTTPRoute patch (ua) відповідає abie.mdc`)
|
|
936
|
+
} else {
|
|
937
|
+
passFn(`${rel}: HTTPRoute patch (ua) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
|
|
861
938
|
}
|
|
862
|
-
passFn(`${rel}: HTTPRoute patch (ua) відповідає abie.mdc`)
|
|
863
939
|
}
|
|
864
940
|
|
|
865
941
|
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
866
942
|
if (ruAbsList.length === 0) {
|
|
867
|
-
|
|
868
|
-
'
|
|
943
|
+
passFn(
|
|
944
|
+
'Немає ru/kustomization.yaml у дереві k8s — patch HTTPRoute (ru) не вимагається (abie.mdc, лише Vite-пакети)'
|
|
869
945
|
)
|
|
870
|
-
return
|
|
871
946
|
}
|
|
872
947
|
for (const abs of ruAbsList) {
|
|
873
948
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
949
|
+
if (abieOverlayRequiresHttpRouteByVite(root, abs)) {
|
|
950
|
+
let raw
|
|
951
|
+
try {
|
|
952
|
+
raw = await readFile(abs, 'utf8')
|
|
953
|
+
} catch (error) {
|
|
954
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
955
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
956
|
+
return
|
|
957
|
+
}
|
|
958
|
+
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
959
|
+
const v = validateAbieNginxRunHttpRoutePatches(combined, 'ru', raw)
|
|
960
|
+
if (v !== null) {
|
|
961
|
+
fail(`${rel}: ${v}`)
|
|
962
|
+
return
|
|
963
|
+
}
|
|
964
|
+
passFn(`${rel}: HTTPRoute patch (ru) відповідає abie.mdc`)
|
|
965
|
+
} else {
|
|
966
|
+
passFn(`${rel}: HTTPRoute patch (ru) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
|
|
887
967
|
}
|
|
888
|
-
passFn(`${rel}: HTTPRoute patch (ru) відповідає abie.mdc`)
|
|
889
968
|
}
|
|
890
969
|
}
|
|
891
970
|
|
|
@@ -977,7 +1056,7 @@ export async function check() {
|
|
|
977
1056
|
|
|
978
1057
|
if (deploymentDirs.size > 0) {
|
|
979
1058
|
pass('Є Deployment — перевіряємо nodeSelector у ua/ru kustomization (abie.mdc)')
|
|
980
|
-
await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, fail, pass)
|
|
1059
|
+
await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, deploymentDirs, fail, pass)
|
|
981
1060
|
pass('Є Deployment — перевіряємо HTTPRoute у ua/ru kustomization (abie.mdc)')
|
|
982
1061
|
await ensureUaRuAbieHttpRoutePatches(root, yamlFiles, fail, pass)
|
|
983
1062
|
}
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -735,7 +735,7 @@ function extractJson6902OpsFromArray(arr) {
|
|
|
735
735
|
* Витягує операції JSON6902 з тексту inline **patch** або окремого файлу patch (YAML-масив або JSON-масив).
|
|
736
736
|
* Інший вміст (strategic merge, `$patch: delete` тощо) дає порожній масив.
|
|
737
737
|
* @param {string} patchText вміст поля **patch** або файлу
|
|
738
|
-
* @returns {Array<{ op: string, path: string }>}
|
|
738
|
+
* @returns {Array<{ op: string, path: string }>} нормалізовані **op** / **path** або порожній масив, якщо не JSON6902-масив
|
|
739
739
|
*/
|
|
740
740
|
export function collectJson6902OperationsFromPatchText(patchText) {
|
|
741
741
|
const t = typeof patchText === 'string' ? patchText.trim() : ''
|
|
@@ -745,12 +745,11 @@ export function collectJson6902OperationsFromPatchText(patchText) {
|
|
|
745
745
|
try {
|
|
746
746
|
const docs = parseAllDocuments(t)
|
|
747
747
|
for (const d of docs) {
|
|
748
|
-
if (d.errors.length
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
return extractJson6902OpsFromArray(j)
|
|
748
|
+
if (d.errors.length === 0) {
|
|
749
|
+
const j = d.toJSON()
|
|
750
|
+
if (Array.isArray(j)) {
|
|
751
|
+
return extractJson6902OpsFromArray(j)
|
|
752
|
+
}
|
|
754
753
|
}
|
|
755
754
|
}
|
|
756
755
|
} catch {
|
|
@@ -778,13 +777,12 @@ export function json6902PathsWithRemoveAndAddOnSamePath(ops) {
|
|
|
778
777
|
/** @type {Map<string, Set<string>>} */
|
|
779
778
|
const byPath = new Map()
|
|
780
779
|
for (const { op, path } of ops) {
|
|
781
|
-
if (
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
byPath.
|
|
780
|
+
if (path) {
|
|
781
|
+
if (!byPath.has(path)) {
|
|
782
|
+
byPath.set(path, new Set())
|
|
783
|
+
}
|
|
784
|
+
byPath.get(path).add(op)
|
|
786
785
|
}
|
|
787
|
-
byPath.get(path).add(op)
|
|
788
786
|
}
|
|
789
787
|
/** @type {string[]} */
|
|
790
788
|
const out = []
|
|
@@ -806,93 +804,91 @@ export function json6902PathsWithRemoveAndAddOnSamePath(ops) {
|
|
|
806
804
|
async function validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFilesAbs, fail) {
|
|
807
805
|
const rootNorm = resolve(root)
|
|
808
806
|
for (const kustAbs of yamlFilesAbs) {
|
|
809
|
-
if (basename(kustAbs).toLowerCase()
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
|
807
|
+
if (basename(kustAbs).toLowerCase() === 'kustomization.yaml') {
|
|
808
|
+
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
809
|
+
/** @type {string | undefined} */
|
|
810
|
+
let raw
|
|
811
|
+
let readOk = false
|
|
812
|
+
try {
|
|
813
|
+
raw = await readFile(kustAbs, 'utf8')
|
|
814
|
+
readOk = true
|
|
815
|
+
} catch (error) {
|
|
816
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
817
|
+
fail(`${rel}: не вдалося прочитати для перевірки JSON6902 (${msg})`)
|
|
845
818
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
}
|
|
819
|
+
if (readOk && raw !== undefined) {
|
|
820
|
+
const lines = toLines(raw)
|
|
821
|
+
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
822
|
+
/** @type {import('yaml').Document[] | null} */
|
|
823
|
+
let docs = null
|
|
824
|
+
try {
|
|
825
|
+
docs = parseAllDocuments(body)
|
|
826
|
+
} catch {
|
|
827
|
+
docs = null
|
|
861
828
|
}
|
|
862
|
-
if (
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
829
|
+
if (docs !== null) {
|
|
830
|
+
for (const doc of docs) {
|
|
831
|
+
if (doc.errors.length === 0) {
|
|
832
|
+
const rootObj = doc.toJSON()
|
|
833
|
+
if (rootObj !== null && typeof rootObj === 'object' && !Array.isArray(rootObj)) {
|
|
834
|
+
const rec = /** @type {Record<string, unknown>} */ (rootObj)
|
|
835
|
+
if (rec.kind === 'Kustomization') {
|
|
836
|
+
const patches = rec.patches
|
|
837
|
+
if (Array.isArray(patches)) {
|
|
838
|
+
let patchIdx = 0
|
|
839
|
+
for (const p of patches) {
|
|
840
|
+
patchIdx++
|
|
841
|
+
if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
|
|
842
|
+
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
843
|
+
if (typeof pr.patch === 'string' && pr.patch.trim() !== '') {
|
|
844
|
+
const ops = collectJson6902OperationsFromPatchText(pr.patch)
|
|
845
|
+
const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
|
|
846
|
+
if (bad.length > 0) {
|
|
847
|
+
fail(
|
|
848
|
+
`${rel}: patches[${patchIdx}] inline JSON6902: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
|
|
849
|
+
)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (typeof pr.path === 'string' && pr.path.trim() !== '') {
|
|
853
|
+
const patchRef = pr.path.trim()
|
|
854
|
+
const resolved = resolve(dirname(kustAbs), patchRef)
|
|
855
|
+
if (resolvedFilePathIsUnderRoot(rootNorm, resolved) && existsSync(resolved)) {
|
|
856
|
+
/** @type {import('node:fs').Stats | null} */
|
|
857
|
+
let st = null
|
|
858
|
+
try {
|
|
859
|
+
st = await stat(resolved)
|
|
860
|
+
} catch {
|
|
861
|
+
st = null
|
|
862
|
+
}
|
|
863
|
+
if (st !== null && st.isFile()) {
|
|
864
|
+
/** @type {string | undefined} */
|
|
865
|
+
let pRaw
|
|
866
|
+
try {
|
|
867
|
+
pRaw = await readFile(resolved, 'utf8')
|
|
868
|
+
} catch {
|
|
869
|
+
pRaw = undefined
|
|
870
|
+
}
|
|
871
|
+
if (pRaw !== undefined) {
|
|
872
|
+
const ops = collectJson6902OperationsFromPatchText(pRaw)
|
|
873
|
+
if (ops.length > 0) {
|
|
874
|
+
const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
|
|
875
|
+
if (bad.length > 0) {
|
|
876
|
+
const relPatch = (relative(root, resolved) || patchRef).replaceAll('\\', '/')
|
|
877
|
+
fail(
|
|
878
|
+
`${rel}: patch-файл «${relPatch}»: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
|
|
879
|
+
)
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
896
892
|
}
|
|
897
893
|
}
|
|
898
894
|
}
|