@nitra/cursor 1.8.105 → 1.8.108
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/bin/auto-rules.md +2 -2
- package/bin/n-cursor.js +5 -15
- package/mdc/js-pino.mdc +2 -2
- package/mdc/k8s.mdc +22 -12
- package/mdc/text.mdc +3 -3
- package/package.json +1 -1
- package/scripts/check-abie.mjs +515 -528
- package/scripts/check-bun.mjs +106 -78
- package/scripts/check-ga.mjs +151 -119
- package/scripts/check-js-lint.mjs +256 -179
- package/scripts/check-js-pino.mjs +48 -3
- package/scripts/check-k8s.mjs +403 -34
- package/scripts/check-nginx-default-tpl.mjs +109 -91
- package/scripts/check-npm-module.mjs +163 -116
- package/scripts/check-style-lint.mjs +74 -61
- package/scripts/check-text.mjs +289 -209
- package/scripts/check-vue.mjs +108 -67
- package/scripts/utils/bunyan-imports.mjs +182 -0
- package/scripts/utils/gha-workflow.mjs +3 -1
package/scripts/check-abie.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* **k8s:** якщо під деревом із сегментом **`k8s`** є YAML з **`kind: Deployment`**, у тій самій директорії
|
|
14
14
|
* має існувати **`hc.yaml`** із **`HealthCheckPolicy`** (**`networking.gke.io/v1`**), modeline **`$schema`**
|
|
15
|
-
* як у abie.mdc, **`/healthz`**, порт **8080**, **`targetRef`** на **headless Service** (
|
|
15
|
+
* як у abie.mdc, **`/healthz`**, порт **8080**, **`targetRef`** на **headless Service** (ім'я з суфіксом **`-hl`**):
|
|
16
16
|
* якщо **`metadata.name`** уже закінчується на **`-hl`**, **`targetRef.name`** має збігатися з ним; інакше **`targetRef.name`** = **`${metadata.name}-hl`**.
|
|
17
17
|
* Загальні вимоги до **`# yaml-language-server: $schema`** для інших YAML під **`k8s`** — у **check-k8s.mjs** / **k8s.mdc** (наприклад **HttpBackendGroup** `alb.yc.io/v1alpha1` — **без** modeline).
|
|
18
18
|
* Якщо в дереві **k8s** є **HealthCheckPolicy**, перевіряється **`ru/kustomization.yaml`** з patch **`$patch: delete`**
|
|
@@ -53,7 +53,7 @@ const CONFIG_FILE = '.n-cursor.json'
|
|
|
53
53
|
const HASURA_JWT_SECRET_IN_KUSTOMIZATION = 'HASURA_GRAPHQL_JWT_SECRET'
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* Спільні **Service** (**`-hl`**) у **dev**: у base-**HTTPRoute**
|
|
56
|
+
* Спільні **Service** (**`-hl`**) у **dev**: у base-**HTTPRoute** обов'язково **`namespace: dev`**, у overlay — patch **`…/backendRefs/…/namespace`** (abie.mdc).
|
|
57
57
|
* Експорт для споживачів / тестів.
|
|
58
58
|
*/
|
|
59
59
|
export const ABIE_SHARED_CROSS_NS_BACKEND_NAMES = Object.freeze(['auth-run-hl', 'filelint-hl'])
|
|
@@ -78,8 +78,10 @@ const PATCH_PREEM_FALSE_RE = /\bpreem:\s*['"]?false['"]?\b/u
|
|
|
78
78
|
const PATCH_YANDEX_PREEMPTIBLE_FALSE_RE = /yandex\.cloud\/preemptible:\s*['"]?false['"]?/u
|
|
79
79
|
const TRAILING_SLASH_RE = /\/$/u
|
|
80
80
|
const PATCH_HOSTNAMES_PATH_RE = /path:\s*\/spec\/hostnames\b/mu
|
|
81
|
-
const PATCH_PARENT_REF_NS_UA_RE =
|
|
82
|
-
|
|
81
|
+
const PATCH_PARENT_REF_NS_UA_RE =
|
|
82
|
+
/path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua['"]?(?:\s|$)/mu
|
|
83
|
+
const PATCH_PARENT_REF_NS_RU_RE =
|
|
84
|
+
/path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru['"]?(?:\s|$)/mu
|
|
83
85
|
const WEBSOCKET_ANNOTATION_RE = /gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/mu
|
|
84
86
|
const LEADING_EMPTY_LINE_RE = /^\s*\n/u
|
|
85
87
|
const REMOVE_CLUSTER_IP_AFTER_OP_RE = /op:\s*remove\b[\s\S]{0,200}?path:\s*\/spec\/clusterIP\b/mu
|
|
@@ -91,7 +93,7 @@ const REMOVE_CLUSTER_IPS_BEFORE_OP_RE = /path:\s*\/spec\/clusterIPs\b[\s\S]{0,20
|
|
|
91
93
|
export const ABIE_REQUIRED_IGNORE_BRANCHES = ['dev', 'ua', 'ru']
|
|
92
94
|
|
|
93
95
|
/**
|
|
94
|
-
* Чи відносний шлях вказує на **`ru/kustomization.yaml`** (сегмент **`ru`** перед
|
|
96
|
+
* Чи відносний шлях вказує на **`ru/kustomization.yaml`** (сегмент **`ru`** перед ім'ям файлу) — специфіка abie overlay.
|
|
95
97
|
* @param {string} rel шлях від кореня репозиторію
|
|
96
98
|
* @returns {boolean} true, якщо це `…/ru/kustomization.yaml`
|
|
97
99
|
*/
|
|
@@ -101,7 +103,7 @@ export function isRuKustomizationPath(rel) {
|
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
/**
|
|
104
|
-
* Чи відносний шлях вказує на **`ua/kustomization.yaml`** (сегмент **`ua`** перед
|
|
106
|
+
* Чи відносний шлях вказує на **`ua/kustomization.yaml`** (сегмент **`ua`** перед ім'ям файлу) — специфіка abie overlay.
|
|
105
107
|
* @param {string} rel шлях від кореня репозиторію
|
|
106
108
|
* @returns {boolean} true, якщо це `…/ua/kustomization.yaml`
|
|
107
109
|
*/
|
|
@@ -473,43 +475,41 @@ export function jsonPatchTextSetsServiceTypeNodePort(patchText) {
|
|
|
473
475
|
return true
|
|
474
476
|
}
|
|
475
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Витягує ім'я та текст patch для Service з елемента patches.
|
|
480
|
+
* @param {unknown} p елемент масиву patches
|
|
481
|
+
* @returns {{ name: string, patchStr: string } | null} ім'я та текст patch або null
|
|
482
|
+
*/
|
|
483
|
+
function extractServicePatchEntry(p) {
|
|
484
|
+
if (p === null || typeof p !== 'object' || Array.isArray(p)) return null
|
|
485
|
+
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
486
|
+
const target = pr.target
|
|
487
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) return null
|
|
488
|
+
const tg = /** @type {Record<string, unknown>} */ (target)
|
|
489
|
+
if (tg.kind !== 'Service' || typeof tg.name !== 'string' || tg.name.trim() === '') return null
|
|
490
|
+
const patchStr = pr.patch
|
|
491
|
+
if (typeof patchStr !== 'string' || patchStr.trim() === '') return null
|
|
492
|
+
return { name: tg.name, patchStr }
|
|
493
|
+
}
|
|
494
|
+
|
|
476
495
|
/**
|
|
477
496
|
* З одного документа **Kustomization** збирає пари **Service name → patch text** для **inline patches** з **target.kind: Service**.
|
|
478
497
|
* @param {import('yaml').Document} doc документ після **parseAllDocuments**
|
|
479
|
-
* @returns {Map<string, string>}
|
|
498
|
+
* @returns {Map<string, string>} ім'я сервісу → текст **patch**
|
|
480
499
|
*/
|
|
481
500
|
function collectAbieServicePatchTextsByNameFromKustomizationDoc(doc) {
|
|
482
501
|
/** @type {Map<string, string>} */
|
|
483
502
|
const out = new Map()
|
|
484
|
-
if (doc.errors.length > 0)
|
|
485
|
-
return out
|
|
486
|
-
}
|
|
503
|
+
if (doc.errors.length > 0) return out
|
|
487
504
|
const root = doc.toJSON()
|
|
488
|
-
if (root === null || typeof root !== 'object' || Array.isArray(root))
|
|
489
|
-
return out
|
|
490
|
-
}
|
|
505
|
+
if (root === null || typeof root !== 'object' || Array.isArray(root)) return out
|
|
491
506
|
const rec = /** @type {Record<string, unknown>} */ (root)
|
|
492
|
-
if (rec.kind !== 'Kustomization')
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
499
|
-
for (const p of patches) {
|
|
500
|
-
if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
|
|
501
|
-
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
502
|
-
const target = pr.target
|
|
503
|
-
if (target !== null && typeof target === 'object' && !Array.isArray(target)) {
|
|
504
|
-
const tg = /** @type {Record<string, unknown>} */ (target)
|
|
505
|
-
if (tg.kind === 'Service' && typeof tg.name === 'string' && tg.name.trim() !== '') {
|
|
506
|
-
const patchStr = pr.patch
|
|
507
|
-
if (typeof patchStr === 'string' && patchStr.trim() !== '') {
|
|
508
|
-
const prev = out.get(tg.name)
|
|
509
|
-
out.set(tg.name, prev === undefined ? patchStr : `${prev}\n${patchStr}`)
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
507
|
+
if (rec.kind !== 'Kustomization' || !Array.isArray(rec.patches)) return out
|
|
508
|
+
for (const p of rec.patches) {
|
|
509
|
+
const entry = extractServicePatchEntry(p)
|
|
510
|
+
if (entry) {
|
|
511
|
+
const prev = out.get(entry.name)
|
|
512
|
+
out.set(entry.name, prev === undefined ? entry.patchStr : `${prev}\n${entry.patchStr}`)
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
515
|
return out
|
|
@@ -518,7 +518,7 @@ function collectAbieServicePatchTextsByNameFromKustomizationDoc(doc) {
|
|
|
518
518
|
/**
|
|
519
519
|
* Збирає тексти **patch** на **Service** з **kustomization.yaml** (усі документи).
|
|
520
520
|
* @param {string} raw повний текст **kustomization.yaml**
|
|
521
|
-
* @returns {Map<string, string>} **target.name** →
|
|
521
|
+
* @returns {Map<string, string>} **target.name** → об'єднаний текст **patch**
|
|
522
522
|
*/
|
|
523
523
|
function collectAbieRuServicePatchTextByTargetNameFromRaw(raw) {
|
|
524
524
|
const body = stripBom(raw)
|
|
@@ -544,41 +544,46 @@ function collectAbieRuServicePatchTextByTargetNameFromRaw(raw) {
|
|
|
544
544
|
return byName
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
+
/**
|
|
548
|
+
* Збирає помилки patch для одного Service за ім'ям.
|
|
549
|
+
* @param {string} name ім'я Service
|
|
550
|
+
* @param {string | undefined} pt текст patch або undefined
|
|
551
|
+
* @param {{ requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove?: boolean } | undefined} flags прапорці
|
|
552
|
+
* @param {string[]} errors масив для запису помилок
|
|
553
|
+
*/
|
|
554
|
+
function collectServicePatchErrors(name, pt, flags, errors) {
|
|
555
|
+
if (pt === undefined || String(pt).trim() === '') {
|
|
556
|
+
errors.push(`${name}: немає inline patch для kind: Service`)
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
if (!jsonPatchTextSetsServiceTypeNodePort(pt)) {
|
|
560
|
+
errors.push(`${name}: потрібен JSON6902 path /spec/type та value NodePort`)
|
|
561
|
+
}
|
|
562
|
+
if (flags?.requiresClusterIPNoneClear === true && !jsonPatchTextClearsHeadlessServiceClusterIPNone(pt)) {
|
|
563
|
+
errors.push(
|
|
564
|
+
`${name}: для spec.clusterIP: None додай у той самий patch op: remove для path /spec/clusterIP (abie.mdc)`
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
if (flags?.requiresClusterIPsRemove === true && !jsonPatchRemovesPath(pt, '/spec/clusterIPs')) {
|
|
568
|
+
errors.push(
|
|
569
|
+
`${name}: у base задано spec.clusterIPs — додай op: remove для path /spec/clusterIPs (інакше NodePort з None у clusterIPs; abie.mdc)`
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
547
574
|
/**
|
|
548
575
|
* Повідомлення про порушення patch **Service** у **ru/kustomization.yaml** (abie.mdc).
|
|
549
576
|
* @param {string} raw повний текст **kustomization.yaml**
|
|
550
|
-
* @param {Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove?: boolean }>} targetsByName
|
|
577
|
+
* @param {Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove?: boolean }>} targetsByName ім'я **Service** → прапорці patch
|
|
551
578
|
* @returns {string[]} порожньо, якщо все OK
|
|
552
579
|
*/
|
|
553
580
|
export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
|
|
554
|
-
if (targetsByName.size === 0)
|
|
555
|
-
return []
|
|
556
|
-
}
|
|
581
|
+
if (targetsByName.size === 0) return []
|
|
557
582
|
const byName = collectAbieRuServicePatchTextByTargetNameFromRaw(raw)
|
|
558
583
|
/** @type {string[]} */
|
|
559
584
|
const errors = []
|
|
560
585
|
for (const name of [...targetsByName.keys()].toSorted((a, b) => a.localeCompare(b))) {
|
|
561
|
-
|
|
562
|
-
const requiresClusterIPRemove = flags?.requiresClusterIPNoneClear === true
|
|
563
|
-
const requiresClusterIPsRemove = flags?.requiresClusterIPsRemove === true
|
|
564
|
-
const pt = byName.get(name)
|
|
565
|
-
if (pt === undefined || String(pt).trim() === '') {
|
|
566
|
-
errors.push(`${name}: немає inline patch для kind: Service`)
|
|
567
|
-
} else {
|
|
568
|
-
if (!jsonPatchTextSetsServiceTypeNodePort(pt)) {
|
|
569
|
-
errors.push(`${name}: потрібен JSON6902 path /spec/type та value NodePort`)
|
|
570
|
-
}
|
|
571
|
-
if (requiresClusterIPRemove && !jsonPatchTextClearsHeadlessServiceClusterIPNone(pt)) {
|
|
572
|
-
errors.push(
|
|
573
|
-
`${name}: для spec.clusterIP: None додай у той самий patch op: remove для path /spec/clusterIP (abie.mdc)`
|
|
574
|
-
)
|
|
575
|
-
}
|
|
576
|
-
if (requiresClusterIPsRemove && !jsonPatchRemovesPath(pt, '/spec/clusterIPs')) {
|
|
577
|
-
errors.push(
|
|
578
|
-
`${name}: у base задано spec.clusterIPs — додай op: remove для path /spec/clusterIPs (інакше NodePort з None у clusterIPs; abie.mdc)`
|
|
579
|
-
)
|
|
580
|
-
}
|
|
581
|
-
}
|
|
586
|
+
collectServicePatchErrors(name, byName.get(name), targetsByName.get(name), errors)
|
|
582
587
|
}
|
|
583
588
|
return errors
|
|
584
589
|
}
|
|
@@ -588,68 +593,64 @@ export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
|
|
|
588
593
|
* @param {string} root корінь репозиторію
|
|
589
594
|
* @param {string[]} yamlAbs абсолютні шляхи yaml під **k8s**
|
|
590
595
|
* @param {(msg: string) => void} fail реєстрація помилки читання/парсингу
|
|
591
|
-
* @returns {Promise<Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>>} **pkgAbs** → (
|
|
596
|
+
* @returns {Promise<Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>>} **pkgAbs** → (**ім'я** → прапорці)
|
|
597
|
+
*/
|
|
598
|
+
/**
|
|
599
|
+
* Обробляє один Service-документ для збору NodePort-патч цілей.
|
|
600
|
+
* @param {unknown} obj YAML-документ (toJSON)
|
|
601
|
+
* @param {string} pkgAbs абсолютний шлях до пакета
|
|
602
|
+
* @param {Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>} map результуючий Map
|
|
603
|
+
*/
|
|
604
|
+
function processServiceDocForNodePortTargets(obj, pkgAbs, map) {
|
|
605
|
+
if (!serviceDocumentRequiresAbieRuNodePortOverlay(obj)) return
|
|
606
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
607
|
+
const meta = /** @type {Record<string, unknown>} */ (rec.metadata)
|
|
608
|
+
const n = meta.name
|
|
609
|
+
if (typeof n !== 'string' || n.trim() === '') return
|
|
610
|
+
let inner = map.get(pkgAbs)
|
|
611
|
+
if (!inner) {
|
|
612
|
+
inner = new Map()
|
|
613
|
+
map.set(pkgAbs, inner)
|
|
614
|
+
}
|
|
615
|
+
const needClear = serviceDocumentRequiresRuClusterIPNoneRemoval(obj)
|
|
616
|
+
const needClusterIPsRemove = serviceDocumentBaseDeclaresClusterIPsField(obj)
|
|
617
|
+
const prev = inner.get(n)
|
|
618
|
+
inner.set(n, {
|
|
619
|
+
requiresClusterIPNoneClear: prev?.requiresClusterIPNoneClear === true || needClear,
|
|
620
|
+
requiresClusterIPsRemove: prev?.requiresClusterIPsRemove === true || needClusterIPsRemove
|
|
621
|
+
})
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Обробляє YAML-документи з одного файлу для збору NodePort-патч цілей.
|
|
626
|
+
* @param {import('yaml').Document[]} docs документи з файлу
|
|
627
|
+
* @param {string} pkgAbs абсолютний шлях пакета
|
|
628
|
+
* @param {Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>} map результуючий Map
|
|
629
|
+
*/
|
|
630
|
+
function collectNodePortTargetsFromDocs(docs, pkgAbs, map) {
|
|
631
|
+
for (const doc of docs) {
|
|
632
|
+
if (doc.errors.length === 0) {
|
|
633
|
+
processServiceDocForNodePortTargets(doc.toJSON(), pkgAbs, map)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Для кожного пакета збирає **Service**, які в overlay **ru** мають стати **NodePort** (abie.mdc).
|
|
640
|
+
* @param {string} root корінь репозиторію
|
|
641
|
+
* @param {string[]} yamlAbs абсолютні шляхи yaml під **k8s**
|
|
642
|
+
* @param {(msg: string) => void} fail реєстрація помилки читання/парсингу
|
|
643
|
+
* @returns {Promise<Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>>} пакет → назва сервісу → прапори NodePort
|
|
592
644
|
*/
|
|
593
645
|
async function collectAbieRuNodePortServiceTargetsByPackage(root, yamlAbs, fail) {
|
|
594
646
|
/** @type {Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>} */
|
|
595
647
|
const map = new Map()
|
|
596
648
|
for (const abs of yamlAbs) {
|
|
597
649
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
let readOk = false
|
|
603
|
-
try {
|
|
604
|
-
raw = await readFile(abs, 'utf8')
|
|
605
|
-
readOk = true
|
|
606
|
-
} catch (error) {
|
|
607
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
608
|
-
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
609
|
-
}
|
|
610
|
-
if (readOk) {
|
|
611
|
-
const body = stripBom(raw)
|
|
612
|
-
const lines = body.split(LINE_SPLIT_RE)
|
|
613
|
-
const first = lines[0] ?? ''
|
|
614
|
-
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
615
|
-
/** @type {import('yaml').Document[]} */
|
|
616
|
-
let docs
|
|
617
|
-
let parseOk = false
|
|
618
|
-
try {
|
|
619
|
-
docs = parseAllDocuments(rest)
|
|
620
|
-
parseOk = true
|
|
621
|
-
} catch (error) {
|
|
622
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
623
|
-
fail(`${rel}: YAML (${msg})`)
|
|
624
|
-
}
|
|
625
|
-
if (parseOk) {
|
|
626
|
-
for (const doc of docs) {
|
|
627
|
-
if (doc.errors.length === 0) {
|
|
628
|
-
const obj = doc.toJSON()
|
|
629
|
-
if (serviceDocumentRequiresAbieRuNodePortOverlay(obj)) {
|
|
630
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
631
|
-
const meta = /** @type {Record<string, unknown>} */ (rec.metadata)
|
|
632
|
-
const n = meta.name
|
|
633
|
-
if (typeof n === 'string' && n.trim() !== '') {
|
|
634
|
-
let inner = map.get(pkgAbs)
|
|
635
|
-
if (!inner) {
|
|
636
|
-
inner = new Map()
|
|
637
|
-
map.set(pkgAbs, inner)
|
|
638
|
-
}
|
|
639
|
-
const needClear = serviceDocumentRequiresRuClusterIPNoneRemoval(obj)
|
|
640
|
-
const needClusterIPsRemove = serviceDocumentBaseDeclaresClusterIPsField(obj)
|
|
641
|
-
const prev = inner.get(n)
|
|
642
|
-
inner.set(n, {
|
|
643
|
-
requiresClusterIPNoneClear: prev?.requiresClusterIPNoneClear === true || needClear,
|
|
644
|
-
requiresClusterIPsRemove: prev?.requiresClusterIPsRemove === true || needClusterIPsRemove
|
|
645
|
-
})
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
650
|
+
const pkgAbs = k8sYamlRelOutsideUaRuOverlays(rel) ? abiePackageDirFromK8sYamlRel(root, rel) : null
|
|
651
|
+
if (pkgAbs) {
|
|
652
|
+
const docs = await readAndParseYamlDocs(abs, rel, fail)
|
|
653
|
+
if (docs) collectNodePortTargetsFromDocs(docs, pkgAbs, map)
|
|
653
654
|
}
|
|
654
655
|
}
|
|
655
656
|
return map
|
|
@@ -709,38 +710,12 @@ async function collectDeploymentDirs(root, yamlAbs, fail) {
|
|
|
709
710
|
/** @type {Set<string>} */
|
|
710
711
|
const dirs = new Set()
|
|
711
712
|
for (const abs of yamlAbs) {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
719
|
-
fail(`${relative(root, abs) || abs}: не вдалося прочитати (${msg})`)
|
|
720
|
-
}
|
|
721
|
-
if (readOk) {
|
|
722
|
-
const body = stripBom(raw)
|
|
723
|
-
const lines = body.split(LINE_SPLIT_RE)
|
|
724
|
-
const first = lines[0] ?? ''
|
|
725
|
-
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
726
|
-
/** @type {import('yaml').Document[]} */
|
|
727
|
-
let docs
|
|
728
|
-
let parseOk = false
|
|
729
|
-
try {
|
|
730
|
-
docs = parseAllDocuments(rest)
|
|
731
|
-
parseOk = true
|
|
732
|
-
} catch (error) {
|
|
733
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
734
|
-
fail(`${relative(root, abs) || abs}: YAML (${msg})`)
|
|
735
|
-
}
|
|
736
|
-
if (parseOk) {
|
|
737
|
-
for (const doc of docs) {
|
|
738
|
-
if (doc.errors.length === 0) {
|
|
739
|
-
const obj = doc.toJSON()
|
|
740
|
-
if (isDeploymentDoc(obj)) {
|
|
741
|
-
dirs.add(dirname(abs))
|
|
742
|
-
}
|
|
743
|
-
}
|
|
713
|
+
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
714
|
+
const docs = await readAndParseYamlDocs(abs, rel, fail)
|
|
715
|
+
if (docs) {
|
|
716
|
+
for (const doc of docs) {
|
|
717
|
+
if (doc.errors.length === 0 && isDeploymentDoc(doc.toJSON())) {
|
|
718
|
+
dirs.add(dirname(abs))
|
|
744
719
|
}
|
|
745
720
|
}
|
|
746
721
|
}
|
|
@@ -748,6 +723,31 @@ async function collectDeploymentDirs(root, yamlAbs, fail) {
|
|
|
748
723
|
return dirs
|
|
749
724
|
}
|
|
750
725
|
|
|
726
|
+
/**
|
|
727
|
+
* Перевіряє документи з одного файлу на наявність Deployment з preem nodeSelector.
|
|
728
|
+
* @param {import('yaml').Document[]} docs документи з файлу
|
|
729
|
+
* @param {string} rel відносний шлях файлу
|
|
730
|
+
* @param {(msg: string) => void} fail callback
|
|
731
|
+
* @returns {'violation' | 'found' | 'none'} результат перевірки
|
|
732
|
+
*/
|
|
733
|
+
function checkBaseDeploymentDocsForPreem(docs, rel, fail) {
|
|
734
|
+
for (const doc of docs) {
|
|
735
|
+
if (doc.errors.length === 0) {
|
|
736
|
+
const obj = doc.toJSON()
|
|
737
|
+
if (isDeploymentDoc(obj)) {
|
|
738
|
+
if (!deploymentDocumentHasAbieBasePreemNodeSelector(obj)) {
|
|
739
|
+
fail(
|
|
740
|
+
`${rel}: Deployment у base: потрібен spec.template.spec.nodeSelector.preem: true (або 'true') — abie.mdc`
|
|
741
|
+
)
|
|
742
|
+
return 'violation'
|
|
743
|
+
}
|
|
744
|
+
return 'found'
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return 'none'
|
|
749
|
+
}
|
|
750
|
+
|
|
751
751
|
/**
|
|
752
752
|
* Для кожного **Deployment** у YAML під **`k8s`** з шляхом **`…/base/…`** вимагає **`spec.template.spec.nodeSelector.preem: true`** (abie.mdc).
|
|
753
753
|
* @param {string} root корінь репозиторію
|
|
@@ -757,48 +757,15 @@ async function collectDeploymentDirs(root, yamlAbs, fail) {
|
|
|
757
757
|
* @returns {Promise<void>}
|
|
758
758
|
*/
|
|
759
759
|
async function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fail, passFn) {
|
|
760
|
-
const baseFiles = yamlFilesAbs.filter(abs =>
|
|
761
|
-
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
762
|
-
return isAbieK8sBaseYamlPath(rel)
|
|
763
|
-
})
|
|
760
|
+
const baseFiles = yamlFilesAbs.filter(abs => isAbieK8sBaseYamlPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
764
761
|
let anyBaseDeployment = false
|
|
765
762
|
for (const abs of baseFiles) {
|
|
766
763
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
773
|
-
return
|
|
774
|
-
}
|
|
775
|
-
const body = stripBom(raw)
|
|
776
|
-
const lines = body.split(LINE_SPLIT_RE)
|
|
777
|
-
const first = lines[0] ?? ''
|
|
778
|
-
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
779
|
-
/** @type {import('yaml').Document[]} */
|
|
780
|
-
let docs
|
|
781
|
-
try {
|
|
782
|
-
docs = parseAllDocuments(rest)
|
|
783
|
-
} catch (error) {
|
|
784
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
785
|
-
fail(`${rel}: YAML (${msg})`)
|
|
786
|
-
return
|
|
787
|
-
}
|
|
788
|
-
for (const doc of docs) {
|
|
789
|
-
if (doc.errors.length === 0) {
|
|
790
|
-
const obj = doc.toJSON()
|
|
791
|
-
if (isDeploymentDoc(obj)) {
|
|
792
|
-
anyBaseDeployment = true
|
|
793
|
-
if (!deploymentDocumentHasAbieBasePreemNodeSelector(obj)) {
|
|
794
|
-
fail(
|
|
795
|
-
`${rel}: Deployment у base: потрібен spec.template.spec.nodeSelector.preem: true (або 'true') — abie.mdc`
|
|
796
|
-
)
|
|
797
|
-
return
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
}
|
|
764
|
+
const docs = await readAndParseYamlDocs(abs, rel, fail)
|
|
765
|
+
if (!docs) return
|
|
766
|
+
const r = checkBaseDeploymentDocsForPreem(docs, rel, fail)
|
|
767
|
+
if (r === 'violation') return
|
|
768
|
+
if (r === 'found') anyBaseDeployment = true
|
|
802
769
|
}
|
|
803
770
|
if (anyBaseDeployment) {
|
|
804
771
|
passFn('Deployment у …/base/…: nodeSelector.preem відповідає abie.mdc')
|
|
@@ -816,6 +783,46 @@ function stripBom(s) {
|
|
|
816
783
|
return s.startsWith('\uFEFF') ? s.slice(1) : s
|
|
817
784
|
}
|
|
818
785
|
|
|
786
|
+
/**
|
|
787
|
+
* Зчитує та парсить YAML-документи з файлу.
|
|
788
|
+
* При помилці читання викликає `failFn` і повертає `null`.
|
|
789
|
+
* При помилці парсингу викликає `failFn` і повертає `null`.
|
|
790
|
+
* Автоматично видаляє BOM та modeline (перший рядок з `$schema`).
|
|
791
|
+
* @param {string} abs абсолютний шлях до файлу
|
|
792
|
+
* @param {string} rel відносний шлях (для повідомлень)
|
|
793
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
794
|
+
* @returns {Promise<import('yaml').Document[] | null>} масив документів або null при помилці
|
|
795
|
+
*/
|
|
796
|
+
async function readAndParseYamlDocs(abs, rel, failFn) {
|
|
797
|
+
let raw
|
|
798
|
+
try {
|
|
799
|
+
raw = await readFile(abs, 'utf8')
|
|
800
|
+
} catch (error) {
|
|
801
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
802
|
+
failFn(`${rel}: не вдалося прочитати (${msg})`)
|
|
803
|
+
return null
|
|
804
|
+
}
|
|
805
|
+
const body = stripBom(raw)
|
|
806
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
807
|
+
const first = lines[0] ?? ''
|
|
808
|
+
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
809
|
+
try {
|
|
810
|
+
return parseAllDocuments(rest)
|
|
811
|
+
} catch (error) {
|
|
812
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
813
|
+
failFn(`${rel}: YAML (${msg})`)
|
|
814
|
+
return null
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* No-op fail handler для функцій, що повертають null/порожній масив при помилці.
|
|
820
|
+
* @param {string} _msg повідомлення ігнорується
|
|
821
|
+
*/
|
|
822
|
+
const silentFail = _msg => {
|
|
823
|
+
/* silent — пошкоджені файли ловить check-k8s */
|
|
824
|
+
}
|
|
825
|
+
|
|
819
826
|
/**
|
|
820
827
|
* Чи рядок inline JSON6902 patch містить очікуваний **ua** nodeSelector (**preem: false** на **`/spec/template/spec/nodeSelector`**).
|
|
821
828
|
* Конкретний **`op`** не перевіряється — див. **k8s.mdc**.
|
|
@@ -959,6 +966,24 @@ export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelF
|
|
|
959
966
|
return !after.startsWith('ua/') && !after.startsWith('ru/')
|
|
960
967
|
}
|
|
961
968
|
|
|
969
|
+
/**
|
|
970
|
+
* Перевіряє один backendRef на відповідність abie.mdc.
|
|
971
|
+
* @param {unknown} br параметр br
|
|
972
|
+
* @param {string} rel відносний шлях (для повідомлень)
|
|
973
|
+
* @param {string[]} errors масив для запису помилок
|
|
974
|
+
* @returns {number} 1 якщо знайдено shared backend, 0 інакше
|
|
975
|
+
*/
|
|
976
|
+
function checkSharedBackendRef(br, rel, errors) {
|
|
977
|
+
if (br === null || typeof br !== 'object' || Array.isArray(br)) return 0
|
|
978
|
+
const brRec = /** @type {Record<string, unknown>} */ (br)
|
|
979
|
+
const name = brRec.name
|
|
980
|
+
if (typeof name !== 'string' || !ABIE_SHARED_CROSS_NS_BACKEND_SET.has(name)) return 0
|
|
981
|
+
if (typeof brRec.namespace !== 'string' || brRec.namespace !== 'dev') {
|
|
982
|
+
errors.push(`${rel}: HTTPRoute backendRefs до ${name} має містити namespace: dev (abie.mdc)`)
|
|
983
|
+
}
|
|
984
|
+
return 1
|
|
985
|
+
}
|
|
986
|
+
|
|
962
987
|
/**
|
|
963
988
|
* З HTTPRoute-документа рахує **`backendRefs`** до **`auth-run-hl`** / **`filelint-hl`** і порушення **`namespace: dev`**.
|
|
964
989
|
* @param {unknown} obj корінь YAML
|
|
@@ -968,38 +993,20 @@ export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelF
|
|
|
968
993
|
function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
|
|
969
994
|
/** @type {string[]} */
|
|
970
995
|
const errors = []
|
|
971
|
-
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
972
|
-
return { refCount: 0, errors }
|
|
973
|
-
}
|
|
996
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return { refCount: 0, errors }
|
|
974
997
|
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
975
|
-
if (rec.kind !== 'HTTPRoute') {
|
|
976
|
-
return { refCount: 0, errors }
|
|
977
|
-
}
|
|
998
|
+
if (rec.kind !== 'HTTPRoute') return { refCount: 0, errors }
|
|
978
999
|
const spec = rec.spec
|
|
979
|
-
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
980
|
-
return { refCount: 0, errors }
|
|
981
|
-
}
|
|
1000
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return { refCount: 0, errors }
|
|
982
1001
|
const rules = /** @type {Record<string, unknown>} */ (spec).rules
|
|
983
|
-
if (!Array.isArray(rules)) {
|
|
984
|
-
return { refCount: 0, errors }
|
|
985
|
-
}
|
|
1002
|
+
if (!Array.isArray(rules)) return { refCount: 0, errors }
|
|
986
1003
|
let refCount = 0
|
|
987
1004
|
for (const rule of rules) {
|
|
988
1005
|
if (rule !== null && typeof rule === 'object' && !Array.isArray(rule)) {
|
|
989
1006
|
const brs = /** @type {Record<string, unknown>} */ (rule).backendRefs
|
|
990
1007
|
if (Array.isArray(brs)) {
|
|
991
1008
|
for (const br of brs) {
|
|
992
|
-
|
|
993
|
-
const brRec = /** @type {Record<string, unknown>} */ (br)
|
|
994
|
-
const name = brRec.name
|
|
995
|
-
if (typeof name === 'string' && ABIE_SHARED_CROSS_NS_BACKEND_SET.has(name)) {
|
|
996
|
-
refCount++
|
|
997
|
-
const ns = brRec.namespace
|
|
998
|
-
if (typeof ns !== 'string' || ns !== 'dev') {
|
|
999
|
-
errors.push(`${rel}: HTTPRoute backendRefs до ${name} має містити namespace: dev (abie.mdc)`)
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1009
|
+
refCount += checkSharedBackendRef(br, rel, errors)
|
|
1003
1010
|
}
|
|
1004
1011
|
}
|
|
1005
1012
|
}
|
|
@@ -1022,32 +1029,13 @@ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yam
|
|
|
1022
1029
|
for (const abs of yamlFilesAbs) {
|
|
1023
1030
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1024
1031
|
if (isK8sYamlInAbiePackageExcludingUaRuOverlays(rel, pkgRel)) {
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
const body = stripBom(raw)
|
|
1033
|
-
const lines = body.split(LINE_SPLIT_RE)
|
|
1034
|
-
const first = lines[0] ?? ''
|
|
1035
|
-
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
1036
|
-
/** @type {import('yaml').Document[] | undefined} */
|
|
1037
|
-
let docs
|
|
1038
|
-
try {
|
|
1039
|
-
docs = parseAllDocuments(rest)
|
|
1040
|
-
} catch {
|
|
1041
|
-
docs = undefined
|
|
1042
|
-
}
|
|
1043
|
-
if (docs !== undefined) {
|
|
1044
|
-
for (const doc of docs) {
|
|
1045
|
-
if (doc.errors.length === 0) {
|
|
1046
|
-
const obj = doc.toJSON()
|
|
1047
|
-
const st = httpRouteDocSharedCrossNsBackendStats(obj, rel)
|
|
1048
|
-
refCount += st.refCount
|
|
1049
|
-
baseErrors.push(...st.errors)
|
|
1050
|
-
}
|
|
1032
|
+
const docs = await readAndParseYamlDocs(abs, rel, silentFail)
|
|
1033
|
+
if (docs) {
|
|
1034
|
+
for (const doc of docs) {
|
|
1035
|
+
if (doc.errors.length === 0) {
|
|
1036
|
+
const st = httpRouteDocSharedCrossNsBackendStats(doc.toJSON(), rel)
|
|
1037
|
+
refCount += st.refCount
|
|
1038
|
+
baseErrors.push(...st.errors)
|
|
1051
1039
|
}
|
|
1052
1040
|
}
|
|
1053
1041
|
}
|
|
@@ -1081,43 +1069,38 @@ const ABIE_RU_HTTPROUTE_HOST_MARKERS = [
|
|
|
1081
1069
|
'*.выбирайонлайн.рф'
|
|
1082
1070
|
]
|
|
1083
1071
|
|
|
1072
|
+
/**
|
|
1073
|
+
* Витягує текст patch HTTPRoute з елемента patches.
|
|
1074
|
+
* @param {unknown} p елемент масиву patches
|
|
1075
|
+
* @returns {string | null} текст patch або null
|
|
1076
|
+
*/
|
|
1077
|
+
function extractHttpRoutePatchString(p) {
|
|
1078
|
+
if (p === null || typeof p !== 'object' || Array.isArray(p)) return null
|
|
1079
|
+
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
1080
|
+
const target = pr.target
|
|
1081
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) return null
|
|
1082
|
+
const tg = /** @type {Record<string, unknown>} */ (target)
|
|
1083
|
+
if (tg.kind !== 'HTTPRoute' || typeof tg.name !== 'string' || tg.name.trim() === '') return null
|
|
1084
|
+
const patchStr = pr.patch
|
|
1085
|
+
return typeof patchStr === 'string' && patchStr.trim() !== '' ? patchStr : null
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1084
1088
|
/**
|
|
1085
1089
|
* Збирає тексти inline **patch** для **HTTPRoute** (будь-який непорожній **target.name**) з одного документа **Kustomization**.
|
|
1086
1090
|
* @param {import('yaml').Document} doc документ після **parseAllDocuments**
|
|
1087
1091
|
* @returns {string[]} непорожні рядки **patch**
|
|
1088
1092
|
*/
|
|
1089
1093
|
function collectAbieHttpRoutePatchStringsFromKustomizationDoc(doc) {
|
|
1090
|
-
if (doc.errors.length > 0)
|
|
1091
|
-
return []
|
|
1092
|
-
}
|
|
1094
|
+
if (doc.errors.length > 0) return []
|
|
1093
1095
|
const root = doc.toJSON()
|
|
1094
|
-
if (root === null || typeof root !== 'object' || Array.isArray(root))
|
|
1095
|
-
return []
|
|
1096
|
-
}
|
|
1096
|
+
if (root === null || typeof root !== 'object' || Array.isArray(root)) return []
|
|
1097
1097
|
const rec = /** @type {Record<string, unknown>} */ (root)
|
|
1098
|
-
if (rec.kind !== 'Kustomization')
|
|
1099
|
-
return []
|
|
1100
|
-
}
|
|
1101
|
-
const patches = rec.patches
|
|
1102
|
-
if (!Array.isArray(patches)) {
|
|
1103
|
-
return []
|
|
1104
|
-
}
|
|
1098
|
+
if (rec.kind !== 'Kustomization' || !Array.isArray(rec.patches)) return []
|
|
1105
1099
|
/** @type {string[]} */
|
|
1106
1100
|
const out = []
|
|
1107
|
-
for (const p of patches) {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
const target = pr.target
|
|
1111
|
-
if (target !== null && typeof target === 'object' && !Array.isArray(target)) {
|
|
1112
|
-
const tg = /** @type {Record<string, unknown>} */ (target)
|
|
1113
|
-
if (tg.kind === 'HTTPRoute' && typeof tg.name === 'string' && tg.name.trim() !== '') {
|
|
1114
|
-
const patchStr = pr.patch
|
|
1115
|
-
if (typeof patchStr === 'string' && patchStr.trim() !== '') {
|
|
1116
|
-
out.push(patchStr)
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1101
|
+
for (const p of rec.patches) {
|
|
1102
|
+
const s = extractHttpRoutePatchString(p)
|
|
1103
|
+
if (s !== null) out.push(s)
|
|
1121
1104
|
}
|
|
1122
1105
|
return out
|
|
1123
1106
|
}
|
|
@@ -1208,54 +1191,12 @@ export function kustomizationHasAbieNginxRunHttpRoutePatch(raw, mode) {
|
|
|
1208
1191
|
}
|
|
1209
1192
|
|
|
1210
1193
|
/**
|
|
1211
|
-
* Перевіряє
|
|
1212
|
-
* @param {string}
|
|
1213
|
-
* @param {string} relPath відносний шлях для повідомлень
|
|
1214
|
-
* @returns {string | null} текст помилки або
|
|
1194
|
+
* Перевіряє об'єкт HealthCheckPolicy на відповідність abie.mdc.
|
|
1195
|
+
* @param {Record<string, unknown>} policy розібраний HealthCheckPolicy
|
|
1196
|
+
* @param {string} relPath відносний шлях (для повідомлень)
|
|
1197
|
+
* @returns {string | null} текст помилки або null якщо OK
|
|
1215
1198
|
*/
|
|
1216
|
-
|
|
1217
|
-
const body = stripBom(raw)
|
|
1218
|
-
const lines = body.split(LINE_SPLIT_RE)
|
|
1219
|
-
if (lines.length === 0 || lines[0].trim() === '') {
|
|
1220
|
-
return `${relPath}: перший рядок порожній — потрібен # yaml-language-server: $schema=… (abie.mdc)`
|
|
1221
|
-
}
|
|
1222
|
-
const m = lines[0].match(MODELINE_RE)
|
|
1223
|
-
if (!m) {
|
|
1224
|
-
return `${relPath}: перший рядок має бути modeline $schema (abie.mdc)`
|
|
1225
|
-
}
|
|
1226
|
-
if (m[1] !== ABIE_HC_SCHEMA_URL) {
|
|
1227
|
-
return `${relPath}: $schema має бути\n ${ABIE_HC_SCHEMA_URL}\n (abie.mdc)`
|
|
1228
|
-
}
|
|
1229
|
-
const yamlBody = lines
|
|
1230
|
-
.slice(1)
|
|
1231
|
-
.join('\n')
|
|
1232
|
-
.replace(LEADING_EMPTY_LINE_RE, '')
|
|
1233
|
-
/** @type {import('yaml').Document[]} */
|
|
1234
|
-
let docs
|
|
1235
|
-
try {
|
|
1236
|
-
docs = parseAllDocuments(yamlBody)
|
|
1237
|
-
} catch (error) {
|
|
1238
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1239
|
-
return `${relPath}: не вдалося розібрати YAML (${msg})`
|
|
1240
|
-
}
|
|
1241
|
-
/** @type {Record<string, unknown> | null} */
|
|
1242
|
-
let policy = null
|
|
1243
|
-
for (const doc of docs) {
|
|
1244
|
-
if (doc.errors.length > 0) {
|
|
1245
|
-
return `${relPath}: YAML: ${doc.errors.map(e => e.message).join('; ')}`
|
|
1246
|
-
}
|
|
1247
|
-
const obj = doc.toJSON()
|
|
1248
|
-
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
1249
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
1250
|
-
if (rec.kind === 'HealthCheckPolicy') {
|
|
1251
|
-
policy = rec
|
|
1252
|
-
break
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
if (!policy) {
|
|
1257
|
-
return `${relPath}: очікується документ kind: HealthCheckPolicy (abie.mdc)`
|
|
1258
|
-
}
|
|
1199
|
+
function validateAbieHcPolicy(policy, relPath) {
|
|
1259
1200
|
if (policy.apiVersion !== 'networking.gke.io/v1') {
|
|
1260
1201
|
return `${relPath}: apiVersion має бути networking.gke.io/v1 (abie.mdc)`
|
|
1261
1202
|
}
|
|
@@ -1271,7 +1212,8 @@ export function validateAbieHcYaml(raw, relPath) {
|
|
|
1271
1212
|
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
1272
1213
|
return `${relPath}: відсутній spec (abie.mdc)`
|
|
1273
1214
|
}
|
|
1274
|
-
const
|
|
1215
|
+
const specRec = /** @type {Record<string, unknown>} */ (spec)
|
|
1216
|
+
const def = specRec.default
|
|
1275
1217
|
if (def === null || typeof def !== 'object' || Array.isArray(def)) {
|
|
1276
1218
|
return `${relPath}: відсутній spec.default (abie.mdc)`
|
|
1277
1219
|
}
|
|
@@ -1279,34 +1221,99 @@ export function validateAbieHcYaml(raw, relPath) {
|
|
|
1279
1221
|
if (config === null || typeof config !== 'object' || Array.isArray(config)) {
|
|
1280
1222
|
return `${relPath}: відсутній spec.default.config (abie.mdc)`
|
|
1281
1223
|
}
|
|
1282
|
-
if (config.type !== 'HTTP') {
|
|
1283
|
-
return `${relPath}: spec.default.config.type має бути HTTP (abie.mdc)`
|
|
1284
|
-
}
|
|
1224
|
+
if (config.type !== 'HTTP') return `${relPath}: spec.default.config.type має бути HTTP (abie.mdc)`
|
|
1285
1225
|
const httpHc = /** @type {Record<string, unknown>} */ (config).httpHealthCheck
|
|
1286
1226
|
if (httpHc === null || typeof httpHc !== 'object' || Array.isArray(httpHc)) {
|
|
1287
1227
|
return `${relPath}: відсутній httpHealthCheck (abie.mdc)`
|
|
1288
1228
|
}
|
|
1289
|
-
if (httpHc.requestPath !== '/healthz') {
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
if (httpHc.port !== 8080) {
|
|
1293
|
-
return `${relPath}: httpHealthCheck.port має бути 8080 (abie.mdc)`
|
|
1294
|
-
}
|
|
1295
|
-
const targetRef = /** @type {Record<string, unknown>} */ (spec).targetRef
|
|
1229
|
+
if (httpHc.requestPath !== '/healthz') return `${relPath}: httpHealthCheck.requestPath має бути /healthz (abie.mdc)`
|
|
1230
|
+
if (httpHc.port !== 8080) return `${relPath}: httpHealthCheck.port має бути 8080 (abie.mdc)`
|
|
1231
|
+
const targetRef = specRec.targetRef
|
|
1296
1232
|
if (targetRef === null || typeof targetRef !== 'object' || Array.isArray(targetRef)) {
|
|
1297
1233
|
return `${relPath}: відсутній targetRef (abie.mdc)`
|
|
1298
1234
|
}
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
}
|
|
1302
|
-
const svcName = targetRef.name
|
|
1235
|
+
const tr = /** @type {Record<string, unknown>} */ (targetRef)
|
|
1236
|
+
if (tr.kind !== 'Service') return `${relPath}: targetRef.kind має бути Service (abie.mdc)`
|
|
1303
1237
|
const expectedHl = name.endsWith('-hl') ? name : `${name}-hl`
|
|
1304
|
-
if (typeof
|
|
1238
|
+
if (typeof tr.name !== 'string' || tr.name !== expectedHl) {
|
|
1305
1239
|
return `${relPath}: targetRef.name має посилатися на headless Service (очікується ${expectedHl}, суфікс -hl) (abie.mdc)`
|
|
1306
1240
|
}
|
|
1307
1241
|
return null
|
|
1308
1242
|
}
|
|
1309
1243
|
|
|
1244
|
+
/**
|
|
1245
|
+
* Шукає HealthCheckPolicy серед YAML-документів.
|
|
1246
|
+
* @param {import('yaml').Document[]} docs документи
|
|
1247
|
+
* @param {string} relPath відносний шлях для повідомлень
|
|
1248
|
+
* @returns {{ policy: Record<string, unknown> } | { error: string }} знайдений документ або помилка
|
|
1249
|
+
*/
|
|
1250
|
+
function findHealthCheckPolicyInDocs(docs, relPath) {
|
|
1251
|
+
for (const doc of docs) {
|
|
1252
|
+
if (doc.errors.length > 0) {
|
|
1253
|
+
return { error: `${relPath}: YAML: ${doc.errors.map(e => e.message).join('; ')}` }
|
|
1254
|
+
}
|
|
1255
|
+
const obj = doc.toJSON()
|
|
1256
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
1257
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
1258
|
+
if (rec.kind === 'HealthCheckPolicy') {
|
|
1259
|
+
return { policy: rec }
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return { policy: /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (null)) }
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Перевіряє hc.yaml на відповідність схемі та структурі HealthCheckPolicy (abie.mdc).
|
|
1268
|
+
* @param {string} raw вміст файлу
|
|
1269
|
+
* @param {string} relPath відносний шлях (для повідомлень)
|
|
1270
|
+
* @returns {string | null} null якщо OK, рядок з помилкою
|
|
1271
|
+
*/
|
|
1272
|
+
export function validateAbieHcYaml(raw, relPath) {
|
|
1273
|
+
const body = stripBom(raw)
|
|
1274
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
1275
|
+
if (lines.length === 0 || lines[0].trim() === '') {
|
|
1276
|
+
return `${relPath}: перший рядок порожній — потрібен # yaml-language-server: $schema=… (abie.mdc)`
|
|
1277
|
+
}
|
|
1278
|
+
const m = lines[0].match(MODELINE_RE)
|
|
1279
|
+
if (!m) return `${relPath}: перший рядок має бути modeline $schema (abie.mdc)`
|
|
1280
|
+
if (m[1] !== ABIE_HC_SCHEMA_URL) return `${relPath}: $schema має бути\n ${ABIE_HC_SCHEMA_URL}\n (abie.mdc)`
|
|
1281
|
+
|
|
1282
|
+
const yamlBody = lines.slice(1).join('\n').replace(LEADING_EMPTY_LINE_RE, '')
|
|
1283
|
+
/** @type {import('yaml').Document[]} */
|
|
1284
|
+
let docs
|
|
1285
|
+
try {
|
|
1286
|
+
docs = parseAllDocuments(yamlBody)
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
1289
|
+
return `${relPath}: не вдалося розібрати YAML (${msg})`
|
|
1290
|
+
}
|
|
1291
|
+
const result = findHealthCheckPolicyInDocs(docs, relPath)
|
|
1292
|
+
if ('error' in result) return result.error
|
|
1293
|
+
if (!result.policy) return `${relPath}: очікується документ kind: HealthCheckPolicy (abie.mdc)`
|
|
1294
|
+
return validateAbieHcPolicy(result.policy, relPath)
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* Збирає відносний шлях із документів, що містять HealthCheckPolicy.
|
|
1299
|
+
* @param {import('yaml').Document[]} docs документи з файлу
|
|
1300
|
+
* @param {string} rel відносний шлях файлу
|
|
1301
|
+
* @param {string[]} out масив для запису шляхів
|
|
1302
|
+
*/
|
|
1303
|
+
function collectHealthCheckPolicyRelFromDocs(docs, rel, out) {
|
|
1304
|
+
for (const doc of docs) {
|
|
1305
|
+
if (doc.errors.length === 0) {
|
|
1306
|
+
const obj = doc.toJSON()
|
|
1307
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
1308
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
1309
|
+
if (rec.kind === 'HealthCheckPolicy' && !out.includes(rel)) {
|
|
1310
|
+
out.push(rel)
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1310
1317
|
/**
|
|
1311
1318
|
* Збирає відносні шляхи файлів із **HealthCheckPolicy** у дереві k8s.
|
|
1312
1319
|
* @param {string} root корінь
|
|
@@ -1318,34 +1325,8 @@ async function collectHealthCheckPolicyRelPaths(root, yamlAbs) {
|
|
|
1318
1325
|
const out = []
|
|
1319
1326
|
for (const abs of yamlAbs) {
|
|
1320
1327
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
raw = await readFile(abs, 'utf8')
|
|
1324
|
-
} catch {
|
|
1325
|
-
raw = null
|
|
1326
|
-
}
|
|
1327
|
-
if (raw !== null) {
|
|
1328
|
-
const body = stripBom(raw)
|
|
1329
|
-
const lines = body.split(LINE_SPLIT_RE)
|
|
1330
|
-
const first = lines[0] ?? ''
|
|
1331
|
-
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
1332
|
-
try {
|
|
1333
|
-
const docs = parseAllDocuments(rest)
|
|
1334
|
-
for (const doc of docs) {
|
|
1335
|
-
if (doc.errors.length === 0) {
|
|
1336
|
-
const obj = doc.toJSON()
|
|
1337
|
-
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
1338
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
1339
|
-
if (rec.kind === 'HealthCheckPolicy' && !out.includes(rel)) {
|
|
1340
|
-
out.push(rel)
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
} catch {
|
|
1346
|
-
/* пропускаємо пошкоджені файли — їх ловить check-k8s */
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1328
|
+
const docs = await readAndParseYamlDocs(abs, rel, silentFail)
|
|
1329
|
+
if (docs) collectHealthCheckPolicyRelFromDocs(docs, rel, out)
|
|
1349
1330
|
}
|
|
1350
1331
|
return out
|
|
1351
1332
|
}
|
|
@@ -1388,14 +1369,45 @@ async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, health
|
|
|
1388
1369
|
}
|
|
1389
1370
|
|
|
1390
1371
|
/**
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1372
|
+
* Перевіряє одну kustomization.yaml на nodeSelector patch для заданого overlay.
|
|
1373
|
+
* @param {string} abs абсолютний шлях до файлу
|
|
1374
|
+
* @param {string} rel відносний шлях (для повідомлень)
|
|
1375
|
+
* @param {'ua' | 'ru'} mode параметр mode
|
|
1376
|
+
* @param {Set<string>} deploymentDirs директорії з Deployment (Set)
|
|
1393
1377
|
* @param {string} root корінь репозиторію
|
|
1394
|
-
* @param {string
|
|
1395
|
-
* @param {
|
|
1396
|
-
* @
|
|
1397
|
-
|
|
1398
|
-
|
|
1378
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
1379
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1380
|
+
* @returns {Promise<boolean>} false якщо виявлено помилку і слід зупинитись
|
|
1381
|
+
*/
|
|
1382
|
+
async function checkNodeSelectorKustomization(abs, rel, mode, deploymentDirs, root, fail, passFn) {
|
|
1383
|
+
if (!abieOverlayK8sTreeHasDeployment(deploymentDirs, root, abs)) {
|
|
1384
|
+
passFn(`${rel}: nodeSelector patch (${mode}) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
|
|
1385
|
+
return true
|
|
1386
|
+
}
|
|
1387
|
+
let raw
|
|
1388
|
+
try {
|
|
1389
|
+
raw = await readFile(abs, 'utf8')
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
1392
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
1393
|
+
return false
|
|
1394
|
+
}
|
|
1395
|
+
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode)) {
|
|
1396
|
+
const detail = mode === 'ua' ? 'preem: false' : 'yandex.cloud/preemptible: false'
|
|
1397
|
+
fail(`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та ${detail} (abie.mdc)`)
|
|
1398
|
+
return false
|
|
1399
|
+
}
|
|
1400
|
+
passFn(`${rel}: nodeSelector patch (${mode}) відповідає abie.mdc`)
|
|
1401
|
+
return true
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* Перевіряє наявність патчів nodeSelector для ua/ru overlay у k8s.
|
|
1406
|
+
* @param {string} root корінь репозиторію
|
|
1407
|
+
* @param {string[]} yamlFilesAbs абсолютні шляхи yaml-файлів під k8s
|
|
1408
|
+
* @param {Set<string>} deploymentDirs директорії з Deployment (Set)
|
|
1409
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
1410
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1399
1411
|
*/
|
|
1400
1412
|
async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentDirs, fail, passFn) {
|
|
1401
1413
|
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
@@ -1407,25 +1419,8 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentD
|
|
|
1407
1419
|
}
|
|
1408
1420
|
for (const abs of uaAbsList) {
|
|
1409
1421
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
try {
|
|
1413
|
-
raw = await readFile(abs, 'utf8')
|
|
1414
|
-
} catch (error) {
|
|
1415
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1416
|
-
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
1417
|
-
return
|
|
1418
|
-
}
|
|
1419
|
-
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
|
|
1420
|
-
fail(
|
|
1421
|
-
`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`
|
|
1422
|
-
)
|
|
1423
|
-
return
|
|
1424
|
-
}
|
|
1425
|
-
passFn(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
|
|
1426
|
-
} else {
|
|
1427
|
-
passFn(`${rel}: nodeSelector patch (ua) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
|
|
1428
|
-
}
|
|
1422
|
+
const ok = await checkNodeSelectorKustomization(abs, rel, 'ua', deploymentDirs, root, fail, passFn)
|
|
1423
|
+
if (!ok) return
|
|
1429
1424
|
}
|
|
1430
1425
|
|
|
1431
1426
|
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
@@ -1437,26 +1432,59 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentD
|
|
|
1437
1432
|
}
|
|
1438
1433
|
for (const abs of ruAbsList) {
|
|
1439
1434
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1435
|
+
const ok = await checkNodeSelectorKustomization(abs, rel, 'ru', deploymentDirs, root, fail, passFn)
|
|
1436
|
+
if (!ok) return
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/**
|
|
1441
|
+
* Перевіряє HTTPRoute patch для одного overlay (ua/ru).
|
|
1442
|
+
* @param {string} abs абсолютний шлях до kustomization.yaml
|
|
1443
|
+
* @param {string} rel відносний шлях (для повідомлень)
|
|
1444
|
+
* @param {'ua' | 'ru'} mode overlay
|
|
1445
|
+
* @param {string} root корінь репозиторію
|
|
1446
|
+
* @param {string[]} yamlFilesAbs абсолютні шляхи yaml-файлів під k8s
|
|
1447
|
+
* @param {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} cache кеш аналізу shared backend refs
|
|
1448
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
1449
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1450
|
+
* @returns {Promise<boolean>} false якщо виявлено помилку і слід зупинитись
|
|
1451
|
+
*/
|
|
1452
|
+
async function checkHttpRouteKustomization(abs, rel, mode, root, yamlFilesAbs, cache, fail, passFn) {
|
|
1453
|
+
if (!abieOverlayRequiresHttpRouteByVite(root, abs)) {
|
|
1454
|
+
passFn(`${rel}: HTTPRoute patch (${mode}) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
|
|
1455
|
+
return true
|
|
1456
|
+
}
|
|
1457
|
+
const pkgAbs = abiePackageDirFromK8sOverlay(root, abs)
|
|
1458
|
+
if (!pkgAbs) {
|
|
1459
|
+
fail(`${rel}: внутрішня помилка abie overlay (немає каталогу пакета)`)
|
|
1460
|
+
return false
|
|
1461
|
+
}
|
|
1462
|
+
let p = cache.get(pkgAbs)
|
|
1463
|
+
if (!p) {
|
|
1464
|
+
p = analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yamlFilesAbs)
|
|
1465
|
+
cache.set(pkgAbs, p)
|
|
1466
|
+
}
|
|
1467
|
+
const sharedAnalysis = await p
|
|
1468
|
+
for (const err of sharedAnalysis.baseErrors) {
|
|
1469
|
+
fail(err)
|
|
1470
|
+
return false
|
|
1471
|
+
}
|
|
1472
|
+
let raw
|
|
1473
|
+
try {
|
|
1474
|
+
raw = await readFile(abs, 'utf8')
|
|
1475
|
+
} catch (error) {
|
|
1476
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
1477
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
1478
|
+
return false
|
|
1479
|
+
}
|
|
1480
|
+
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
1481
|
+
const v = validateAbieNginxRunHttpRoutePatches(combined, mode, raw, sharedAnalysis.refCount)
|
|
1482
|
+
if (v !== null) {
|
|
1483
|
+
fail(`${rel}: ${v}`)
|
|
1484
|
+
return false
|
|
1459
1485
|
}
|
|
1486
|
+
passFn(`${rel}: HTTPRoute patch (${mode}) відповідає abie.mdc`)
|
|
1487
|
+
return true
|
|
1460
1488
|
}
|
|
1461
1489
|
|
|
1462
1490
|
/**
|
|
@@ -1464,25 +1492,12 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentD
|
|
|
1464
1492
|
* лише для пакетів з **vite.config.{js,mjs,ts}** у каталозі пакета (батько **k8s**).
|
|
1465
1493
|
* @param {string} root корінь репозиторію
|
|
1466
1494
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
1467
|
-
* @param {(msg: string) => void} fail callback
|
|
1468
|
-
* @param {(msg: string) => void} passFn
|
|
1469
|
-
* @returns {Promise<void>}
|
|
1495
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
1496
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1470
1497
|
*/
|
|
1471
1498
|
async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
|
|
1472
1499
|
/** @type {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} */
|
|
1473
|
-
const
|
|
1474
|
-
/**
|
|
1475
|
-
* @param {string} pkgAbs абсолютний шлях до каталогу пакета
|
|
1476
|
-
* @returns {Promise<{ refCount: number, baseErrors: string[] }>} кількість посилань і базові помилки
|
|
1477
|
-
*/
|
|
1478
|
-
const getSharedBackendAnalysis = pkgAbs => {
|
|
1479
|
-
let p = sharedBackendAnalysisByPkg.get(pkgAbs)
|
|
1480
|
-
if (!p) {
|
|
1481
|
-
p = analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yamlFilesAbs)
|
|
1482
|
-
sharedBackendAnalysisByPkg.set(pkgAbs, p)
|
|
1483
|
-
}
|
|
1484
|
-
return p
|
|
1485
|
-
}
|
|
1500
|
+
const cache = new Map()
|
|
1486
1501
|
|
|
1487
1502
|
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
1488
1503
|
if (uaAbsList.length === 0) {
|
|
@@ -1492,35 +1507,8 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
|
|
|
1492
1507
|
}
|
|
1493
1508
|
for (const abs of uaAbsList) {
|
|
1494
1509
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
if (!pkgAbs) {
|
|
1498
|
-
fail(`${rel}: внутрішня помилка abie overlay (немає каталогу пакета)`)
|
|
1499
|
-
return
|
|
1500
|
-
}
|
|
1501
|
-
const sharedAnalysis = await getSharedBackendAnalysis(pkgAbs)
|
|
1502
|
-
for (const err of sharedAnalysis.baseErrors) {
|
|
1503
|
-
fail(err)
|
|
1504
|
-
return
|
|
1505
|
-
}
|
|
1506
|
-
let raw
|
|
1507
|
-
try {
|
|
1508
|
-
raw = await readFile(abs, 'utf8')
|
|
1509
|
-
} catch (error) {
|
|
1510
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1511
|
-
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
1512
|
-
return
|
|
1513
|
-
}
|
|
1514
|
-
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
1515
|
-
const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua', raw, sharedAnalysis.refCount)
|
|
1516
|
-
if (v !== null) {
|
|
1517
|
-
fail(`${rel}: ${v}`)
|
|
1518
|
-
return
|
|
1519
|
-
}
|
|
1520
|
-
passFn(`${rel}: HTTPRoute patch (ua) відповідає abie.mdc`)
|
|
1521
|
-
} else {
|
|
1522
|
-
passFn(`${rel}: HTTPRoute patch (ua) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
|
|
1523
|
-
}
|
|
1510
|
+
const ok = await checkHttpRouteKustomization(abs, rel, 'ua', root, yamlFilesAbs, cache, fail, passFn)
|
|
1511
|
+
if (!ok) return
|
|
1524
1512
|
}
|
|
1525
1513
|
|
|
1526
1514
|
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
@@ -1531,35 +1519,8 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
|
|
|
1531
1519
|
}
|
|
1532
1520
|
for (const abs of ruAbsList) {
|
|
1533
1521
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
if (!pkgAbs) {
|
|
1537
|
-
fail(`${rel}: внутрішня помилка abie overlay (немає каталогу пакета)`)
|
|
1538
|
-
return
|
|
1539
|
-
}
|
|
1540
|
-
const sharedAnalysis = await getSharedBackendAnalysis(pkgAbs)
|
|
1541
|
-
for (const err of sharedAnalysis.baseErrors) {
|
|
1542
|
-
fail(err)
|
|
1543
|
-
return
|
|
1544
|
-
}
|
|
1545
|
-
let raw
|
|
1546
|
-
try {
|
|
1547
|
-
raw = await readFile(abs, 'utf8')
|
|
1548
|
-
} catch (error) {
|
|
1549
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1550
|
-
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
1551
|
-
return
|
|
1552
|
-
}
|
|
1553
|
-
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
1554
|
-
const v = validateAbieNginxRunHttpRoutePatches(combined, 'ru', raw, sharedAnalysis.refCount)
|
|
1555
|
-
if (v !== null) {
|
|
1556
|
-
fail(`${rel}: ${v}`)
|
|
1557
|
-
return
|
|
1558
|
-
}
|
|
1559
|
-
passFn(`${rel}: HTTPRoute patch (ru) відповідає abie.mdc`)
|
|
1560
|
-
} else {
|
|
1561
|
-
passFn(`${rel}: HTTPRoute patch (ru) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
|
|
1562
|
-
}
|
|
1522
|
+
const ok = await checkHttpRouteKustomization(abs, rel, 'ru', root, yamlFilesAbs, cache, fail, passFn)
|
|
1523
|
+
if (!ok) return
|
|
1563
1524
|
}
|
|
1564
1525
|
}
|
|
1565
1526
|
|
|
@@ -1591,6 +1552,85 @@ function ensureNoFirebaseHostingArtifacts(root, passFn, failFn) {
|
|
|
1591
1552
|
* Перевіряє відповідність проєкту правилам abie.mdc.
|
|
1592
1553
|
* @returns {Promise<number>} 0 — OK, 1 — є порушення
|
|
1593
1554
|
*/
|
|
1555
|
+
/**
|
|
1556
|
+
* Перевіряє clean-merged-branch.yml на ignore_branches.
|
|
1557
|
+
* @param {string} root корінь репозиторію
|
|
1558
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
1559
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
1560
|
+
*/
|
|
1561
|
+
async function checkCleanMergedBranch(root, pass, fail) {
|
|
1562
|
+
const cleanMergedPath = join(root, '.github/workflows/clean-merged-branch.yml')
|
|
1563
|
+
if (!existsSync(cleanMergedPath)) {
|
|
1564
|
+
fail(`Відсутній ${cleanMergedPath} — потрібен для ignore_branches (abie.mdc)`)
|
|
1565
|
+
return
|
|
1566
|
+
}
|
|
1567
|
+
let wfRaw
|
|
1568
|
+
try {
|
|
1569
|
+
wfRaw = await readFile(cleanMergedPath, 'utf8')
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
1572
|
+
fail(`Не вдалося прочитати clean-merged-branch.yml (${msg})`)
|
|
1573
|
+
return
|
|
1574
|
+
}
|
|
1575
|
+
const ib = parseCleanMergedIgnoreBranches(wfRaw)
|
|
1576
|
+
if (ib === null || ib.trim() === '') {
|
|
1577
|
+
fail(
|
|
1578
|
+
'clean-merged-branch.yml: не знайдено with.ignore_branches у кроці phpdocker-io/github-actions-delete-abandoned-branches (abie.mdc)'
|
|
1579
|
+
)
|
|
1580
|
+
} else if (ignoreBranchesIncludesRequired(ib, ABIE_REQUIRED_IGNORE_BRANCHES)) {
|
|
1581
|
+
pass('clean-merged-branch.yml: ignore_branches містить dev, ua, ru')
|
|
1582
|
+
} else {
|
|
1583
|
+
fail(`clean-merged-branch.yml: ignore_branches має містити dev, ua та ru (зараз: ${JSON.stringify(ib)}) — abie.mdc`)
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/**
|
|
1588
|
+
* Перевіряє один файл hc.yaml на відповідність abie.mdc.
|
|
1589
|
+
* @param {string} root корінь репозиторію
|
|
1590
|
+
* @param {string} dir директорія з Deployment
|
|
1591
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
1592
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
1593
|
+
*/
|
|
1594
|
+
async function checkHcYamlFile(root, dir, pass, fail) {
|
|
1595
|
+
const hcAbs = join(dir, 'hc.yaml')
|
|
1596
|
+
const relHc = relative(root, hcAbs).replaceAll('\\', '/') || 'hc.yaml'
|
|
1597
|
+
if (!existsSync(hcAbs)) {
|
|
1598
|
+
fail(`${relative(root, dir) || dir}: є Deployment, але немає hc.yaml поруч — додай HealthCheckPolicy (abie.mdc)`)
|
|
1599
|
+
return
|
|
1600
|
+
}
|
|
1601
|
+
let hcRaw
|
|
1602
|
+
try {
|
|
1603
|
+
hcRaw = await readFile(hcAbs, 'utf8')
|
|
1604
|
+
} catch (error) {
|
|
1605
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
1606
|
+
fail(`${relHc}: не вдалося прочитати (${msg})`)
|
|
1607
|
+
return
|
|
1608
|
+
}
|
|
1609
|
+
const v = validateAbieHcYaml(hcRaw, relHc)
|
|
1610
|
+
if (v === null) {
|
|
1611
|
+
pass(`${relHc}: відповідає abie.mdc`)
|
|
1612
|
+
} else {
|
|
1613
|
+
fail(v)
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/**
|
|
1618
|
+
* Перевіряє hc.yaml у директоріях з Deployment.
|
|
1619
|
+
* @param {string} root корінь репозиторію
|
|
1620
|
+
* @param {Set<string>} deploymentDirs директорії з Deployment (Set)
|
|
1621
|
+
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
1622
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
1623
|
+
*/
|
|
1624
|
+
async function checkHcYamlFiles(root, deploymentDirs, pass, fail) {
|
|
1625
|
+
for (const dir of [...deploymentDirs].toSorted((a, b) => a.localeCompare(b))) {
|
|
1626
|
+
await checkHcYamlFile(root, dir, pass, fail)
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/**
|
|
1631
|
+
* Перевіряє відповідність проєкту правилам abie.mdc.
|
|
1632
|
+
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
1633
|
+
*/
|
|
1594
1634
|
export async function check() {
|
|
1595
1635
|
const reporter = createCheckReporter()
|
|
1596
1636
|
const { pass, fail } = reporter
|
|
@@ -1604,67 +1644,14 @@ export async function check() {
|
|
|
1604
1644
|
|
|
1605
1645
|
pass('Правило abie увімкнено — виконуємо перевірки')
|
|
1606
1646
|
ensureNoFirebaseHostingArtifacts(root, pass, fail)
|
|
1607
|
-
|
|
1608
|
-
const cleanMergedPath = join(root, '.github/workflows/clean-merged-branch.yml')
|
|
1609
|
-
if (existsSync(cleanMergedPath)) {
|
|
1610
|
-
/** @type {string | undefined} */
|
|
1611
|
-
let wfRaw
|
|
1612
|
-
try {
|
|
1613
|
-
wfRaw = await readFile(cleanMergedPath, 'utf8')
|
|
1614
|
-
} catch (error) {
|
|
1615
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1616
|
-
fail(`Не вдалося прочитати clean-merged-branch.yml (${msg})`)
|
|
1617
|
-
}
|
|
1618
|
-
if (wfRaw !== undefined) {
|
|
1619
|
-
const ib = parseCleanMergedIgnoreBranches(wfRaw)
|
|
1620
|
-
if (ib === null || ib.trim() === '') {
|
|
1621
|
-
fail(
|
|
1622
|
-
'clean-merged-branch.yml: не знайдено with.ignore_branches у кроці phpdocker-io/github-actions-delete-abandoned-branches (abie.mdc)'
|
|
1623
|
-
)
|
|
1624
|
-
} else if (ignoreBranchesIncludesRequired(ib, ABIE_REQUIRED_IGNORE_BRANCHES)) {
|
|
1625
|
-
pass('clean-merged-branch.yml: ignore_branches містить dev, ua, ru')
|
|
1626
|
-
} else {
|
|
1627
|
-
fail(
|
|
1628
|
-
`clean-merged-branch.yml: ignore_branches має містити dev, ua та ru (зараз: ${JSON.stringify(ib)}) — abie.mdc`
|
|
1629
|
-
)
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
} else {
|
|
1633
|
-
fail(`Відсутній ${cleanMergedPath} — потрібен для ignore_branches (abie.mdc)`)
|
|
1634
|
-
}
|
|
1647
|
+
await checkCleanMergedBranch(root, pass, fail)
|
|
1635
1648
|
|
|
1636
1649
|
const yamlFiles = await findK8sYamlFiles(root)
|
|
1637
1650
|
const deploymentDirs = await collectDeploymentDirs(root, yamlFiles, fail)
|
|
1638
1651
|
|
|
1639
1652
|
if (deploymentDirs.size > 0) {
|
|
1640
1653
|
pass(`Знайдено Deployment у ${deploymentDirs.size} директорія(ї/й) k8s — перевіряємо hc.yaml`)
|
|
1641
|
-
|
|
1642
|
-
const hcAbs = join(dir, 'hc.yaml')
|
|
1643
|
-
const relHc = relative(root, hcAbs).replaceAll('\\', '/') || 'hc.yaml'
|
|
1644
|
-
if (existsSync(hcAbs)) {
|
|
1645
|
-
let hcRaw
|
|
1646
|
-
let hcReadOk = false
|
|
1647
|
-
try {
|
|
1648
|
-
hcRaw = await readFile(hcAbs, 'utf8')
|
|
1649
|
-
hcReadOk = true
|
|
1650
|
-
} catch (error) {
|
|
1651
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1652
|
-
fail(`${relHc}: не вдалося прочитати (${msg})`)
|
|
1653
|
-
}
|
|
1654
|
-
if (hcReadOk) {
|
|
1655
|
-
const v = validateAbieHcYaml(hcRaw, relHc)
|
|
1656
|
-
if (v === null) {
|
|
1657
|
-
pass(`${relHc}: відповідає abie.mdc`)
|
|
1658
|
-
} else {
|
|
1659
|
-
fail(v)
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
} else {
|
|
1663
|
-
fail(
|
|
1664
|
-
`${relative(root, dir) || dir}: є Deployment, але немає hc.yaml поруч — додай HealthCheckPolicy (abie.mdc)`
|
|
1665
|
-
)
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1654
|
+
await checkHcYamlFiles(root, deploymentDirs, pass, fail)
|
|
1668
1655
|
pass('Є Deployment — перевіряємо base: spec.template.spec.nodeSelector.preem (abie.mdc)')
|
|
1669
1656
|
await ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFiles, fail, pass)
|
|
1670
1657
|
} else {
|