@nitra/cursor 1.8.98 → 1.8.100
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 +6 -4
- package/package.json +1 -1
- package/scripts/check-abie.mjs +108 -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,19 @@ 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`**, у тому ж **patch** додай **`op: remove`** для **`/spec/clusterIP`** (не додавай **`remove`** на **`/spec/clusterIPs`**: у статичному YAML ключа часто немає — **`kubectl kustomize`** падає з *Unable to remove nonexistent key*). Деталі — **`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
|
|
129
131
|
```
|
|
130
132
|
|
|
131
133
|
## 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`** — у тому ж patch додай **`op: remove`** для **`/spec/clusterIP`** (поле **`clusterIPs`** у статичному YAML часто відсутнє — **`remove`** на **`/spec/clusterIPs`** ламає **`kubectl kustomize`**).
|
|
38
38
|
*/
|
|
39
39
|
import { existsSync } from 'node:fs'
|
|
40
40
|
import { readFile } from 'node:fs/promises'
|
|
@@ -369,6 +369,51 @@ export function serviceDocumentRequiresAbieRuNodePortOverlay(obj) {
|
|
|
369
369
|
return true
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Чи в base-**Service** задано **headless** через **`spec.clusterIP: None`**, який треба прибрати в **ru** перед **NodePort**.
|
|
374
|
+
* @param {unknown} obj корінь YAML (**Service**)
|
|
375
|
+
* @returns {boolean} **true**, якщо **`spec.clusterIP === '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
|
+
return sp.clusterIP === 'None'
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Чи **JSON6902**-текст містить **`op: remove`** для заданого **`path`** (порядок ключів **op** / **path** неважливий).
|
|
392
|
+
* @param {string} patchText поле **patch** у kustomization
|
|
393
|
+
* @param {string} posixPath наприклад **`/spec/clusterIP`**
|
|
394
|
+
* @returns {boolean} true, якщо знайдено пару **remove** + **path**
|
|
395
|
+
*/
|
|
396
|
+
export function jsonPatchRemovesPath(patchText, posixPath) {
|
|
397
|
+
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
398
|
+
return false
|
|
399
|
+
}
|
|
400
|
+
if (posixPath !== '/spec/clusterIP') {
|
|
401
|
+
return false
|
|
402
|
+
}
|
|
403
|
+
const pathRe = String.raw`path:\s*\/spec\/clusterIP\b`
|
|
404
|
+
const opRe = String.raw`op:\s*remove\b`
|
|
405
|
+
return new RegExp(`${opRe}[\\s\\S]{0,200}?${pathRe}`, 'mu').test(patchText) || new RegExp(`${pathRe}[\\s\\S]{0,200}?${opRe}`, 'mu').test(patchText)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Чи patch містить **`op: remove`** для **`/spec/clusterIP`**, щоб прибрати **headless** перед **NodePort**.
|
|
410
|
+
* @param {string} patchText поле **patch** у kustomization
|
|
411
|
+
* @returns {boolean} true, якщо є **remove** для **`/spec/clusterIP`**
|
|
412
|
+
*/
|
|
413
|
+
export function jsonPatchTextClearsHeadlessServiceClusterIPNone(patchText) {
|
|
414
|
+
return jsonPatchRemovesPath(patchText, '/spec/clusterIP')
|
|
415
|
+
}
|
|
416
|
+
|
|
372
417
|
/**
|
|
373
418
|
* Чи фрагмент **JSON6902** у **`patch`** задає **`/spec/type`** зі значенням **NodePort** (abie overlay **ru**).
|
|
374
419
|
* @param {string} patchText поле **patch** у kustomization
|
|
@@ -430,31 +475,24 @@ function collectAbieServicePatchTextsByNameFromKustomizationDoc(doc) {
|
|
|
430
475
|
}
|
|
431
476
|
|
|
432
477
|
/**
|
|
433
|
-
*
|
|
478
|
+
* Збирає тексти **patch** на **Service** з **kustomization.yaml** (усі документи).
|
|
434
479
|
* @param {string} raw повний текст **kustomization.yaml**
|
|
435
|
-
* @
|
|
436
|
-
* @returns {string[]} відсортовані імена без коректного patch
|
|
480
|
+
* @returns {Map<string, string>} **target.name** → об’єднаний текст **patch**
|
|
437
481
|
*/
|
|
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
|
-
}
|
|
482
|
+
function collectAbieRuServicePatchTextByTargetNameFromRaw(raw) {
|
|
445
483
|
const body = stripBom(raw)
|
|
446
484
|
const lines = body.split(/\r?\n/u)
|
|
447
485
|
const first = lines[0] ?? ''
|
|
448
486
|
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
487
|
+
/** @type {Map<string, string>} */
|
|
488
|
+
const byName = new Map()
|
|
449
489
|
/** @type {import('yaml').Document[]} */
|
|
450
490
|
let docs
|
|
451
491
|
try {
|
|
452
492
|
docs = parseAllDocuments(rest)
|
|
453
493
|
} catch {
|
|
454
|
-
return
|
|
494
|
+
return byName
|
|
455
495
|
}
|
|
456
|
-
/** @type {Map<string, string>} */
|
|
457
|
-
const byName = new Map()
|
|
458
496
|
for (const doc of docs) {
|
|
459
497
|
const chunk = collectAbieServicePatchTextsByNameFromKustomizationDoc(doc)
|
|
460
498
|
for (const [k, v] of chunk) {
|
|
@@ -462,21 +500,51 @@ export function getMissingAbieRuServiceNodePortPatchServiceNames(raw, serviceNam
|
|
|
462
500
|
byName.set(k, prev === undefined ? v : `${prev}\n${v}`)
|
|
463
501
|
}
|
|
464
502
|
}
|
|
465
|
-
return
|
|
466
|
-
const pt = byName.get(n)
|
|
467
|
-
return pt === undefined || !jsonPatchTextSetsServiceTypeNodePort(pt)
|
|
468
|
-
})
|
|
503
|
+
return byName
|
|
469
504
|
}
|
|
470
505
|
|
|
471
506
|
/**
|
|
472
|
-
*
|
|
507
|
+
* Повідомлення про порушення patch **Service** у **ru/kustomization.yaml** (abie.mdc).
|
|
508
|
+
* @param {string} raw повний текст **kustomization.yaml**
|
|
509
|
+
* @param {Map<string, { requiresClusterIPNoneClear: boolean }>} targetsByName ім’я **Service** → чи треба прибрати **None**
|
|
510
|
+
* @returns {string[]} порожньо, якщо все OK
|
|
511
|
+
*/
|
|
512
|
+
export function getAbieRuServiceNodePortPatchErrors(raw, targetsByName) {
|
|
513
|
+
if (targetsByName.size === 0) {
|
|
514
|
+
return []
|
|
515
|
+
}
|
|
516
|
+
const byName = collectAbieRuServicePatchTextByTargetNameFromRaw(raw)
|
|
517
|
+
/** @type {string[]} */
|
|
518
|
+
const errors = []
|
|
519
|
+
for (const name of [...targetsByName.keys()].toSorted((a, b) => a.localeCompare(b))) {
|
|
520
|
+
const flags = targetsByName.get(name)
|
|
521
|
+
const requiresClear = flags?.requiresClusterIPNoneClear === true
|
|
522
|
+
const pt = byName.get(name)
|
|
523
|
+
if (pt === undefined || String(pt).trim() === '') {
|
|
524
|
+
errors.push(`${name}: немає inline patch для kind: Service`)
|
|
525
|
+
} else {
|
|
526
|
+
if (!jsonPatchTextSetsServiceTypeNodePort(pt)) {
|
|
527
|
+
errors.push(`${name}: потрібен JSON6902 path /spec/type та value NodePort`)
|
|
528
|
+
}
|
|
529
|
+
if (requiresClear && !jsonPatchTextClearsHeadlessServiceClusterIPNone(pt)) {
|
|
530
|
+
errors.push(
|
|
531
|
+
`${name}: для spec.clusterIP: None додай у той самий patch op: remove для path /spec/clusterIP (abie.mdc)`
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return errors
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Для кожного пакета збирає **Service**, які в overlay **ru** мають стати **NodePort** (abie.mdc).
|
|
473
541
|
* @param {string} root корінь репозиторію
|
|
474
542
|
* @param {string[]} yamlAbs абсолютні шляхи yaml під **k8s**
|
|
475
543
|
* @param {(msg: string) => void} fail реєстрація помилки читання/парсингу
|
|
476
|
-
* @returns {Promise<Map<string,
|
|
544
|
+
* @returns {Promise<Map<string, Map<string, { requiresClusterIPNoneClear: boolean }>>>} **pkgAbs** → (**ім’я** → прапорці)
|
|
477
545
|
*/
|
|
478
|
-
async function
|
|
479
|
-
/** @type {Map<string,
|
|
546
|
+
async function collectAbieRuNodePortServiceTargetsByPackage(root, yamlAbs, fail) {
|
|
547
|
+
/** @type {Map<string, Map<string, { requiresClusterIPNoneClear: boolean }>>} */
|
|
480
548
|
const map = new Map()
|
|
481
549
|
for (const abs of yamlAbs) {
|
|
482
550
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
@@ -516,12 +584,16 @@ async function collectAbieRuNodePortServiceNamesByPackage(root, yamlAbs, fail) {
|
|
|
516
584
|
const meta = /** @type {Record<string, unknown>} */ (rec.metadata)
|
|
517
585
|
const n = meta.name
|
|
518
586
|
if (typeof n === 'string' && n.trim() !== '') {
|
|
519
|
-
let
|
|
520
|
-
if (!
|
|
521
|
-
|
|
522
|
-
map.set(pkgAbs,
|
|
587
|
+
let inner = map.get(pkgAbs)
|
|
588
|
+
if (!inner) {
|
|
589
|
+
inner = new Map()
|
|
590
|
+
map.set(pkgAbs, inner)
|
|
523
591
|
}
|
|
524
|
-
|
|
592
|
+
const needClear = serviceDocumentRequiresRuClusterIPNoneRemoval(obj)
|
|
593
|
+
const prev = inner.get(n)
|
|
594
|
+
inner.set(n, {
|
|
595
|
+
requiresClusterIPNoneClear: (prev?.requiresClusterIPNoneClear === true) || needClear
|
|
596
|
+
})
|
|
525
597
|
}
|
|
526
598
|
}
|
|
527
599
|
}
|
|
@@ -535,7 +607,7 @@ async function collectAbieRuNodePortServiceNamesByPackage(root, yamlAbs, fail) {
|
|
|
535
607
|
}
|
|
536
608
|
|
|
537
609
|
/**
|
|
538
|
-
* У **`k8s/ru/kustomization.yaml`** для кожного **Service** з YAML **`k8s`**, шлях якого без сегментів **`k8s/ua/`** та **`k8s/ru/`** (у т. ч. **headless** / **`-hl`**) — **JSON6902** **`/spec/type` → NodePort
|
|
610
|
+
* У **`k8s/ru/kustomization.yaml`** для кожного **Service** з YAML **`k8s`**, шлях якого без сегментів **`k8s/ua/`** та **`k8s/ru/`** (у т. ч. **headless** / **`-hl`**) — **JSON6902** **`/spec/type` → NodePort**; якщо в base було **`clusterIP: None`** — також **`op: remove`** на **`/spec/clusterIP`** (abie.mdc).
|
|
539
611
|
* @param {string} root корінь
|
|
540
612
|
* @param {string[]} yamlFilesAbs yaml під **k8s**
|
|
541
613
|
* @param {(msg: string) => void} fail callback
|
|
@@ -543,18 +615,19 @@ async function collectAbieRuNodePortServiceNamesByPackage(root, yamlAbs, fail) {
|
|
|
543
615
|
* @returns {Promise<void>}
|
|
544
616
|
*/
|
|
545
617
|
async function ensureRuAbieServiceNodePortPatches(root, yamlFilesAbs, fail, passFn) {
|
|
546
|
-
const byPkg = await
|
|
547
|
-
const entries = [...byPkg.entries()].filter(([,
|
|
618
|
+
const byPkg = await collectAbieRuNodePortServiceTargetsByPackage(root, yamlFilesAbs, fail)
|
|
619
|
+
const entries = [...byPkg.entries()].filter(([, m]) => m.size > 0)
|
|
548
620
|
if (entries.length === 0) {
|
|
549
621
|
passFn('Немає Service у шарі k8s без k8s/ua/ та k8s/ru/ — patch NodePort у k8s/ru/ не вимагається (abie.mdc)')
|
|
550
622
|
return
|
|
551
623
|
}
|
|
552
|
-
for (const [pkgAbs,
|
|
624
|
+
for (const [pkgAbs, targetsByName] of entries.toSorted((a, b) => a[0].localeCompare(b[0]))) {
|
|
553
625
|
const relPkg = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
|
|
554
626
|
const ruAbs = join(pkgAbs, 'k8s', 'ru', 'kustomization.yaml')
|
|
627
|
+
const nameList = [...targetsByName.keys()].toSorted((a, b) => a.localeCompare(b))
|
|
555
628
|
if (!existsSync(ruAbs)) {
|
|
556
629
|
fail(
|
|
557
|
-
`${relPkg}/k8s: є Service
|
|
630
|
+
`${relPkg}/k8s: є Service, для overlay ru потрібен patch Service (NodePort; для headless — ще remove /spec/clusterIP): ${nameList.join(', ')} — додай ru/kustomization.yaml (abie.mdc)`
|
|
558
631
|
)
|
|
559
632
|
return
|
|
560
633
|
}
|
|
@@ -567,11 +640,9 @@ async function ensureRuAbieServiceNodePortPatches(root, yamlFilesAbs, fail, pass
|
|
|
567
640
|
fail(`${relRu}: не вдалося прочитати (${msg})`)
|
|
568
641
|
return
|
|
569
642
|
}
|
|
570
|
-
const
|
|
571
|
-
if (
|
|
572
|
-
fail(
|
|
573
|
-
`${relRu}: для kind: Service потрібен inline JSON6902 з path /spec/type та value NodePort (ім’я target: ${missing.join(', ')}) — abie.mdc`
|
|
574
|
-
)
|
|
643
|
+
const patchErrors = getAbieRuServiceNodePortPatchErrors(raw, targetsByName)
|
|
644
|
+
if (patchErrors.length > 0) {
|
|
645
|
+
fail(`${relRu}: ${patchErrors.join('; ')}`)
|
|
575
646
|
return
|
|
576
647
|
}
|
|
577
648
|
passFn(`${relRu}: patch Service → NodePort (ru) відповідає abie.mdc`)
|