@nitra/cursor 1.8.79 → 1.8.80
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 +4 -4
- package/mdc/k8s.mdc +0 -2
- package/package.json +1 -1
- package/scripts/check-abie.mjs +138 -68
- 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`**. Як обирати **`op`** (**add** / **replace** тощо) у patch — **k8s.mdc** (розділ про JSON patch у kustomization).
|
|
40
40
|
|
|
41
41
|
```yaml title="…/ua/kustomization.yaml (фрагмент)"
|
|
42
42
|
- target:
|
|
@@ -95,7 +95,7 @@ patches:
|
|
|
95
95
|
|
|
96
96
|
## k8s: overlay **ua** / **ru** і nodeSelector
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
У **`…/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
99
|
|
|
100
100
|
```yaml title="…/ua/kustomization.yaml (фрагмент)"
|
|
101
101
|
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,11 +17,12 @@
|
|
|
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):**
|
|
24
|
+
* **HTTPRoute (overlay):** лише якщо в каталозі пакета (батько **`k8s`**) є **`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`**
|
|
25
|
+
* — тоді в **`ua`/`ru` kustomization** потрібен patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
|
|
25
26
|
* (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** — **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**.
|
|
26
27
|
* Вибір **`op`** — **k8s.mdc**.
|
|
27
28
|
*/
|
|
@@ -66,6 +67,58 @@ export function isUaKustomizationPath(rel) {
|
|
|
66
67
|
return /(^|\/)ua\/kustomization\.yaml$/u.test(norm)
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Каталог пакета: шлях перед сегментом **`/k8s/`** для overlay **`…/k8s/(ua|ru)/kustomization.yaml`**.
|
|
72
|
+
* @param {string} root корінь репозиторію
|
|
73
|
+
* @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
|
|
74
|
+
* @returns {string | null} абсолютний шлях до каталогу пакета або null, якщо шлях не overlay ua чи ru
|
|
75
|
+
*/
|
|
76
|
+
export function abiePackageDirFromK8sOverlay(root, kustomizationAbs) {
|
|
77
|
+
const rel = relative(root, kustomizationAbs).replaceAll('\\', '/') || kustomizationAbs
|
|
78
|
+
const m = rel.match(/^(.+)\/k8s\/(?:ua|ru)\/kustomization\.yaml$/u)
|
|
79
|
+
return m ? join(root, m[1]) : null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Чи для цього overlay застосовувати вимоги **HTTPRoute** (лише Vite-пакети).
|
|
84
|
+
* @param {string} root корінь репозиторію
|
|
85
|
+
* @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
|
|
86
|
+
* @returns {boolean} **true**, якщо поруч із **k8s** є **vite.config** (**js** / **mjs** / **ts**)
|
|
87
|
+
*/
|
|
88
|
+
export function abieOverlayRequiresHttpRouteByVite(root, kustomizationAbs) {
|
|
89
|
+
const pkg = abiePackageDirFromK8sOverlay(root, kustomizationAbs)
|
|
90
|
+
if (!pkg) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
return (
|
|
94
|
+
existsSync(join(pkg, 'vite.config.js')) ||
|
|
95
|
+
existsSync(join(pkg, 'vite.config.mjs')) ||
|
|
96
|
+
existsSync(join(pkg, 'vite.config.ts'))
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Чи в дереві **k8s** того ж пакета, що й overlay **ua** або **ru**, є **Deployment** (за каталогами з **collectDeploymentDirs**).
|
|
102
|
+
* @param {Set<string>} deploymentDirs абсолютні каталоги YAML-файлів із **Deployment**
|
|
103
|
+
* @param {string} root корінь репозиторію
|
|
104
|
+
* @param {string} kustomizationAbs абсолютний шлях до **ua** або **ru** kustomization.yaml
|
|
105
|
+
* @returns {boolean} **true**, якщо хоч один каталог із **deploymentDirs** лежить під **`…/k8s/`** цього пакета
|
|
106
|
+
*/
|
|
107
|
+
export function abieOverlayK8sTreeHasDeployment(deploymentDirs, root, kustomizationAbs) {
|
|
108
|
+
const pkg = abiePackageDirFromK8sOverlay(root, kustomizationAbs)
|
|
109
|
+
if (!pkg) {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
const k8sRoot = join(pkg, 'k8s').replaceAll('\\', '/')
|
|
113
|
+
for (const dir of deploymentDirs) {
|
|
114
|
+
const norm = dir.replaceAll('\\', '/')
|
|
115
|
+
if (norm === k8sRoot || norm.startsWith(`${k8sRoot}/`)) {
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
|
|
69
122
|
/**
|
|
70
123
|
* Чи відносний шлях до YAML під **k8s** вказує на файл у каталозі **`base`** (сегмент **`base`** у шляху), abie.mdc.
|
|
71
124
|
* @param {string} rel шлях від кореня репозиторію
|
|
@@ -766,14 +819,16 @@ async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, health
|
|
|
766
819
|
}
|
|
767
820
|
|
|
768
821
|
/**
|
|
769
|
-
* Якщо є **Deployment** під **k8s**, вимагає в
|
|
822
|
+
* Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** та **ru** (**kustomization.yaml**) JSON6902 patch nodeSelector (abie.mdc)
|
|
823
|
+
* лише для kustomization того пакета, у дереві **k8s** якого є **Deployment**.
|
|
770
824
|
* @param {string} root корінь репозиторію
|
|
771
825
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
826
|
+
* @param {Set<string>} deploymentDirs абсолютні каталоги YAML-файлів із **Deployment**
|
|
772
827
|
* @param {(msg: string) => void} fail callback
|
|
773
828
|
* @param {(msg: string) => void} passFn успішне повідомлення
|
|
774
829
|
* @returns {Promise<void>}
|
|
775
830
|
*/
|
|
776
|
-
async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passFn) {
|
|
831
|
+
async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentDirs, fail, passFn) {
|
|
777
832
|
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
778
833
|
if (uaAbsList.length === 0) {
|
|
779
834
|
fail(
|
|
@@ -783,21 +838,25 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
783
838
|
}
|
|
784
839
|
for (const abs of uaAbsList) {
|
|
785
840
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
841
|
+
if (abieOverlayK8sTreeHasDeployment(deploymentDirs, root, abs)) {
|
|
842
|
+
let raw
|
|
843
|
+
try {
|
|
844
|
+
raw = await readFile(abs, 'utf8')
|
|
845
|
+
} catch (error) {
|
|
846
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
847
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
848
|
+
return
|
|
849
|
+
}
|
|
850
|
+
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
|
|
851
|
+
fail(
|
|
852
|
+
`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`
|
|
853
|
+
)
|
|
854
|
+
return
|
|
855
|
+
}
|
|
856
|
+
passFn(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
|
|
857
|
+
} else {
|
|
858
|
+
passFn(`${rel}: nodeSelector patch (ua) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
|
|
799
859
|
}
|
|
800
|
-
passFn(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
|
|
801
860
|
}
|
|
802
861
|
|
|
803
862
|
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
@@ -809,26 +868,31 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
809
868
|
}
|
|
810
869
|
for (const abs of ruAbsList) {
|
|
811
870
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
871
|
+
if (abieOverlayK8sTreeHasDeployment(deploymentDirs, root, abs)) {
|
|
872
|
+
let raw
|
|
873
|
+
try {
|
|
874
|
+
raw = await readFile(abs, 'utf8')
|
|
875
|
+
} catch (error) {
|
|
876
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
877
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
878
|
+
return
|
|
879
|
+
}
|
|
880
|
+
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ru')) {
|
|
881
|
+
fail(
|
|
882
|
+
`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та yandex.cloud/preemptible: false (abie.mdc)`
|
|
883
|
+
)
|
|
884
|
+
return
|
|
885
|
+
}
|
|
886
|
+
passFn(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
|
|
887
|
+
} else {
|
|
888
|
+
passFn(`${rel}: nodeSelector patch (ru) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
|
|
825
889
|
}
|
|
826
|
-
passFn(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
|
|
827
890
|
}
|
|
828
891
|
}
|
|
829
892
|
|
|
830
893
|
/**
|
|
831
|
-
* Якщо є **Deployment** під **k8s**, вимагає в
|
|
894
|
+
* Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** та **ru** patch **HTTPRoute** (непорожній **target.name**) за abie.mdc
|
|
895
|
+
* лише для пакетів з **vite.config.{js,mjs,ts}** у каталозі пакета (батько **k8s**).
|
|
832
896
|
* @param {string} root корінь репозиторію
|
|
833
897
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
834
898
|
* @param {(msg: string) => void} fail callback
|
|
@@ -838,54 +902,60 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
838
902
|
async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
|
|
839
903
|
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
840
904
|
if (uaAbsList.length === 0) {
|
|
841
|
-
|
|
842
|
-
'
|
|
905
|
+
passFn(
|
|
906
|
+
'Немає ua/kustomization.yaml у дереві k8s — patch HTTPRoute (ua) не вимагається (abie.mdc, лише Vite-пакети)'
|
|
843
907
|
)
|
|
844
|
-
return
|
|
845
908
|
}
|
|
846
909
|
for (const abs of uaAbsList) {
|
|
847
910
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
911
|
+
if (abieOverlayRequiresHttpRouteByVite(root, abs)) {
|
|
912
|
+
let raw
|
|
913
|
+
try {
|
|
914
|
+
raw = await readFile(abs, 'utf8')
|
|
915
|
+
} catch (error) {
|
|
916
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
917
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
918
|
+
return
|
|
919
|
+
}
|
|
920
|
+
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
921
|
+
const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua')
|
|
922
|
+
if (v !== null) {
|
|
923
|
+
fail(`${rel}: ${v}`)
|
|
924
|
+
return
|
|
925
|
+
}
|
|
926
|
+
passFn(`${rel}: HTTPRoute patch (ua) відповідає abie.mdc`)
|
|
927
|
+
} else {
|
|
928
|
+
passFn(`${rel}: HTTPRoute patch (ua) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
|
|
861
929
|
}
|
|
862
|
-
passFn(`${rel}: HTTPRoute patch (ua) відповідає abie.mdc`)
|
|
863
930
|
}
|
|
864
931
|
|
|
865
932
|
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
866
933
|
if (ruAbsList.length === 0) {
|
|
867
|
-
|
|
868
|
-
'
|
|
934
|
+
passFn(
|
|
935
|
+
'Немає ru/kustomization.yaml у дереві k8s — patch HTTPRoute (ru) не вимагається (abie.mdc, лише Vite-пакети)'
|
|
869
936
|
)
|
|
870
|
-
return
|
|
871
937
|
}
|
|
872
938
|
for (const abs of ruAbsList) {
|
|
873
939
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
940
|
+
if (abieOverlayRequiresHttpRouteByVite(root, abs)) {
|
|
941
|
+
let raw
|
|
942
|
+
try {
|
|
943
|
+
raw = await readFile(abs, 'utf8')
|
|
944
|
+
} catch (error) {
|
|
945
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
946
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
947
|
+
return
|
|
948
|
+
}
|
|
949
|
+
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
950
|
+
const v = validateAbieNginxRunHttpRoutePatches(combined, 'ru')
|
|
951
|
+
if (v !== null) {
|
|
952
|
+
fail(`${rel}: ${v}`)
|
|
953
|
+
return
|
|
954
|
+
}
|
|
955
|
+
passFn(`${rel}: HTTPRoute patch (ru) відповідає abie.mdc`)
|
|
956
|
+
} else {
|
|
957
|
+
passFn(`${rel}: HTTPRoute patch (ru) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
|
|
887
958
|
}
|
|
888
|
-
passFn(`${rel}: HTTPRoute patch (ru) відповідає abie.mdc`)
|
|
889
959
|
}
|
|
890
960
|
}
|
|
891
961
|
|
|
@@ -977,7 +1047,7 @@ export async function check() {
|
|
|
977
1047
|
|
|
978
1048
|
if (deploymentDirs.size > 0) {
|
|
979
1049
|
pass('Є Deployment — перевіряємо nodeSelector у ua/ru kustomization (abie.mdc)')
|
|
980
|
-
await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, fail, pass)
|
|
1050
|
+
await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, deploymentDirs, fail, pass)
|
|
981
1051
|
pass('Є Deployment — перевіряємо HTTPRoute у ua/ru kustomization (abie.mdc)')
|
|
982
1052
|
await ensureUaRuAbieHttpRoutePatches(root, yamlFiles, fail, pass)
|
|
983
1053
|
}
|
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
|
}
|