@nitra/cursor 1.8.98 → 1.8.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mdc/abie.mdc +8 -4
- package/package.json +1 -1
- package/scripts/check-abie.mjs +118 -37
package/mdc/abie.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Правила для проєктів AbInBev Efes
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.15'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** / **ru** — **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**, для спільних сервісів **`auth-run-hl`** / **`filelint-hl`** — **`namespace: dev`** у base та patch **`…/backendRefs/…/namespace`** у **ua** / **ru**), у overlay **ru** — кожен **Service** (у т. ч. **headless** / **`-hl`**) → **`spec.type: NodePort`** через **JSON6902** у **`kustomization.yaml`**, видалення **HealthCheckPolicy** у **ru**), гілки **dev**, **ua**, **ru** у **clean-merged-branch**, а також заборона артефактів **Firebase Hosting** у корені репозиторію.
|
|
@@ -115,17 +115,21 @@ spec:
|
|
|
115
115
|
|
|
116
116
|
## k8s: overlay **ru** і **Service** (у т. ч. headless → NodePort)
|
|
117
117
|
|
|
118
|
-
Для кожного **Service** в YAML під **`…/k8s/…`**, де шлях файлу **не** містить **`k8s/ua/`** чи **`k8s/ru/`** (маніфести base / спільного шару; у т. ч. **headless** з **`spec.clusterIP: None`** і **`-hl`**), якщо ще не **`spec.type: NodePort`** / **`LoadBalancer`** / **`ExternalName`**, у **`k8s/ru/kustomization.yaml`** того ж пакета (overlay **ru**) додай **inline** **JSON6902** у **`patches`**: **`target.kind: Service`**, **`target.name`** як у маніфеста, **`path: /spec/type`**, **`value: NodePort`**.
|
|
118
|
+
Для кожного **Service** в YAML під **`…/k8s/…`**, де шлях файлу **не** містить **`k8s/ua/`** чи **`k8s/ru/`** (маніфести base / спільного шару; у т. ч. **headless** з **`spec.clusterIP: None`** і **`-hl`**), якщо ще не **`spec.type: NodePort`** / **`LoadBalancer`** / **`ExternalName`**, у **`k8s/ru/kustomization.yaml`** того ж пакета (overlay **ru**) додай **inline** **JSON6902** у **`patches`**: **`target.kind: Service`**, **`target.name`** як у маніфеста, **`path: /spec/type`**, **`value: NodePort`**. Якщо в base було **`spec.clusterIP: None`** або **`spec.clusterIPs`** з **`None`** (типово **headless**), у тому ж **patch** додай **`op: remove`** для **`/spec/clusterIP`** та **`/spec/clusterIPs`** — інакше API відхилить **NodePort** з помилкою на **`clusterIPs`**. Деталі — **`check-abie.mjs`**.
|
|
119
119
|
|
|
120
|
-
```yaml title="…/ru/kustomization.yaml (
|
|
120
|
+
```yaml title="…/ru/kustomization.yaml (фрагмент, headless → NodePort)"
|
|
121
121
|
patches:
|
|
122
122
|
- target:
|
|
123
123
|
kind: Service
|
|
124
|
-
name:
|
|
124
|
+
name: user-site-hl
|
|
125
125
|
patch: |-
|
|
126
126
|
- op: replace
|
|
127
127
|
path: /spec/type
|
|
128
128
|
value: NodePort
|
|
129
|
+
- op: remove
|
|
130
|
+
path: /spec/clusterIP
|
|
131
|
+
- op: remove
|
|
132
|
+
path: /spec/clusterIPs
|
|
129
133
|
```
|
|
130
134
|
|
|
131
135
|
## k8s: overlay **ru** і HealthCheckPolicy
|
package/package.json
CHANGED
package/scripts/check-abie.mjs
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* Вибір **`op`** — **k8s.mdc**.
|
|
35
35
|
*
|
|
36
36
|
* **Service (overlay ru):** для кожного **Service**, оголошеного в YAML під **`…/k8s/…`**, де шлях **не** проходить через **`k8s/ua/`** чи **`k8s/ru/`** (маніфести base / спільного шару, у т. ч. **headless** з **`clusterIP: None`** і **`-hl`**), якщо ще не **NodePort** / **LoadBalancer** / **ExternalName**,
|
|
37
|
-
* у файлі **`k8s/ru/kustomization.yaml`** того ж пакета (overlay середовища **ru**) — inline **JSON6902** на **`kind: Service`** з тим самим **`target.name`**: **`path: /spec/type`**, **`value: NodePort
|
|
37
|
+
* у файлі **`k8s/ru/kustomization.yaml`** того ж пакета (overlay середовища **ru**) — inline **JSON6902** на **`kind: Service`** з тим самим **`target.name`**: **`path: /spec/type`**, **`value: NodePort`**; якщо в base було **`spec.clusterIP: None`** або **`spec.clusterIPs`** з **`None`** — у тому ж patch додай **`op: remove`** для **`/spec/clusterIP`** та **`/spec/clusterIPs`** (інакше **NodePort** з **`None`** відхиляє API).
|
|
38
38
|
*/
|
|
39
39
|
import { existsSync } from 'node:fs'
|
|
40
40
|
import { readFile } from 'node:fs/promises'
|
|
@@ -369,6 +369,61 @@ export function serviceDocumentRequiresAbieRuNodePortOverlay(obj) {
|
|
|
369
369
|
return true
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Чи в base-**Service** є **headless** **`None`**, який треба прибрати в **ru** перед **NodePort** (**clusterIP** / **clusterIPs**).
|
|
374
|
+
* @param {unknown} obj корінь YAML (**Service**)
|
|
375
|
+
* @returns {boolean} **true**, якщо **`spec.clusterIP === 'None'`** або **`spec.clusterIPs`** містить **`'None'`**
|
|
376
|
+
*/
|
|
377
|
+
export function serviceDocumentRequiresRuClusterIPNoneRemoval(obj) {
|
|
378
|
+
if (!isServiceDoc(obj)) {
|
|
379
|
+
return false
|
|
380
|
+
}
|
|
381
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
382
|
+
const spec = rec.spec
|
|
383
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
384
|
+
return false
|
|
385
|
+
}
|
|
386
|
+
const sp = /** @type {Record<string, unknown>} */ (spec)
|
|
387
|
+
if (sp.clusterIP === 'None') {
|
|
388
|
+
return true
|
|
389
|
+
}
|
|
390
|
+
const cips = sp.clusterIPs
|
|
391
|
+
if (Array.isArray(cips) && cips.includes('None')) {
|
|
392
|
+
return true
|
|
393
|
+
}
|
|
394
|
+
return false
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Чи **JSON6902**-текст містить **`op: remove`** для заданого **`path`** (порядок ключів **op** / **path** неважливий).
|
|
399
|
+
* @param {string} patchText поле **patch** у kustomization
|
|
400
|
+
* @param {string} posixPath наприклад **`/spec/clusterIP`**
|
|
401
|
+
* @returns {boolean} true, якщо знайдено пару **remove** + **path**
|
|
402
|
+
*/
|
|
403
|
+
export function jsonPatchRemovesPath(patchText, posixPath) {
|
|
404
|
+
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
405
|
+
return false
|
|
406
|
+
}
|
|
407
|
+
if (posixPath !== '/spec/clusterIP' && posixPath !== '/spec/clusterIPs') {
|
|
408
|
+
return false
|
|
409
|
+
}
|
|
410
|
+
const pathRe =
|
|
411
|
+
posixPath === '/spec/clusterIP'
|
|
412
|
+
? String.raw`path:\s*\/spec\/clusterIP\b`
|
|
413
|
+
: String.raw`path:\s*\/spec\/clusterIPs\b`
|
|
414
|
+
const opRe = String.raw`op:\s*remove\b`
|
|
415
|
+
return new RegExp(`${opRe}[\\s\\S]{0,200}?${pathRe}`, 'mu').test(patchText) || new RegExp(`${pathRe}[\\s\\S]{0,200}?${opRe}`, 'mu').test(patchText)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Чи patch прибирає **headless** поля **`clusterIP`** / **`clusterIPs`**, щоб **NodePort** пройшов валідацію API.
|
|
420
|
+
* @param {string} patchText поле **patch** у kustomization
|
|
421
|
+
* @returns {boolean} true, якщо є **remove** і для **`/spec/clusterIP`**, і для **`/spec/clusterIPs`**
|
|
422
|
+
*/
|
|
423
|
+
export function jsonPatchTextClearsHeadlessServiceClusterIPNone(patchText) {
|
|
424
|
+
return jsonPatchRemovesPath(patchText, '/spec/clusterIP') && jsonPatchRemovesPath(patchText, '/spec/clusterIPs')
|
|
425
|
+
}
|
|
426
|
+
|
|
372
427
|
/**
|
|
373
428
|
* Чи фрагмент **JSON6902** у **`patch`** задає **`/spec/type`** зі значенням **NodePort** (abie overlay **ru**).
|
|
374
429
|
* @param {string} patchText поле **patch** у kustomization
|
|
@@ -430,31 +485,24 @@ function collectAbieServicePatchTextsByNameFromKustomizationDoc(doc) {
|
|
|
430
485
|
}
|
|
431
486
|
|
|
432
487
|
/**
|
|
433
|
-
*
|
|
488
|
+
* Збирає тексти **patch** на **Service** з **kustomization.yaml** (усі документи).
|
|
434
489
|
* @param {string} raw повний текст **kustomization.yaml**
|
|
435
|
-
* @
|
|
436
|
-
* @returns {string[]} відсортовані імена без коректного patch
|
|
490
|
+
* @returns {Map<string, string>} **target.name** → об’єднаний текст **patch**
|
|
437
491
|
*/
|
|
438
|
-
|
|
439
|
-
const req = [...new Set([...serviceNames].filter(n => typeof n === 'string' && n.trim() !== ''))].toSorted((a, b) =>
|
|
440
|
-
a.localeCompare(b)
|
|
441
|
-
)
|
|
442
|
-
if (req.length === 0) {
|
|
443
|
-
return []
|
|
444
|
-
}
|
|
492
|
+
function collectAbieRuServicePatchTextByTargetNameFromRaw(raw) {
|
|
445
493
|
const body = stripBom(raw)
|
|
446
494
|
const lines = body.split(/\r?\n/u)
|
|
447
495
|
const first = lines[0] ?? ''
|
|
448
496
|
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
497
|
+
/** @type {Map<string, string>} */
|
|
498
|
+
const byName = new Map()
|
|
449
499
|
/** @type {import('yaml').Document[]} */
|
|
450
500
|
let docs
|
|
451
501
|
try {
|
|
452
502
|
docs = parseAllDocuments(rest)
|
|
453
503
|
} catch {
|
|
454
|
-
return
|
|
504
|
+
return byName
|
|
455
505
|
}
|
|
456
|
-
/** @type {Map<string, string>} */
|
|
457
|
-
const byName = new Map()
|
|
458
506
|
for (const doc of docs) {
|
|
459
507
|
const chunk = collectAbieServicePatchTextsByNameFromKustomizationDoc(doc)
|
|
460
508
|
for (const [k, v] of chunk) {
|
|
@@ -462,21 +510,51 @@ export function getMissingAbieRuServiceNodePortPatchServiceNames(raw, serviceNam
|
|
|
462
510
|
byName.set(k, prev === undefined ? v : `${prev}\n${v}`)
|
|
463
511
|
}
|
|
464
512
|
}
|
|
465
|
-
return
|
|
466
|
-
const pt = byName.get(n)
|
|
467
|
-
return pt === undefined || !jsonPatchTextSetsServiceTypeNodePort(pt)
|
|
468
|
-
})
|
|
513
|
+
return byName
|
|
469
514
|
}
|
|
470
515
|
|
|
471
516
|
/**
|
|
472
|
-
*
|
|
517
|
+
* Повідомлення про порушення patch **Service** у **ru/kustomization.yaml** (abie.mdc).
|
|
518
|
+
* @param {string} raw повний текст **kustomization.yaml**
|
|
519
|
+
* @param {Map<string, { requiresClusterIPNoneClear: boolean }>} targetsByName ім’я **Service** → чи треба прибрати **None**
|
|
520
|
+
* @returns {string[]} порожньо, якщо все OK
|
|
521
|
+
*/
|
|
522
|
+
export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
|
|
523
|
+
if (targetsByName.size === 0) {
|
|
524
|
+
return []
|
|
525
|
+
}
|
|
526
|
+
const byName = collectAbieRuServicePatchTextByTargetNameFromRaw(raw)
|
|
527
|
+
/** @type {string[]} */
|
|
528
|
+
const errors = []
|
|
529
|
+
for (const name of [...targetsByName.keys()].toSorted((a, b) => a.localeCompare(b))) {
|
|
530
|
+
const flags = targetsByName.get(name)
|
|
531
|
+
const requiresClear = flags?.requiresClusterIPNoneClear === true
|
|
532
|
+
const pt = byName.get(name)
|
|
533
|
+
if (pt === undefined || String(pt).trim() === '') {
|
|
534
|
+
errors.push(`${name}: немає inline patch для kind: Service`)
|
|
535
|
+
} else {
|
|
536
|
+
if (!jsonPatchTextSetsServiceTypeNodePort(pt)) {
|
|
537
|
+
errors.push(`${name}: потрібен JSON6902 path /spec/type та value NodePort`)
|
|
538
|
+
}
|
|
539
|
+
if (requiresClear && !jsonPatchTextClearsHeadlessServiceClusterIPNone(pt)) {
|
|
540
|
+
errors.push(
|
|
541
|
+
`${name}: для spec.clusterIP/spec.clusterIPs: None додай у той самий patch op: remove для path /spec/clusterIP та /spec/clusterIPs (abie.mdc)`
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return errors
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Для кожного пакета збирає **Service**, які в overlay **ru** мають стати **NodePort** (abie.mdc).
|
|
473
551
|
* @param {string} root корінь репозиторію
|
|
474
552
|
* @param {string[]} yamlAbs абсолютні шляхи yaml під **k8s**
|
|
475
553
|
* @param {(msg: string) => void} fail реєстрація помилки читання/парсингу
|
|
476
|
-
* @returns {Promise<Map<string,
|
|
554
|
+
* @returns {Promise<Map<string, Map<string, { requiresClusterIPNoneClear: boolean }>>>} **pkgAbs** → (**ім’я** → прапорці)
|
|
477
555
|
*/
|
|
478
|
-
async function
|
|
479
|
-
/** @type {Map<string,
|
|
556
|
+
async function collectAbieRuNodePortServiceTargetsByPackage(root, yamlAbs, fail) {
|
|
557
|
+
/** @type {Map<string, Map<string, { requiresClusterIPNoneClear: boolean }>>} */
|
|
480
558
|
const map = new Map()
|
|
481
559
|
for (const abs of yamlAbs) {
|
|
482
560
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
@@ -516,12 +594,16 @@ async function collectAbieRuNodePortServiceNamesByPackage(root, yamlAbs, fail) {
|
|
|
516
594
|
const meta = /** @type {Record<string, unknown>} */ (rec.metadata)
|
|
517
595
|
const n = meta.name
|
|
518
596
|
if (typeof n === 'string' && n.trim() !== '') {
|
|
519
|
-
let
|
|
520
|
-
if (!
|
|
521
|
-
|
|
522
|
-
map.set(pkgAbs,
|
|
597
|
+
let inner = map.get(pkgAbs)
|
|
598
|
+
if (!inner) {
|
|
599
|
+
inner = new Map()
|
|
600
|
+
map.set(pkgAbs, inner)
|
|
523
601
|
}
|
|
524
|
-
|
|
602
|
+
const needClear = serviceDocumentRequiresRuClusterIPNoneRemoval(obj)
|
|
603
|
+
const prev = inner.get(n)
|
|
604
|
+
inner.set(n, {
|
|
605
|
+
requiresClusterIPNoneClear: (prev?.requiresClusterIPNoneClear === true) || needClear
|
|
606
|
+
})
|
|
525
607
|
}
|
|
526
608
|
}
|
|
527
609
|
}
|
|
@@ -535,7 +617,7 @@ async function collectAbieRuNodePortServiceNamesByPackage(root, yamlAbs, fail) {
|
|
|
535
617
|
}
|
|
536
618
|
|
|
537
619
|
/**
|
|
538
|
-
* У **`k8s/ru/kustomization.yaml`** для кожного **Service** з YAML **`k8s`**, шлях якого без сегментів **`k8s/ua/`** та **`k8s/ru/`** (у т. ч. **headless** / **`-hl`**) — **JSON6902** **`/spec/type` → NodePort
|
|
620
|
+
* У **`k8s/ru/kustomization.yaml`** для кожного **Service** з YAML **`k8s`**, шлях якого без сегментів **`k8s/ua/`** та **`k8s/ru/`** (у т. ч. **headless** / **`-hl`**) — **JSON6902** **`/spec/type` → NodePort**; якщо в base було **`clusterIP: None`** / **`clusterIPs: None`** — також **`op: remove`** на **`/spec/clusterIP`** та **`/spec/clusterIPs`** (abie.mdc).
|
|
539
621
|
* @param {string} root корінь
|
|
540
622
|
* @param {string[]} yamlFilesAbs yaml під **k8s**
|
|
541
623
|
* @param {(msg: string) => void} fail callback
|
|
@@ -543,18 +625,19 @@ async function collectAbieRuNodePortServiceNamesByPackage(root, yamlAbs, fail) {
|
|
|
543
625
|
* @returns {Promise<void>}
|
|
544
626
|
*/
|
|
545
627
|
async function ensureRuAbieServiceNodePortPatches(root, yamlFilesAbs, fail, passFn) {
|
|
546
|
-
const byPkg = await
|
|
547
|
-
const entries = [...byPkg.entries()].filter(([,
|
|
628
|
+
const byPkg = await collectAbieRuNodePortServiceTargetsByPackage(root, yamlFilesAbs, fail)
|
|
629
|
+
const entries = [...byPkg.entries()].filter(([, m]) => m.size > 0)
|
|
548
630
|
if (entries.length === 0) {
|
|
549
631
|
passFn('Немає Service у шарі k8s без k8s/ua/ та k8s/ru/ — patch NodePort у k8s/ru/ не вимагається (abie.mdc)')
|
|
550
632
|
return
|
|
551
633
|
}
|
|
552
|
-
for (const [pkgAbs,
|
|
634
|
+
for (const [pkgAbs, targetsByName] of entries.toSorted((a, b) => a[0].localeCompare(b[0]))) {
|
|
553
635
|
const relPkg = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
|
|
554
636
|
const ruAbs = join(pkgAbs, 'k8s', 'ru', 'kustomization.yaml')
|
|
637
|
+
const nameList = [...targetsByName.keys()].toSorted((a, b) => a.localeCompare(b))
|
|
555
638
|
if (!existsSync(ruAbs)) {
|
|
556
639
|
fail(
|
|
557
|
-
`${relPkg}/k8s: є Service
|
|
640
|
+
`${relPkg}/k8s: є Service, для overlay ru потрібен patch Service (NodePort; для headless — ще remove clusterIP/clusterIPs): ${nameList.join(', ')} — додай ru/kustomization.yaml (abie.mdc)`
|
|
558
641
|
)
|
|
559
642
|
return
|
|
560
643
|
}
|
|
@@ -567,11 +650,9 @@ async function ensureRuAbieServiceNodePortPatches(root, yamlFilesAbs, fail, pass
|
|
|
567
650
|
fail(`${relRu}: не вдалося прочитати (${msg})`)
|
|
568
651
|
return
|
|
569
652
|
}
|
|
570
|
-
const
|
|
571
|
-
if (
|
|
572
|
-
fail(
|
|
573
|
-
`${relRu}: для kind: Service потрібен inline JSON6902 з path /spec/type та value NodePort (ім’я target: ${missing.join(', ')}) — abie.mdc`
|
|
574
|
-
)
|
|
653
|
+
const patchErrors = getAbieRuServiceNodePortPatchErrors(raw, targetsByName)
|
|
654
|
+
if (patchErrors.length > 0) {
|
|
655
|
+
fail(`${relRu}: ${patchErrors.join('; ')}`)
|
|
575
656
|
return
|
|
576
657
|
}
|
|
577
658
|
passFn(`${relRu}: patch Service → NodePort (ru) відповідає abie.mdc`)
|