@nitra/cursor 1.8.104 → 1.8.106
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 +45 -0
- package/bin/n-cursor.js +270 -149
- package/mdc/graphql.mdc +15 -1
- package/mdc/k8s.mdc +11 -10
- package/package.json +1 -1
- package/schemas/n-cursor.json +16 -0
- package/scripts/auto-rules.mjs +404 -0
- package/scripts/check-abie.mjs +558 -553
- package/scripts/check-bun.mjs +106 -82
- package/scripts/check-ga.mjs +151 -119
- package/scripts/check-graphql.mjs +112 -34
- package/scripts/check-js-lint.mjs +267 -186
- package/scripts/check-k8s.mjs +1148 -673
- package/scripts/check-nginx-default-tpl.mjs +125 -100
- package/scripts/check-npm-module.mjs +165 -118
- package/scripts/check-style-lint.mjs +74 -61
- package/scripts/check-text.mjs +288 -210
- package/scripts/check-vue.mjs +110 -69
- package/scripts/utils/docker-hadolint.mjs +9 -5
- package/scripts/utils/gha-workflow.mjs +92 -72
- package/scripts/utils/workspaces.mjs +39 -16
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'])
|
|
@@ -64,28 +64,52 @@ const ABIE_SHARED_CROSS_NS_BACKEND_SET = new Set(ABIE_SHARED_CROSS_NS_BACKEND_NA
|
|
|
64
64
|
export const ABIE_HC_SCHEMA_URL = 'https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json'
|
|
65
65
|
|
|
66
66
|
const MODELINE_RE = /^#\s*yaml-language-server:\s*\$schema=(\S+)\s*$/
|
|
67
|
+
const LINE_SPLIT_RE = /\r?\n/u
|
|
68
|
+
const RU_KUSTOMIZATION_PATH_RE = /(^|\/)ru\/kustomization\.yaml$/u
|
|
69
|
+
const UA_KUSTOMIZATION_PATH_RE = /(^|\/)ua\/kustomization\.yaml$/u
|
|
70
|
+
const OVERLAY_PACKAGE_DIR_RE = /^(.+)\/k8s\/(?:ua|ru)\/kustomization\.yaml$/u
|
|
71
|
+
const BASE_SEGMENT_RE = /(^|\/)base\//u
|
|
72
|
+
const YAML_EXTENSION_RE = /\.ya?ml$/iu
|
|
73
|
+
const K8S_PACKAGE_DIR_RE = /^(.+)\/k8s\//u
|
|
74
|
+
const PATCH_PATH_TYPE_RE = /path:\s*\/spec\/type\b/u
|
|
75
|
+
const PATCH_VALUE_NODE_PORT_RE = /value:\s*['"]?NodePort['"]?(?:\s|$)/iu
|
|
76
|
+
const PATCH_NODE_SELECTOR_PATH_RE = /path:\s*\/spec\/template\/spec\/nodeSelector\b/u
|
|
77
|
+
const PATCH_PREEM_FALSE_RE = /\bpreem:\s*['"]?false['"]?\b/u
|
|
78
|
+
const PATCH_YANDEX_PREEMPTIBLE_FALSE_RE = /yandex\.cloud\/preemptible:\s*['"]?false['"]?/u
|
|
79
|
+
const TRAILING_SLASH_RE = /\/$/u
|
|
80
|
+
const PATCH_HOSTNAMES_PATH_RE = /path:\s*\/spec\/hostnames\b/mu
|
|
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
|
|
85
|
+
const WEBSOCKET_ANNOTATION_RE = /gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/mu
|
|
86
|
+
const LEADING_EMPTY_LINE_RE = /^\s*\n/u
|
|
87
|
+
const REMOVE_CLUSTER_IP_AFTER_OP_RE = /op:\s*remove\b[\s\S]{0,200}?path:\s*\/spec\/clusterIP\b/mu
|
|
88
|
+
const REMOVE_CLUSTER_IP_BEFORE_OP_RE = /path:\s*\/spec\/clusterIP\b[\s\S]{0,200}?op:\s*remove\b/mu
|
|
89
|
+
const REMOVE_CLUSTER_IPS_AFTER_OP_RE = /op:\s*remove\b[\s\S]{0,200}?path:\s*\/spec\/clusterIPs\b/mu
|
|
90
|
+
const REMOVE_CLUSTER_IPS_BEFORE_OP_RE = /path:\s*\/spec\/clusterIPs\b[\s\S]{0,200}?op:\s*remove\b/mu
|
|
67
91
|
|
|
68
92
|
/** Гілки, які мають бути в **`ignore_branches`** за abie.mdc. */
|
|
69
93
|
export const ABIE_REQUIRED_IGNORE_BRANCHES = ['dev', 'ua', 'ru']
|
|
70
94
|
|
|
71
95
|
/**
|
|
72
|
-
* Чи відносний шлях вказує на **`ru/kustomization.yaml`** (сегмент **`ru`** перед
|
|
96
|
+
* Чи відносний шлях вказує на **`ru/kustomization.yaml`** (сегмент **`ru`** перед ім'ям файлу) — специфіка abie overlay.
|
|
73
97
|
* @param {string} rel шлях від кореня репозиторію
|
|
74
98
|
* @returns {boolean} true, якщо це `…/ru/kustomization.yaml`
|
|
75
99
|
*/
|
|
76
100
|
export function isRuKustomizationPath(rel) {
|
|
77
101
|
const norm = rel.replaceAll('\\', '/')
|
|
78
|
-
return
|
|
102
|
+
return RU_KUSTOMIZATION_PATH_RE.test(norm)
|
|
79
103
|
}
|
|
80
104
|
|
|
81
105
|
/**
|
|
82
|
-
* Чи відносний шлях вказує на **`ua/kustomization.yaml`** (сегмент **`ua`** перед
|
|
106
|
+
* Чи відносний шлях вказує на **`ua/kustomization.yaml`** (сегмент **`ua`** перед ім'ям файлу) — специфіка abie overlay.
|
|
83
107
|
* @param {string} rel шлях від кореня репозиторію
|
|
84
108
|
* @returns {boolean} true, якщо це `…/ua/kustomization.yaml`
|
|
85
109
|
*/
|
|
86
110
|
export function isUaKustomizationPath(rel) {
|
|
87
111
|
const norm = rel.replaceAll('\\', '/')
|
|
88
|
-
return
|
|
112
|
+
return UA_KUSTOMIZATION_PATH_RE.test(norm)
|
|
89
113
|
}
|
|
90
114
|
|
|
91
115
|
/**
|
|
@@ -96,7 +120,7 @@ export function isUaKustomizationPath(rel) {
|
|
|
96
120
|
*/
|
|
97
121
|
export function abiePackageDirFromK8sOverlay(root, kustomizationAbs) {
|
|
98
122
|
const rel = relative(root, kustomizationAbs).replaceAll('\\', '/') || kustomizationAbs
|
|
99
|
-
const m = rel.match(
|
|
123
|
+
const m = rel.match(OVERLAY_PACKAGE_DIR_RE)
|
|
100
124
|
return m ? join(root, m[1]) : null
|
|
101
125
|
}
|
|
102
126
|
|
|
@@ -147,7 +171,7 @@ export function abieOverlayK8sTreeHasDeployment(deploymentDirs, root, kustomizat
|
|
|
147
171
|
*/
|
|
148
172
|
export function isAbieK8sBaseYamlPath(rel) {
|
|
149
173
|
const norm = rel.replaceAll('\\', '/')
|
|
150
|
-
return
|
|
174
|
+
return BASE_SEGMENT_RE.test(norm)
|
|
151
175
|
}
|
|
152
176
|
|
|
153
177
|
/**
|
|
@@ -276,7 +300,7 @@ async function findK8sYamlFiles(root) {
|
|
|
276
300
|
if (!pathHasK8sSegment(p)) {
|
|
277
301
|
return
|
|
278
302
|
}
|
|
279
|
-
if (
|
|
303
|
+
if (!YAML_EXTENSION_RE.test(p)) {
|
|
280
304
|
return
|
|
281
305
|
}
|
|
282
306
|
out.push(p)
|
|
@@ -335,7 +359,7 @@ function k8sYamlRelOutsideUaRuOverlays(relFromRoot) {
|
|
|
335
359
|
*/
|
|
336
360
|
function abiePackageDirFromK8sYamlRel(root, relFromRoot) {
|
|
337
361
|
const norm = relFromRoot.replaceAll('\\', '/')
|
|
338
|
-
const m = norm.match(
|
|
362
|
+
const m = norm.match(K8S_PACKAGE_DIR_RE)
|
|
339
363
|
return m ? join(root, m[1]) : null
|
|
340
364
|
}
|
|
341
365
|
|
|
@@ -418,12 +442,10 @@ export function jsonPatchRemovesPath(patchText, posixPath) {
|
|
|
418
442
|
if (posixPath !== '/spec/clusterIP' && posixPath !== '/spec/clusterIPs') {
|
|
419
443
|
return false
|
|
420
444
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const opRe = String.raw`op:\s*remove\b`
|
|
426
|
-
return new RegExp(String.raw`${opRe}[\s\S]{0,200}?${pathRe}`, 'mu').test(patchText) || new RegExp(String.raw`${pathRe}[\s\S]{0,200}?${opRe}`, 'mu').test(patchText)
|
|
445
|
+
if (posixPath === '/spec/clusterIP') {
|
|
446
|
+
return REMOVE_CLUSTER_IP_AFTER_OP_RE.test(patchText) || REMOVE_CLUSTER_IP_BEFORE_OP_RE.test(patchText)
|
|
447
|
+
}
|
|
448
|
+
return REMOVE_CLUSTER_IPS_AFTER_OP_RE.test(patchText) || REMOVE_CLUSTER_IPS_BEFORE_OP_RE.test(patchText)
|
|
427
449
|
}
|
|
428
450
|
|
|
429
451
|
/**
|
|
@@ -444,52 +466,50 @@ export function jsonPatchTextSetsServiceTypeNodePort(patchText) {
|
|
|
444
466
|
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
445
467
|
return false
|
|
446
468
|
}
|
|
447
|
-
if (
|
|
469
|
+
if (!PATCH_PATH_TYPE_RE.test(patchText)) {
|
|
448
470
|
return false
|
|
449
471
|
}
|
|
450
|
-
if (
|
|
472
|
+
if (!PATCH_VALUE_NODE_PORT_RE.test(patchText)) {
|
|
451
473
|
return false
|
|
452
474
|
}
|
|
453
475
|
return true
|
|
454
476
|
}
|
|
455
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
|
+
|
|
456
495
|
/**
|
|
457
496
|
* З одного документа **Kustomization** збирає пари **Service name → patch text** для **inline patches** з **target.kind: Service**.
|
|
458
497
|
* @param {import('yaml').Document} doc документ після **parseAllDocuments**
|
|
459
|
-
* @returns {Map<string, string>}
|
|
498
|
+
* @returns {Map<string, string>} ім'я сервісу → текст **patch**
|
|
460
499
|
*/
|
|
461
500
|
function collectAbieServicePatchTextsByNameFromKustomizationDoc(doc) {
|
|
462
501
|
/** @type {Map<string, string>} */
|
|
463
502
|
const out = new Map()
|
|
464
|
-
if (doc.errors.length > 0)
|
|
465
|
-
return out
|
|
466
|
-
}
|
|
503
|
+
if (doc.errors.length > 0) return out
|
|
467
504
|
const root = doc.toJSON()
|
|
468
|
-
if (root === null || typeof root !== 'object' || Array.isArray(root))
|
|
469
|
-
return out
|
|
470
|
-
}
|
|
505
|
+
if (root === null || typeof root !== 'object' || Array.isArray(root)) return out
|
|
471
506
|
const rec = /** @type {Record<string, unknown>} */ (root)
|
|
472
|
-
if (rec.kind !== 'Kustomization')
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
for (const p of patches) {
|
|
480
|
-
if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
|
|
481
|
-
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
482
|
-
const target = pr.target
|
|
483
|
-
if (target !== null && typeof target === 'object' && !Array.isArray(target)) {
|
|
484
|
-
const tg = /** @type {Record<string, unknown>} */ (target)
|
|
485
|
-
if (tg.kind === 'Service' && typeof tg.name === 'string' && tg.name.trim() !== '') {
|
|
486
|
-
const patchStr = pr.patch
|
|
487
|
-
if (typeof patchStr === 'string' && patchStr.trim() !== '') {
|
|
488
|
-
const prev = out.get(tg.name)
|
|
489
|
-
out.set(tg.name, prev === undefined ? patchStr : `${prev}\n${patchStr}`)
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
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}`)
|
|
493
513
|
}
|
|
494
514
|
}
|
|
495
515
|
return out
|
|
@@ -498,11 +518,11 @@ function collectAbieServicePatchTextsByNameFromKustomizationDoc(doc) {
|
|
|
498
518
|
/**
|
|
499
519
|
* Збирає тексти **patch** на **Service** з **kustomization.yaml** (усі документи).
|
|
500
520
|
* @param {string} raw повний текст **kustomization.yaml**
|
|
501
|
-
* @returns {Map<string, string>} **target.name** →
|
|
521
|
+
* @returns {Map<string, string>} **target.name** → об'єднаний текст **patch**
|
|
502
522
|
*/
|
|
503
523
|
function collectAbieRuServicePatchTextByTargetNameFromRaw(raw) {
|
|
504
524
|
const body = stripBom(raw)
|
|
505
|
-
const lines = body.split(
|
|
525
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
506
526
|
const first = lines[0] ?? ''
|
|
507
527
|
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
508
528
|
/** @type {Map<string, string>} */
|
|
@@ -524,41 +544,46 @@ function collectAbieRuServicePatchTextByTargetNameFromRaw(raw) {
|
|
|
524
544
|
return byName
|
|
525
545
|
}
|
|
526
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
|
+
|
|
527
574
|
/**
|
|
528
575
|
* Повідомлення про порушення patch **Service** у **ru/kustomization.yaml** (abie.mdc).
|
|
529
576
|
* @param {string} raw повний текст **kustomization.yaml**
|
|
530
|
-
* @param {Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove?: boolean }>} targetsByName
|
|
577
|
+
* @param {Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove?: boolean }>} targetsByName ім'я **Service** → прапорці patch
|
|
531
578
|
* @returns {string[]} порожньо, якщо все OK
|
|
532
579
|
*/
|
|
533
580
|
export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
|
|
534
|
-
if (targetsByName.size === 0)
|
|
535
|
-
return []
|
|
536
|
-
}
|
|
581
|
+
if (targetsByName.size === 0) return []
|
|
537
582
|
const byName = collectAbieRuServicePatchTextByTargetNameFromRaw(raw)
|
|
538
583
|
/** @type {string[]} */
|
|
539
584
|
const errors = []
|
|
540
585
|
for (const name of [...targetsByName.keys()].toSorted((a, b) => a.localeCompare(b))) {
|
|
541
|
-
|
|
542
|
-
const requiresClusterIPRemove = flags?.requiresClusterIPNoneClear === true
|
|
543
|
-
const requiresClusterIPsRemove = flags?.requiresClusterIPsRemove === true
|
|
544
|
-
const pt = byName.get(name)
|
|
545
|
-
if (pt === undefined || String(pt).trim() === '') {
|
|
546
|
-
errors.push(`${name}: немає inline patch для kind: Service`)
|
|
547
|
-
} else {
|
|
548
|
-
if (!jsonPatchTextSetsServiceTypeNodePort(pt)) {
|
|
549
|
-
errors.push(`${name}: потрібен JSON6902 path /spec/type та value NodePort`)
|
|
550
|
-
}
|
|
551
|
-
if (requiresClusterIPRemove && !jsonPatchTextClearsHeadlessServiceClusterIPNone(pt)) {
|
|
552
|
-
errors.push(
|
|
553
|
-
`${name}: для spec.clusterIP: None додай у той самий patch op: remove для path /spec/clusterIP (abie.mdc)`
|
|
554
|
-
)
|
|
555
|
-
}
|
|
556
|
-
if (requiresClusterIPsRemove && !jsonPatchRemovesPath(pt, '/spec/clusterIPs')) {
|
|
557
|
-
errors.push(
|
|
558
|
-
`${name}: у base задано spec.clusterIPs — додай op: remove для path /spec/clusterIPs (інакше NodePort з None у clusterIPs; abie.mdc)`
|
|
559
|
-
)
|
|
560
|
-
}
|
|
561
|
-
}
|
|
586
|
+
collectServicePatchErrors(name, byName.get(name), targetsByName.get(name), errors)
|
|
562
587
|
}
|
|
563
588
|
return errors
|
|
564
589
|
}
|
|
@@ -568,68 +593,64 @@ export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
|
|
|
568
593
|
* @param {string} root корінь репозиторію
|
|
569
594
|
* @param {string[]} yamlAbs абсолютні шляхи yaml під **k8s**
|
|
570
595
|
* @param {(msg: string) => void} fail реєстрація помилки читання/парсингу
|
|
571
|
-
* @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
|
|
572
644
|
*/
|
|
573
645
|
async function collectAbieRuNodePortServiceTargetsByPackage(root, yamlAbs, fail) {
|
|
574
646
|
/** @type {Map<string, Map<string, { requiresClusterIPNoneClear: boolean, requiresClusterIPsRemove: boolean }>>} */
|
|
575
647
|
const map = new Map()
|
|
576
648
|
for (const abs of yamlAbs) {
|
|
577
649
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
let readOk = false
|
|
583
|
-
try {
|
|
584
|
-
raw = await readFile(abs, 'utf8')
|
|
585
|
-
readOk = true
|
|
586
|
-
} catch (error) {
|
|
587
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
588
|
-
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
589
|
-
}
|
|
590
|
-
if (readOk) {
|
|
591
|
-
const body = stripBom(raw)
|
|
592
|
-
const lines = body.split(/\r?\n/u)
|
|
593
|
-
const first = lines[0] ?? ''
|
|
594
|
-
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
595
|
-
/** @type {import('yaml').Document[]} */
|
|
596
|
-
let docs
|
|
597
|
-
let parseOk = false
|
|
598
|
-
try {
|
|
599
|
-
docs = parseAllDocuments(rest)
|
|
600
|
-
parseOk = true
|
|
601
|
-
} catch (error) {
|
|
602
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
603
|
-
fail(`${rel}: YAML (${msg})`)
|
|
604
|
-
}
|
|
605
|
-
if (parseOk) {
|
|
606
|
-
for (const doc of docs) {
|
|
607
|
-
if (doc.errors.length === 0) {
|
|
608
|
-
const obj = doc.toJSON()
|
|
609
|
-
if (serviceDocumentRequiresAbieRuNodePortOverlay(obj)) {
|
|
610
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
611
|
-
const meta = /** @type {Record<string, unknown>} */ (rec.metadata)
|
|
612
|
-
const n = meta.name
|
|
613
|
-
if (typeof n === 'string' && n.trim() !== '') {
|
|
614
|
-
let inner = map.get(pkgAbs)
|
|
615
|
-
if (!inner) {
|
|
616
|
-
inner = new Map()
|
|
617
|
-
map.set(pkgAbs, inner)
|
|
618
|
-
}
|
|
619
|
-
const needClear = serviceDocumentRequiresRuClusterIPNoneRemoval(obj)
|
|
620
|
-
const needClusterIPsRemove = serviceDocumentBaseDeclaresClusterIPsField(obj)
|
|
621
|
-
const prev = inner.get(n)
|
|
622
|
-
inner.set(n, {
|
|
623
|
-
requiresClusterIPNoneClear: (prev?.requiresClusterIPNoneClear === true) || needClear,
|
|
624
|
-
requiresClusterIPsRemove: (prev?.requiresClusterIPsRemove === true) || needClusterIPsRemove
|
|
625
|
-
})
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
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)
|
|
633
654
|
}
|
|
634
655
|
}
|
|
635
656
|
return map
|
|
@@ -689,38 +710,12 @@ async function collectDeploymentDirs(root, yamlAbs, fail) {
|
|
|
689
710
|
/** @type {Set<string>} */
|
|
690
711
|
const dirs = new Set()
|
|
691
712
|
for (const abs of yamlAbs) {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
699
|
-
fail(`${relative(root, abs) || abs}: не вдалося прочитати (${msg})`)
|
|
700
|
-
}
|
|
701
|
-
if (readOk) {
|
|
702
|
-
const body = stripBom(raw)
|
|
703
|
-
const lines = body.split(/\r?\n/u)
|
|
704
|
-
const first = lines[0] ?? ''
|
|
705
|
-
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
706
|
-
/** @type {import('yaml').Document[]} */
|
|
707
|
-
let docs
|
|
708
|
-
let parseOk = false
|
|
709
|
-
try {
|
|
710
|
-
docs = parseAllDocuments(rest)
|
|
711
|
-
parseOk = true
|
|
712
|
-
} catch (error) {
|
|
713
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
714
|
-
fail(`${relative(root, abs) || abs}: YAML (${msg})`)
|
|
715
|
-
}
|
|
716
|
-
if (parseOk) {
|
|
717
|
-
for (const doc of docs) {
|
|
718
|
-
if (doc.errors.length === 0) {
|
|
719
|
-
const obj = doc.toJSON()
|
|
720
|
-
if (isDeploymentDoc(obj)) {
|
|
721
|
-
dirs.add(dirname(abs))
|
|
722
|
-
}
|
|
723
|
-
}
|
|
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))
|
|
724
719
|
}
|
|
725
720
|
}
|
|
726
721
|
}
|
|
@@ -728,6 +723,31 @@ async function collectDeploymentDirs(root, yamlAbs, fail) {
|
|
|
728
723
|
return dirs
|
|
729
724
|
}
|
|
730
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
|
+
|
|
731
751
|
/**
|
|
732
752
|
* Для кожного **Deployment** у YAML під **`k8s`** з шляхом **`…/base/…`** вимагає **`spec.template.spec.nodeSelector.preem: true`** (abie.mdc).
|
|
733
753
|
* @param {string} root корінь репозиторію
|
|
@@ -737,48 +757,15 @@ async function collectDeploymentDirs(root, yamlAbs, fail) {
|
|
|
737
757
|
* @returns {Promise<void>}
|
|
738
758
|
*/
|
|
739
759
|
async function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fail, passFn) {
|
|
740
|
-
const baseFiles = yamlFilesAbs.filter(abs =>
|
|
741
|
-
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
742
|
-
return isAbieK8sBaseYamlPath(rel)
|
|
743
|
-
})
|
|
760
|
+
const baseFiles = yamlFilesAbs.filter(abs => isAbieK8sBaseYamlPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
744
761
|
let anyBaseDeployment = false
|
|
745
762
|
for (const abs of baseFiles) {
|
|
746
763
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
753
|
-
return
|
|
754
|
-
}
|
|
755
|
-
const body = stripBom(raw)
|
|
756
|
-
const lines = body.split(/\r?\n/u)
|
|
757
|
-
const first = lines[0] ?? ''
|
|
758
|
-
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
759
|
-
/** @type {import('yaml').Document[]} */
|
|
760
|
-
let docs
|
|
761
|
-
try {
|
|
762
|
-
docs = parseAllDocuments(rest)
|
|
763
|
-
} catch (error) {
|
|
764
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
765
|
-
fail(`${rel}: YAML (${msg})`)
|
|
766
|
-
return
|
|
767
|
-
}
|
|
768
|
-
for (const doc of docs) {
|
|
769
|
-
if (doc.errors.length === 0) {
|
|
770
|
-
const obj = doc.toJSON()
|
|
771
|
-
if (isDeploymentDoc(obj)) {
|
|
772
|
-
anyBaseDeployment = true
|
|
773
|
-
if (!deploymentDocumentHasAbieBasePreemNodeSelector(obj)) {
|
|
774
|
-
fail(
|
|
775
|
-
`${rel}: Deployment у base: потрібен spec.template.spec.nodeSelector.preem: true (або 'true') — abie.mdc`
|
|
776
|
-
)
|
|
777
|
-
return
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
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
|
|
782
769
|
}
|
|
783
770
|
if (anyBaseDeployment) {
|
|
784
771
|
passFn('Deployment у …/base/…: nodeSelector.preem відповідає abie.mdc')
|
|
@@ -796,6 +783,46 @@ function stripBom(s) {
|
|
|
796
783
|
return s.startsWith('\uFEFF') ? s.slice(1) : s
|
|
797
784
|
}
|
|
798
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
|
+
|
|
799
826
|
/**
|
|
800
827
|
* Чи рядок inline JSON6902 patch містить очікуваний **ua** nodeSelector (**preem: false** на **`/spec/template/spec/nodeSelector`**).
|
|
801
828
|
* Конкретний **`op`** не перевіряється — див. **k8s.mdc**.
|
|
@@ -806,10 +833,10 @@ function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
|
|
|
806
833
|
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
807
834
|
return false
|
|
808
835
|
}
|
|
809
|
-
if (
|
|
836
|
+
if (!PATCH_NODE_SELECTOR_PATH_RE.test(patchText)) {
|
|
810
837
|
return false
|
|
811
838
|
}
|
|
812
|
-
if (
|
|
839
|
+
if (!PATCH_PREEM_FALSE_RE.test(patchText)) {
|
|
813
840
|
return false
|
|
814
841
|
}
|
|
815
842
|
return true
|
|
@@ -825,10 +852,10 @@ function jsonPatchTextHasRuDeploymentNodeSelector(patchText) {
|
|
|
825
852
|
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
826
853
|
return false
|
|
827
854
|
}
|
|
828
|
-
if (
|
|
855
|
+
if (!PATCH_NODE_SELECTOR_PATH_RE.test(patchText)) {
|
|
829
856
|
return false
|
|
830
857
|
}
|
|
831
|
-
if (
|
|
858
|
+
if (!PATCH_YANDEX_PREEMPTIBLE_FALSE_RE.test(patchText)) {
|
|
832
859
|
return false
|
|
833
860
|
}
|
|
834
861
|
return true
|
|
@@ -904,7 +931,7 @@ function kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode) {
|
|
|
904
931
|
*/
|
|
905
932
|
export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
|
|
906
933
|
const body = stripBom(raw)
|
|
907
|
-
const lines = body.split(
|
|
934
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
908
935
|
const first = lines[0] ?? ''
|
|
909
936
|
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
910
937
|
/** @type {import('yaml').Document[]} */
|
|
@@ -930,7 +957,7 @@ export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
|
|
|
930
957
|
*/
|
|
931
958
|
export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelFromRoot) {
|
|
932
959
|
const normRel = relFromRoot.replaceAll('\\', '/')
|
|
933
|
-
const pkg = pkgRelFromRoot.replaceAll('\\', '/').replace(
|
|
960
|
+
const pkg = pkgRelFromRoot.replaceAll('\\', '/').replace(TRAILING_SLASH_RE, '')
|
|
934
961
|
const prefix = `${pkg}/k8s/`
|
|
935
962
|
if (!normRel.startsWith(prefix)) {
|
|
936
963
|
return false
|
|
@@ -939,6 +966,24 @@ export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelF
|
|
|
939
966
|
return !after.startsWith('ua/') && !after.startsWith('ru/')
|
|
940
967
|
}
|
|
941
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
|
+
|
|
942
987
|
/**
|
|
943
988
|
* З HTTPRoute-документа рахує **`backendRefs`** до **`auth-run-hl`** / **`filelint-hl`** і порушення **`namespace: dev`**.
|
|
944
989
|
* @param {unknown} obj корінь YAML
|
|
@@ -948,38 +993,20 @@ export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelF
|
|
|
948
993
|
function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
|
|
949
994
|
/** @type {string[]} */
|
|
950
995
|
const errors = []
|
|
951
|
-
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
952
|
-
return { refCount: 0, errors }
|
|
953
|
-
}
|
|
996
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return { refCount: 0, errors }
|
|
954
997
|
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
955
|
-
if (rec.kind !== 'HTTPRoute') {
|
|
956
|
-
return { refCount: 0, errors }
|
|
957
|
-
}
|
|
998
|
+
if (rec.kind !== 'HTTPRoute') return { refCount: 0, errors }
|
|
958
999
|
const spec = rec.spec
|
|
959
|
-
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
960
|
-
return { refCount: 0, errors }
|
|
961
|
-
}
|
|
1000
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return { refCount: 0, errors }
|
|
962
1001
|
const rules = /** @type {Record<string, unknown>} */ (spec).rules
|
|
963
|
-
if (!Array.isArray(rules)) {
|
|
964
|
-
return { refCount: 0, errors }
|
|
965
|
-
}
|
|
1002
|
+
if (!Array.isArray(rules)) return { refCount: 0, errors }
|
|
966
1003
|
let refCount = 0
|
|
967
1004
|
for (const rule of rules) {
|
|
968
1005
|
if (rule !== null && typeof rule === 'object' && !Array.isArray(rule)) {
|
|
969
1006
|
const brs = /** @type {Record<string, unknown>} */ (rule).backendRefs
|
|
970
1007
|
if (Array.isArray(brs)) {
|
|
971
1008
|
for (const br of brs) {
|
|
972
|
-
|
|
973
|
-
const brRec = /** @type {Record<string, unknown>} */ (br)
|
|
974
|
-
const name = brRec.name
|
|
975
|
-
if (typeof name === 'string' && ABIE_SHARED_CROSS_NS_BACKEND_SET.has(name)) {
|
|
976
|
-
refCount++
|
|
977
|
-
const ns = brRec.namespace
|
|
978
|
-
if (typeof ns !== 'string' || ns !== 'dev') {
|
|
979
|
-
errors.push(`${rel}: HTTPRoute backendRefs до ${name} має містити namespace: dev (abie.mdc)`)
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
}
|
|
1009
|
+
refCount += checkSharedBackendRef(br, rel, errors)
|
|
983
1010
|
}
|
|
984
1011
|
}
|
|
985
1012
|
}
|
|
@@ -1002,32 +1029,13 @@ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yam
|
|
|
1002
1029
|
for (const abs of yamlFilesAbs) {
|
|
1003
1030
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1004
1031
|
if (isK8sYamlInAbiePackageExcludingUaRuOverlays(rel, pkgRel)) {
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
const body = stripBom(raw)
|
|
1013
|
-
const lines = body.split(/\r?\n/u)
|
|
1014
|
-
const first = lines[0] ?? ''
|
|
1015
|
-
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
1016
|
-
/** @type {import('yaml').Document[] | undefined} */
|
|
1017
|
-
let docs
|
|
1018
|
-
try {
|
|
1019
|
-
docs = parseAllDocuments(rest)
|
|
1020
|
-
} catch {
|
|
1021
|
-
docs = undefined
|
|
1022
|
-
}
|
|
1023
|
-
if (docs !== undefined) {
|
|
1024
|
-
for (const doc of docs) {
|
|
1025
|
-
if (doc.errors.length === 0) {
|
|
1026
|
-
const obj = doc.toJSON()
|
|
1027
|
-
const st = httpRouteDocSharedCrossNsBackendStats(obj, rel)
|
|
1028
|
-
refCount += st.refCount
|
|
1029
|
-
baseErrors.push(...st.errors)
|
|
1030
|
-
}
|
|
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)
|
|
1031
1039
|
}
|
|
1032
1040
|
}
|
|
1033
1041
|
}
|
|
@@ -1061,43 +1069,38 @@ const ABIE_RU_HTTPROUTE_HOST_MARKERS = [
|
|
|
1061
1069
|
'*.выбирайонлайн.рф'
|
|
1062
1070
|
]
|
|
1063
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
|
+
|
|
1064
1088
|
/**
|
|
1065
1089
|
* Збирає тексти inline **patch** для **HTTPRoute** (будь-який непорожній **target.name**) з одного документа **Kustomization**.
|
|
1066
1090
|
* @param {import('yaml').Document} doc документ після **parseAllDocuments**
|
|
1067
1091
|
* @returns {string[]} непорожні рядки **patch**
|
|
1068
1092
|
*/
|
|
1069
1093
|
function collectAbieHttpRoutePatchStringsFromKustomizationDoc(doc) {
|
|
1070
|
-
if (doc.errors.length > 0)
|
|
1071
|
-
return []
|
|
1072
|
-
}
|
|
1094
|
+
if (doc.errors.length > 0) return []
|
|
1073
1095
|
const root = doc.toJSON()
|
|
1074
|
-
if (root === null || typeof root !== 'object' || Array.isArray(root))
|
|
1075
|
-
return []
|
|
1076
|
-
}
|
|
1096
|
+
if (root === null || typeof root !== 'object' || Array.isArray(root)) return []
|
|
1077
1097
|
const rec = /** @type {Record<string, unknown>} */ (root)
|
|
1078
|
-
if (rec.kind !== 'Kustomization')
|
|
1079
|
-
return []
|
|
1080
|
-
}
|
|
1081
|
-
const patches = rec.patches
|
|
1082
|
-
if (!Array.isArray(patches)) {
|
|
1083
|
-
return []
|
|
1084
|
-
}
|
|
1098
|
+
if (rec.kind !== 'Kustomization' || !Array.isArray(rec.patches)) return []
|
|
1085
1099
|
/** @type {string[]} */
|
|
1086
1100
|
const out = []
|
|
1087
|
-
for (const p of patches) {
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
const target = pr.target
|
|
1091
|
-
if (target !== null && typeof target === 'object' && !Array.isArray(target)) {
|
|
1092
|
-
const tg = /** @type {Record<string, unknown>} */ (target)
|
|
1093
|
-
if (tg.kind === 'HTTPRoute' && typeof tg.name === 'string' && tg.name.trim() !== '') {
|
|
1094
|
-
const patchStr = pr.patch
|
|
1095
|
-
if (typeof patchStr === 'string' && patchStr.trim() !== '') {
|
|
1096
|
-
out.push(patchStr)
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
+
for (const p of rec.patches) {
|
|
1102
|
+
const s = extractHttpRoutePatchString(p)
|
|
1103
|
+
if (s !== null) out.push(s)
|
|
1101
1104
|
}
|
|
1102
1105
|
return out
|
|
1103
1106
|
}
|
|
@@ -1109,7 +1112,7 @@ function collectAbieHttpRoutePatchStringsFromKustomizationDoc(doc) {
|
|
|
1109
1112
|
*/
|
|
1110
1113
|
export function getCombinedNginxRunPatchTextFromKustomization(raw) {
|
|
1111
1114
|
const body = stripBom(raw)
|
|
1112
|
-
const lines = body.split(
|
|
1115
|
+
const lines = body.split(LINE_SPLIT_RE)
|
|
1113
1116
|
const first = lines[0] ?? ''
|
|
1114
1117
|
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
1115
1118
|
/** @type {import('yaml').Document[]} */
|
|
@@ -1144,7 +1147,7 @@ export function validateAbieNginxRunHttpRoutePatches(
|
|
|
1144
1147
|
if (typeof combined !== 'string' || combined.trim() === '') {
|
|
1145
1148
|
return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}; для ru — gwin… websocket лише за наявності HASURA_GRAPHQL_JWT_SECRET у файлі) — abie.mdc`
|
|
1146
1149
|
}
|
|
1147
|
-
if (
|
|
1150
|
+
if (!PATCH_HOSTNAMES_PATH_RE.test(combined)) {
|
|
1148
1151
|
return 'HTTPRoute: потрібен path /spec/hostnames у patch (abie.mdc)'
|
|
1149
1152
|
}
|
|
1150
1153
|
const markers = mode === 'ua' ? ABIE_UA_HTTPROUTE_HOST_MARKERS : ABIE_RU_HTTPROUTE_HOST_MARKERS
|
|
@@ -1152,9 +1155,7 @@ export function validateAbieNginxRunHttpRoutePatches(
|
|
|
1152
1155
|
return `HTTPRoute: у value для /spec/hostnames має бути один із доменів abie (${markers.join(', ')}) — abie.mdc`
|
|
1153
1156
|
}
|
|
1154
1157
|
const namespaceOk =
|
|
1155
|
-
mode === 'ua'
|
|
1156
|
-
? /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua['"]?(?:\s|$)/mu.test(combined)
|
|
1157
|
-
: /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru['"]?(?:\s|$)/mu.test(combined)
|
|
1158
|
+
mode === 'ua' ? PATCH_PARENT_REF_NS_UA_RE.test(combined) : PATCH_PARENT_REF_NS_RU_RE.test(combined)
|
|
1158
1159
|
if (!namespaceOk) {
|
|
1159
1160
|
return `HTTPRoute: потрібен path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
|
|
1160
1161
|
}
|
|
@@ -1162,7 +1163,7 @@ export function validateAbieNginxRunHttpRoutePatches(
|
|
|
1162
1163
|
mode === 'ru' &&
|
|
1163
1164
|
typeof fullKustomizationRaw === 'string' &&
|
|
1164
1165
|
fullKustomizationRaw.includes(HASURA_JWT_SECRET_IN_KUSTOMIZATION)
|
|
1165
|
-
if (ruNeedsWebsocket &&
|
|
1166
|
+
if (ruNeedsWebsocket && !WEBSOCKET_ANNOTATION_RE.test(combined)) {
|
|
1166
1167
|
return 'HTTPRoute (ru): за наявності HASURA_GRAPHQL_JWT_SECRET у kustomization потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
|
|
1167
1168
|
}
|
|
1168
1169
|
const sharedCount =
|
|
@@ -1190,54 +1191,12 @@ export function kustomizationHasAbieNginxRunHttpRoutePatch(raw, mode) {
|
|
|
1190
1191
|
}
|
|
1191
1192
|
|
|
1192
1193
|
/**
|
|
1193
|
-
* Перевіряє
|
|
1194
|
-
* @param {string}
|
|
1195
|
-
* @param {string} relPath відносний шлях для повідомлень
|
|
1196
|
-
* @returns {string | null} текст помилки або
|
|
1194
|
+
* Перевіряє об'єкт HealthCheckPolicy на відповідність abie.mdc.
|
|
1195
|
+
* @param {Record<string, unknown>} policy розібраний HealthCheckPolicy
|
|
1196
|
+
* @param {string} relPath відносний шлях (для повідомлень)
|
|
1197
|
+
* @returns {string | null} текст помилки або null якщо OK
|
|
1197
1198
|
*/
|
|
1198
|
-
|
|
1199
|
-
const body = stripBom(raw)
|
|
1200
|
-
const lines = body.split(/\r?\n/u)
|
|
1201
|
-
if (lines.length === 0 || lines[0].trim() === '') {
|
|
1202
|
-
return `${relPath}: перший рядок порожній — потрібен # yaml-language-server: $schema=… (abie.mdc)`
|
|
1203
|
-
}
|
|
1204
|
-
const m = lines[0].match(MODELINE_RE)
|
|
1205
|
-
if (!m) {
|
|
1206
|
-
return `${relPath}: перший рядок має бути modeline $schema (abie.mdc)`
|
|
1207
|
-
}
|
|
1208
|
-
if (m[1] !== ABIE_HC_SCHEMA_URL) {
|
|
1209
|
-
return `${relPath}: $schema має бути\n ${ABIE_HC_SCHEMA_URL}\n (abie.mdc)`
|
|
1210
|
-
}
|
|
1211
|
-
const yamlBody = lines
|
|
1212
|
-
.slice(1)
|
|
1213
|
-
.join('\n')
|
|
1214
|
-
.replace(/^\s*\n/u, '')
|
|
1215
|
-
/** @type {import('yaml').Document[]} */
|
|
1216
|
-
let docs
|
|
1217
|
-
try {
|
|
1218
|
-
docs = parseAllDocuments(yamlBody)
|
|
1219
|
-
} catch (error) {
|
|
1220
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1221
|
-
return `${relPath}: не вдалося розібрати YAML (${msg})`
|
|
1222
|
-
}
|
|
1223
|
-
/** @type {Record<string, unknown> | null} */
|
|
1224
|
-
let policy = null
|
|
1225
|
-
for (const doc of docs) {
|
|
1226
|
-
if (doc.errors.length > 0) {
|
|
1227
|
-
return `${relPath}: YAML: ${doc.errors.map(e => e.message).join('; ')}`
|
|
1228
|
-
}
|
|
1229
|
-
const obj = doc.toJSON()
|
|
1230
|
-
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
1231
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
1232
|
-
if (rec.kind === 'HealthCheckPolicy') {
|
|
1233
|
-
policy = rec
|
|
1234
|
-
break
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
if (!policy) {
|
|
1239
|
-
return `${relPath}: очікується документ kind: HealthCheckPolicy (abie.mdc)`
|
|
1240
|
-
}
|
|
1199
|
+
function validateAbieHcPolicy(policy, relPath) {
|
|
1241
1200
|
if (policy.apiVersion !== 'networking.gke.io/v1') {
|
|
1242
1201
|
return `${relPath}: apiVersion має бути networking.gke.io/v1 (abie.mdc)`
|
|
1243
1202
|
}
|
|
@@ -1253,7 +1212,8 @@ export function validateAbieHcYaml(raw, relPath) {
|
|
|
1253
1212
|
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
1254
1213
|
return `${relPath}: відсутній spec (abie.mdc)`
|
|
1255
1214
|
}
|
|
1256
|
-
const
|
|
1215
|
+
const specRec = /** @type {Record<string, unknown>} */ (spec)
|
|
1216
|
+
const def = specRec.default
|
|
1257
1217
|
if (def === null || typeof def !== 'object' || Array.isArray(def)) {
|
|
1258
1218
|
return `${relPath}: відсутній spec.default (abie.mdc)`
|
|
1259
1219
|
}
|
|
@@ -1261,34 +1221,99 @@ export function validateAbieHcYaml(raw, relPath) {
|
|
|
1261
1221
|
if (config === null || typeof config !== 'object' || Array.isArray(config)) {
|
|
1262
1222
|
return `${relPath}: відсутній spec.default.config (abie.mdc)`
|
|
1263
1223
|
}
|
|
1264
|
-
if (config.type !== 'HTTP') {
|
|
1265
|
-
return `${relPath}: spec.default.config.type має бути HTTP (abie.mdc)`
|
|
1266
|
-
}
|
|
1224
|
+
if (config.type !== 'HTTP') return `${relPath}: spec.default.config.type має бути HTTP (abie.mdc)`
|
|
1267
1225
|
const httpHc = /** @type {Record<string, unknown>} */ (config).httpHealthCheck
|
|
1268
1226
|
if (httpHc === null || typeof httpHc !== 'object' || Array.isArray(httpHc)) {
|
|
1269
1227
|
return `${relPath}: відсутній httpHealthCheck (abie.mdc)`
|
|
1270
1228
|
}
|
|
1271
|
-
if (httpHc.requestPath !== '/healthz') {
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
if (httpHc.port !== 8080) {
|
|
1275
|
-
return `${relPath}: httpHealthCheck.port має бути 8080 (abie.mdc)`
|
|
1276
|
-
}
|
|
1277
|
-
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
|
|
1278
1232
|
if (targetRef === null || typeof targetRef !== 'object' || Array.isArray(targetRef)) {
|
|
1279
1233
|
return `${relPath}: відсутній targetRef (abie.mdc)`
|
|
1280
1234
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
}
|
|
1284
|
-
const svcName = targetRef.name
|
|
1235
|
+
const tr = /** @type {Record<string, unknown>} */ (targetRef)
|
|
1236
|
+
if (tr.kind !== 'Service') return `${relPath}: targetRef.kind має бути Service (abie.mdc)`
|
|
1285
1237
|
const expectedHl = name.endsWith('-hl') ? name : `${name}-hl`
|
|
1286
|
-
if (typeof
|
|
1238
|
+
if (typeof tr.name !== 'string' || tr.name !== expectedHl) {
|
|
1287
1239
|
return `${relPath}: targetRef.name має посилатися на headless Service (очікується ${expectedHl}, суфікс -hl) (abie.mdc)`
|
|
1288
1240
|
}
|
|
1289
1241
|
return null
|
|
1290
1242
|
}
|
|
1291
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
|
+
|
|
1292
1317
|
/**
|
|
1293
1318
|
* Збирає відносні шляхи файлів із **HealthCheckPolicy** у дереві k8s.
|
|
1294
1319
|
* @param {string} root корінь
|
|
@@ -1300,34 +1325,8 @@ async function collectHealthCheckPolicyRelPaths(root, yamlAbs) {
|
|
|
1300
1325
|
const out = []
|
|
1301
1326
|
for (const abs of yamlAbs) {
|
|
1302
1327
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
raw = await readFile(abs, 'utf8')
|
|
1306
|
-
} catch {
|
|
1307
|
-
raw = null
|
|
1308
|
-
}
|
|
1309
|
-
if (raw !== null) {
|
|
1310
|
-
const body = stripBom(raw)
|
|
1311
|
-
const lines = body.split(/\r?\n/u)
|
|
1312
|
-
const first = lines[0] ?? ''
|
|
1313
|
-
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
1314
|
-
try {
|
|
1315
|
-
const docs = parseAllDocuments(rest)
|
|
1316
|
-
for (const doc of docs) {
|
|
1317
|
-
if (doc.errors.length === 0) {
|
|
1318
|
-
const obj = doc.toJSON()
|
|
1319
|
-
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
1320
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
1321
|
-
if (rec.kind === 'HealthCheckPolicy' && !out.includes(rel)) {
|
|
1322
|
-
out.push(rel)
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
} catch {
|
|
1328
|
-
/* пропускаємо пошкоджені файли — їх ловить check-k8s */
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1328
|
+
const docs = await readAndParseYamlDocs(abs, rel, silentFail)
|
|
1329
|
+
if (docs) collectHealthCheckPolicyRelFromDocs(docs, rel, out)
|
|
1331
1330
|
}
|
|
1332
1331
|
return out
|
|
1333
1332
|
}
|
|
@@ -1370,14 +1369,45 @@ async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, health
|
|
|
1370
1369
|
}
|
|
1371
1370
|
|
|
1372
1371
|
/**
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
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)
|
|
1375
1377
|
* @param {string} root корінь репозиторію
|
|
1376
|
-
* @param {string
|
|
1377
|
-
* @param {
|
|
1378
|
-
* @
|
|
1379
|
-
|
|
1380
|
-
|
|
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 при успішній перевірці
|
|
1381
1411
|
*/
|
|
1382
1412
|
async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentDirs, fail, passFn) {
|
|
1383
1413
|
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
@@ -1389,25 +1419,8 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentD
|
|
|
1389
1419
|
}
|
|
1390
1420
|
for (const abs of uaAbsList) {
|
|
1391
1421
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
try {
|
|
1395
|
-
raw = await readFile(abs, 'utf8')
|
|
1396
|
-
} catch (error) {
|
|
1397
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1398
|
-
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
1399
|
-
return
|
|
1400
|
-
}
|
|
1401
|
-
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
|
|
1402
|
-
fail(
|
|
1403
|
-
`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`
|
|
1404
|
-
)
|
|
1405
|
-
return
|
|
1406
|
-
}
|
|
1407
|
-
passFn(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
|
|
1408
|
-
} else {
|
|
1409
|
-
passFn(`${rel}: nodeSelector patch (ua) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
|
|
1410
|
-
}
|
|
1422
|
+
const ok = await checkNodeSelectorKustomization(abs, rel, 'ua', deploymentDirs, root, fail, passFn)
|
|
1423
|
+
if (!ok) return
|
|
1411
1424
|
}
|
|
1412
1425
|
|
|
1413
1426
|
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
@@ -1419,52 +1432,72 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentD
|
|
|
1419
1432
|
}
|
|
1420
1433
|
for (const abs of ruAbsList) {
|
|
1421
1434
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
try {
|
|
1425
|
-
raw = await readFile(abs, 'utf8')
|
|
1426
|
-
} catch (error) {
|
|
1427
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1428
|
-
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
1429
|
-
return
|
|
1430
|
-
}
|
|
1431
|
-
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ru')) {
|
|
1432
|
-
fail(
|
|
1433
|
-
`${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та yandex.cloud/preemptible: false (abie.mdc)`
|
|
1434
|
-
)
|
|
1435
|
-
return
|
|
1436
|
-
}
|
|
1437
|
-
passFn(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
|
|
1438
|
-
} else {
|
|
1439
|
-
passFn(`${rel}: nodeSelector patch (ru) не застосовується — немає Deployment у дереві k8s цього пакета (abie)`)
|
|
1440
|
-
}
|
|
1435
|
+
const ok = await checkNodeSelectorKustomization(abs, rel, 'ru', deploymentDirs, root, fail, passFn)
|
|
1436
|
+
if (!ok) return
|
|
1441
1437
|
}
|
|
1442
1438
|
}
|
|
1443
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
|
|
1485
|
+
}
|
|
1486
|
+
passFn(`${rel}: HTTPRoute patch (${mode}) відповідає abie.mdc`)
|
|
1487
|
+
return true
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1444
1490
|
/**
|
|
1445
1491
|
* Якщо є **Deployment** під **k8s**, вимагає в overlay **ua** та **ru** patch **HTTPRoute** (непорожній **target.name**) за abie.mdc
|
|
1446
1492
|
* лише для пакетів з **vite.config.{js,mjs,ts}** у каталозі пакета (батько **k8s**).
|
|
1447
1493
|
* @param {string} root корінь репозиторію
|
|
1448
1494
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
1449
|
-
* @param {(msg: string) => void} fail callback
|
|
1450
|
-
* @param {(msg: string) => void} passFn
|
|
1451
|
-
* @returns {Promise<void>}
|
|
1495
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
1496
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
1452
1497
|
*/
|
|
1453
1498
|
async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
|
|
1454
1499
|
/** @type {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} */
|
|
1455
|
-
const
|
|
1456
|
-
/**
|
|
1457
|
-
* @param {string} pkgAbs абсолютний шлях до каталогу пакета
|
|
1458
|
-
* @returns {Promise<{ refCount: number, baseErrors: string[] }>} кількість посилань і базові помилки
|
|
1459
|
-
*/
|
|
1460
|
-
const getSharedBackendAnalysis = pkgAbs => {
|
|
1461
|
-
let p = sharedBackendAnalysisByPkg.get(pkgAbs)
|
|
1462
|
-
if (!p) {
|
|
1463
|
-
p = analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yamlFilesAbs)
|
|
1464
|
-
sharedBackendAnalysisByPkg.set(pkgAbs, p)
|
|
1465
|
-
}
|
|
1466
|
-
return p
|
|
1467
|
-
}
|
|
1500
|
+
const cache = new Map()
|
|
1468
1501
|
|
|
1469
1502
|
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
1470
1503
|
if (uaAbsList.length === 0) {
|
|
@@ -1474,35 +1507,8 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
|
|
|
1474
1507
|
}
|
|
1475
1508
|
for (const abs of uaAbsList) {
|
|
1476
1509
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
if (!pkgAbs) {
|
|
1480
|
-
fail(`${rel}: внутрішня помилка abie overlay (немає каталогу пакета)`)
|
|
1481
|
-
return
|
|
1482
|
-
}
|
|
1483
|
-
const sharedAnalysis = await getSharedBackendAnalysis(pkgAbs)
|
|
1484
|
-
for (const err of sharedAnalysis.baseErrors) {
|
|
1485
|
-
fail(err)
|
|
1486
|
-
return
|
|
1487
|
-
}
|
|
1488
|
-
let raw
|
|
1489
|
-
try {
|
|
1490
|
-
raw = await readFile(abs, 'utf8')
|
|
1491
|
-
} catch (error) {
|
|
1492
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1493
|
-
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
1494
|
-
return
|
|
1495
|
-
}
|
|
1496
|
-
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
1497
|
-
const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua', raw, sharedAnalysis.refCount)
|
|
1498
|
-
if (v !== null) {
|
|
1499
|
-
fail(`${rel}: ${v}`)
|
|
1500
|
-
return
|
|
1501
|
-
}
|
|
1502
|
-
passFn(`${rel}: HTTPRoute patch (ua) відповідає abie.mdc`)
|
|
1503
|
-
} else {
|
|
1504
|
-
passFn(`${rel}: HTTPRoute patch (ua) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
|
|
1505
|
-
}
|
|
1510
|
+
const ok = await checkHttpRouteKustomization(abs, rel, 'ua', root, yamlFilesAbs, cache, fail, passFn)
|
|
1511
|
+
if (!ok) return
|
|
1506
1512
|
}
|
|
1507
1513
|
|
|
1508
1514
|
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
@@ -1513,35 +1519,8 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
|
|
|
1513
1519
|
}
|
|
1514
1520
|
for (const abs of ruAbsList) {
|
|
1515
1521
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
if (!pkgAbs) {
|
|
1519
|
-
fail(`${rel}: внутрішня помилка abie overlay (немає каталогу пакета)`)
|
|
1520
|
-
return
|
|
1521
|
-
}
|
|
1522
|
-
const sharedAnalysis = await getSharedBackendAnalysis(pkgAbs)
|
|
1523
|
-
for (const err of sharedAnalysis.baseErrors) {
|
|
1524
|
-
fail(err)
|
|
1525
|
-
return
|
|
1526
|
-
}
|
|
1527
|
-
let raw
|
|
1528
|
-
try {
|
|
1529
|
-
raw = await readFile(abs, 'utf8')
|
|
1530
|
-
} catch (error) {
|
|
1531
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1532
|
-
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
1533
|
-
return
|
|
1534
|
-
}
|
|
1535
|
-
const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
|
|
1536
|
-
const v = validateAbieNginxRunHttpRoutePatches(combined, 'ru', raw, sharedAnalysis.refCount)
|
|
1537
|
-
if (v !== null) {
|
|
1538
|
-
fail(`${rel}: ${v}`)
|
|
1539
|
-
return
|
|
1540
|
-
}
|
|
1541
|
-
passFn(`${rel}: HTTPRoute patch (ru) відповідає abie.mdc`)
|
|
1542
|
-
} else {
|
|
1543
|
-
passFn(`${rel}: HTTPRoute patch (ru) не застосовується — немає vite.config.{js,mjs,ts} у пакеті (abie)`)
|
|
1544
|
-
}
|
|
1522
|
+
const ok = await checkHttpRouteKustomization(abs, rel, 'ru', root, yamlFilesAbs, cache, fail, passFn)
|
|
1523
|
+
if (!ok) return
|
|
1545
1524
|
}
|
|
1546
1525
|
}
|
|
1547
1526
|
|
|
@@ -1573,6 +1552,85 @@ function ensureNoFirebaseHostingArtifacts(root, passFn, failFn) {
|
|
|
1573
1552
|
* Перевіряє відповідність проєкту правилам abie.mdc.
|
|
1574
1553
|
* @returns {Promise<number>} 0 — OK, 1 — є порушення
|
|
1575
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
|
+
*/
|
|
1576
1634
|
export async function check() {
|
|
1577
1635
|
const reporter = createCheckReporter()
|
|
1578
1636
|
const { pass, fail } = reporter
|
|
@@ -1586,67 +1644,14 @@ export async function check() {
|
|
|
1586
1644
|
|
|
1587
1645
|
pass('Правило abie увімкнено — виконуємо перевірки')
|
|
1588
1646
|
ensureNoFirebaseHostingArtifacts(root, pass, fail)
|
|
1589
|
-
|
|
1590
|
-
const cleanMergedPath = join(root, '.github/workflows/clean-merged-branch.yml')
|
|
1591
|
-
if (existsSync(cleanMergedPath)) {
|
|
1592
|
-
/** @type {string | undefined} */
|
|
1593
|
-
let wfRaw
|
|
1594
|
-
try {
|
|
1595
|
-
wfRaw = await readFile(cleanMergedPath, 'utf8')
|
|
1596
|
-
} catch (error) {
|
|
1597
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1598
|
-
fail(`Не вдалося прочитати clean-merged-branch.yml (${msg})`)
|
|
1599
|
-
}
|
|
1600
|
-
if (wfRaw !== undefined) {
|
|
1601
|
-
const ib = parseCleanMergedIgnoreBranches(wfRaw)
|
|
1602
|
-
if (ib === null || ib.trim() === '') {
|
|
1603
|
-
fail(
|
|
1604
|
-
'clean-merged-branch.yml: не знайдено with.ignore_branches у кроці phpdocker-io/github-actions-delete-abandoned-branches (abie.mdc)'
|
|
1605
|
-
)
|
|
1606
|
-
} else if (ignoreBranchesIncludesRequired(ib, ABIE_REQUIRED_IGNORE_BRANCHES)) {
|
|
1607
|
-
pass('clean-merged-branch.yml: ignore_branches містить dev, ua, ru')
|
|
1608
|
-
} else {
|
|
1609
|
-
fail(
|
|
1610
|
-
`clean-merged-branch.yml: ignore_branches має містити dev, ua та ru (зараз: ${JSON.stringify(ib)}) — abie.mdc`
|
|
1611
|
-
)
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
} else {
|
|
1615
|
-
fail(`Відсутній ${cleanMergedPath} — потрібен для ignore_branches (abie.mdc)`)
|
|
1616
|
-
}
|
|
1647
|
+
await checkCleanMergedBranch(root, pass, fail)
|
|
1617
1648
|
|
|
1618
1649
|
const yamlFiles = await findK8sYamlFiles(root)
|
|
1619
1650
|
const deploymentDirs = await collectDeploymentDirs(root, yamlFiles, fail)
|
|
1620
1651
|
|
|
1621
1652
|
if (deploymentDirs.size > 0) {
|
|
1622
1653
|
pass(`Знайдено Deployment у ${deploymentDirs.size} директорія(ї/й) k8s — перевіряємо hc.yaml`)
|
|
1623
|
-
|
|
1624
|
-
const hcAbs = join(dir, 'hc.yaml')
|
|
1625
|
-
const relHc = relative(root, hcAbs).replaceAll('\\', '/') || 'hc.yaml'
|
|
1626
|
-
if (existsSync(hcAbs)) {
|
|
1627
|
-
let hcRaw
|
|
1628
|
-
let hcReadOk = false
|
|
1629
|
-
try {
|
|
1630
|
-
hcRaw = await readFile(hcAbs, 'utf8')
|
|
1631
|
-
hcReadOk = true
|
|
1632
|
-
} catch (error) {
|
|
1633
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1634
|
-
fail(`${relHc}: не вдалося прочитати (${msg})`)
|
|
1635
|
-
}
|
|
1636
|
-
if (hcReadOk) {
|
|
1637
|
-
const v = validateAbieHcYaml(hcRaw, relHc)
|
|
1638
|
-
if (v === null) {
|
|
1639
|
-
pass(`${relHc}: відповідає abie.mdc`)
|
|
1640
|
-
} else {
|
|
1641
|
-
fail(v)
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
} else {
|
|
1645
|
-
fail(
|
|
1646
|
-
`${relative(root, dir) || dir}: є Deployment, але немає hc.yaml поруч — додай HealthCheckPolicy (abie.mdc)`
|
|
1647
|
-
)
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1654
|
+
await checkHcYamlFiles(root, deploymentDirs, pass, fail)
|
|
1650
1655
|
pass('Є Deployment — перевіряємо base: spec.template.spec.nodeSelector.preem (abie.mdc)')
|
|
1651
1656
|
await ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFiles, fail, pass)
|
|
1652
1657
|
} else {
|