@nitra/cursor 1.8.104 → 1.8.105
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 +280 -149
- package/mdc/graphql.mdc +15 -1
- package/package.json +1 -1
- package/schemas/n-cursor.json +16 -0
- package/scripts/auto-rules.mjs +404 -0
- package/scripts/check-abie.mjs +54 -36
- package/scripts/check-bun.mjs +2 -6
- package/scripts/check-graphql.mjs +112 -34
- package/scripts/check-js-lint.mjs +15 -11
- package/scripts/check-k8s.mjs +1165 -674
- package/scripts/check-nginx-default-tpl.mjs +17 -10
- package/scripts/check-npm-module.mjs +3 -3
- package/scripts/check-text.mjs +1 -3
- package/scripts/check-vue.mjs +2 -2
- package/scripts/utils/docker-hadolint.mjs +9 -5
- package/scripts/utils/gha-workflow.mjs +90 -72
- package/scripts/utils/workspaces.mjs +39 -16
package/scripts/check-k8s.mjs
CHANGED
|
@@ -142,15 +142,38 @@ const EXPLICIT_K8S_SCHEMAS = new Map([
|
|
|
142
142
|
]
|
|
143
143
|
])
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Прибирає зовнішні лапки зі скаляра YAML (`"x"` / `'x'`), якщо вони парні.
|
|
147
|
+
* @param {string | undefined} raw значення з `match(…)[1]` або подібне
|
|
148
|
+
* @returns {string | undefined} рядок без лапок або undefined, якщо вхід undefined
|
|
149
|
+
*/
|
|
150
|
+
function trimYamlScalarQuotes(raw) {
|
|
151
|
+
if (raw === undefined) {
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
const s = String(raw)
|
|
155
|
+
if (s.length >= 2 && ((s[0] === '"' && s.at(-1) === '"') || (s[0] === "'" && s.at(-1) === "'"))) {
|
|
156
|
+
return s.slice(1, -1)
|
|
157
|
+
}
|
|
158
|
+
return s
|
|
159
|
+
}
|
|
160
|
+
|
|
145
161
|
/**
|
|
146
162
|
* Витягує кореневе поле **`type:`** з документа (без повного YAML-парсера).
|
|
147
163
|
* @param {string} doc фрагмент YAML одного документа
|
|
148
164
|
* @returns {string | undefined} значення без лапок або undefined, якщо поля немає
|
|
149
165
|
*/
|
|
150
166
|
function extractTopLevelManifestType(doc) {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
167
|
+
for (const line of doc.split(YAML_LINE_SPLIT_RE)) {
|
|
168
|
+
const m = line.match(TYPE_FIELD_RE)
|
|
169
|
+
if (m) {
|
|
170
|
+
const raw = trimYamlScalarQuotes(m[1])
|
|
171
|
+
if (raw === undefined || raw === '') {
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
return raw
|
|
175
|
+
}
|
|
176
|
+
}
|
|
154
177
|
}
|
|
155
178
|
|
|
156
179
|
/**
|
|
@@ -198,6 +221,22 @@ const YANNH_GROUPS = new Set([
|
|
|
198
221
|
])
|
|
199
222
|
|
|
200
223
|
const MODELINE_RE = /^#\s*yaml-language-server:\s*\$schema=(\S+)\s*$/
|
|
224
|
+
const PATH_SPLIT_RE = /[/\\]/u
|
|
225
|
+
const YAML_EXTENSION_RE = /\.ya?ml$/iu
|
|
226
|
+
const YAML_LINE_SPLIT_RE = /\r?\n/u
|
|
227
|
+
const API_VERSION_FIELD_RE = /^\s*apiVersion:\s*(\S+)\s*$/
|
|
228
|
+
const KIND_FIELD_RE = /^\s*kind:\s*(\S+)\s*$/
|
|
229
|
+
const TYPE_FIELD_RE = /^\s*type:\s*(\S+)\s*$/
|
|
230
|
+
const YAML_DOC_SEPARATOR_LINE_RE = /^---\s*$/
|
|
231
|
+
const HEALTHCHECK_DELETE_RE = /\$patch:\s*delete/u
|
|
232
|
+
const HEALTHCHECK_KIND_RE = /kind:\s*HealthCheckPolicy/u
|
|
233
|
+
const METADATA_LINE_RE = /metadata:/u
|
|
234
|
+
const NAME_NON_EMPTY_RE = /name:\s*\S+/u
|
|
235
|
+
const K8S_BASE_KUSTOMIZATION_PATH_RE = /(^|\/)k8s\/base\/kustomization\.yaml$/u
|
|
236
|
+
const K8S_BASE_SEGMENT_RE = /(^|\/)k8s\/base\//u
|
|
237
|
+
const OXLINT_SCHEMA_MODELINE_RE = /^\s*#\s*yaml-language-server:\s*\$schema=\S+/u
|
|
238
|
+
const HTTPS_SCHEMA_RE = /^https:/iu
|
|
239
|
+
const HASURA_GRAPHQL_ENGINE_RE = /(^|\/)hasura\/graphql-engine(?::|$)/u
|
|
201
240
|
|
|
202
241
|
/**
|
|
203
242
|
* Чи містить шлях сегмент директорії `k8s` (рівно ця назва компонента).
|
|
@@ -205,7 +244,7 @@ const MODELINE_RE = /^#\s*yaml-language-server:\s*\$schema=(\S+)\s*$/
|
|
|
205
244
|
* @returns {boolean} true, якщо серед компонентів шляху є каталог `k8s`
|
|
206
245
|
*/
|
|
207
246
|
export function pathHasK8sSegment(filePath) {
|
|
208
|
-
const parts = filePath.split(
|
|
247
|
+
const parts = filePath.split(PATH_SPLIT_RE)
|
|
209
248
|
return parts.includes('k8s')
|
|
210
249
|
}
|
|
211
250
|
|
|
@@ -347,6 +386,45 @@ export function kustomizationSvcYamlMissingSvcHlViolation(kustomizationDir, path
|
|
|
347
386
|
return null
|
|
348
387
|
}
|
|
349
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Один файл **`kustomization.yaml`**: **`svc.yaml`** у шляхах має мати парний **`svc-hl.yaml`**.
|
|
391
|
+
* @param {string} root корінь репозиторію
|
|
392
|
+
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
393
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
394
|
+
* @returns {Promise<void>}
|
|
395
|
+
*/
|
|
396
|
+
async function validateOneKustomizationSvcHlWithSvc(root, kustAbs, fail) {
|
|
397
|
+
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
398
|
+
let raw
|
|
399
|
+
try {
|
|
400
|
+
raw = await readFile(kustAbs, 'utf8')
|
|
401
|
+
} catch (error) {
|
|
402
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
403
|
+
fail(`${rel}: не вдалося прочитати для перевірки svc.yaml/svc-hl.yaml у kustomization (${msg})`)
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
const lines = toLines(raw)
|
|
407
|
+
const body = yamlBodyAfterModeline(lines)
|
|
408
|
+
/** @type {import('yaml').Document[] | undefined} */
|
|
409
|
+
let docs
|
|
410
|
+
try {
|
|
411
|
+
docs = parseAllDocuments(body)
|
|
412
|
+
} catch {
|
|
413
|
+
fail(`${rel}: не вдалося розпарсити YAML для перевірки svc.yaml/svc-hl.yaml у kustomization (див. k8s.mdc)`)
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
const first = docs[0]?.toJSON()
|
|
417
|
+
if (first === null || first === undefined || typeof first !== 'object' || Array.isArray(first)) {
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
const pathRefs = pathsFromKustomizationObject(first)
|
|
421
|
+
const kustDir = dirname(kustAbs)
|
|
422
|
+
const v = kustomizationSvcYamlMissingSvcHlViolation(kustDir, pathRefs)
|
|
423
|
+
if (v !== null) {
|
|
424
|
+
fail(`${rel}: ${v}`)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
350
428
|
/**
|
|
351
429
|
* Перевіряє всі **`kustomization.yaml`** під **`k8s`**: разом із **`svc.yaml`** має бути **`svc-hl.yaml`** у полях шляхів.
|
|
352
430
|
* @param {string} root корінь репозиторію
|
|
@@ -355,39 +433,8 @@ export function kustomizationSvcYamlMissingSvcHlViolation(kustomizationDir, path
|
|
|
355
433
|
* @returns {Promise<void>}
|
|
356
434
|
*/
|
|
357
435
|
async function validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail) {
|
|
358
|
-
for (const kustAbs of yamlFiles) {
|
|
359
|
-
|
|
360
|
-
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
361
|
-
let raw
|
|
362
|
-
try {
|
|
363
|
-
raw = await readFile(kustAbs, 'utf8')
|
|
364
|
-
} catch (error) {
|
|
365
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
366
|
-
fail(`${rel}: не вдалося прочитати для перевірки svc.yaml/svc-hl.yaml у kustomization (${msg})`)
|
|
367
|
-
}
|
|
368
|
-
if (raw !== undefined) {
|
|
369
|
-
const lines = toLines(raw)
|
|
370
|
-
const body = yamlBodyAfterModeline(lines)
|
|
371
|
-
/** @type {import('yaml').Document[] | undefined} */
|
|
372
|
-
let docs
|
|
373
|
-
try {
|
|
374
|
-
docs = parseAllDocuments(body)
|
|
375
|
-
} catch {
|
|
376
|
-
fail(`${rel}: не вдалося розпарсити YAML для перевірки svc.yaml/svc-hl.yaml у kustomization (див. k8s.mdc)`)
|
|
377
|
-
}
|
|
378
|
-
if (docs !== undefined) {
|
|
379
|
-
const first = docs[0]?.toJSON()
|
|
380
|
-
if (first !== null && first !== undefined && typeof first === 'object' && !Array.isArray(first)) {
|
|
381
|
-
const pathRefs = pathsFromKustomizationObject(first)
|
|
382
|
-
const kustDir = dirname(kustAbs)
|
|
383
|
-
const v = kustomizationSvcYamlMissingSvcHlViolation(kustDir, pathRefs)
|
|
384
|
-
if (v !== null) {
|
|
385
|
-
fail(`${rel}: ${v}`)
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
436
|
+
for (const kustAbs of yamlFiles.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')) {
|
|
437
|
+
await validateOneKustomizationSvcHlWithSvc(root, kustAbs, fail)
|
|
391
438
|
}
|
|
392
439
|
}
|
|
393
440
|
|
|
@@ -441,31 +488,44 @@ export async function collectKustomizeManagedRelPaths(root, yamlFilesAbs) {
|
|
|
441
488
|
const kustDir = dirname(normKust)
|
|
442
489
|
const pathRefs = pathsFromKustomizationObject(first)
|
|
443
490
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
491
|
+
/**
|
|
492
|
+
* @param {string} ref шлях з kustomization
|
|
493
|
+
* @returns {Promise<void>}
|
|
494
|
+
*/
|
|
495
|
+
async function handleKustomizeManagedPathRef(ref) {
|
|
496
|
+
if (ref.includes('://')) {
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
const resolved = resolve(kustDir, ref)
|
|
500
|
+
let st
|
|
501
|
+
try {
|
|
502
|
+
st = await stat(resolved)
|
|
503
|
+
} catch {
|
|
504
|
+
st = undefined
|
|
505
|
+
}
|
|
506
|
+
if (!st) {
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
if (st.isFile()) {
|
|
510
|
+
if (YAML_EXTENSION_RE.test(resolved)) {
|
|
511
|
+
const pr = posixRelFromAbs(root, resolved)
|
|
512
|
+
if (pr !== null) {
|
|
513
|
+
managed.add(pr)
|
|
466
514
|
}
|
|
467
515
|
}
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
if (!st.isDirectory()) {
|
|
519
|
+
return
|
|
468
520
|
}
|
|
521
|
+
const childK = existsSync(join(resolved, 'kustomization.yaml')) ? join(resolved, 'kustomization.yaml') : null
|
|
522
|
+
if (childK !== null) {
|
|
523
|
+
await walkKustomization(childK)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
for (const ref of pathRefs) {
|
|
528
|
+
await handleKustomizeManagedPathRef(ref)
|
|
469
529
|
}
|
|
470
530
|
}
|
|
471
531
|
|
|
@@ -628,6 +688,32 @@ export function kustomizeResourceDescriptorsIdentityEqual(a, b) {
|
|
|
628
688
|
)
|
|
629
689
|
}
|
|
630
690
|
|
|
691
|
+
/**
|
|
692
|
+
* Непорожнє **`metadata.name`**, якщо задано коректно.
|
|
693
|
+
* @param {unknown} meta значення **metadata**
|
|
694
|
+
* @returns {string} ім’я або порожній рядок
|
|
695
|
+
*/
|
|
696
|
+
function metadataNameTrimmed(meta) {
|
|
697
|
+
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
|
|
698
|
+
return ''
|
|
699
|
+
}
|
|
700
|
+
const n = /** @type {Record<string, unknown>} */ (meta).name
|
|
701
|
+
return typeof n === 'string' && n.trim() !== '' ? n.trim() : ''
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Непорожній **`metadata.namespace`**, якщо задано коректно.
|
|
706
|
+
* @param {unknown} meta значення **metadata**
|
|
707
|
+
* @returns {string} namespace або порожній рядок
|
|
708
|
+
*/
|
|
709
|
+
function metadataNamespaceTrimmed(meta) {
|
|
710
|
+
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
|
|
711
|
+
return ''
|
|
712
|
+
}
|
|
713
|
+
const ns = /** @type {Record<string, unknown>} */ (meta).namespace
|
|
714
|
+
return typeof ns === 'string' && ns.trim() !== '' ? ns.trim() : ''
|
|
715
|
+
}
|
|
716
|
+
|
|
631
717
|
/**
|
|
632
718
|
* Будує дескриптор з маніфесту (пропускає **Kustomization** та об’єкти без **metadata.name**).
|
|
633
719
|
* @param {Record<string, unknown>} obj корінь документа
|
|
@@ -647,28 +733,14 @@ export function kustomizeResourceDescriptorFromManifest(obj, kustomizationDefaul
|
|
|
647
733
|
return null
|
|
648
734
|
}
|
|
649
735
|
const meta = obj.metadata
|
|
650
|
-
|
|
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
|
-
}
|
|
736
|
+
const name = metadataNameTrimmed(meta)
|
|
658
737
|
if (name === '') {
|
|
659
738
|
return null
|
|
660
739
|
}
|
|
661
740
|
const { group, version } = splitK8sApiVersion(obj.apiVersion)
|
|
662
741
|
let namespace = ''
|
|
663
742
|
if (!isClusterScopedKubernetesKind(kind)) {
|
|
664
|
-
|
|
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
|
-
}
|
|
743
|
+
const metaNs = metadataNamespaceTrimmed(meta)
|
|
672
744
|
const def =
|
|
673
745
|
typeof kustomizationDefaultNs === 'string' && kustomizationDefaultNs.trim() !== ''
|
|
674
746
|
? kustomizationDefaultNs.trim()
|
|
@@ -691,8 +763,7 @@ async function readK8sYamlDocumentRootsForInventory(abs) {
|
|
|
691
763
|
return []
|
|
692
764
|
}
|
|
693
765
|
const lines = toLines(raw)
|
|
694
|
-
const body =
|
|
695
|
-
lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
766
|
+
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
696
767
|
/** @type {unknown[]} */
|
|
697
768
|
const roots = parseK8sYamlDocumentObjectRoots(body)
|
|
698
769
|
/** @type {Record<string, unknown>[]} */
|
|
@@ -727,8 +798,7 @@ export async function collectResourceDescriptorsForKustomizationWalk(kustAbs, ro
|
|
|
727
798
|
return []
|
|
728
799
|
}
|
|
729
800
|
const lines = toLines(raw)
|
|
730
|
-
const body =
|
|
731
|
-
lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
801
|
+
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
732
802
|
|
|
733
803
|
/** @type {import('yaml').Document[] | undefined} */
|
|
734
804
|
let docs
|
|
@@ -749,44 +819,52 @@ export async function collectResourceDescriptorsForKustomizationWalk(kustAbs, ro
|
|
|
749
819
|
/** @type {KustomizeResourceDescriptor[]} */
|
|
750
820
|
const out = []
|
|
751
821
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
visitedKustomization
|
|
781
|
-
)
|
|
782
|
-
out.push(...sub)
|
|
783
|
-
}
|
|
784
|
-
}
|
|
822
|
+
/**
|
|
823
|
+
* @param {string} ref шлях з resources/bases/…
|
|
824
|
+
* @returns {Promise<void>}
|
|
825
|
+
*/
|
|
826
|
+
async function handleResourceDescriptorPathRef(ref) {
|
|
827
|
+
if (typeof ref !== 'string' || ref.includes('://')) {
|
|
828
|
+
return
|
|
829
|
+
}
|
|
830
|
+
const resolved = resolve(kustDir, ref)
|
|
831
|
+
if (!resolvedFilePathIsUnderRoot(rootNorm, resolved)) {
|
|
832
|
+
return
|
|
833
|
+
}
|
|
834
|
+
/** @type {import('node:fs').Stats | undefined} */
|
|
835
|
+
let st
|
|
836
|
+
try {
|
|
837
|
+
st = await stat(resolved)
|
|
838
|
+
} catch {
|
|
839
|
+
st = undefined
|
|
840
|
+
}
|
|
841
|
+
if (st === undefined) {
|
|
842
|
+
return
|
|
843
|
+
}
|
|
844
|
+
if (st.isFile() && YAML_EXTENSION_RE.test(resolved)) {
|
|
845
|
+
const roots = await readK8sYamlDocumentRootsForInventory(resolved)
|
|
846
|
+
for (const o of roots) {
|
|
847
|
+
const d = kustomizeResourceDescriptorFromManifest(o, kustNs)
|
|
848
|
+
if (d !== null) {
|
|
849
|
+
out.push(d)
|
|
785
850
|
}
|
|
786
851
|
}
|
|
852
|
+
return
|
|
853
|
+
}
|
|
854
|
+
if (!st.isDirectory()) {
|
|
855
|
+
return
|
|
856
|
+
}
|
|
857
|
+
const childK = existsSync(join(resolved, 'kustomization.yaml')) ? join(resolved, 'kustomization.yaml') : null
|
|
858
|
+
if (childK !== null) {
|
|
859
|
+
const sub = await collectResourceDescriptorsForKustomizationWalk(childK, rootNorm, visitedKustomization)
|
|
860
|
+
out.push(...sub)
|
|
787
861
|
}
|
|
788
862
|
}
|
|
789
863
|
|
|
864
|
+
for (const ref of pathRefs) {
|
|
865
|
+
await handleResourceDescriptorPathRef(ref)
|
|
866
|
+
}
|
|
867
|
+
|
|
790
868
|
return out
|
|
791
869
|
}
|
|
792
870
|
|
|
@@ -861,6 +939,233 @@ function formatKustomizePatchTargetForMessage(target) {
|
|
|
861
939
|
return parts.length > 0 ? parts.join(', ') : JSON.stringify(t)
|
|
862
940
|
}
|
|
863
941
|
|
|
942
|
+
/**
|
|
943
|
+
* Явні **patches[].target** / **patchesJson6902[].target** — ресурс має бути в інвентарі.
|
|
944
|
+
* @param {string} rel відносний шлях до kustomization.yaml
|
|
945
|
+
* @param {Record<string, unknown>} first корінь Kustomization
|
|
946
|
+
* @param {KustomizeResourceDescriptor[]} catalog інвентар resources/bases/…
|
|
947
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
948
|
+
* @returns {void}
|
|
949
|
+
*/
|
|
950
|
+
function failIfExplicitPatchTargetsNotInCatalog(rel, first, catalog, fail) {
|
|
951
|
+
for (const { section, index, target } of extractExplicitPatchTargetsFromKustomization(first)) {
|
|
952
|
+
if (
|
|
953
|
+
shouldValidateKustomizePatchTarget(target) &&
|
|
954
|
+
!kustomizeResourceCatalogMatchesPatchTarget(catalog, target)
|
|
955
|
+
) {
|
|
956
|
+
fail(
|
|
957
|
+
`${rel}: ${section}[${index}].target — немає відповідного ресурсу в resources/bases/components/crds (рекурсивно): ${formatKustomizePatchTargetForMessage(target)}`
|
|
958
|
+
)
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Документи з YAML-файлу мають мати дескриптор у **catalog** (інвентар resources).
|
|
965
|
+
* @param {string} rel відносний шлях до kustomization.yaml
|
|
966
|
+
* @param {string} resolvedAbs абсолютний шлях до patch-файлу
|
|
967
|
+
* @param {string} root корінь репо
|
|
968
|
+
* @param {string} relPatchFallback якщо **relative** дає порожньо
|
|
969
|
+
* @param {string} violationIntro префікс повідомлення (`patches[1] path` або `patchesStrategicMerge[2]`)
|
|
970
|
+
* @param {KustomizeResourceDescriptor[]} catalog інвентар
|
|
971
|
+
* @param {string} kustNs default namespace
|
|
972
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
973
|
+
* @returns {Promise<void>}
|
|
974
|
+
*/
|
|
975
|
+
async function failIfYamlFileRootsMissingFromCatalog(
|
|
976
|
+
rel,
|
|
977
|
+
resolvedAbs,
|
|
978
|
+
root,
|
|
979
|
+
relPatchFallback,
|
|
980
|
+
violationIntro,
|
|
981
|
+
catalog,
|
|
982
|
+
kustNs,
|
|
983
|
+
fail
|
|
984
|
+
) {
|
|
985
|
+
const roots = await readK8sYamlDocumentRootsForInventory(resolvedAbs)
|
|
986
|
+
let docIdx = 0
|
|
987
|
+
for (const o of roots) {
|
|
988
|
+
docIdx++
|
|
989
|
+
const d = kustomizeResourceDescriptorFromManifest(o, kustNs)
|
|
990
|
+
if (d !== null && !catalog.some(c => kustomizeResourceDescriptorsIdentityEqual(c, d))) {
|
|
991
|
+
const relPatch = (relative(root, resolvedAbs) || relPatchFallback).replaceAll('\\', '/')
|
|
992
|
+
fail(
|
|
993
|
+
`${rel}: ${violationIntro} «${relPatch}» документ ${docIdx} — у каталозі resources немає ресурсу ${d.kind}/${d.name} (namespace=${d.namespace || '(порожньо)'}, apiVersion group/version=${d.group || 'core'}/${d.version})`
|
|
994
|
+
)
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Вирішує відносний шлях до існуючого **.yaml** під root і перевіряє, що це файл.
|
|
1001
|
+
* @param {string} kustDir каталог kustomization
|
|
1002
|
+
* @param {string} pathStr відносний шлях
|
|
1003
|
+
* @param {string} rootNorm нормалізований корінь репо
|
|
1004
|
+
* @returns {Promise<string | null>} абсолютний шлях або null
|
|
1005
|
+
*/
|
|
1006
|
+
async function resolveExistingYamlFileUnderRoot(kustDir, pathStr, rootNorm) {
|
|
1007
|
+
const resolved = resolve(kustDir, pathStr)
|
|
1008
|
+
if (!resolvedFilePathIsUnderRoot(rootNorm, resolved) || !existsSync(resolved)) {
|
|
1009
|
+
return null
|
|
1010
|
+
}
|
|
1011
|
+
/** @type {import('node:fs').Stats | null} */
|
|
1012
|
+
let st = null
|
|
1013
|
+
try {
|
|
1014
|
+
st = await stat(resolved)
|
|
1015
|
+
} catch {
|
|
1016
|
+
st = null
|
|
1017
|
+
}
|
|
1018
|
+
if (st === null || !st.isFile() || !YAML_EXTENSION_RE.test(resolved)) {
|
|
1019
|
+
return null
|
|
1020
|
+
}
|
|
1021
|
+
return resolved
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Один елемент **patches[]** лише з **path** (без **target**, без inline patch): корені файлу проти інвентарю.
|
|
1026
|
+
* @param {string} rel відносний шлях до kustomization.yaml
|
|
1027
|
+
* @param {unknown} p елемент **patches**
|
|
1028
|
+
* @param {number} pIdx 1-based індекс у масиві
|
|
1029
|
+
* @param {string} kustDir каталог kustomization.yaml
|
|
1030
|
+
* @param {string} rootNorm нормалізований корінь репо
|
|
1031
|
+
* @param {string} root корінь репо
|
|
1032
|
+
* @param {KustomizeResourceDescriptor[]} catalog інвентар
|
|
1033
|
+
* @param {string} kustNs default namespace з kustomization
|
|
1034
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
1035
|
+
* @returns {Promise<void>}
|
|
1036
|
+
*/
|
|
1037
|
+
async function failIfOnePathOnlyPatchNotInCatalog(rel, p, pIdx, kustDir, rootNorm, root, catalog, kustNs, fail) {
|
|
1038
|
+
if (p === null || typeof p !== 'object' || Array.isArray(p)) {
|
|
1039
|
+
return
|
|
1040
|
+
}
|
|
1041
|
+
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
1042
|
+
const hasTargetKey = 'target' in pr && pr.target !== undefined && pr.target !== null
|
|
1043
|
+
const pathStr = typeof pr.path === 'string' ? pr.path.trim() : ''
|
|
1044
|
+
const inlinePatch = typeof pr.patch === 'string' && pr.patch.trim() !== ''
|
|
1045
|
+
if (hasTargetKey || pathStr === '' || inlinePatch || pathStr.includes('://')) {
|
|
1046
|
+
return
|
|
1047
|
+
}
|
|
1048
|
+
const resolved = await resolveExistingYamlFileUnderRoot(kustDir, pathStr, rootNorm)
|
|
1049
|
+
if (resolved === null) {
|
|
1050
|
+
return
|
|
1051
|
+
}
|
|
1052
|
+
await failIfYamlFileRootsMissingFromCatalog(
|
|
1053
|
+
rel,
|
|
1054
|
+
resolved,
|
|
1055
|
+
root,
|
|
1056
|
+
pathStr,
|
|
1057
|
+
`patches[${pIdx}] path`,
|
|
1058
|
+
catalog,
|
|
1059
|
+
kustNs,
|
|
1060
|
+
fail
|
|
1061
|
+
)
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* **patches[]** лише з **path** (без **target**, без inline patch) — документи у файлі мають збігатися з інвентарем.
|
|
1066
|
+
* @param {string} rel відносний шлях до kustomization.yaml
|
|
1067
|
+
* @param {unknown} patches поле **patches**
|
|
1068
|
+
* @param {string} kustDir каталог kustomization.yaml
|
|
1069
|
+
* @param {string} rootNorm нормалізований корінь репо
|
|
1070
|
+
* @param {string} root корінь репо
|
|
1071
|
+
* @param {KustomizeResourceDescriptor[]} catalog інвентар
|
|
1072
|
+
* @param {string} kustNs default namespace з kustomization
|
|
1073
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
1074
|
+
* @returns {Promise<void>}
|
|
1075
|
+
*/
|
|
1076
|
+
async function failIfPathOnlyPatchesNotInCatalog(rel, patches, kustDir, rootNorm, root, catalog, kustNs, fail) {
|
|
1077
|
+
if (!Array.isArray(patches)) {
|
|
1078
|
+
return
|
|
1079
|
+
}
|
|
1080
|
+
let pIdx = 0
|
|
1081
|
+
for (const p of patches) {
|
|
1082
|
+
pIdx++
|
|
1083
|
+
await failIfOnePathOnlyPatchNotInCatalog(rel, p, pIdx, kustDir, rootNorm, root, catalog, kustNs, fail)
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* **patchesStrategicMerge** — кожен документ у файлі має збігатися з інвентарем.
|
|
1089
|
+
* @param {string} rel відносний шлях до kustomization.yaml
|
|
1090
|
+
* @param {unknown} sm поле **patchesStrategicMerge**
|
|
1091
|
+
* @param {string} kustDir каталог kustomization.yaml
|
|
1092
|
+
* @param {string} rootNorm нормалізований корінь репо
|
|
1093
|
+
* @param {string} root корінь репо
|
|
1094
|
+
* @param {KustomizeResourceDescriptor[]} catalog інвентар
|
|
1095
|
+
* @param {string} kustNs default namespace з kustomization
|
|
1096
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
1097
|
+
* @returns {Promise<void>}
|
|
1098
|
+
*/
|
|
1099
|
+
async function failIfStrategicMergePatchesNotInCatalog(rel, sm, kustDir, rootNorm, root, catalog, kustNs, fail) {
|
|
1100
|
+
if (!Array.isArray(sm)) {
|
|
1101
|
+
return
|
|
1102
|
+
}
|
|
1103
|
+
let smIdx = 0
|
|
1104
|
+
for (const ref of sm) {
|
|
1105
|
+
smIdx++
|
|
1106
|
+
if (typeof ref === 'string' && ref.trim() !== '' && !ref.includes('://')) {
|
|
1107
|
+
const resolved = await resolveExistingYamlFileUnderRoot(kustDir, ref.trim(), rootNorm)
|
|
1108
|
+
if (resolved !== null) {
|
|
1109
|
+
await failIfYamlFileRootsMissingFromCatalog(
|
|
1110
|
+
rel,
|
|
1111
|
+
resolved,
|
|
1112
|
+
root,
|
|
1113
|
+
ref,
|
|
1114
|
+
`patchesStrategicMerge[${smIdx}]`,
|
|
1115
|
+
catalog,
|
|
1116
|
+
kustNs,
|
|
1117
|
+
fail
|
|
1118
|
+
)
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Один **`kustomization.yaml`**: patch **target**, **path** без target, **patchesStrategicMerge**.
|
|
1126
|
+
* @param {string} root корінь репозиторію
|
|
1127
|
+
* @param {string} kustAbs абсолютний шлях до файлу
|
|
1128
|
+
* @param {string} rootNorm нормалізований корінь
|
|
1129
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
1130
|
+
* @returns {Promise<void>}
|
|
1131
|
+
*/
|
|
1132
|
+
async function validatePatchTargetsOneKustomizationFile(root, kustAbs, rootNorm, fail) {
|
|
1133
|
+
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
1134
|
+
let raw
|
|
1135
|
+
try {
|
|
1136
|
+
raw = await readFile(kustAbs, 'utf8')
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
1139
|
+
fail(`${rel}: не вдалося прочитати для перевірки patch target (${msg})`)
|
|
1140
|
+
return
|
|
1141
|
+
}
|
|
1142
|
+
const lines = toLines(raw)
|
|
1143
|
+
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
1144
|
+
/** @type {import('yaml').Document[] | null} */
|
|
1145
|
+
let docs = null
|
|
1146
|
+
try {
|
|
1147
|
+
docs = parseAllDocuments(body)
|
|
1148
|
+
} catch {
|
|
1149
|
+
fail(`${rel}: не вдалося розпарсити YAML для перевірки patch target`)
|
|
1150
|
+
return
|
|
1151
|
+
}
|
|
1152
|
+
const first = docs[0]?.toJSON()
|
|
1153
|
+
if (first === null || first === undefined || typeof first !== 'object' || Array.isArray(first)) {
|
|
1154
|
+
return
|
|
1155
|
+
}
|
|
1156
|
+
const rec = /** @type {Record<string, unknown>} */ (first)
|
|
1157
|
+
if (rec.kind !== 'Kustomization') {
|
|
1158
|
+
return
|
|
1159
|
+
}
|
|
1160
|
+
const visited = new Set()
|
|
1161
|
+
const catalog = await collectResourceDescriptorsForKustomizationWalk(kustAbs, rootNorm, visited)
|
|
1162
|
+
const kustDir = dirname(resolve(kustAbs))
|
|
1163
|
+
const kustNs = typeof rec.namespace === 'string' && rec.namespace.trim() !== '' ? rec.namespace.trim() : ''
|
|
1164
|
+
failIfExplicitPatchTargetsNotInCatalog(rel, first, catalog, fail)
|
|
1165
|
+
await failIfPathOnlyPatchesNotInCatalog(rel, rec.patches, kustDir, rootNorm, root, catalog, kustNs, fail)
|
|
1166
|
+
await failIfStrategicMergePatchesNotInCatalog(rel, rec.patchesStrategicMerge, kustDir, rootNorm, root, catalog, kustNs, fail)
|
|
1167
|
+
}
|
|
1168
|
+
|
|
864
1169
|
/**
|
|
865
1170
|
* Перевіряє всі **`kustomization.yaml`** під **`k8s`**: **target** patch і strategic-merge посилання не вказують на ресурс поза інвентарем **resources** / **bases** / **components** / **crds**.
|
|
866
1171
|
* @param {string} root корінь репозиторію
|
|
@@ -870,136 +1175,8 @@ function formatKustomizePatchTargetForMessage(target) {
|
|
|
870
1175
|
*/
|
|
871
1176
|
async function validateKustomizationPatchTargetsResolved(root, yamlFilesAbs, fail) {
|
|
872
1177
|
const rootNorm = resolve(root)
|
|
873
|
-
for (const kustAbs of yamlFilesAbs) {
|
|
874
|
-
|
|
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
|
-
}
|
|
1178
|
+
for (const kustAbs of yamlFilesAbs.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')) {
|
|
1179
|
+
await validatePatchTargetsOneKustomizationFile(root, kustAbs, rootNorm, fail)
|
|
1003
1180
|
}
|
|
1004
1181
|
}
|
|
1005
1182
|
|
|
@@ -1010,7 +1187,7 @@ async function validateKustomizationPatchTargetsResolved(root, yamlFilesAbs, fai
|
|
|
1010
1187
|
*/
|
|
1011
1188
|
export function isBaseKustomizationPath(rel) {
|
|
1012
1189
|
const n = rel.replaceAll('\\', '/')
|
|
1013
|
-
return
|
|
1190
|
+
return K8S_BASE_KUSTOMIZATION_PATH_RE.test(n)
|
|
1014
1191
|
}
|
|
1015
1192
|
|
|
1016
1193
|
/**
|
|
@@ -1040,10 +1217,10 @@ async function findK8sYamlFiles(root) {
|
|
|
1040
1217
|
const out = []
|
|
1041
1218
|
await walkDir(root, p => {
|
|
1042
1219
|
if (!pathHasK8sSegment(p)) return
|
|
1043
|
-
if (
|
|
1220
|
+
if (!YAML_EXTENSION_RE.test(p)) return
|
|
1044
1221
|
out.push(p)
|
|
1045
1222
|
})
|
|
1046
|
-
|
|
1223
|
+
|
|
1047
1224
|
return out.toSorted((a, b) => a.localeCompare(b))
|
|
1048
1225
|
}
|
|
1049
1226
|
|
|
@@ -1059,6 +1236,22 @@ function k8sYamlBodyForDocumentParse(lines) {
|
|
|
1059
1236
|
return lines.join('\n')
|
|
1060
1237
|
}
|
|
1061
1238
|
|
|
1239
|
+
/**
|
|
1240
|
+
* Оновлює прапорці наявності **BackendConfig** / інших **kind** у документі.
|
|
1241
|
+
* @param {unknown} kind значення **kind**
|
|
1242
|
+
* @param {{ hasBc: boolean, hasOther: boolean }} acc накопичувач
|
|
1243
|
+
* @returns {void}
|
|
1244
|
+
*/
|
|
1245
|
+
function updateBackendConfigKindFlags(kind, acc) {
|
|
1246
|
+
if (kind === 'BackendConfig') {
|
|
1247
|
+
acc.hasBc = true
|
|
1248
|
+
return
|
|
1249
|
+
}
|
|
1250
|
+
if (kind !== undefined && kind !== null && String(kind).trim() !== '') {
|
|
1251
|
+
acc.hasOther = true
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1062
1255
|
/**
|
|
1063
1256
|
* Чи всі нетривіальні документи у тілі — **`kind: BackendConfig`**, чи є змішування з іншими kind.
|
|
1064
1257
|
* @param {string} body YAML без обов’язкового modeline (див. `k8sYamlBodyForDocumentParse`)
|
|
@@ -1073,24 +1266,22 @@ export function classifyBackendConfigManifestPresence(body) {
|
|
|
1073
1266
|
return 'unparsed'
|
|
1074
1267
|
}
|
|
1075
1268
|
|
|
1076
|
-
|
|
1077
|
-
let hasOther = false
|
|
1269
|
+
const acc = { hasBc: false, hasOther: false }
|
|
1078
1270
|
for (const doc of docs) {
|
|
1079
1271
|
if (doc.errors.length === 0) {
|
|
1080
1272
|
const obj = doc.toJSON()
|
|
1081
1273
|
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
1082
|
-
|
|
1083
|
-
if (kind === 'BackendConfig') {
|
|
1084
|
-
hasBc = true
|
|
1085
|
-
} else if (kind !== undefined && kind !== null && String(kind).trim() !== '') {
|
|
1086
|
-
hasOther = true
|
|
1087
|
-
}
|
|
1274
|
+
updateBackendConfigKindFlags(obj.kind, acc)
|
|
1088
1275
|
}
|
|
1089
1276
|
}
|
|
1090
1277
|
}
|
|
1091
1278
|
|
|
1092
|
-
if (!hasBc)
|
|
1093
|
-
|
|
1279
|
+
if (!acc.hasBc) {
|
|
1280
|
+
return 'none'
|
|
1281
|
+
}
|
|
1282
|
+
if (acc.hasOther) {
|
|
1283
|
+
return 'mixed'
|
|
1284
|
+
}
|
|
1094
1285
|
return 'only'
|
|
1095
1286
|
}
|
|
1096
1287
|
|
|
@@ -1136,7 +1327,7 @@ async function removeBackendConfigOnlyK8sYamlFiles(root, fail, pass) {
|
|
|
1136
1327
|
*/
|
|
1137
1328
|
function toLines(content) {
|
|
1138
1329
|
const body = content.startsWith('\uFEFF') ? content.slice(1) : content
|
|
1139
|
-
return body.split(
|
|
1330
|
+
return body.split(YAML_LINE_SPLIT_RE)
|
|
1140
1331
|
}
|
|
1141
1332
|
|
|
1142
1333
|
/**
|
|
@@ -1187,8 +1378,15 @@ function parseK8sYamlDocumentObjectRoots(body) {
|
|
|
1187
1378
|
* @returns {string} перший документ без зайвих пробілів по краях
|
|
1188
1379
|
*/
|
|
1189
1380
|
function firstYamlDocument(body) {
|
|
1190
|
-
const
|
|
1191
|
-
|
|
1381
|
+
const lines = body.split(YAML_LINE_SPLIT_RE)
|
|
1382
|
+
const out = []
|
|
1383
|
+
for (const line of lines) {
|
|
1384
|
+
if (YAML_DOC_SEPARATOR_LINE_RE.test(line)) {
|
|
1385
|
+
break
|
|
1386
|
+
}
|
|
1387
|
+
out.push(line)
|
|
1388
|
+
}
|
|
1389
|
+
return out.join('\n').trim()
|
|
1192
1390
|
}
|
|
1193
1391
|
|
|
1194
1392
|
/**
|
|
@@ -1197,12 +1395,28 @@ function firstYamlDocument(body) {
|
|
|
1197
1395
|
* @returns {{ apiVersion?: string, kind?: string }} знайдені поля або властивості відсутні
|
|
1198
1396
|
*/
|
|
1199
1397
|
function extractApiVersionAndKind(doc) {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1398
|
+
/** @type {string | undefined} */
|
|
1399
|
+
let apiVersion
|
|
1400
|
+
/** @type {string | undefined} */
|
|
1401
|
+
let kind
|
|
1402
|
+
for (const line of doc.split(YAML_LINE_SPLIT_RE)) {
|
|
1403
|
+
if (apiVersion === undefined) {
|
|
1404
|
+
const av = line.match(API_VERSION_FIELD_RE)
|
|
1405
|
+
if (av) {
|
|
1406
|
+
apiVersion = trimYamlScalarQuotes(av[1])
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
if (kind === undefined) {
|
|
1410
|
+
const k = line.match(KIND_FIELD_RE)
|
|
1411
|
+
if (k) {
|
|
1412
|
+
kind = trimYamlScalarQuotes(k[1])
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (apiVersion !== undefined && kind !== undefined) {
|
|
1416
|
+
break
|
|
1417
|
+
}
|
|
1205
1418
|
}
|
|
1419
|
+
return { apiVersion, kind }
|
|
1206
1420
|
}
|
|
1207
1421
|
|
|
1208
1422
|
/**
|
|
@@ -1223,10 +1437,10 @@ export function k8sYamlFirstDocIsAlbYcHttpBackendGroup(yamlBody) {
|
|
|
1223
1437
|
* @returns {boolean} true, якщо є `$patch: delete` і блоки kind/metadata для HealthCheckPolicy
|
|
1224
1438
|
*/
|
|
1225
1439
|
export function ruKustomizationHasHealthCheckDeletePatch(raw) {
|
|
1226
|
-
if (
|
|
1227
|
-
if (
|
|
1228
|
-
if (
|
|
1229
|
-
if (
|
|
1440
|
+
if (!HEALTHCHECK_DELETE_RE.test(raw)) return false
|
|
1441
|
+
if (!HEALTHCHECK_KIND_RE.test(raw)) return false
|
|
1442
|
+
if (!METADATA_LINE_RE.test(raw)) return false
|
|
1443
|
+
if (!NAME_NON_EMPTY_RE.test(raw)) return false
|
|
1230
1444
|
return true
|
|
1231
1445
|
}
|
|
1232
1446
|
|
|
@@ -1343,137 +1557,249 @@ export function json6902PathsWithRemoveAndAddOnSamePath(ops) {
|
|
|
1343
1557
|
}
|
|
1344
1558
|
|
|
1345
1559
|
/**
|
|
1346
|
-
*
|
|
1347
|
-
* @param {string}
|
|
1348
|
-
* @param {string
|
|
1560
|
+
* Реєструє порушення, якщо в JSON6902-операціях є **remove** і **add** на один **path**.
|
|
1561
|
+
* @param {string} rel відносний шлях до kustomization.yaml
|
|
1562
|
+
* @param {string} label фрагмент повідомлення (наприклад `patches[1] inline JSON6902`)
|
|
1563
|
+
* @param {string} patchText текст patch
|
|
1349
1564
|
* @param {(msg: string) => void} fail реєстрація порушення
|
|
1350
|
-
* @returns {
|
|
1565
|
+
* @returns {void}
|
|
1351
1566
|
*/
|
|
1352
|
-
|
|
1353
|
-
const
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
/** @type {string | undefined} */
|
|
1358
|
-
let raw
|
|
1359
|
-
let readOk = false
|
|
1360
|
-
try {
|
|
1361
|
-
raw = await readFile(kustAbs, 'utf8')
|
|
1362
|
-
readOk = true
|
|
1363
|
-
} catch (error) {
|
|
1364
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1365
|
-
fail(`${rel}: не вдалося прочитати для перевірки JSON6902 (${msg})`)
|
|
1366
|
-
}
|
|
1367
|
-
if (readOk && raw !== undefined) {
|
|
1368
|
-
const lines = toLines(raw)
|
|
1369
|
-
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
1370
|
-
/** @type {import('yaml').Document[] | null} */
|
|
1371
|
-
let docs = null
|
|
1372
|
-
try {
|
|
1373
|
-
docs = parseAllDocuments(body)
|
|
1374
|
-
} catch {
|
|
1375
|
-
docs = null
|
|
1376
|
-
}
|
|
1377
|
-
if (docs !== null) {
|
|
1378
|
-
for (const doc of docs) {
|
|
1379
|
-
if (doc.errors.length === 0) {
|
|
1380
|
-
const rootObj = doc.toJSON()
|
|
1381
|
-
if (rootObj !== null && typeof rootObj === 'object' && !Array.isArray(rootObj)) {
|
|
1382
|
-
const rec = /** @type {Record<string, unknown>} */ (rootObj)
|
|
1383
|
-
if (rec.kind === 'Kustomization') {
|
|
1384
|
-
const patches = rec.patches
|
|
1385
|
-
if (Array.isArray(patches)) {
|
|
1386
|
-
let patchIdx = 0
|
|
1387
|
-
for (const p of patches) {
|
|
1388
|
-
patchIdx++
|
|
1389
|
-
if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
|
|
1390
|
-
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
1391
|
-
if (typeof pr.patch === 'string' && pr.patch.trim() !== '') {
|
|
1392
|
-
const ops = collectJson6902OperationsFromPatchText(pr.patch)
|
|
1393
|
-
const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
|
|
1394
|
-
if (bad.length > 0) {
|
|
1395
|
-
fail(
|
|
1396
|
-
`${rel}: patches[${patchIdx}] inline JSON6902: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
|
|
1397
|
-
)
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
if (typeof pr.path === 'string' && pr.path.trim() !== '') {
|
|
1401
|
-
const patchRef = pr.path.trim()
|
|
1402
|
-
const resolved = resolve(dirname(kustAbs), patchRef)
|
|
1403
|
-
if (resolvedFilePathIsUnderRoot(rootNorm, resolved) && existsSync(resolved)) {
|
|
1404
|
-
/** @type {import('node:fs').Stats | null} */
|
|
1405
|
-
let st = null
|
|
1406
|
-
try {
|
|
1407
|
-
st = await stat(resolved)
|
|
1408
|
-
} catch {
|
|
1409
|
-
st = null
|
|
1410
|
-
}
|
|
1411
|
-
if (st !== null && st.isFile()) {
|
|
1412
|
-
/** @type {string | undefined} */
|
|
1413
|
-
let pRaw
|
|
1414
|
-
try {
|
|
1415
|
-
pRaw = await readFile(resolved, 'utf8')
|
|
1416
|
-
} catch {
|
|
1417
|
-
pRaw = undefined
|
|
1418
|
-
}
|
|
1419
|
-
if (pRaw !== undefined) {
|
|
1420
|
-
const ops = collectJson6902OperationsFromPatchText(pRaw)
|
|
1421
|
-
if (ops.length > 0) {
|
|
1422
|
-
const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
|
|
1423
|
-
if (bad.length > 0) {
|
|
1424
|
-
const relPatch = (relative(root, resolved) || patchRef).replaceAll('\\', '/')
|
|
1425
|
-
fail(
|
|
1426
|
-
`${rel}: patch-файл «${relPatch}»: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
|
|
1427
|
-
)
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1567
|
+
function failIfJson6902RemoveAddConflictOnSamePath(rel, label, patchText, fail) {
|
|
1568
|
+
const ops = collectJson6902OperationsFromPatchText(patchText)
|
|
1569
|
+
const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
|
|
1570
|
+
if (bad.length > 0) {
|
|
1571
|
+
fail(`${rel}: ${label}: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`)
|
|
1444
1572
|
}
|
|
1445
1573
|
}
|
|
1446
1574
|
|
|
1447
1575
|
/**
|
|
1448
|
-
*
|
|
1449
|
-
* @param {string} rel відносний шлях до
|
|
1450
|
-
* @param {string}
|
|
1451
|
-
* @param {
|
|
1452
|
-
* @
|
|
1576
|
+
* Зовнішній patch-файл (масив JSON6902): remove+add на один path.
|
|
1577
|
+
* @param {string} rel відносний шлях до kustomization.yaml
|
|
1578
|
+
* @param {string} resolved абсолютний шлях до файлу patch
|
|
1579
|
+
* @param {string} root корінь репо
|
|
1580
|
+
* @param {string} patchRef відносне посилання з kustomization
|
|
1581
|
+
* @param {(msg: string) => void} fail реєстрація порушення
|
|
1582
|
+
* @returns {Promise<void>}
|
|
1453
1583
|
*/
|
|
1454
|
-
function
|
|
1455
|
-
/** @type {import('
|
|
1456
|
-
let
|
|
1584
|
+
async function auditJson6902PatchExternalFile(rel, resolved, root, patchRef, fail) {
|
|
1585
|
+
/** @type {import('node:fs').Stats | null} */
|
|
1586
|
+
let st = null
|
|
1457
1587
|
try {
|
|
1458
|
-
|
|
1588
|
+
st = await stat(resolved)
|
|
1459
1589
|
} catch {
|
|
1590
|
+
st = null
|
|
1591
|
+
}
|
|
1592
|
+
if (st === null || !st.isFile()) {
|
|
1460
1593
|
return
|
|
1461
1594
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
1468
|
-
if (rec.kind === 'Ingress') {
|
|
1469
|
-
fail(
|
|
1470
|
-
`${rel}: знайдено kind: Ingress (документ ${di + 1}) — заміни на Gateway API: HTTPRoute (hr.yaml), HealthCheckPolicy (hc.yaml) (див. k8s.mdc)`
|
|
1471
|
-
)
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1595
|
+
let pRaw
|
|
1596
|
+
try {
|
|
1597
|
+
pRaw = await readFile(resolved, 'utf8')
|
|
1598
|
+
} catch {
|
|
1599
|
+
return
|
|
1475
1600
|
}
|
|
1476
|
-
|
|
1601
|
+
const ops = collectJson6902OperationsFromPatchText(pRaw)
|
|
1602
|
+
if (ops.length === 0) {
|
|
1603
|
+
return
|
|
1604
|
+
}
|
|
1605
|
+
const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
|
|
1606
|
+
if (bad.length === 0) {
|
|
1607
|
+
return
|
|
1608
|
+
}
|
|
1609
|
+
const relPatch = (relative(root, resolved) || patchRef).replaceAll('\\', '/')
|
|
1610
|
+
fail(`${rel}: patch-файл «${relPatch}»: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`)
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
/**
|
|
1614
|
+
* Один елемент **`patches[]`**: inline JSON6902 або зовнішній patch-файл.
|
|
1615
|
+
* @param {string} rel відносний шлях до kustomization.yaml
|
|
1616
|
+
* @param {Record<string, unknown>} pr об’єкт patch
|
|
1617
|
+
* @param {number} patchIdx 1-based індекс у масиві
|
|
1618
|
+
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
1619
|
+
* @param {string} rootNorm нормалізований корінь репо
|
|
1620
|
+
* @param {string} root корінь репо
|
|
1621
|
+
* @param {(msg: string) => void} fail реєстрація порушення
|
|
1622
|
+
* @returns {Promise<void>}
|
|
1623
|
+
*/
|
|
1624
|
+
async function auditOneKustomizationJson6902Patch(
|
|
1625
|
+
rel,
|
|
1626
|
+
pr,
|
|
1627
|
+
patchIdx,
|
|
1628
|
+
kustAbs,
|
|
1629
|
+
rootNorm,
|
|
1630
|
+
root,
|
|
1631
|
+
fail
|
|
1632
|
+
) {
|
|
1633
|
+
if (typeof pr.patch === 'string' && pr.patch.trim() !== '') {
|
|
1634
|
+
failIfJson6902RemoveAddConflictOnSamePath(rel, `patches[${patchIdx}] inline JSON6902`, pr.patch, fail)
|
|
1635
|
+
}
|
|
1636
|
+
if (typeof pr.path !== 'string' || pr.path.trim() === '') {
|
|
1637
|
+
return
|
|
1638
|
+
}
|
|
1639
|
+
const patchRef = pr.path.trim()
|
|
1640
|
+
const resolved = resolve(dirname(kustAbs), patchRef)
|
|
1641
|
+
if (!resolvedFilePathIsUnderRoot(rootNorm, resolved) || !existsSync(resolved)) {
|
|
1642
|
+
return
|
|
1643
|
+
}
|
|
1644
|
+
await auditJson6902PatchExternalFile(rel, resolved, root, patchRef, fail)
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
/**
|
|
1648
|
+
* Усі **`patches[]`** у Kustomization: inline та зовнішні файли.
|
|
1649
|
+
* @param {string} rel відносний шлях до kustomization.yaml
|
|
1650
|
+
* @param {unknown} patches поле **patches**
|
|
1651
|
+
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
1652
|
+
* @param {string} rootNorm нормалізований корінь репо
|
|
1653
|
+
* @param {string} root корінь репо
|
|
1654
|
+
* @param {(msg: string) => void} fail реєстрація порушення
|
|
1655
|
+
* @returns {Promise<void>}
|
|
1656
|
+
*/
|
|
1657
|
+
async function auditKustomizationPatchesJson6902(rel, patches, kustAbs, rootNorm, root, fail) {
|
|
1658
|
+
if (!Array.isArray(patches)) {
|
|
1659
|
+
return
|
|
1660
|
+
}
|
|
1661
|
+
let patchIdx = 0
|
|
1662
|
+
for (const p of patches) {
|
|
1663
|
+
patchIdx++
|
|
1664
|
+
if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
|
|
1665
|
+
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
1666
|
+
await auditOneKustomizationJson6902Patch(rel, pr, patchIdx, kustAbs, rootNorm, root, fail)
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
/**
|
|
1672
|
+
* Один YAML-документ: якщо це Kustomization — перевірка **patches** на JSON6902 remove+add.
|
|
1673
|
+
* @param {string} rel відносний шлях до kustomization.yaml
|
|
1674
|
+
* @param {unknown} rootObj корінь документа
|
|
1675
|
+
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
1676
|
+
* @param {string} rootNorm нормалізований корінь репо
|
|
1677
|
+
* @param {string} root корінь репо
|
|
1678
|
+
* @param {(msg: string) => void} fail реєстрація порушення
|
|
1679
|
+
* @returns {Promise<void>}
|
|
1680
|
+
*/
|
|
1681
|
+
async function auditJson6902ForKustomizationYamlDoc(rel, rootObj, kustAbs, rootNorm, root, fail) {
|
|
1682
|
+
const rec = /** @type {Record<string, unknown>} */ (rootObj)
|
|
1683
|
+
if (rec.kind !== 'Kustomization') {
|
|
1684
|
+
return
|
|
1685
|
+
}
|
|
1686
|
+
await auditKustomizationPatchesJson6902(rel, rec.patches, kustAbs, rootNorm, root, fail)
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* Один **`kustomization.yaml`**: JSON6902 remove+add на одному path.
|
|
1691
|
+
* @param {string} root корінь репозиторію
|
|
1692
|
+
* @param {string} rootNorm нормалізований корінь
|
|
1693
|
+
* @param {string} kustAbs абсолютний шлях до файлу
|
|
1694
|
+
* @param {(msg: string) => void} fail реєстрація порушення
|
|
1695
|
+
* @returns {Promise<void>}
|
|
1696
|
+
*/
|
|
1697
|
+
async function auditJson6902OneKustomizationYamlFile(root, rootNorm, kustAbs, fail) {
|
|
1698
|
+
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
1699
|
+
let raw
|
|
1700
|
+
try {
|
|
1701
|
+
raw = await readFile(kustAbs, 'utf8')
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
1704
|
+
fail(`${rel}: не вдалося прочитати для перевірки JSON6902 (${msg})`)
|
|
1705
|
+
return
|
|
1706
|
+
}
|
|
1707
|
+
const lines = toLines(raw)
|
|
1708
|
+
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
1709
|
+
/** @type {import('yaml').Document[] | null} */
|
|
1710
|
+
let docs = null
|
|
1711
|
+
try {
|
|
1712
|
+
docs = parseAllDocuments(body)
|
|
1713
|
+
} catch {
|
|
1714
|
+
return
|
|
1715
|
+
}
|
|
1716
|
+
for (const doc of docs) {
|
|
1717
|
+
if (doc.errors.length === 0) {
|
|
1718
|
+
const rootObj = doc.toJSON()
|
|
1719
|
+
if (rootObj !== null && typeof rootObj === 'object' && !Array.isArray(rootObj)) {
|
|
1720
|
+
await auditJson6902ForKustomizationYamlDoc(rel, rootObj, kustAbs, rootNorm, root, fail)
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
/**
|
|
1727
|
+
* Перевіряє всі **`kustomization.yaml`** під **`k8s`**: у inline **`patch`** і у зовнішніх patch-файлах не має бути **remove** і **add** на той самий **path**.
|
|
1728
|
+
* @param {string} root корінь репозиторію
|
|
1729
|
+
* @param {string[]} yamlFilesAbs абсолютні шляхи до yaml під k8s
|
|
1730
|
+
* @param {(msg: string) => void} fail реєстрація порушення
|
|
1731
|
+
* @returns {Promise<void>}
|
|
1732
|
+
*/
|
|
1733
|
+
async function validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFilesAbs, fail) {
|
|
1734
|
+
const rootNorm = resolve(root)
|
|
1735
|
+
for (const kustAbs of yamlFilesAbs.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')) {
|
|
1736
|
+
await auditJson6902OneKustomizationYamlFile(root, rootNorm, kustAbs, fail)
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
/**
|
|
1741
|
+
* Заборонений **kind: Ingress** у документі.
|
|
1742
|
+
* @param {string} rel відносний шлях до файлу
|
|
1743
|
+
* @param {number} docIndex 1-based індекс документа
|
|
1744
|
+
* @param {Record<string, unknown>} rec корінь маніфесту
|
|
1745
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
1746
|
+
* @returns {void}
|
|
1747
|
+
*/
|
|
1748
|
+
function failIfIngressInDocument(rel, docIndex, rec, fail) {
|
|
1749
|
+
if (rec.kind !== 'Ingress') {
|
|
1750
|
+
return
|
|
1751
|
+
}
|
|
1752
|
+
fail(
|
|
1753
|
+
`${rel}: знайдено kind: Ingress (документ ${docIndex}) — заміни на Gateway API: HTTPRoute (hr.yaml), HealthCheckPolicy (hc.yaml) (див. k8s.mdc)`
|
|
1754
|
+
)
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/**
|
|
1758
|
+
* Шукає **Ingress** у розібраних документах; реєструє порушення.
|
|
1759
|
+
* @param {string} rel відносний шлях до файлу
|
|
1760
|
+
* @param {string} body YAML після modeline
|
|
1761
|
+
* @param {(msg: string) => void} fail callback для помилки (Ingress)
|
|
1762
|
+
* @returns {void}
|
|
1763
|
+
*/
|
|
1764
|
+
function scanIngressInYamlDocuments(rel, body, fail) {
|
|
1765
|
+
/** @type {import('yaml').Document[]} */
|
|
1766
|
+
let docs
|
|
1767
|
+
try {
|
|
1768
|
+
docs = parseAllDocuments(body)
|
|
1769
|
+
} catch {
|
|
1770
|
+
return
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
for (const [di, doc] of docs.entries()) {
|
|
1774
|
+
if (doc.errors.length === 0) {
|
|
1775
|
+
const obj = doc.toJSON()
|
|
1776
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
1777
|
+
failIfIngressInDocument(rel, di + 1, /** @type {Record<string, unknown>} */ (obj), fail)
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
/**
|
|
1784
|
+
* Перевірка поля **resources** для одного контейнера **Deployment**.
|
|
1785
|
+
* @param {unknown} c елемент **containers[]**
|
|
1786
|
+
* @param {string} label підпис у повідомленні
|
|
1787
|
+
* @returns {string | null} текст порушення або null
|
|
1788
|
+
*/
|
|
1789
|
+
function deploymentContainerResourcesViolation(c, label) {
|
|
1790
|
+
if (c === null || c === undefined || typeof c !== 'object' || Array.isArray(c)) {
|
|
1791
|
+
return null
|
|
1792
|
+
}
|
|
1793
|
+
const cont = /** @type {Record<string, unknown>} */ (c)
|
|
1794
|
+
if (!('resources' in cont)) {
|
|
1795
|
+
return `контейнер "${label}": відсутнє поле resources — додай resources: {} (див. k8s.mdc)`
|
|
1796
|
+
}
|
|
1797
|
+
const r = cont.resources
|
|
1798
|
+
if (r === null || typeof r !== 'object' || Array.isArray(r)) {
|
|
1799
|
+
return `контейнер "${label}": resources має бути записом у YAML (наприклад порожній: resources: {})`
|
|
1800
|
+
}
|
|
1801
|
+
return null
|
|
1802
|
+
}
|
|
1477
1803
|
|
|
1478
1804
|
/**
|
|
1479
1805
|
* Чи порушує маніфест вимогу **`Deployment.spec.template.spec.containers[].resources`** (див. k8s.mdc).
|
|
@@ -1500,15 +1826,9 @@ export function deploymentResourcesViolation(manifest) {
|
|
|
1500
1826
|
typeof c === 'object' && c !== null && !Array.isArray(c) && typeof c.name === 'string' && c.name !== ''
|
|
1501
1827
|
? c.name
|
|
1502
1828
|
: `#${i + 1}`
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
return `контейнер "${label}": відсутнє поле resources — додай resources: {} (див. k8s.mdc)`
|
|
1507
|
-
}
|
|
1508
|
-
const r = cont.resources
|
|
1509
|
-
if (r === null || typeof r !== 'object' || Array.isArray(r)) {
|
|
1510
|
-
return `контейнер "${label}": resources має бути записом у YAML (наприклад порожній: resources: {})`
|
|
1511
|
-
}
|
|
1829
|
+
const v = deploymentContainerResourcesViolation(c, label)
|
|
1830
|
+
if (v !== null) {
|
|
1831
|
+
return v
|
|
1512
1832
|
}
|
|
1513
1833
|
}
|
|
1514
1834
|
|
|
@@ -1532,31 +1852,48 @@ function stripImageDigest(image) {
|
|
|
1532
1852
|
*/
|
|
1533
1853
|
function isHasuraGraphqlEngineImageRef(image) {
|
|
1534
1854
|
const s = stripImageDigest(image)
|
|
1535
|
-
return
|
|
1855
|
+
return HASURA_GRAPHQL_ENGINE_RE.test(s)
|
|
1536
1856
|
}
|
|
1537
1857
|
|
|
1538
1858
|
/**
|
|
1539
|
-
*
|
|
1859
|
+
* Перевірка образу Hasura для одного контейнера у списку **containers** / **initContainers**.
|
|
1540
1860
|
* @param {string} list ім’я поля для повідомлення (`containers` / `initContainers`)
|
|
1541
|
-
* @param {unknown}
|
|
1861
|
+
* @param {unknown} c елемент масиву
|
|
1862
|
+
* @param {number} i індекс
|
|
1863
|
+
* @returns {string | null} текст порушення або null
|
|
1864
|
+
*/
|
|
1865
|
+
function hasuraGraphqlEngineViolationForOneContainer(list, c, i) {
|
|
1866
|
+
const label =
|
|
1867
|
+
typeof c === 'object' && c !== null && !Array.isArray(c) && typeof c.name === 'string' && c.name !== ''
|
|
1868
|
+
? c.name
|
|
1869
|
+
: `#${i + 1}`
|
|
1870
|
+
if (c === null || c === undefined || typeof c !== 'object' || Array.isArray(c)) {
|
|
1871
|
+
return null
|
|
1872
|
+
}
|
|
1873
|
+
const cont = /** @type {Record<string, unknown>} */ (c)
|
|
1874
|
+
const image = cont.image
|
|
1875
|
+
if (typeof image !== 'string' || image.trim() === '' || !isHasuraGraphqlEngineImageRef(image)) {
|
|
1876
|
+
return null
|
|
1877
|
+
}
|
|
1878
|
+
const normalized = stripImageDigest(image)
|
|
1879
|
+
if (!HASURA_GRAPHQL_ENGINE_ALLOWED_IMAGES.has(normalized)) {
|
|
1880
|
+
return `${list} "${label}": образ hasura/graphql-engine має бути ${HASURA_GRAPHQL_ENGINE_IMAGE} (зараз: ${image}) (див. k8s.mdc)`
|
|
1881
|
+
}
|
|
1882
|
+
return null
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
/**
|
|
1886
|
+
* Перевіряє масив **containers** / **initContainers** на зафіксований образ Hasura.
|
|
1887
|
+
* @param {string} list **containers** або **initContainers** (для тексту помилки)
|
|
1888
|
+
* @param {unknown} containers значення поля з маніфесту
|
|
1542
1889
|
* @returns {string | null} текст порушення або null
|
|
1543
1890
|
*/
|
|
1544
1891
|
function hasuraGraphqlEngineViolationInContainerList(list, containers) {
|
|
1545
1892
|
if (!Array.isArray(containers)) return null
|
|
1546
1893
|
for (const [i, c] of containers.entries()) {
|
|
1547
|
-
const
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
: `#${i + 1}`
|
|
1551
|
-
if (c !== null && c !== undefined && typeof c === 'object' && !Array.isArray(c)) {
|
|
1552
|
-
const cont = /** @type {Record<string, unknown>} */ (c)
|
|
1553
|
-
const image = cont.image
|
|
1554
|
-
if (typeof image === 'string' && image.trim() !== '' && isHasuraGraphqlEngineImageRef(image)) {
|
|
1555
|
-
const normalized = stripImageDigest(image)
|
|
1556
|
-
if (!HASURA_GRAPHQL_ENGINE_ALLOWED_IMAGES.has(normalized)) {
|
|
1557
|
-
return `${list} "${label}": образ hasura/graphql-engine має бути ${HASURA_GRAPHQL_ENGINE_IMAGE} (зараз: ${image}) (див. k8s.mdc)`
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1894
|
+
const v = hasuraGraphqlEngineViolationForOneContainer(list, c, i)
|
|
1895
|
+
if (v !== null) {
|
|
1896
|
+
return v
|
|
1560
1897
|
}
|
|
1561
1898
|
}
|
|
1562
1899
|
return null
|
|
@@ -1762,6 +2099,35 @@ export function collectGatewayApiRouteBackendServiceNames(spec) {
|
|
|
1762
2099
|
return out
|
|
1763
2100
|
}
|
|
1764
2101
|
|
|
2102
|
+
/**
|
|
2103
|
+
* Один документ: маршрут Gateway API має посилатися на **Service** з суфіксом **`-hl`**.
|
|
2104
|
+
* @param {string} rel відносний шлях до файлу
|
|
2105
|
+
* @param {number} docIndex 1-based індекс документа
|
|
2106
|
+
* @param {Record<string, unknown>} rec корінь маніфесту
|
|
2107
|
+
* @param {(msg: string) => void} fail callback помилки
|
|
2108
|
+
* @returns {void}
|
|
2109
|
+
*/
|
|
2110
|
+
function failIfGatewayRouteUsesNonHeadlessService(rel, docIndex, rec, fail) {
|
|
2111
|
+
const av = rec.apiVersion
|
|
2112
|
+
const kind = rec.kind
|
|
2113
|
+
if (
|
|
2114
|
+
typeof av !== 'string' ||
|
|
2115
|
+
!av.startsWith(GATEWAY_API_GROUP_PREFIX) ||
|
|
2116
|
+
typeof kind !== 'string' ||
|
|
2117
|
+
!GATEWAY_API_ROUTE_KINDS.has(kind)
|
|
2118
|
+
) {
|
|
2119
|
+
return
|
|
2120
|
+
}
|
|
2121
|
+
const names = collectGatewayApiRouteBackendServiceNames(rec.spec)
|
|
2122
|
+
for (const svcName of names) {
|
|
2123
|
+
if (!svcName.endsWith(SVC_HL_NAME_SUFFIX)) {
|
|
2124
|
+
fail(
|
|
2125
|
+
`${rel}: Gateway API ${kind} (документ ${docIndex}): backendRef до Service має вказувати headless-сервіс з суфіксом «${SVC_HL_NAME_SUFFIX}» у name (зараз: «${svcName}»; див. k8s.mdc)`
|
|
2126
|
+
)
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
|
|
1765
2131
|
/**
|
|
1766
2132
|
* Реєструє порушення: маршрути Gateway API мають посилатися на **Service** з суфіксом **`-hl`**.
|
|
1767
2133
|
* @param {string} rel відносний шлях до файлу
|
|
@@ -1782,27 +2148,141 @@ function scanGatewayApiRouteBackendRefsInYamlBody(rel, body, fail) {
|
|
|
1782
2148
|
if (doc.errors.length === 0) {
|
|
1783
2149
|
const obj = doc.toJSON()
|
|
1784
2150
|
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
2151
|
+
failIfGatewayRouteUsesNonHeadlessService(rel, di + 1, /** @type {Record<string, unknown>} */ (obj), fail)
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
/**
|
|
2158
|
+
* Збирає **`metadata.name`** для **kind: Service** у коренях документів; при помилці викликає **fail** і повертає false.
|
|
2159
|
+
* @param {Record<string, unknown>[]} roots корені YAML-документів
|
|
2160
|
+
* @param {string} relForMsg відносний шлях до файлу для повідомлення
|
|
2161
|
+
* @param {string} fileLabel **svc.yaml** / **svc-hl.yaml**
|
|
2162
|
+
* @param {string[]} names накопичувач імен
|
|
2163
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
2164
|
+
* @returns {boolean} false, якщо зафіксовано порушення
|
|
2165
|
+
*/
|
|
2166
|
+
function appendServiceNamesFromSvcRoots(roots, relForMsg, fileLabel, names, fail) {
|
|
2167
|
+
for (const [i, rootObj] of roots.entries()) {
|
|
2168
|
+
const r = /** @type {Record<string, unknown>} */ (rootObj)
|
|
2169
|
+
if (r.kind === 'Service') {
|
|
2170
|
+
const meta = r.metadata
|
|
2171
|
+
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
|
|
2172
|
+
fail(`${relForMsg}: ${fileLabel} (документ ${i + 1}): Service без metadata (див. k8s.mdc)`)
|
|
2173
|
+
return false
|
|
1803
2174
|
}
|
|
2175
|
+
const nm = /** @type {Record<string, unknown>} */ (meta).name
|
|
2176
|
+
if (typeof nm !== 'string') {
|
|
2177
|
+
fail(`${relForMsg}: ${fileLabel} (документ ${i + 1}): Service без metadata.name (див. k8s.mdc)`)
|
|
2178
|
+
return false
|
|
2179
|
+
}
|
|
2180
|
+
names.push(nm)
|
|
1804
2181
|
}
|
|
1805
2182
|
}
|
|
2183
|
+
return true
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
/**
|
|
2187
|
+
* Узгодженість імен **Service** між **svc.yaml** та **svc-hl.yaml**.
|
|
2188
|
+
* @param {string} relSvc відносний шлях до **svc.yaml**
|
|
2189
|
+
* @param {string} relHl відносний шлях до **svc-hl.yaml**
|
|
2190
|
+
* @param {string[]} svcNames імена з **svc.yaml**
|
|
2191
|
+
* @param {string[]} hlNames імена з **svc-hl.yaml**
|
|
2192
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
2193
|
+
* @returns {void}
|
|
2194
|
+
*/
|
|
2195
|
+
function validateSvcHlServiceNamePairing(relSvc, relHl, svcNames, hlNames, fail) {
|
|
2196
|
+
if (svcNames.length === 0) {
|
|
2197
|
+
fail(`${relSvc}: svc.yaml має містити принаймні один kind: Service (див. k8s.mdc)`)
|
|
2198
|
+
return
|
|
2199
|
+
}
|
|
2200
|
+
if (hlNames.length === 0) {
|
|
2201
|
+
fail(`${relHl}: svc-hl.yaml має містити принаймні один kind: Service (див. k8s.mdc)`)
|
|
2202
|
+
return
|
|
2203
|
+
}
|
|
2204
|
+
const hlSet = new Set(hlNames)
|
|
2205
|
+
for (const n of svcNames) {
|
|
2206
|
+
const expectHl = `${n}${SVC_HL_NAME_SUFFIX}`
|
|
2207
|
+
if (!hlSet.has(expectHl)) {
|
|
2208
|
+
fail(
|
|
2209
|
+
`${relSvc}: для Service «${n}» у svc.yaml у svc-hl.yaml має бути Service з metadata.name «${expectHl}» (див. k8s.mdc)`
|
|
2210
|
+
)
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
for (const h of hlNames) {
|
|
2214
|
+
if (h.endsWith(SVC_HL_NAME_SUFFIX)) {
|
|
2215
|
+
const base = h.slice(0, -SVC_HL_NAME_SUFFIX.length)
|
|
2216
|
+
if (!svcNames.includes(base)) {
|
|
2217
|
+
fail(
|
|
2218
|
+
`${relHl}: Service «${h}» у svc-hl.yaml не відповідає жодному Service у svc.yaml (очікується базове ім’я «${base}»; див. k8s.mdc)`
|
|
2219
|
+
)
|
|
2220
|
+
}
|
|
2221
|
+
} else {
|
|
2222
|
+
fail(
|
|
2223
|
+
`${relHl}: Service «${h}» у svc-hl.yaml: metadata.name має закінчуватися на «${SVC_HL_NAME_SUFFIX}» (див. k8s.mdc)`
|
|
2224
|
+
)
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
/**
|
|
2230
|
+
* **svc-hl.yaml** без **svc.yaml** у тому самому каталозі.
|
|
2231
|
+
* @param {string} root корінь репозиторію
|
|
2232
|
+
* @param {string[]} yamlFiles абсолютні шляхи
|
|
2233
|
+
* @param {Set<string>} absSet той самий набір шляхів
|
|
2234
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
2235
|
+
* @returns {void}
|
|
2236
|
+
*/
|
|
2237
|
+
function failIfSvcHlWithoutSiblingSvc(root, yamlFiles, absSet, fail) {
|
|
2238
|
+
for (const abs of yamlFiles.filter(p => basename(p).toLowerCase() === 'svc-hl.yaml')) {
|
|
2239
|
+
const svcAbs = join(dirname(abs), 'svc.yaml')
|
|
2240
|
+
if (!absSet.has(svcAbs)) {
|
|
2241
|
+
const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
|
|
2242
|
+
fail(`${rel}: svc-hl.yaml потребує svc.yaml у тому самому каталозі (див. k8s.mdc)`)
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
/**
|
|
2248
|
+
* Одна пара **svc.yaml** / **svc-hl.yaml**: читання, імена **Service**, узгодженість.
|
|
2249
|
+
* @param {string} root корінь репозиторію
|
|
2250
|
+
* @param {Set<string>} absSet наявні yaml під k8s
|
|
2251
|
+
* @param {string} svcAbs абсолютний шлях до **svc.yaml**
|
|
2252
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
2253
|
+
* @returns {Promise<void>}
|
|
2254
|
+
*/
|
|
2255
|
+
async function validateOneSvcYamlHlPair(root, absSet, svcAbs, fail) {
|
|
2256
|
+
const rel = (relative(root, svcAbs) || svcAbs).replaceAll('\\', '/')
|
|
2257
|
+
const hlAbs = join(dirname(svcAbs), 'svc-hl.yaml')
|
|
2258
|
+
if (!absSet.has(hlAbs)) {
|
|
2259
|
+
fail(`${rel}: поруч обов’язковий svc-hl.yaml (headless-копія з суфіксом -hl у metadata.name; див. k8s.mdc)`)
|
|
2260
|
+
return
|
|
2261
|
+
}
|
|
2262
|
+
const hlRel = (relative(root, hlAbs) || hlAbs).replaceAll('\\', '/')
|
|
2263
|
+
let svcBody
|
|
2264
|
+
let hlBody
|
|
2265
|
+
try {
|
|
2266
|
+
svcBody = await readK8sYamlBodyAfterModelineForSvcPair(svcAbs)
|
|
2267
|
+
hlBody = await readK8sYamlBodyAfterModelineForSvcPair(hlAbs)
|
|
2268
|
+
} catch (error) {
|
|
2269
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
2270
|
+
fail(`${rel}: не вдалося прочитати svc.yaml / svc-hl.yaml (${msg})`)
|
|
2271
|
+
return
|
|
2272
|
+
}
|
|
2273
|
+
const svcRoots = parseK8sYamlDocumentObjectRoots(svcBody)
|
|
2274
|
+
const hlRoots = parseK8sYamlDocumentObjectRoots(hlBody)
|
|
2275
|
+
/** @type {string[]} */
|
|
2276
|
+
const svcNames = []
|
|
2277
|
+
if (!appendServiceNamesFromSvcRoots(svcRoots, rel, 'svc.yaml', svcNames, fail)) {
|
|
2278
|
+
return
|
|
2279
|
+
}
|
|
2280
|
+
/** @type {string[]} */
|
|
2281
|
+
const hlNames = []
|
|
2282
|
+
if (!appendServiceNamesFromSvcRoots(hlRoots, hlRel, 'svc-hl.yaml', hlNames, fail)) {
|
|
2283
|
+
return
|
|
2284
|
+
}
|
|
2285
|
+
validateSvcHlServiceNamePairing(rel, hlRel, svcNames, hlNames, fail)
|
|
1806
2286
|
}
|
|
1807
2287
|
|
|
1808
2288
|
/**
|
|
@@ -1814,117 +2294,9 @@ function scanGatewayApiRouteBackendRefsInYamlBody(rel, body, fail) {
|
|
|
1814
2294
|
*/
|
|
1815
2295
|
async function validateSvcYamlAndSvcHlPairs(root, yamlFiles, fail) {
|
|
1816
2296
|
const absSet = new Set(yamlFiles)
|
|
1817
|
-
|
|
1818
|
-
for (const
|
|
1819
|
-
|
|
1820
|
-
const svcAbs = join(dirname(abs), 'svc.yaml')
|
|
1821
|
-
if (!absSet.has(svcAbs)) {
|
|
1822
|
-
const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
|
|
1823
|
-
fail(`${rel}: svc-hl.yaml потребує svc.yaml у тому самому каталозі (див. k8s.mdc)`)
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
for (const svcAbs of yamlFiles) {
|
|
1829
|
-
if (basename(svcAbs).toLowerCase() === 'svc.yaml') {
|
|
1830
|
-
const rel = (relative(root, svcAbs) || svcAbs).replaceAll('\\', '/')
|
|
1831
|
-
const hlAbs = join(dirname(svcAbs), 'svc-hl.yaml')
|
|
1832
|
-
if (absSet.has(hlAbs)) {
|
|
1833
|
-
/** @type {string | undefined} */
|
|
1834
|
-
let svcBody
|
|
1835
|
-
/** @type {string | undefined} */
|
|
1836
|
-
let hlBody
|
|
1837
|
-
try {
|
|
1838
|
-
svcBody = await readK8sYamlBodyAfterModelineForSvcPair(svcAbs)
|
|
1839
|
-
hlBody = await readK8sYamlBodyAfterModelineForSvcPair(hlAbs)
|
|
1840
|
-
} catch (error) {
|
|
1841
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
1842
|
-
fail(`${rel}: не вдалося прочитати svc.yaml / svc-hl.yaml (${msg})`)
|
|
1843
|
-
}
|
|
1844
|
-
if (svcBody !== undefined && hlBody !== undefined) {
|
|
1845
|
-
const svcRoots = parseK8sYamlDocumentObjectRoots(svcBody)
|
|
1846
|
-
const hlRoots = parseK8sYamlDocumentObjectRoots(hlBody)
|
|
1847
|
-
|
|
1848
|
-
/** @type {string[]} */
|
|
1849
|
-
const svcNames = []
|
|
1850
|
-
for (const [i, rootObj] of svcRoots.entries()) {
|
|
1851
|
-
const r = /** @type {Record<string, unknown>} */ (rootObj)
|
|
1852
|
-
if (r.kind === 'Service') {
|
|
1853
|
-
const meta = r.metadata
|
|
1854
|
-
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
|
|
1855
|
-
const nm = /** @type {Record<string, unknown>} */ (meta).name
|
|
1856
|
-
if (typeof nm === 'string') {
|
|
1857
|
-
svcNames.push(nm)
|
|
1858
|
-
} else {
|
|
1859
|
-
fail(`${rel}: svc.yaml (документ ${i + 1}): Service без metadata.name (див. k8s.mdc)`)
|
|
1860
|
-
}
|
|
1861
|
-
} else {
|
|
1862
|
-
fail(`${rel}: svc.yaml (документ ${i + 1}): Service без metadata (див. k8s.mdc)`)
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
if (svcNames.length === 0) {
|
|
1868
|
-
fail(`${rel}: svc.yaml має містити принаймні один kind: Service (див. k8s.mdc)`)
|
|
1869
|
-
} else {
|
|
1870
|
-
/** @type {string[]} */
|
|
1871
|
-
const hlNames = []
|
|
1872
|
-
for (const [i, rootObj] of hlRoots.entries()) {
|
|
1873
|
-
const r = /** @type {Record<string, unknown>} */ (rootObj)
|
|
1874
|
-
if (r.kind === 'Service') {
|
|
1875
|
-
const meta = r.metadata
|
|
1876
|
-
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
|
|
1877
|
-
const nm = /** @type {Record<string, unknown>} */ (meta).name
|
|
1878
|
-
if (typeof nm === 'string') {
|
|
1879
|
-
hlNames.push(nm)
|
|
1880
|
-
} else {
|
|
1881
|
-
const hlRel = (relative(root, hlAbs) || hlAbs).replaceAll('\\', '/')
|
|
1882
|
-
fail(`${hlRel}: svc-hl.yaml (документ ${i + 1}): Service без metadata.name (див. k8s.mdc)`)
|
|
1883
|
-
}
|
|
1884
|
-
} else {
|
|
1885
|
-
const hlRel = (relative(root, hlAbs) || hlAbs).replaceAll('\\', '/')
|
|
1886
|
-
fail(`${hlRel}: svc-hl.yaml (документ ${i + 1}): Service без metadata (див. k8s.mdc)`)
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
if (hlNames.length === 0) {
|
|
1892
|
-
const hlRel = (relative(root, hlAbs) || hlAbs).replaceAll('\\', '/')
|
|
1893
|
-
fail(`${hlRel}: svc-hl.yaml має містити принаймні один kind: Service (див. k8s.mdc)`)
|
|
1894
|
-
} else {
|
|
1895
|
-
const hlSet = new Set(hlNames)
|
|
1896
|
-
for (const n of svcNames) {
|
|
1897
|
-
const expectHl = `${n}${SVC_HL_NAME_SUFFIX}`
|
|
1898
|
-
if (!hlSet.has(expectHl)) {
|
|
1899
|
-
fail(
|
|
1900
|
-
`${rel}: для Service «${n}» у svc.yaml у svc-hl.yaml має бути Service з metadata.name «${expectHl}» (див. k8s.mdc)`
|
|
1901
|
-
)
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
for (const h of hlNames) {
|
|
1906
|
-
if (h.endsWith(SVC_HL_NAME_SUFFIX)) {
|
|
1907
|
-
const base = h.slice(0, -SVC_HL_NAME_SUFFIX.length)
|
|
1908
|
-
if (!svcNames.includes(base)) {
|
|
1909
|
-
const hlRel = (relative(root, hlAbs) || hlAbs).replaceAll('\\', '/')
|
|
1910
|
-
fail(
|
|
1911
|
-
`${hlRel}: Service «${h}» у svc-hl.yaml не відповідає жодному Service у svc.yaml (очікується базове ім’я «${base}»; див. k8s.mdc)`
|
|
1912
|
-
)
|
|
1913
|
-
}
|
|
1914
|
-
} else {
|
|
1915
|
-
const hlRel = (relative(root, hlAbs) || hlAbs).replaceAll('\\', '/')
|
|
1916
|
-
fail(
|
|
1917
|
-
`${hlRel}: Service «${h}» у svc-hl.yaml: metadata.name має закінчуватися на «${SVC_HL_NAME_SUFFIX}» (див. k8s.mdc)`
|
|
1918
|
-
)
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
} else {
|
|
1925
|
-
fail(`${rel}: поруч обов’язковий svc-hl.yaml (headless-копія з суфіксом -hl у metadata.name; див. k8s.mdc)`)
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
2297
|
+
failIfSvcHlWithoutSiblingSvc(root, yamlFiles, absSet, fail)
|
|
2298
|
+
for (const svcAbs of yamlFiles.filter(p => basename(p).toLowerCase() === 'svc.yaml')) {
|
|
2299
|
+
await validateOneSvcYamlHlPair(root, absSet, svcAbs, fail)
|
|
1928
2300
|
}
|
|
1929
2301
|
}
|
|
1930
2302
|
|
|
@@ -1992,7 +2364,90 @@ function isKustomizationFileName(baseLower) {
|
|
|
1992
2364
|
export function isK8sBaseManifestYamlPath(rel, baseLower) {
|
|
1993
2365
|
if (isKustomizationFileName(baseLower)) return false
|
|
1994
2366
|
const n = rel.replaceAll('\\', '/')
|
|
1995
|
-
return
|
|
2367
|
+
return K8S_BASE_SEGMENT_RE.test(n)
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
/**
|
|
2371
|
+
* Правила **metadata.namespace** для одного документа.
|
|
2372
|
+
* @param {string} rel відносний шлях
|
|
2373
|
+
* @param {number} docIndex 1-based
|
|
2374
|
+
* @param {unknown} obj корінь документа
|
|
2375
|
+
* @param {boolean} skipMetaNs пропуск для **kustomization.yaml**
|
|
2376
|
+
* @param {boolean} inBaseManifest файл у **k8s/base/**
|
|
2377
|
+
* @param {boolean} kustomizeManaged файл у графі kustomization
|
|
2378
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
2379
|
+
* @returns {void}
|
|
2380
|
+
*/
|
|
2381
|
+
function failIfK8sPolicyNamespaceRulesViolated(
|
|
2382
|
+
rel,
|
|
2383
|
+
docIndex,
|
|
2384
|
+
obj,
|
|
2385
|
+
skipMetaNs,
|
|
2386
|
+
inBaseManifest,
|
|
2387
|
+
kustomizeManaged,
|
|
2388
|
+
fail
|
|
2389
|
+
) {
|
|
2390
|
+
if (skipMetaNs) {
|
|
2391
|
+
return
|
|
2392
|
+
}
|
|
2393
|
+
if (inBaseManifest) {
|
|
2394
|
+
const req = metadataNamespaceRequiredViolation(obj, true)
|
|
2395
|
+
if (req !== null) {
|
|
2396
|
+
fail(`${rel}: документ ${docIndex}: ${req}`)
|
|
2397
|
+
}
|
|
2398
|
+
return
|
|
2399
|
+
}
|
|
2400
|
+
if (kustomizeManaged) {
|
|
2401
|
+
const ns = metadataNamespaceForbiddenViolation(obj)
|
|
2402
|
+
if (ns !== null) {
|
|
2403
|
+
fail(`${rel}: документ ${docIndex}: ${ns}`)
|
|
2404
|
+
}
|
|
2405
|
+
return
|
|
2406
|
+
}
|
|
2407
|
+
const req = metadataNamespaceRequiredViolation(obj, false)
|
|
2408
|
+
if (req !== null) {
|
|
2409
|
+
fail(`${rel}: документ ${docIndex}: ${req}`)
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
/**
|
|
2414
|
+
* Deployment / Service / HealthCheckPolicy — політики для одного документа.
|
|
2415
|
+
* @param {string} rel відносний шлях
|
|
2416
|
+
* @param {string} baseLower basename (нижній регістр)
|
|
2417
|
+
* @param {number} docIndex 1-based
|
|
2418
|
+
* @param {unknown} obj корінь документа
|
|
2419
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
2420
|
+
* @returns {void}
|
|
2421
|
+
*/
|
|
2422
|
+
function failIfK8sPolicyResourceRulesViolated(rel, baseLower, docIndex, obj, fail) {
|
|
2423
|
+
const resV = deploymentResourcesViolation(obj)
|
|
2424
|
+
if (resV !== null) {
|
|
2425
|
+
fail(`${rel}: Deployment (документ ${docIndex}): ${resV}`)
|
|
2426
|
+
}
|
|
2427
|
+
const hasuraV = deploymentHasuraGraphqlEngineImageViolation(obj)
|
|
2428
|
+
if (hasuraV !== null) {
|
|
2429
|
+
fail(`${rel}: Deployment (документ ${docIndex}): ${hasuraV}`)
|
|
2430
|
+
}
|
|
2431
|
+
const svcGcpV = serviceForbiddenGcpAnnotationsViolation(obj)
|
|
2432
|
+
if (svcGcpV !== null) {
|
|
2433
|
+
fail(`${rel}: Service (документ ${docIndex}): ${svcGcpV}`)
|
|
2434
|
+
}
|
|
2435
|
+
if (baseLower === 'svc.yaml') {
|
|
2436
|
+
const svcT = serviceSvcYamlClusterIpTypeViolation(obj)
|
|
2437
|
+
if (svcT !== null) {
|
|
2438
|
+
fail(`${rel}: Service (документ ${docIndex}): ${svcT}`)
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
if (baseLower === 'svc-hl.yaml') {
|
|
2442
|
+
const svcH = serviceSvcHlYamlHeadlessViolation(obj)
|
|
2443
|
+
if (svcH !== null) {
|
|
2444
|
+
fail(`${rel}: Service (документ ${docIndex}): ${svcH}`)
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
const hcpHl = healthCheckPolicyTargetRefHeadlessServiceViolation(obj)
|
|
2448
|
+
if (hcpHl !== null) {
|
|
2449
|
+
fail(`${rel}: документ ${docIndex}: ${hcpHl}`)
|
|
2450
|
+
}
|
|
1996
2451
|
}
|
|
1997
2452
|
|
|
1998
2453
|
/**
|
|
@@ -2024,52 +2479,16 @@ function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeMan
|
|
|
2024
2479
|
fail(`${rel}: YAML (документ ${di + 1}): ${doc.errors.map(e => e.message).join('; ')}`)
|
|
2025
2480
|
} else {
|
|
2026
2481
|
const obj = doc.toJSON()
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
}
|
|
2038
|
-
} else {
|
|
2039
|
-
const req = metadataNamespaceRequiredViolation(obj, false)
|
|
2040
|
-
if (req !== null) {
|
|
2041
|
-
fail(`${rel}: документ ${di + 1}: ${req}`)
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
|
-
const resV = deploymentResourcesViolation(obj)
|
|
2046
|
-
if (resV !== null) {
|
|
2047
|
-
fail(`${rel}: Deployment (документ ${di + 1}): ${resV}`)
|
|
2048
|
-
}
|
|
2049
|
-
const hasuraV = deploymentHasuraGraphqlEngineImageViolation(obj)
|
|
2050
|
-
if (hasuraV !== null) {
|
|
2051
|
-
fail(`${rel}: Deployment (документ ${di + 1}): ${hasuraV}`)
|
|
2052
|
-
}
|
|
2053
|
-
const svcGcpV = serviceForbiddenGcpAnnotationsViolation(obj)
|
|
2054
|
-
if (svcGcpV !== null) {
|
|
2055
|
-
fail(`${rel}: Service (документ ${di + 1}): ${svcGcpV}`)
|
|
2056
|
-
}
|
|
2057
|
-
if (baseLower === 'svc.yaml') {
|
|
2058
|
-
const svcT = serviceSvcYamlClusterIpTypeViolation(obj)
|
|
2059
|
-
if (svcT !== null) {
|
|
2060
|
-
fail(`${rel}: Service (документ ${di + 1}): ${svcT}`)
|
|
2061
|
-
}
|
|
2062
|
-
}
|
|
2063
|
-
if (baseLower === 'svc-hl.yaml') {
|
|
2064
|
-
const svcH = serviceSvcHlYamlHeadlessViolation(obj)
|
|
2065
|
-
if (svcH !== null) {
|
|
2066
|
-
fail(`${rel}: Service (документ ${di + 1}): ${svcH}`)
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
const hcpHl = healthCheckPolicyTargetRefHeadlessServiceViolation(obj)
|
|
2070
|
-
if (hcpHl !== null) {
|
|
2071
|
-
fail(`${rel}: документ ${di + 1}: ${hcpHl}`)
|
|
2072
|
-
}
|
|
2482
|
+
failIfK8sPolicyNamespaceRulesViolated(
|
|
2483
|
+
rel,
|
|
2484
|
+
di + 1,
|
|
2485
|
+
obj,
|
|
2486
|
+
skipMetaNs,
|
|
2487
|
+
inBaseManifest,
|
|
2488
|
+
kustomizeManaged,
|
|
2489
|
+
fail
|
|
2490
|
+
)
|
|
2491
|
+
failIfK8sPolicyResourceRulesViolated(rel, baseLower, di + 1, obj, fail)
|
|
2073
2492
|
}
|
|
2074
2493
|
}
|
|
2075
2494
|
}
|
|
@@ -2080,31 +2499,24 @@ function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeMan
|
|
|
2080
2499
|
* @returns {string} рядок для шаблону імені файлу схеми
|
|
2081
2500
|
*/
|
|
2082
2501
|
function kindToSchemaFilePart(kind) {
|
|
2083
|
-
|
|
2502
|
+
let out = ''
|
|
2503
|
+
for (const ch of kind) {
|
|
2504
|
+
const c = ch.codePointAt(0)
|
|
2505
|
+
if (c !== undefined && ((c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122))) {
|
|
2506
|
+
out += ch
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
return out.toLowerCase()
|
|
2084
2510
|
}
|
|
2085
2511
|
|
|
2086
2512
|
/**
|
|
2087
|
-
* Очікуваний
|
|
2088
|
-
* @param {string}
|
|
2089
|
-
* @param {string}
|
|
2090
|
-
* @
|
|
2513
|
+
* Очікуваний URL схеми за **apiVersion/kind** (не **kustomization.yaml**).
|
|
2514
|
+
* @param {string} doc текст першого документа
|
|
2515
|
+
* @param {string} apiVersion значення **apiVersion** з маніфесту
|
|
2516
|
+
* @param {string} kind значення **kind** з маніфесту
|
|
2517
|
+
* @returns {{ expected: string | null, reason: string }} очікуваний URL і пояснення для повідомлень
|
|
2091
2518
|
*/
|
|
2092
|
-
|
|
2093
|
-
const base = basename(filePath)
|
|
2094
|
-
const baseLower = base.toLowerCase()
|
|
2095
|
-
|
|
2096
|
-
if (baseLower === 'kustomization.yaml') {
|
|
2097
|
-
return { expected: KUSTOMIZATION_SCHEMA, reason: 'kustomization (ім’я файлу)' }
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
const { apiVersion, kind } = extractApiVersionAndKind(doc)
|
|
2101
|
-
if (!apiVersion || !kind) {
|
|
2102
|
-
return {
|
|
2103
|
-
expected: null,
|
|
2104
|
-
reason: 'не знайдено apiVersion/kind у першому документі (потрібні для перевірки $schema)'
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2519
|
+
function expectedSchemaUrlForTypedManifest(doc, apiVersion, kind) {
|
|
2108
2520
|
const manifestType = extractTopLevelManifestType(doc)
|
|
2109
2521
|
const explicit = lookupExplicitK8sSchema(apiVersion, kind, manifestType)
|
|
2110
2522
|
if (explicit) {
|
|
@@ -2140,13 +2552,122 @@ export function expectedSchemaUrl(filePath, doc) {
|
|
|
2140
2552
|
return { expected: url, reason: 'CRD / група поза yannh (datree CRDs-catalog)' }
|
|
2141
2553
|
}
|
|
2142
2554
|
|
|
2555
|
+
/**
|
|
2556
|
+
* Очікуваний $schema для маніфесту згідно з k8s.mdc.
|
|
2557
|
+
* @param {string} filePath шлях до файлу (для імені kustomization)
|
|
2558
|
+
* @param {string} doc перший YAML-документ після modeline
|
|
2559
|
+
* @returns {{ expected: string | null, reason: string }} reason — для повідомлень про помилку
|
|
2560
|
+
*/
|
|
2561
|
+
export function expectedSchemaUrl(filePath, doc) {
|
|
2562
|
+
const base = basename(filePath)
|
|
2563
|
+
const baseLower = base.toLowerCase()
|
|
2564
|
+
|
|
2565
|
+
if (baseLower === 'kustomization.yaml') {
|
|
2566
|
+
return { expected: KUSTOMIZATION_SCHEMA, reason: 'kustomization (ім’я файлу)' }
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
const { apiVersion, kind } = extractApiVersionAndKind(doc)
|
|
2570
|
+
if (!apiVersion || !kind) {
|
|
2571
|
+
return {
|
|
2572
|
+
expected: null,
|
|
2573
|
+
reason: 'не знайдено apiVersion/kind у першому документі (потрібні для перевірки $schema)'
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
return expectedSchemaUrlForTypedManifest(doc, apiVersion, kind)
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2143
2580
|
/**
|
|
2144
2581
|
* Підраховує рядки з modeline $schema у файлі.
|
|
2145
2582
|
* @param {string[]} lines рядки файлу
|
|
2146
2583
|
* @returns {number} скільки рядків містять modeline `$schema`
|
|
2147
2584
|
*/
|
|
2148
2585
|
function countSchemaModelines(lines) {
|
|
2149
|
-
return lines.filter(l =>
|
|
2586
|
+
return lines.filter(l => OXLINT_SCHEMA_MODELINE_RE.test(l.trim())).length
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
/**
|
|
2590
|
+
* Політики маніфестів і Gateway backendRefs після розбору тіла.
|
|
2591
|
+
* @param {string} rel відносний шлях
|
|
2592
|
+
* @param {string} baseLower basename (нижній регістр)
|
|
2593
|
+
* @param {string} body YAML після modeline
|
|
2594
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
2595
|
+
* @param {Set<string>} kustomizeManagedRel kustomize-managed шляхи
|
|
2596
|
+
* @returns {void}
|
|
2597
|
+
*/
|
|
2598
|
+
function runK8sYamlPolicyAndGatewayScans(rel, baseLower, body, fail, kustomizeManagedRel) {
|
|
2599
|
+
const kustomizeManaged = kustomizeManagedRel.has(rel)
|
|
2600
|
+
validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeManaged)
|
|
2601
|
+
scanGatewayApiRouteBackendRefsInYamlBody(rel, body, fail)
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
/**
|
|
2605
|
+
* Файл з першим документом **HttpBackendGroup** (ALB Yandex): без modeline **$schema**.
|
|
2606
|
+
* @param {string} rel відносний шлях
|
|
2607
|
+
* @param {string} baseLower basename
|
|
2608
|
+
* @param {string[]} lines рядки файлу
|
|
2609
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
2610
|
+
* @param {(msg: string) => void} pass реєстрація успіху
|
|
2611
|
+
* @param {Set<string>} kustomizeManagedRel kustomize-managed шляхи
|
|
2612
|
+
* @returns {void}
|
|
2613
|
+
*/
|
|
2614
|
+
function checkK8sYamlHttpBackendGroupFile(rel, baseLower, lines, fail, pass, kustomizeManagedRel) {
|
|
2615
|
+
const body = lines.join('\n')
|
|
2616
|
+
scanIngressInYamlDocuments(rel, body, fail)
|
|
2617
|
+
pass(`${rel}: HttpBackendGroup (alb.yc.io/v1alpha1) — modeline $schema не застосовується (k8s.mdc)`)
|
|
2618
|
+
runK8sYamlPolicyAndGatewayScans(rel, baseLower, body, fail, kustomizeManagedRel)
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
/**
|
|
2622
|
+
* Стандартний файл: перший рядок — modeline **$schema**, далі перевірка URL і політики.
|
|
2623
|
+
* @param {string} abs абсолютний шлях
|
|
2624
|
+
* @param {string} rel відносний шлях
|
|
2625
|
+
* @param {string} baseLower basename
|
|
2626
|
+
* @param {string[]} lines рядки файлу
|
|
2627
|
+
* @param {(msg: string) => void} fail реєстрація помилки
|
|
2628
|
+
* @param {(msg: string) => void} pass реєстрація успіху
|
|
2629
|
+
* @param {Set<string>} kustomizeManagedRel kustomize-managed шляхи
|
|
2630
|
+
* @returns {void}
|
|
2631
|
+
*/
|
|
2632
|
+
function checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pass, kustomizeManagedRel) {
|
|
2633
|
+
const match = lines[0].match(MODELINE_RE)
|
|
2634
|
+
if (!match) {
|
|
2635
|
+
fail(`${rel}: некоректний modeline $schema у першому рядку`)
|
|
2636
|
+
return
|
|
2637
|
+
}
|
|
2638
|
+
const schemaUrl = match[1]
|
|
2639
|
+
if (countSchemaModelines(lines) > 1) {
|
|
2640
|
+
fail(`${rel}: кілька рядків yaml-language-server $schema — лиш один modeline на файл (див. k8s.mdc)`)
|
|
2641
|
+
return
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
const body = yamlBodyAfterModeline(lines)
|
|
2645
|
+
|
|
2646
|
+
scanIngressInYamlDocuments(rel, body, fail)
|
|
2647
|
+
|
|
2648
|
+
if (schemaUrl.startsWith('file:')) {
|
|
2649
|
+
pass(`${rel}: локальна схема (file:) — перевірка URL за apiVersion/kind пропущена`)
|
|
2650
|
+
} else if (HTTPS_SCHEMA_RE.test(schemaUrl)) {
|
|
2651
|
+
const doc = firstYamlDocument(body)
|
|
2652
|
+
const { expected, reason } = expectedSchemaUrl(abs, doc)
|
|
2653
|
+
|
|
2654
|
+
if (expected === null) {
|
|
2655
|
+
fail(`${rel}: ${reason}`)
|
|
2656
|
+
return
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
if (schemaUrl !== expected) {
|
|
2660
|
+
fail(`${rel}: $schema не відповідає правилу (${reason}). Очікується:\n ${expected}\n Зараз: ${schemaUrl}`)
|
|
2661
|
+
return
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
pass(`${rel}: $schema узгоджено (${reason})`)
|
|
2665
|
+
} else {
|
|
2666
|
+
fail(`${rel}: $schema має бути https URL або file: (див. k8s.mdc)`)
|
|
2667
|
+
return
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
runK8sYamlPolicyAndGatewayScans(rel, baseLower, body, fail, kustomizeManagedRel)
|
|
2150
2671
|
}
|
|
2151
2672
|
|
|
2152
2673
|
/**
|
|
@@ -2199,12 +2720,7 @@ async function checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel) {
|
|
|
2199
2720
|
)
|
|
2200
2721
|
return
|
|
2201
2722
|
}
|
|
2202
|
-
|
|
2203
|
-
scanIngressInYamlDocuments(rel, body, fail)
|
|
2204
|
-
pass(`${rel}: HttpBackendGroup (alb.yc.io/v1alpha1) — modeline $schema не застосовується (k8s.mdc)`)
|
|
2205
|
-
const kustomizeManaged = kustomizeManagedRel.has(rel)
|
|
2206
|
-
validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeManaged)
|
|
2207
|
-
scanGatewayApiRouteBackendRefsInYamlBody(rel, body, fail)
|
|
2723
|
+
checkK8sYamlHttpBackendGroupFile(rel, baseLower, lines, fail, pass, kustomizeManagedRel)
|
|
2208
2724
|
return
|
|
2209
2725
|
}
|
|
2210
2726
|
|
|
@@ -2213,43 +2729,7 @@ async function checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel) {
|
|
|
2213
2729
|
return
|
|
2214
2730
|
}
|
|
2215
2731
|
|
|
2216
|
-
|
|
2217
|
-
const schemaUrl = m[1]
|
|
2218
|
-
if (countSchemaModelines(lines) > 1) {
|
|
2219
|
-
fail(`${rel}: кілька рядків yaml-language-server $schema — лиш один modeline на файл (див. k8s.mdc)`)
|
|
2220
|
-
return
|
|
2221
|
-
}
|
|
2222
|
-
|
|
2223
|
-
const body = yamlBodyAfterModeline(lines)
|
|
2224
|
-
|
|
2225
|
-
scanIngressInYamlDocuments(rel, body, fail)
|
|
2226
|
-
|
|
2227
|
-
if (schemaUrl.startsWith('file:')) {
|
|
2228
|
-
pass(`${rel}: локальна схема (file:) — перевірка URL за apiVersion/kind пропущена`)
|
|
2229
|
-
} else if (/^https:/iu.test(schemaUrl)) {
|
|
2230
|
-
const doc = firstYamlDocument(body)
|
|
2231
|
-
const { expected, reason } = expectedSchemaUrl(abs, doc)
|
|
2232
|
-
|
|
2233
|
-
if (expected === null) {
|
|
2234
|
-
fail(`${rel}: ${reason}`)
|
|
2235
|
-
return
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
if (schemaUrl !== expected) {
|
|
2239
|
-
fail(`${rel}: $schema не відповідає правилу (${reason}). Очікується:\n ${expected}\n Зараз: ${schemaUrl}`)
|
|
2240
|
-
return
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
pass(`${rel}: $schema узгоджено (${reason})`)
|
|
2244
|
-
} else {
|
|
2245
|
-
fail(`${rel}: $schema має бути https URL або file: (див. k8s.mdc)`)
|
|
2246
|
-
return
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
const kustomizeManaged = kustomizeManagedRel.has(rel)
|
|
2250
|
-
validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeManaged)
|
|
2251
|
-
|
|
2252
|
-
scanGatewayApiRouteBackendRefsInYamlBody(rel, body, fail)
|
|
2732
|
+
checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pass, kustomizeManagedRel)
|
|
2253
2733
|
}
|
|
2254
2734
|
|
|
2255
2735
|
/**
|
|
@@ -2268,6 +2748,38 @@ function assertNoForbiddenK8sDevPaths(yamlFiles, root, fail) {
|
|
|
2268
2748
|
}
|
|
2269
2749
|
}
|
|
2270
2750
|
|
|
2751
|
+
/**
|
|
2752
|
+
* Один файл **k8s/base/kustomization.yaml**: непорожній **namespace:**.
|
|
2753
|
+
* @param {string} root корінь репозиторію
|
|
2754
|
+
* @param {string} abs абсолютний шлях до файлу
|
|
2755
|
+
* @param {(msg: string) => void} fail реєстрація порушення
|
|
2756
|
+
* @returns {Promise<void>}
|
|
2757
|
+
*/
|
|
2758
|
+
async function verifyBaseKustomizationNamespaceOnFile(root, abs, fail) {
|
|
2759
|
+
const rel = relative(root, abs).replaceAll('\\', '/')
|
|
2760
|
+
try {
|
|
2761
|
+
const raw = await readFile(abs, 'utf8')
|
|
2762
|
+
const lines = toLines(raw)
|
|
2763
|
+
const body = yamlBodyAfterModeline(lines)
|
|
2764
|
+
/** @type {import('yaml').Document[] | undefined} */
|
|
2765
|
+
let docs
|
|
2766
|
+
try {
|
|
2767
|
+
docs = parseAllDocuments(body)
|
|
2768
|
+
} catch {
|
|
2769
|
+
fail(`${rel}: не вдалося розпарсити YAML для перевірки namespace у base (див. k8s.mdc)`)
|
|
2770
|
+
return
|
|
2771
|
+
}
|
|
2772
|
+
const first = docs[0]?.toJSON()
|
|
2773
|
+
const v = baseKustomizationNamespaceViolation(first)
|
|
2774
|
+
if (v) {
|
|
2775
|
+
fail(`${rel}: ${v}`)
|
|
2776
|
+
}
|
|
2777
|
+
} catch (error) {
|
|
2778
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
2779
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2271
2783
|
/**
|
|
2272
2784
|
* Якщо є **`k8s/base/kustomization.yaml`**, у ньому **завжди** має бути непорожній **`namespace:`**.
|
|
2273
2785
|
* @param {string} root корінь репозиторію
|
|
@@ -2279,28 +2791,7 @@ async function ensureBaseKustomizationHasNamespace(root, yamlFiles, fail) {
|
|
|
2279
2791
|
for (const abs of yamlFiles) {
|
|
2280
2792
|
const rel = relative(root, abs).replaceAll('\\', '/')
|
|
2281
2793
|
if (isBaseKustomizationPath(rel)) {
|
|
2282
|
-
|
|
2283
|
-
const raw = await readFile(abs, 'utf8')
|
|
2284
|
-
const lines = toLines(raw)
|
|
2285
|
-
const body = yamlBodyAfterModeline(lines)
|
|
2286
|
-
/** @type {import('yaml').Document[] | undefined} */
|
|
2287
|
-
let docs
|
|
2288
|
-
try {
|
|
2289
|
-
docs = parseAllDocuments(body)
|
|
2290
|
-
} catch {
|
|
2291
|
-
fail(`${rel}: не вдалося розпарсити YAML для перевірки namespace у base (див. k8s.mdc)`)
|
|
2292
|
-
}
|
|
2293
|
-
if (docs !== undefined) {
|
|
2294
|
-
const first = docs[0]?.toJSON()
|
|
2295
|
-
const v = baseKustomizationNamespaceViolation(first)
|
|
2296
|
-
if (v) {
|
|
2297
|
-
fail(`${rel}: ${v}`)
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
} catch (error) {
|
|
2301
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
2302
|
-
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
2303
|
-
}
|
|
2794
|
+
await verifyBaseKustomizationNamespaceOnFile(root, abs, fail)
|
|
2304
2795
|
}
|
|
2305
2796
|
}
|
|
2306
2797
|
}
|