@nitra/cursor 1.8.74 → 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/k8s.mdc +0 -4
- package/package.json +1 -1
- package/scripts/check-abie.mjs +18 -24
- package/scripts/check-k8s.mjs +223 -0
package/mdc/k8s.mdc
CHANGED
|
@@ -226,11 +226,7 @@ patches:
|
|
|
226
226
|
|
|
227
227
|
**Не входить у check k8s:** наприклад **ReferenceGrant** і доступ між **namespace** (лише рекомендації тут), **kubeconform** / **kubescape** — це **`bun run lint-k8s`**.
|
|
228
228
|
|
|
229
|
-
## Що саме в скрипті `check-k8s.mjs`
|
|
230
229
|
|
|
231
|
-
Повний перелік умов, константи (**`YANNH_PIN`**, **`CLUSTER_SCOPED_KINDS`**, **`HASURA_GRAPHQL_ENGINE_IMAGE`** тощо) і допоміжні функції — у файлі скрипта; змінив вимогу для **check** — онови **JSDoc** і за потреби тести в **`npm/tests/check-k8s-schema.test.mjs`**.
|
|
232
|
-
|
|
233
|
-
При зміні **PIN** версії Kubernetes узгодь **`check-k8s.mjs`**, **`run-k8s.mjs`** (**`KUBERNETES_VERSION`**, **`DATREE_CRD_SCHEMA_LOCATION`**) і цей файл (**lint-k8s**, **Визначення схеми YAML**).
|
|
234
230
|
|
|
235
231
|
## Коли застосовувати (агентам)
|
|
236
232
|
|
package/package.json
CHANGED
package/scripts/check-abie.mjs
CHANGED
|
@@ -17,14 +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 (overlay):**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**.
|
|
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**.
|
|
28
27
|
*/
|
|
29
28
|
import { existsSync } from 'node:fs'
|
|
30
29
|
import { readFile } from 'node:fs/promises'
|
|
@@ -344,7 +343,8 @@ function stripBom(s) {
|
|
|
344
343
|
}
|
|
345
344
|
|
|
346
345
|
/**
|
|
347
|
-
* Чи рядок inline JSON6902 patch містить очікуваний **ua** nodeSelector (**
|
|
346
|
+
* Чи рядок inline JSON6902 patch містить очікуваний **ua** nodeSelector (**preem: false** на **`/spec/template/spec/nodeSelector`**).
|
|
347
|
+
* Конкретний **`op`** не перевіряється — див. **k8s.mdc**.
|
|
348
348
|
* @param {string} patchText поле **patch** у kustomization
|
|
349
349
|
* @returns {boolean} true, якщо критерії abie.mdc виконано
|
|
350
350
|
*/
|
|
@@ -352,9 +352,6 @@ function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
|
|
|
352
352
|
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
353
353
|
return false
|
|
354
354
|
}
|
|
355
|
-
if (!/op:\s*add\b/u.test(patchText)) {
|
|
356
|
-
return false
|
|
357
|
-
}
|
|
358
355
|
if (!/path:\s*\/spec\/template\/spec\/nodeSelector\b/u.test(patchText)) {
|
|
359
356
|
return false
|
|
360
357
|
}
|
|
@@ -365,7 +362,8 @@ function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
|
|
|
365
362
|
}
|
|
366
363
|
|
|
367
364
|
/**
|
|
368
|
-
* Чи рядок inline JSON6902 patch містить очікуваний **ru** nodeSelector (**
|
|
365
|
+
* Чи рядок inline JSON6902 patch містить очікуваний **ru** nodeSelector (**yandex.cloud/preemptible: false** на **`/spec/template/spec/nodeSelector`**).
|
|
366
|
+
* Конкретний **`op`** не перевіряється — див. **k8s.mdc**.
|
|
369
367
|
* @param {string} patchText поле **patch** у kustomization
|
|
370
368
|
* @returns {boolean} true, якщо критерії abie.mdc виконано
|
|
371
369
|
*/
|
|
@@ -373,9 +371,6 @@ function jsonPatchTextHasRuDeploymentNodeSelector(patchText) {
|
|
|
373
371
|
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
374
372
|
return false
|
|
375
373
|
}
|
|
376
|
-
if (!/op:\s*replace\b/u.test(patchText)) {
|
|
377
|
-
return false
|
|
378
|
-
}
|
|
379
374
|
if (!/path:\s*\/spec\/template\/spec\/nodeSelector\b/u.test(patchText)) {
|
|
380
375
|
return false
|
|
381
376
|
}
|
|
@@ -558,11 +553,10 @@ export function getCombinedNginxRunPatchTextFromKustomization(raw) {
|
|
|
558
553
|
*/
|
|
559
554
|
export function validateAbieNginxRunHttpRoutePatches(combined, mode) {
|
|
560
555
|
if (typeof combined !== 'string' || combined.trim() === '') {
|
|
561
|
-
return `очікується patch target kind HTTPRoute з непорожнім target.name (
|
|
556
|
+
return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}; для ru — також gwin… websocket) — abie.mdc`
|
|
562
557
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
return 'HTTPRoute: потрібен блок 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)'
|
|
566
560
|
}
|
|
567
561
|
const markers = mode === 'ua' ? ABIE_UA_HTTPROUTE_HOST_MARKERS : ABIE_RU_HTTPROUTE_HOST_MARKERS
|
|
568
562
|
if (!markers.some(m => combined.includes(m))) {
|
|
@@ -573,7 +567,7 @@ export function validateAbieNginxRunHttpRoutePatches(combined, mode) {
|
|
|
573
567
|
? /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua['"]?(?:\s|$)/mu.test(combined)
|
|
574
568
|
: /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru['"]?(?:\s|$)/mu.test(combined)
|
|
575
569
|
if (!namespaceOk) {
|
|
576
|
-
return `HTTPRoute: потрібен
|
|
570
|
+
return `HTTPRoute: потрібен path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
|
|
577
571
|
}
|
|
578
572
|
if (mode === 'ru' && !/gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/m.test(combined)) {
|
|
579
573
|
return 'HTTPRoute (ru): потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
|
|
@@ -783,7 +777,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
783
777
|
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
784
778
|
if (uaAbsList.length === 0) {
|
|
785
779
|
fail(
|
|
786
|
-
'Є Deployment у k8s — додай ua/kustomization.yaml з
|
|
780
|
+
'Є Deployment у k8s — додай ua/kustomization.yaml з patch на Deployment: path /spec/template/spec/nodeSelector, preem false (abie.mdc)'
|
|
787
781
|
)
|
|
788
782
|
return
|
|
789
783
|
}
|
|
@@ -799,7 +793,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
799
793
|
}
|
|
800
794
|
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
|
|
801
795
|
fail(
|
|
802
|
-
`${rel}: потрібен patch target kind Deployment
|
|
796
|
+
`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`
|
|
803
797
|
)
|
|
804
798
|
return
|
|
805
799
|
}
|
|
@@ -809,7 +803,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
809
803
|
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
810
804
|
if (ruAbsList.length === 0) {
|
|
811
805
|
fail(
|
|
812
|
-
'Є Deployment у k8s — додай ru/kustomization.yaml з
|
|
806
|
+
'Є Deployment у k8s — додай ru/kustomization.yaml з patch на Deployment: path /spec/template/spec/nodeSelector, yandex.cloud/preemptible false (abie.mdc)'
|
|
813
807
|
)
|
|
814
808
|
return
|
|
815
809
|
}
|
|
@@ -825,7 +819,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
|
|
|
825
819
|
}
|
|
826
820
|
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ru')) {
|
|
827
821
|
fail(
|
|
828
|
-
`${rel}: потрібен patch target kind Deployment
|
|
822
|
+
`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та yandex.cloud/preemptible: false (abie.mdc)`
|
|
829
823
|
)
|
|
830
824
|
return
|
|
831
825
|
}
|
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()
|