@nitra/cursor 1.8.103 → 1.8.104
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/package.json +1 -1
- package/scripts/check-k8s.mjs +534 -0
package/package.json
CHANGED
package/scripts/check-k8s.mjs
CHANGED
|
@@ -45,6 +45,11 @@
|
|
|
45
45
|
* **Inline JSON6902** у **`patches`** (і зовнішні файли з **`patches[].path`** під **`k8s`**, якщо вміст — масив JSON Patch): не допускається пара **`remove`** і **`add`**
|
|
46
46
|
* на один і той самий **`path`** у межах одного фрагмента — потрібен **`op: replace`** (k8s.mdc). **check-k8s** це перевіряє.
|
|
47
47
|
*
|
|
48
|
+
* **Мішень patch:** у **`patches[].target`** і **`patchesJson6902[].target`** (без **labelSelector** / **annotationSelector**)
|
|
49
|
+
* має існувати відповідний ресурс у зібраному з **`resources`**, **`bases`**, **`components`**, **`crds`** каталозі (рекурсивно для підкаталогів з **`kustomization.yaml`**).
|
|
50
|
+
* Для **`patchesStrategicMerge`** і для **`patches[].path`** без **`target`** і без inline **`patch`** (зовнішній strategic-merge)
|
|
51
|
+
* кожен YAML-документ з кореневим **`kind`** і **`metadata.name`** також звіряється з цим каталогом.
|
|
52
|
+
*
|
|
48
53
|
* Явні винятки до загальної логіки yannh/datree — таблиця **`EXPLICIT_K8S_SCHEMAS`** (`Map`): ключ
|
|
49
54
|
* **`apiVersion`, `kind`, `type`** (для CRD без поля `type` у маніфесті — зірочка **`*`** як третій
|
|
50
55
|
* компонент). Спочатку шукається збіг за фактичним `type`, потім за **`*`**.
|
|
@@ -471,6 +476,533 @@ export async function collectKustomizeManagedRelPaths(root, yamlFilesAbs) {
|
|
|
471
476
|
return managed
|
|
472
477
|
}
|
|
473
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Шляхи лише з полів ресурсів Kustomization (**без** patch-файлів).
|
|
481
|
+
* @param {unknown} obj корінь першого документа Kustomization
|
|
482
|
+
* @returns {string[]} відносні посилання
|
|
483
|
+
*/
|
|
484
|
+
function resourcePathRefsFromKustomizationObject(obj) {
|
|
485
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return []
|
|
486
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
487
|
+
/** @type {string[]} */
|
|
488
|
+
const out = []
|
|
489
|
+
pushStringPaths(rec.resources, out)
|
|
490
|
+
pushStringPaths(rec.bases, out)
|
|
491
|
+
pushStringPaths(rec.components, out)
|
|
492
|
+
pushStringPaths(rec.crds, out)
|
|
493
|
+
return out
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Дескриптор ресурсу для звірки з **`target`** Kustomize / strategic-merge фрагментом.
|
|
498
|
+
* @typedef {{ group: string, version: string, kind: string, name: string, namespace: string }} KustomizeResourceDescriptor
|
|
499
|
+
*/
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Розбиває **`apiVersion`** Kubernetes на **group** і **version**.
|
|
503
|
+
* @param {unknown} apiVersion значення з YAML
|
|
504
|
+
* @returns {{ group: string, version: string }} для `group/version` — два сегменти; для `v1` — core (**group** порожній).
|
|
505
|
+
*/
|
|
506
|
+
export function splitK8sApiVersion(apiVersion) {
|
|
507
|
+
if (typeof apiVersion !== 'string') {
|
|
508
|
+
return { group: '', version: '' }
|
|
509
|
+
}
|
|
510
|
+
const t = apiVersion.trim()
|
|
511
|
+
if (t === '') {
|
|
512
|
+
return { group: '', version: '' }
|
|
513
|
+
}
|
|
514
|
+
const i = t.indexOf('/')
|
|
515
|
+
if (i === -1) {
|
|
516
|
+
return { group: '', version: t }
|
|
517
|
+
}
|
|
518
|
+
return { group: t.slice(0, i), version: t.slice(i + 1) }
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Чи patch-**target** використовує **labelSelector** / **annotationSelector** (тоді статична перевірка за іменем не застосовується).
|
|
523
|
+
* @param {Record<string, unknown>} t об’єкт **target**
|
|
524
|
+
* @returns {boolean} true, якщо є непорожній селектор
|
|
525
|
+
*/
|
|
526
|
+
function patchTargetUsesSelector(t) {
|
|
527
|
+
const ls = t.labelSelector
|
|
528
|
+
if (
|
|
529
|
+
ls !== undefined &&
|
|
530
|
+
ls !== null &&
|
|
531
|
+
ls !== '' &&
|
|
532
|
+
((typeof ls === 'object' && !Array.isArray(ls) && Object.keys(ls).length > 0) ||
|
|
533
|
+
(typeof ls === 'string' && ls.trim() !== ''))
|
|
534
|
+
) {
|
|
535
|
+
return true
|
|
536
|
+
}
|
|
537
|
+
const asel = t.annotationSelector
|
|
538
|
+
if (
|
|
539
|
+
asel !== undefined &&
|
|
540
|
+
asel !== null &&
|
|
541
|
+
asel !== '' &&
|
|
542
|
+
((typeof asel === 'object' && !Array.isArray(asel) && Object.keys(asel).length > 0) ||
|
|
543
|
+
(typeof asel === 'string' && asel.trim() !== ''))
|
|
544
|
+
) {
|
|
545
|
+
return true
|
|
546
|
+
}
|
|
547
|
+
return false
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Чи варто перевіряти **target** на наявність ресурсу в каталозі (є **kind** і **name**, немає селекторів).
|
|
552
|
+
* @param {unknown} target значення **patches[].target**
|
|
553
|
+
* @returns {boolean} true, якщо перевірка доречна
|
|
554
|
+
*/
|
|
555
|
+
export function shouldValidateKustomizePatchTarget(target) {
|
|
556
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) {
|
|
557
|
+
return false
|
|
558
|
+
}
|
|
559
|
+
const t = /** @type {Record<string, unknown>} */ (target)
|
|
560
|
+
const kind = t.kind
|
|
561
|
+
const name = t.name
|
|
562
|
+
if (typeof kind !== 'string' || kind.trim() === '' || typeof name !== 'string' || name.trim() === '') {
|
|
563
|
+
return false
|
|
564
|
+
}
|
|
565
|
+
return !patchTargetUsesSelector(t)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Чи **target** Kustomize відповідає дескриптору ресурсу (узгоджено з правилами відбору Kustomize: пропущені поля **target** не звужують).
|
|
570
|
+
* @param {unknown} target об’єкт **target**
|
|
571
|
+
* @param {KustomizeResourceDescriptor} res дескриптор з інвентарю
|
|
572
|
+
* @returns {boolean} true, якщо збігається
|
|
573
|
+
*/
|
|
574
|
+
export function kustomizePatchTargetMatchesDescriptor(target, res) {
|
|
575
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) {
|
|
576
|
+
return false
|
|
577
|
+
}
|
|
578
|
+
const rec = /** @type {Record<string, unknown>} */ (target)
|
|
579
|
+
const tk = rec.kind
|
|
580
|
+
const tn = rec.name
|
|
581
|
+
if (typeof tk !== 'string' || typeof tn !== 'string') {
|
|
582
|
+
return false
|
|
583
|
+
}
|
|
584
|
+
if (tk.trim() !== res.kind || tn.trim() !== res.name) {
|
|
585
|
+
return false
|
|
586
|
+
}
|
|
587
|
+
const tgtGroup = rec.group
|
|
588
|
+
if (typeof tgtGroup === 'string' && tgtGroup.trim() !== '' && res.group !== tgtGroup.trim()) {
|
|
589
|
+
return false
|
|
590
|
+
}
|
|
591
|
+
const tgtVersion = rec.version
|
|
592
|
+
if (typeof tgtVersion === 'string' && tgtVersion.trim() !== '' && res.version !== tgtVersion.trim()) {
|
|
593
|
+
return false
|
|
594
|
+
}
|
|
595
|
+
const tgtNs = rec.namespace
|
|
596
|
+
if (typeof tgtNs === 'string' && tgtNs.trim() !== '' && res.namespace !== tgtNs.trim()) {
|
|
597
|
+
return false
|
|
598
|
+
}
|
|
599
|
+
return true
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Чи є в каталозі ресурс, який задовольняє **target**.
|
|
604
|
+
* @param {KustomizeResourceDescriptor[]} catalog зібрані дескриптори
|
|
605
|
+
* @param {unknown} target об’єкт **target**
|
|
606
|
+
* @returns {boolean} true, якщо перевірка не потрібна або знайдено збіг
|
|
607
|
+
*/
|
|
608
|
+
export function kustomizeResourceCatalogMatchesPatchTarget(catalog, target) {
|
|
609
|
+
if (!shouldValidateKustomizePatchTarget(target)) {
|
|
610
|
+
return true
|
|
611
|
+
}
|
|
612
|
+
return catalog.some(res => kustomizePatchTargetMatchesDescriptor(target, res))
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Чи два дескриптори повністю збігаються (для strategic-merge фрагмента).
|
|
617
|
+
* @param {KustomizeResourceDescriptor} a перший
|
|
618
|
+
* @param {KustomizeResourceDescriptor} b другий
|
|
619
|
+
* @returns {boolean} true, якщо всі поля однакові
|
|
620
|
+
*/
|
|
621
|
+
export function kustomizeResourceDescriptorsIdentityEqual(a, b) {
|
|
622
|
+
return (
|
|
623
|
+
a.group === b.group &&
|
|
624
|
+
a.version === b.version &&
|
|
625
|
+
a.kind === b.kind &&
|
|
626
|
+
a.name === b.name &&
|
|
627
|
+
a.namespace === b.namespace
|
|
628
|
+
)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Будує дескриптор з маніфесту (пропускає **Kustomization** та об’єкти без **metadata.name**).
|
|
633
|
+
* @param {Record<string, unknown>} obj корінь документа
|
|
634
|
+
* @param {string} kustomizationDefaultNs значення **`namespace:`** з kustomization, що підключив файл
|
|
635
|
+
* @returns {KustomizeResourceDescriptor | null} дескриптор для звірки або **null**, якщо документ не підходить.
|
|
636
|
+
*/
|
|
637
|
+
export function kustomizeResourceDescriptorFromManifest(obj, kustomizationDefaultNs) {
|
|
638
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
639
|
+
return null
|
|
640
|
+
}
|
|
641
|
+
const kindRaw = obj.kind
|
|
642
|
+
if (typeof kindRaw !== 'string' || kindRaw.trim() === '') {
|
|
643
|
+
return null
|
|
644
|
+
}
|
|
645
|
+
const kind = kindRaw.trim()
|
|
646
|
+
if (kind === 'Kustomization') {
|
|
647
|
+
return null
|
|
648
|
+
}
|
|
649
|
+
const meta = obj.metadata
|
|
650
|
+
let name = ''
|
|
651
|
+
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
|
|
652
|
+
const m = /** @type {Record<string, unknown>} */ (meta)
|
|
653
|
+
const n = m.name
|
|
654
|
+
if (typeof n === 'string' && n.trim() !== '') {
|
|
655
|
+
name = n.trim()
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (name === '') {
|
|
659
|
+
return null
|
|
660
|
+
}
|
|
661
|
+
const { group, version } = splitK8sApiVersion(obj.apiVersion)
|
|
662
|
+
let namespace = ''
|
|
663
|
+
if (!isClusterScopedKubernetesKind(kind)) {
|
|
664
|
+
let metaNs = ''
|
|
665
|
+
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
|
|
666
|
+
const m = /** @type {Record<string, unknown>} */ (meta)
|
|
667
|
+
const ns = m.namespace
|
|
668
|
+
if (typeof ns === 'string' && ns.trim() !== '') {
|
|
669
|
+
metaNs = ns.trim()
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const def =
|
|
673
|
+
typeof kustomizationDefaultNs === 'string' && kustomizationDefaultNs.trim() !== ''
|
|
674
|
+
? kustomizationDefaultNs.trim()
|
|
675
|
+
: ''
|
|
676
|
+
namespace = metaNs || def
|
|
677
|
+
}
|
|
678
|
+
return { group, version, kind, name, namespace }
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Читає k8s YAML і повертає корені документів-об’єктів (після modeline, якщо він є).
|
|
683
|
+
* @param {string} abs абсолютний шлях до файлу
|
|
684
|
+
* @returns {Promise<Record<string, unknown>[]>} масив коренів-об’єктів YAML-документів (без масивів на корені).
|
|
685
|
+
*/
|
|
686
|
+
async function readK8sYamlDocumentRootsForInventory(abs) {
|
|
687
|
+
let raw
|
|
688
|
+
try {
|
|
689
|
+
raw = await readFile(abs, 'utf8')
|
|
690
|
+
} catch {
|
|
691
|
+
return []
|
|
692
|
+
}
|
|
693
|
+
const lines = toLines(raw)
|
|
694
|
+
const body =
|
|
695
|
+
lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
696
|
+
/** @type {unknown[]} */
|
|
697
|
+
const roots = parseK8sYamlDocumentObjectRoots(body)
|
|
698
|
+
/** @type {Record<string, unknown>[]} */
|
|
699
|
+
const out = []
|
|
700
|
+
for (const r of roots) {
|
|
701
|
+
if (r !== null && typeof r === 'object' && !Array.isArray(r)) {
|
|
702
|
+
out.push(/** @type {Record<string, unknown>} */ (r))
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return out
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Збирає дескриптори ресурсів з **`resources` / `bases` / `components` / `crds`** для одного дерева kustomization.
|
|
710
|
+
* Повторний вхід у той самий **`kustomization.yaml`** дає порожній внесок (як у **`collectKustomizeManagedRelPaths`**).
|
|
711
|
+
* @param {string} kustAbs абсолютний шлях до **kustomization.yaml**
|
|
712
|
+
* @param {string} rootNorm нормалізований абсолютний корінь репозиторію
|
|
713
|
+
* @param {Set<string>} visitedKustomization нормалізовані абсолютні шляхи відвіданих **kustomization.yaml**
|
|
714
|
+
* @returns {Promise<KustomizeResourceDescriptor[]>} плоский список дескрипторів із дерева **resources** / **bases** / **components** / **crds**.
|
|
715
|
+
*/
|
|
716
|
+
export async function collectResourceDescriptorsForKustomizationWalk(kustAbs, rootNorm, visitedKustomization) {
|
|
717
|
+
const normKust = resolve(kustAbs)
|
|
718
|
+
if (visitedKustomization.has(normKust)) {
|
|
719
|
+
return []
|
|
720
|
+
}
|
|
721
|
+
visitedKustomization.add(normKust)
|
|
722
|
+
|
|
723
|
+
let raw
|
|
724
|
+
try {
|
|
725
|
+
raw = await readFile(normKust, 'utf8')
|
|
726
|
+
} catch {
|
|
727
|
+
return []
|
|
728
|
+
}
|
|
729
|
+
const lines = toLines(raw)
|
|
730
|
+
const body =
|
|
731
|
+
lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
732
|
+
|
|
733
|
+
/** @type {import('yaml').Document[] | undefined} */
|
|
734
|
+
let docs
|
|
735
|
+
try {
|
|
736
|
+
docs = parseAllDocuments(body)
|
|
737
|
+
} catch {
|
|
738
|
+
return []
|
|
739
|
+
}
|
|
740
|
+
const first = docs[0]?.toJSON()
|
|
741
|
+
if (first === null || first === undefined || typeof first !== 'object' || Array.isArray(first)) {
|
|
742
|
+
return []
|
|
743
|
+
}
|
|
744
|
+
const rec = /** @type {Record<string, unknown>} */ (first)
|
|
745
|
+
const kustNs = typeof rec.namespace === 'string' && rec.namespace.trim() !== '' ? rec.namespace.trim() : ''
|
|
746
|
+
const kustDir = dirname(normKust)
|
|
747
|
+
const pathRefs = resourcePathRefsFromKustomizationObject(first)
|
|
748
|
+
|
|
749
|
+
/** @type {KustomizeResourceDescriptor[]} */
|
|
750
|
+
const out = []
|
|
751
|
+
|
|
752
|
+
for (const ref of pathRefs) {
|
|
753
|
+
if (typeof ref === 'string' && !ref.includes('://')) {
|
|
754
|
+
const resolved = resolve(kustDir, ref)
|
|
755
|
+
if (resolvedFilePathIsUnderRoot(rootNorm, resolved)) {
|
|
756
|
+
/** @type {import('node:fs').Stats | undefined} */
|
|
757
|
+
let st
|
|
758
|
+
try {
|
|
759
|
+
st = await stat(resolved)
|
|
760
|
+
} catch {
|
|
761
|
+
st = undefined
|
|
762
|
+
}
|
|
763
|
+
if (st !== undefined) {
|
|
764
|
+
if (st.isFile() && /\.ya?ml$/iu.test(resolved)) {
|
|
765
|
+
const roots = await readK8sYamlDocumentRootsForInventory(resolved)
|
|
766
|
+
for (const o of roots) {
|
|
767
|
+
const d = kustomizeResourceDescriptorFromManifest(o, kustNs)
|
|
768
|
+
if (d !== null) {
|
|
769
|
+
out.push(d)
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
} else if (st.isDirectory()) {
|
|
773
|
+
const childK = existsSync(join(resolved, 'kustomization.yaml'))
|
|
774
|
+
? join(resolved, 'kustomization.yaml')
|
|
775
|
+
: null
|
|
776
|
+
if (childK !== null) {
|
|
777
|
+
const sub = await collectResourceDescriptorsForKustomizationWalk(
|
|
778
|
+
childK,
|
|
779
|
+
rootNorm,
|
|
780
|
+
visitedKustomization
|
|
781
|
+
)
|
|
782
|
+
out.push(...sub)
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return out
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Витягує записи з явним **target** з **patches** / **patchesJson6902**.
|
|
795
|
+
* @param {unknown} obj перший документ Kustomization
|
|
796
|
+
* @returns {Array<{ section: string, index: number, target: unknown }>} пари **section** + індекс (1-based) і **target** з YAML.
|
|
797
|
+
*/
|
|
798
|
+
function extractExplicitPatchTargetsFromKustomization(obj) {
|
|
799
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
800
|
+
return []
|
|
801
|
+
}
|
|
802
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
803
|
+
/** @type {Array<{ section: string, index: number, target: unknown }>} */
|
|
804
|
+
const out = []
|
|
805
|
+
/**
|
|
806
|
+
* @param {string} section ім’я поля
|
|
807
|
+
* @param {unknown} arr масив з YAML
|
|
808
|
+
* @returns {void}
|
|
809
|
+
*/
|
|
810
|
+
const push = (section, arr) => {
|
|
811
|
+
if (!Array.isArray(arr)) {
|
|
812
|
+
return
|
|
813
|
+
}
|
|
814
|
+
let i = 0
|
|
815
|
+
for (const item of arr) {
|
|
816
|
+
i++
|
|
817
|
+
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
|
|
818
|
+
const it = /** @type {Record<string, unknown>} */ (item)
|
|
819
|
+
if ('target' in it) {
|
|
820
|
+
out.push({ section, index: i, target: it.target })
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
push('patches', rec.patches)
|
|
826
|
+
push('patchesJson6902', rec.patchesJson6902)
|
|
827
|
+
return out
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Людинозчитуваний опис **target** для повідомлення про помилку.
|
|
832
|
+
* @param {unknown} target об’єкт **target**
|
|
833
|
+
* @returns {string} короткий рядок
|
|
834
|
+
*/
|
|
835
|
+
function formatKustomizePatchTargetForMessage(target) {
|
|
836
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) {
|
|
837
|
+
return String(target)
|
|
838
|
+
}
|
|
839
|
+
const t = /** @type {Record<string, unknown>} */ (target)
|
|
840
|
+
const parts = []
|
|
841
|
+
const g = t.group
|
|
842
|
+
const v = t.version
|
|
843
|
+
const k = t.kind
|
|
844
|
+
const n = t.name
|
|
845
|
+
const ns = t.namespace
|
|
846
|
+
if (typeof g === 'string' && g.trim() !== '') {
|
|
847
|
+
parts.push(`group=${g.trim()}`)
|
|
848
|
+
}
|
|
849
|
+
if (typeof v === 'string' && v.trim() !== '') {
|
|
850
|
+
parts.push(`version=${v.trim()}`)
|
|
851
|
+
}
|
|
852
|
+
if (typeof k === 'string' && k.trim() !== '') {
|
|
853
|
+
parts.push(`kind=${k.trim()}`)
|
|
854
|
+
}
|
|
855
|
+
if (typeof n === 'string' && n.trim() !== '') {
|
|
856
|
+
parts.push(`name=${n.trim()}`)
|
|
857
|
+
}
|
|
858
|
+
if (typeof ns === 'string' && ns.trim() !== '') {
|
|
859
|
+
parts.push(`namespace=${ns.trim()}`)
|
|
860
|
+
}
|
|
861
|
+
return parts.length > 0 ? parts.join(', ') : JSON.stringify(t)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Перевіряє всі **`kustomization.yaml`** під **`k8s`**: **target** patch і strategic-merge посилання не вказують на ресурс поза інвентарем **resources** / **bases** / **components** / **crds**.
|
|
866
|
+
* @param {string} root корінь репозиторію
|
|
867
|
+
* @param {string[]} yamlFilesAbs абсолютні шляхи до yaml під k8s
|
|
868
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
869
|
+
* @returns {Promise<void>}
|
|
870
|
+
*/
|
|
871
|
+
async function validateKustomizationPatchTargetsResolved(root, yamlFilesAbs, fail) {
|
|
872
|
+
const rootNorm = resolve(root)
|
|
873
|
+
for (const kustAbs of yamlFilesAbs) {
|
|
874
|
+
if (basename(kustAbs).toLowerCase() === 'kustomization.yaml') {
|
|
875
|
+
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
876
|
+
/** @type {string | undefined} */
|
|
877
|
+
let raw
|
|
878
|
+
let readOk = false
|
|
879
|
+
try {
|
|
880
|
+
raw = await readFile(kustAbs, 'utf8')
|
|
881
|
+
readOk = true
|
|
882
|
+
} catch (error) {
|
|
883
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
884
|
+
fail(`${rel}: не вдалося прочитати для перевірки patch target (${msg})`)
|
|
885
|
+
}
|
|
886
|
+
if (readOk && raw !== undefined) {
|
|
887
|
+
const lines = toLines(raw)
|
|
888
|
+
const body =
|
|
889
|
+
lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
890
|
+
/** @type {import('yaml').Document[] | null} */
|
|
891
|
+
let docs = null
|
|
892
|
+
try {
|
|
893
|
+
docs = parseAllDocuments(body)
|
|
894
|
+
} catch {
|
|
895
|
+
fail(`${rel}: не вдалося розпарсити YAML для перевірки patch target`)
|
|
896
|
+
}
|
|
897
|
+
if (docs !== null) {
|
|
898
|
+
const first = docs[0]?.toJSON()
|
|
899
|
+
if (first !== null && first !== undefined && typeof first === 'object' && !Array.isArray(first)) {
|
|
900
|
+
const rec = /** @type {Record<string, unknown>} */ (first)
|
|
901
|
+
if (rec.kind === 'Kustomization') {
|
|
902
|
+
const visited = new Set()
|
|
903
|
+
const catalog = await collectResourceDescriptorsForKustomizationWalk(kustAbs, rootNorm, visited)
|
|
904
|
+
const kustDir = dirname(resolve(kustAbs))
|
|
905
|
+
const kustNs =
|
|
906
|
+
typeof rec.namespace === 'string' && rec.namespace.trim() !== '' ? rec.namespace.trim() : ''
|
|
907
|
+
|
|
908
|
+
for (const { section, index, target } of extractExplicitPatchTargetsFromKustomization(first)) {
|
|
909
|
+
if (
|
|
910
|
+
shouldValidateKustomizePatchTarget(target) &&
|
|
911
|
+
!kustomizeResourceCatalogMatchesPatchTarget(catalog, target)
|
|
912
|
+
) {
|
|
913
|
+
fail(
|
|
914
|
+
`${rel}: ${section}[${index}].target — немає відповідного ресурсу в resources/bases/components/crds (рекурсивно): ${formatKustomizePatchTargetForMessage(target)}`
|
|
915
|
+
)
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const patchesOnlyPath = rec.patches
|
|
920
|
+
if (Array.isArray(patchesOnlyPath)) {
|
|
921
|
+
let pIdx = 0
|
|
922
|
+
for (const p of patchesOnlyPath) {
|
|
923
|
+
pIdx++
|
|
924
|
+
if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
|
|
925
|
+
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
926
|
+
const hasTargetKey = 'target' in pr && pr.target !== undefined && pr.target !== null
|
|
927
|
+
const pathStr = typeof pr.path === 'string' ? pr.path.trim() : ''
|
|
928
|
+
const inlinePatch = typeof pr.patch === 'string' && pr.patch.trim() !== ''
|
|
929
|
+
if (!hasTargetKey && pathStr !== '' && !inlinePatch && !pathStr.includes('://')) {
|
|
930
|
+
const resolved = resolve(kustDir, pathStr)
|
|
931
|
+
if (resolvedFilePathIsUnderRoot(rootNorm, resolved) && existsSync(resolved)) {
|
|
932
|
+
/** @type {import('node:fs').Stats | null} */
|
|
933
|
+
let st = null
|
|
934
|
+
try {
|
|
935
|
+
st = await stat(resolved)
|
|
936
|
+
} catch {
|
|
937
|
+
st = null
|
|
938
|
+
}
|
|
939
|
+
if (st !== null && st.isFile() && /\.ya?ml$/iu.test(resolved)) {
|
|
940
|
+
const roots = await readK8sYamlDocumentRootsForInventory(resolved)
|
|
941
|
+
let docIdx = 0
|
|
942
|
+
for (const o of roots) {
|
|
943
|
+
docIdx++
|
|
944
|
+
const d = kustomizeResourceDescriptorFromManifest(o, kustNs)
|
|
945
|
+
if (
|
|
946
|
+
d !== null &&
|
|
947
|
+
!catalog.some(c => kustomizeResourceDescriptorsIdentityEqual(c, d))
|
|
948
|
+
) {
|
|
949
|
+
const relPatch = (relative(root, resolved) || pathStr).replaceAll('\\', '/')
|
|
950
|
+
fail(
|
|
951
|
+
`${rel}: patches[${pIdx}] path «${relPatch}» документ ${docIdx} — у каталозі resources немає ресурсу ${d.kind}/${d.name} (namespace=${d.namespace || '(порожньо)'}, apiVersion group/version=${d.group || 'core'}/${d.version})`
|
|
952
|
+
)
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const sm = rec.patchesStrategicMerge
|
|
963
|
+
if (Array.isArray(sm)) {
|
|
964
|
+
let smIdx = 0
|
|
965
|
+
for (const ref of sm) {
|
|
966
|
+
smIdx++
|
|
967
|
+
if (typeof ref === 'string' && ref.trim() !== '' && !ref.includes('://')) {
|
|
968
|
+
const resolved = resolve(kustDir, ref.trim())
|
|
969
|
+
if (resolvedFilePathIsUnderRoot(rootNorm, resolved) && existsSync(resolved)) {
|
|
970
|
+
/** @type {import('node:fs').Stats | null} */
|
|
971
|
+
let st = null
|
|
972
|
+
try {
|
|
973
|
+
st = await stat(resolved)
|
|
974
|
+
} catch {
|
|
975
|
+
st = null
|
|
976
|
+
}
|
|
977
|
+
if (st !== null && st.isFile() && /\.ya?ml$/iu.test(resolved)) {
|
|
978
|
+
const roots = await readK8sYamlDocumentRootsForInventory(resolved)
|
|
979
|
+
let docIdx = 0
|
|
980
|
+
for (const o of roots) {
|
|
981
|
+
docIdx++
|
|
982
|
+
const d = kustomizeResourceDescriptorFromManifest(o, kustNs)
|
|
983
|
+
if (
|
|
984
|
+
d !== null &&
|
|
985
|
+
!catalog.some(c => kustomizeResourceDescriptorsIdentityEqual(c, d))
|
|
986
|
+
) {
|
|
987
|
+
const relPatch = (relative(root, resolved) || ref).replaceAll('\\', '/')
|
|
988
|
+
fail(
|
|
989
|
+
`${rel}: patchesStrategicMerge[${smIdx}] «${relPatch}» документ ${docIdx} — у каталозі resources немає ресурсу ${d.kind}/${d.name} (namespace=${d.namespace || '(порожньо)'}, apiVersion group/version=${d.group || 'core'}/${d.version})`
|
|
990
|
+
)
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
474
1006
|
/**
|
|
475
1007
|
* Чи це **`k8s/base/kustomization.yaml`** (перевірка обов’язкового непорожнього **`namespace:`**).
|
|
476
1008
|
* @param {string} rel шлях від кореня репозиторію
|
|
@@ -1808,6 +2340,8 @@ export async function check() {
|
|
|
1808
2340
|
|
|
1809
2341
|
await validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFiles, fail)
|
|
1810
2342
|
|
|
2343
|
+
await validateKustomizationPatchTargetsResolved(root, yamlFiles, fail)
|
|
2344
|
+
|
|
1811
2345
|
await ensureBaseKustomizationHasNamespace(root, yamlFiles, fail)
|
|
1812
2346
|
|
|
1813
2347
|
return reporter.getExitCode()
|