@nitra/cursor 1.8.61 → 1.8.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mdc/k8s.mdc +9 -1
- package/package.json +1 -1
- package/scripts/check-k8s.mjs +415 -3
package/mdc/k8s.mdc
CHANGED
|
@@ -117,6 +117,14 @@ resources: {}
|
|
|
117
117
|
|
|
118
118
|
Вони потрібні для інтеграції з **Ingress** / класичним балансуванням GKE; після переходу на **Gateway API** їх слід **прибрати** з маніфестів. **`check k8s`** завершиться помилкою, якщо хоча б один із цих ключів присутній.
|
|
119
119
|
|
|
120
|
+
## Service: `svc.yaml` і `svc-hl.yaml` (Gateway API)
|
|
121
|
+
|
|
122
|
+
- У тому самому каталозі, де лежить **`svc.yaml`**, обов’язково має бути **`svc-hl.yaml`**. Якщо файлу ще немає — створи його як копію **`svc.yaml`**: для кожного **Service** задай **`metadata.name`** з суфіксом **`-hl`**, **`spec.clusterIP: None`** (headless), збережи селектор і порти як у кластерного сервісу; **`check k8s`** не генерує файли автоматично.
|
|
123
|
+
- У **`svc.yaml`** кожен **Service** має мати **`spec.type: ClusterIP`**.
|
|
124
|
+
- У **`svc-hl.yaml`** кожен **Service** має мати **`spec.clusterIP: None`** та **`metadata.name`**, що закінчується на **`-hl`**, узгоджено з відповідним ім’ям у **`svc.yaml`** (наприклад **`app`** → **`app-hl`**).
|
|
125
|
+
- У маршрутах **Gateway API** з групи **`gateway.networking.k8s.io`** (**`HTTPRoute`**, **`GRPCRoute`**, **`TCPRoute`**, **`TLSRoute`**, **`UDPRoute`**) посилання **`backendRefs`** / **`backendRef`** на **Service** мають вказувати лише сервіси з цього headless-файлу (ім’я з суфіксом **`-hl`**).
|
|
126
|
+
- Якщо **`svc.yaml`** підключено через **`kustomization.yaml`** (**`resources`**, **`bases`**, **`components`**, **`crds`**, **`patches[].path`**, **`patchesStrategicMerge`**), у **тому ж** **`kustomization.yaml`** додай посилання на відповідний **`svc-hl.yaml`** (той самий відносний префікс каталогу, що й для **`svc.yaml`**).
|
|
127
|
+
|
|
120
128
|
## Kustomize: структура каталогів (`base` / overlays)
|
|
121
129
|
|
|
122
130
|
Трансформуй дерева **`**/k8s`**, щоб **винести спільне** через [Kustomize](https://kustomize.io/): один канонічний **`base`** і тонкі **overlays** для інших середовищ.
|
|
@@ -184,7 +192,7 @@ resources: {}
|
|
|
184
192
|
|
|
185
193
|
Не дублюй тут повний перелік — він у **верхньому JSDoc** **`npm/scripts/check-k8s.mjs`** і в константах (**`YANNH_PIN`**, **`YANNH_GROUPS`**, **`EXPLICIT_K8S_SCHEMAS`**, **`CLUSTER_SCOPED_KINDS`**, **`HASURA_GRAPHQL_ENGINE_IMAGE`** тощо).
|
|
186
194
|
|
|
187
|
-
Коротко: **`$schema`** (один modeline, без **`.yml`** під **`k8s`**), заборона **Ingress**, **Deployment.resources**, пін **hasura/graphql-engine**, **Service** без анотацій NEG/backend-config, **`metadata.namespace`** залежно від **base** і графа Kustomize, заборона **`…/k8s/dev/…`**, непорожній **`namespace:`** у **`k8s/base/kustomization.yaml`**, видалення файлів, де всі документи — лише **BackendConfig** (змішані файли — помилка).
|
|
195
|
+
Коротко: **`$schema`** (один modeline, без **`.yml`** під **`k8s`**), заборона **Ingress**, **Deployment.resources**, пін **hasura/graphql-engine**, **Service** без анотацій NEG/backend-config, пари **`svc.yaml`** / **`svc-hl.yaml`** (**ClusterIP** / headless **`-hl`**) і їхні записи в **`kustomization.yaml`**, **Gateway API** маршрути — **backendRef** лише на **`-hl`**, **`metadata.namespace`** залежно від **base** і графа Kustomize, заборона **`…/k8s/dev/…`**, непорожній **`namespace:`** у **`k8s/base/kustomization.yaml`**, видалення файлів, де всі документи — лише **BackendConfig** (змішані файли — помилка).
|
|
188
196
|
|
|
189
197
|
При зміні **PIN** версії Kubernetes оновлюй узгоджено **`check-k8s.mjs`**, **`run-k8s.mjs`** (**`KUBERNETES_VERSION`**, **`DATREE_CRD_SCHEMA_LOCATION`**) і розділи **lint-k8s** / **Визначення схеми YAML** у цьому файлі (**`YANNH_REF`**, **`YANNH_GROUPS`**, **`DATREE_CRD_BASE`** — за потреби в скрипті).
|
|
190
198
|
|
package/package.json
CHANGED
package/scripts/check-k8s.mjs
CHANGED
|
@@ -27,6 +27,16 @@
|
|
|
27
27
|
* У **`kind: Service`** у **`metadata.annotations`** не повинно бути ключів **`cloud.google.com/neg`**
|
|
28
28
|
* та **`cloud.google.com/backend-config`** (див. k8s.mdc).
|
|
29
29
|
*
|
|
30
|
+
* Файли **`svc.yaml`** / **`svc-hl.yaml`** у **одному каталозі** (див. k8s.mdc): для кожного **`svc.yaml`**
|
|
31
|
+
* поруч обов’язковий **`svc-hl.yaml`** (headless-копія: той самий селектор/порти, **`metadata.name`** з суфіксом **`-hl`**,
|
|
32
|
+
* **`spec.clusterIP: None`**). У **`svc.yaml`** кожен **Service** має **`spec.type: ClusterIP`**. У **`svc-hl.yaml`**
|
|
33
|
+
* кожен **Service** — **`spec.clusterIP: None`** та ім’я на **`-hl`**. У маршрутах **Gateway API**
|
|
34
|
+
* (**`HTTPRoute`**, **`GRPCRoute`**, **`TCPRoute`**, **`TLSRoute`**, **`UDPRoute`**, група **`gateway.networking.k8s.io`**)
|
|
35
|
+
* посилання **`backendRefs` / `backendRef`** на **Service** мають вказувати лише сервіси з суфіксом **`-hl`** у **`name`**.
|
|
36
|
+
* Якщо **`kustomization.yaml`** посилається на **`svc.yaml`** (**`resources`**, **`bases`**, **`components`**, **`crds`**,
|
|
37
|
+
* **`patches[].path`**, **`patchesStrategicMerge`**), у **тому ж** файлі має бути посилання на відповідний **`svc-hl.yaml`**
|
|
38
|
+
* в **тому ж каталозі**, що й **`svc.yaml`** (логіка збігається з **`pathsFromKustomizationObject`**).
|
|
39
|
+
*
|
|
30
40
|
* Структура **Kustomize** (див. k8s.mdc): заборона шляхів **`…/k8s/dev/…`**; у **`k8s/base/kustomization.yaml`**
|
|
31
41
|
* завжди має бути непорожнє поле **`namespace:`** (перевірка, якщо файл існує).
|
|
32
42
|
*
|
|
@@ -296,6 +306,79 @@ function pathsFromKustomizationObject(obj) {
|
|
|
296
306
|
return out
|
|
297
307
|
}
|
|
298
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Чи для кожного посилання kustomization на файл **`svc.yaml`** у списку є посилання на sibling **`svc-hl.yaml`**
|
|
311
|
+
* (той самий каталог після **`resolve`** відносно каталогу **`kustomization.yaml`**).
|
|
312
|
+
* @param {string} kustomizationDir абсолютний шлях до каталогу з **`kustomization.yaml`**
|
|
313
|
+
* @param {string[]} pathRefs рядки з **`pathsFromKustomizationObject`**
|
|
314
|
+
* @returns {string | null} текст порушення або null, якщо ок
|
|
315
|
+
*/
|
|
316
|
+
export function kustomizationSvcYamlMissingSvcHlViolation(kustomizationDir, pathRefs) {
|
|
317
|
+
/** @type {Set<string>} */
|
|
318
|
+
const resolved = new Set()
|
|
319
|
+
for (const ref of pathRefs) {
|
|
320
|
+
if (typeof ref === 'string' && !ref.includes('://')) {
|
|
321
|
+
resolved.add(resolve(kustomizationDir, ref))
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
for (const ref of pathRefs) {
|
|
325
|
+
if (typeof ref === 'string' && !ref.includes('://')) {
|
|
326
|
+
const abs = resolve(kustomizationDir, ref)
|
|
327
|
+
if (basename(abs).toLowerCase() === 'svc.yaml') {
|
|
328
|
+
const hlAbs = resolve(dirname(abs), 'svc-hl.yaml')
|
|
329
|
+
if (!resolved.has(hlAbs)) {
|
|
330
|
+
return `kustomization посилається на «${ref}» — додай у тому ж kustomization.yaml посилання на відповідний svc-hl.yaml (очікуваний шлях поруч, напр. той самий префікс каталогу + svc-hl.yaml; див. k8s.mdc)`
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return null
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Перевіряє всі **`kustomization.yaml`** під **`k8s`**: разом із **`svc.yaml`** має бути **`svc-hl.yaml`** у полях шляхів.
|
|
340
|
+
* @param {string} root корінь репозиторію
|
|
341
|
+
* @param {string[]} yamlFiles абсолютні шляхи до yaml під k8s
|
|
342
|
+
* @param {(msg: string) => void} fail callback помилки
|
|
343
|
+
* @returns {Promise<void>}
|
|
344
|
+
*/
|
|
345
|
+
async function validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail) {
|
|
346
|
+
for (const kustAbs of yamlFiles) {
|
|
347
|
+
if (basename(kustAbs).toLowerCase() === 'kustomization.yaml') {
|
|
348
|
+
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
349
|
+
let raw
|
|
350
|
+
try {
|
|
351
|
+
raw = await readFile(kustAbs, 'utf8')
|
|
352
|
+
} catch (error) {
|
|
353
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
354
|
+
fail(`${rel}: не вдалося прочитати для перевірки svc.yaml/svc-hl.yaml у kustomization (${msg})`)
|
|
355
|
+
}
|
|
356
|
+
if (raw !== undefined) {
|
|
357
|
+
const lines = toLines(raw)
|
|
358
|
+
const body = yamlBodyAfterModeline(lines)
|
|
359
|
+
/** @type {import('yaml').Document[] | undefined} */
|
|
360
|
+
let docs
|
|
361
|
+
try {
|
|
362
|
+
docs = parseAllDocuments(body)
|
|
363
|
+
} catch {
|
|
364
|
+
fail(`${rel}: не вдалося розпарсити YAML для перевірки svc.yaml/svc-hl.yaml у kustomization (див. k8s.mdc)`)
|
|
365
|
+
}
|
|
366
|
+
if (docs !== undefined) {
|
|
367
|
+
const first = docs[0]?.toJSON()
|
|
368
|
+
if (first !== null && first !== undefined && typeof first === 'object' && !Array.isArray(first)) {
|
|
369
|
+
const pathRefs = pathsFromKustomizationObject(first)
|
|
370
|
+
const kustDir = dirname(kustAbs)
|
|
371
|
+
const v = kustomizationSvcYamlMissingSvcHlViolation(kustDir, pathRefs)
|
|
372
|
+
if (v !== null) {
|
|
373
|
+
fail(`${rel}: ${v}`)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
299
382
|
/**
|
|
300
383
|
* Збирає відносні шляхи (posix) до YAML, підключених до Kustomize з будь-якого **`kustomization.yaml`** під `k8s`.
|
|
301
384
|
* Обходить **`resources`**, **`bases`**, **`components`**, **`crds`**, **`patches[].path`**, **`patchesStrategicMerge`**;
|
|
@@ -439,7 +522,6 @@ function k8sYamlBodyForDocumentParse(lines) {
|
|
|
439
522
|
|
|
440
523
|
/**
|
|
441
524
|
* Чи всі нетривіальні документи у тілі — **`kind: BackendConfig`**, чи є змішування з іншими kind.
|
|
442
|
-
*
|
|
443
525
|
* @param {string} body YAML без обов’язкового modeline (див. `k8sYamlBodyForDocumentParse`)
|
|
444
526
|
* @returns {'none' | 'only' | 'mixed' | 'unparsed'} unparsed — не вдалося розпарсити YAML
|
|
445
527
|
*/
|
|
@@ -475,7 +557,6 @@ export function classifyBackendConfigManifestPresence(body) {
|
|
|
475
557
|
|
|
476
558
|
/**
|
|
477
559
|
* Видаляє під **`k8s`** YAML-файли, що містять **лише** ресурси **BackendConfig**; змішані файли — `fail`.
|
|
478
|
-
*
|
|
479
560
|
* @param {string} root корінь репозиторію
|
|
480
561
|
* @param {(msg: string) => void} fail реєстрація порушення
|
|
481
562
|
* @param {(msg: string) => void} pass реєстрація успіху
|
|
@@ -530,6 +611,37 @@ function yamlBodyAfterModeline(lines) {
|
|
|
530
611
|
return lines.slice(i).join('\n')
|
|
531
612
|
}
|
|
532
613
|
|
|
614
|
+
/**
|
|
615
|
+
* Читає k8s YAML і повертає фрагмент після modeline `$schema`, якщо перший рядок — modeline.
|
|
616
|
+
* Потрібно для парної перевірки **`svc.yaml`** / **`svc-hl.yaml`**.
|
|
617
|
+
* @param {string} abs абсолютний шлях до файлу
|
|
618
|
+
* @returns {Promise<string>} тіло для `parseAllDocuments`
|
|
619
|
+
*/
|
|
620
|
+
async function readK8sYamlBodyAfterModelineForSvcPair(abs) {
|
|
621
|
+
const raw = await readFile(abs, 'utf8')
|
|
622
|
+
const lines = toLines(raw)
|
|
623
|
+
if (lines.length > 0 && MODELINE_RE.test(lines[0])) {
|
|
624
|
+
return yamlBodyAfterModeline(lines)
|
|
625
|
+
}
|
|
626
|
+
return lines.join('\n')
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Розбирає YAML на корені документів-об’єктів (ігнорує зламані документи).
|
|
631
|
+
* @param {string} body фрагмент YAML
|
|
632
|
+
* @returns {unknown[]} масив об’єктів-документів
|
|
633
|
+
*/
|
|
634
|
+
function parseK8sYamlDocumentObjectRoots(body) {
|
|
635
|
+
try {
|
|
636
|
+
return parseAllDocuments(body)
|
|
637
|
+
.filter(d => d.errors.length === 0)
|
|
638
|
+
.map(d => d.toJSON())
|
|
639
|
+
.filter(x => x !== null && x !== undefined && typeof x === 'object' && !Array.isArray(x))
|
|
640
|
+
} catch {
|
|
641
|
+
return []
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
533
645
|
/**
|
|
534
646
|
* Перший YAML-документ (до наступного `---` на окремому рядку).
|
|
535
647
|
* @param {string} body фрагмент YAML
|
|
@@ -747,6 +859,287 @@ export function serviceForbiddenGcpAnnotationsViolation(manifest) {
|
|
|
747
859
|
return `metadata.annotations: прибери заборонені ключі GKE: ${found.join(', ')} (див. k8s.mdc)`
|
|
748
860
|
}
|
|
749
861
|
|
|
862
|
+
/** Суфікс **`metadata.name`** headless-сервісу поруч із **`svc.yaml`** (див. k8s.mdc). */
|
|
863
|
+
const SVC_HL_NAME_SUFFIX = '-hl'
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Kind маршрутів Gateway API, у **`spec`** яких шукаємо **`backendRefs`** / **`backendRef`** до **Service**.
|
|
867
|
+
* @type {Set<string>}
|
|
868
|
+
*/
|
|
869
|
+
const GATEWAY_API_ROUTE_KINDS = new Set(['HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute', 'UDPRoute'])
|
|
870
|
+
|
|
871
|
+
/** Префікс **`apiVersion`** стандартних ресурсів Gateway API. */
|
|
872
|
+
const GATEWAY_API_GROUP_PREFIX = 'gateway.networking.k8s.io/'
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Чи **Service** у **`svc.yaml`** має **`spec.type: ClusterIP`** (k8s.mdc).
|
|
876
|
+
* @param {unknown} manifest корінь YAML-документа
|
|
877
|
+
* @returns {string | null} текст порушення або null
|
|
878
|
+
*/
|
|
879
|
+
export function serviceSvcYamlClusterIpTypeViolation(manifest) {
|
|
880
|
+
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
881
|
+
return null
|
|
882
|
+
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
883
|
+
if (rec.kind !== 'Service') return null
|
|
884
|
+
const spec = rec.spec
|
|
885
|
+
if (spec === null || spec === undefined || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
886
|
+
return 'Service: додай spec.type: ClusterIP (svc.yaml, див. k8s.mdc)'
|
|
887
|
+
}
|
|
888
|
+
const s = /** @type {Record<string, unknown>} */ (spec)
|
|
889
|
+
if (s.type !== 'ClusterIP') {
|
|
890
|
+
const cur = s.type === undefined ? 'відсутнє' : String(s.type)
|
|
891
|
+
return `Service spec.type має бути ClusterIP (svc.yaml; зараз: ${cur}; див. k8s.mdc)`
|
|
892
|
+
}
|
|
893
|
+
return null
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Чи **Service** у **`svc-hl.yaml`** headless (**`spec.clusterIP: None`**) з суфіксом **`-hl`** у **`metadata.name`**.
|
|
898
|
+
* @param {unknown} manifest корінь YAML-документа
|
|
899
|
+
* @returns {string | null} текст порушення або null
|
|
900
|
+
*/
|
|
901
|
+
export function serviceSvcHlYamlHeadlessViolation(manifest) {
|
|
902
|
+
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
903
|
+
return null
|
|
904
|
+
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
905
|
+
if (rec.kind !== 'Service') return null
|
|
906
|
+
const meta = rec.metadata
|
|
907
|
+
if (meta === null || meta === undefined || typeof meta !== 'object' || Array.isArray(meta)) {
|
|
908
|
+
return 'Service: потрібні metadata.name з суфіксом -hl (svc-hl.yaml, див. k8s.mdc)'
|
|
909
|
+
}
|
|
910
|
+
const m = /** @type {Record<string, unknown>} */ (meta)
|
|
911
|
+
const n = m.name
|
|
912
|
+
if (typeof n !== 'string' || !n.endsWith(SVC_HL_NAME_SUFFIX)) {
|
|
913
|
+
return `Service metadata.name має закінчуватися на «${SVC_HL_NAME_SUFFIX}» (svc-hl.yaml; див. k8s.mdc)`
|
|
914
|
+
}
|
|
915
|
+
const spec = rec.spec
|
|
916
|
+
if (spec === null || spec === undefined || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
917
|
+
return 'Service: додай spec.clusterIP: None (svc-hl.yaml, див. k8s.mdc)'
|
|
918
|
+
}
|
|
919
|
+
const s = /** @type {Record<string, unknown>} */ (spec)
|
|
920
|
+
if (s.clusterIP !== 'None') {
|
|
921
|
+
const cur = s.clusterIP === undefined ? 'відсутнє' : String(s.clusterIP)
|
|
922
|
+
return `Service spec.clusterIP має бути None (headless, svc-hl.yaml; зараз: ${cur}; див. k8s.mdc)`
|
|
923
|
+
}
|
|
924
|
+
return null
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Чи об’єкт схожий на **backendRef** до **Kubernetes Service** у Gateway API.
|
|
929
|
+
* @param {unknown} obj вузол у дереві **`spec`**
|
|
930
|
+
* @returns {boolean} true, якщо враховуємо поле **`name`** як посилання на Service
|
|
931
|
+
*/
|
|
932
|
+
function isGatewayApiBackendRefToService(obj) {
|
|
933
|
+
if (obj === null || obj === undefined || typeof obj !== 'object' || Array.isArray(obj)) return false
|
|
934
|
+
const o = /** @type {Record<string, unknown>} */ (obj)
|
|
935
|
+
if (typeof o.name !== 'string') return false
|
|
936
|
+
const kind = o.kind
|
|
937
|
+
if (kind !== undefined && kind !== 'Service') return false
|
|
938
|
+
const group = o.group
|
|
939
|
+
if (typeof group === 'string' && group !== '' && group !== 'core') return false
|
|
940
|
+
return true
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Збирає імена **Service** з **`backendRefs`** / **`backendRef`** у піддереві **`spec`** маршруту Gateway API.
|
|
945
|
+
* @param {unknown} spec значення **`spec`** маршруту
|
|
946
|
+
* @returns {string[]} імена backend-сервісів (можливі дублікати)
|
|
947
|
+
*/
|
|
948
|
+
export function collectGatewayApiRouteBackendServiceNames(spec) {
|
|
949
|
+
/** @type {string[]} */
|
|
950
|
+
const out = []
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* @param {unknown} node вузол для обходу
|
|
954
|
+
* @returns {void}
|
|
955
|
+
*/
|
|
956
|
+
function walk(node) {
|
|
957
|
+
if (node === null || node === undefined) return
|
|
958
|
+
if (Array.isArray(node)) {
|
|
959
|
+
for (const x of node) {
|
|
960
|
+
walk(x)
|
|
961
|
+
}
|
|
962
|
+
return
|
|
963
|
+
}
|
|
964
|
+
if (typeof node !== 'object') return
|
|
965
|
+
if (isGatewayApiBackendRefToService(node)) {
|
|
966
|
+
out.push(String(/** @type {Record<string, unknown>} */ (node).name))
|
|
967
|
+
}
|
|
968
|
+
for (const v of Object.values(node)) {
|
|
969
|
+
walk(v)
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
walk(spec)
|
|
974
|
+
return out
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Реєструє порушення: маршрути Gateway API мають посилатися на **Service** з суфіксом **`-hl`**.
|
|
979
|
+
* @param {string} rel відносний шлях до файлу
|
|
980
|
+
* @param {string} body YAML після modeline
|
|
981
|
+
* @param {(msg: string) => void} fail callback помилки
|
|
982
|
+
* @returns {void}
|
|
983
|
+
*/
|
|
984
|
+
function scanGatewayApiRouteBackendRefsInYamlBody(rel, body, fail) {
|
|
985
|
+
/** @type {import('yaml').Document[]} */
|
|
986
|
+
let docs
|
|
987
|
+
try {
|
|
988
|
+
docs = parseAllDocuments(body)
|
|
989
|
+
} catch {
|
|
990
|
+
return
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
for (const [di, doc] of docs.entries()) {
|
|
994
|
+
if (doc.errors.length === 0) {
|
|
995
|
+
const obj = doc.toJSON()
|
|
996
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
997
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
998
|
+
const av = rec.apiVersion
|
|
999
|
+
const kind = rec.kind
|
|
1000
|
+
if (
|
|
1001
|
+
typeof av === 'string' &&
|
|
1002
|
+
av.startsWith(GATEWAY_API_GROUP_PREFIX) &&
|
|
1003
|
+
typeof kind === 'string' &&
|
|
1004
|
+
GATEWAY_API_ROUTE_KINDS.has(kind)
|
|
1005
|
+
) {
|
|
1006
|
+
const names = collectGatewayApiRouteBackendServiceNames(rec.spec)
|
|
1007
|
+
for (const svcName of names) {
|
|
1008
|
+
if (!svcName.endsWith(SVC_HL_NAME_SUFFIX)) {
|
|
1009
|
+
fail(
|
|
1010
|
+
`${rel}: Gateway API ${kind} (документ ${di + 1}): backendRef до Service має вказувати headless-сервіс з суфіксом «${SVC_HL_NAME_SUFFIX}» у name (зараз: «${svcName}»; див. k8s.mdc)`
|
|
1011
|
+
)
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Перевіряє пари **`svc.yaml`** / **`svc-hl.yaml`** у каталозі (наявність, узгоджені імена **Service**).
|
|
1022
|
+
* @param {string} root корінь репозиторію
|
|
1023
|
+
* @param {string[]} yamlFiles абсолютні шляхи до `*.yaml` під `k8s`
|
|
1024
|
+
* @param {(msg: string) => void} fail callback помилки
|
|
1025
|
+
* @returns {Promise<void>}
|
|
1026
|
+
*/
|
|
1027
|
+
async function validateSvcYamlAndSvcHlPairs(root, yamlFiles, fail) {
|
|
1028
|
+
const absSet = new Set(yamlFiles)
|
|
1029
|
+
|
|
1030
|
+
for (const abs of yamlFiles) {
|
|
1031
|
+
if (basename(abs).toLowerCase() === 'svc-hl.yaml') {
|
|
1032
|
+
const svcAbs = join(dirname(abs), 'svc.yaml')
|
|
1033
|
+
if (!absSet.has(svcAbs)) {
|
|
1034
|
+
const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
|
|
1035
|
+
fail(`${rel}: svc-hl.yaml потребує svc.yaml у тому самому каталозі (див. k8s.mdc)`)
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
for (const svcAbs of yamlFiles) {
|
|
1041
|
+
if (basename(svcAbs).toLowerCase() === 'svc.yaml') {
|
|
1042
|
+
const rel = (relative(root, svcAbs) || svcAbs).replaceAll('\\', '/')
|
|
1043
|
+
const hlAbs = join(dirname(svcAbs), 'svc-hl.yaml')
|
|
1044
|
+
if (absSet.has(hlAbs)) {
|
|
1045
|
+
/** @type {string | undefined} */
|
|
1046
|
+
let svcBody
|
|
1047
|
+
/** @type {string | undefined} */
|
|
1048
|
+
let hlBody
|
|
1049
|
+
try {
|
|
1050
|
+
svcBody = await readK8sYamlBodyAfterModelineForSvcPair(svcAbs)
|
|
1051
|
+
hlBody = await readK8sYamlBodyAfterModelineForSvcPair(hlAbs)
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
1054
|
+
fail(`${rel}: не вдалося прочитати svc.yaml / svc-hl.yaml (${msg})`)
|
|
1055
|
+
}
|
|
1056
|
+
if (svcBody !== undefined && hlBody !== undefined) {
|
|
1057
|
+
const svcRoots = parseK8sYamlDocumentObjectRoots(svcBody)
|
|
1058
|
+
const hlRoots = parseK8sYamlDocumentObjectRoots(hlBody)
|
|
1059
|
+
|
|
1060
|
+
/** @type {string[]} */
|
|
1061
|
+
const svcNames = []
|
|
1062
|
+
for (const [i, rootObj] of svcRoots.entries()) {
|
|
1063
|
+
const r = /** @type {Record<string, unknown>} */ (rootObj)
|
|
1064
|
+
if (r.kind === 'Service') {
|
|
1065
|
+
const meta = r.metadata
|
|
1066
|
+
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
|
|
1067
|
+
const nm = /** @type {Record<string, unknown>} */ (meta).name
|
|
1068
|
+
if (typeof nm === 'string') {
|
|
1069
|
+
svcNames.push(nm)
|
|
1070
|
+
} else {
|
|
1071
|
+
fail(`${rel}: svc.yaml (документ ${i + 1}): Service без metadata.name (див. k8s.mdc)`)
|
|
1072
|
+
}
|
|
1073
|
+
} else {
|
|
1074
|
+
fail(`${rel}: svc.yaml (документ ${i + 1}): Service без metadata (див. k8s.mdc)`)
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (svcNames.length === 0) {
|
|
1080
|
+
fail(`${rel}: svc.yaml має містити принаймні один kind: Service (див. k8s.mdc)`)
|
|
1081
|
+
} else {
|
|
1082
|
+
/** @type {string[]} */
|
|
1083
|
+
const hlNames = []
|
|
1084
|
+
for (const [i, rootObj] of hlRoots.entries()) {
|
|
1085
|
+
const r = /** @type {Record<string, unknown>} */ (rootObj)
|
|
1086
|
+
if (r.kind === 'Service') {
|
|
1087
|
+
const meta = r.metadata
|
|
1088
|
+
if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
|
|
1089
|
+
const nm = /** @type {Record<string, unknown>} */ (meta).name
|
|
1090
|
+
if (typeof nm === 'string') {
|
|
1091
|
+
hlNames.push(nm)
|
|
1092
|
+
} else {
|
|
1093
|
+
const hlRel = (relative(root, hlAbs) || hlAbs).replaceAll('\\', '/')
|
|
1094
|
+
fail(`${hlRel}: svc-hl.yaml (документ ${i + 1}): Service без metadata.name (див. k8s.mdc)`)
|
|
1095
|
+
}
|
|
1096
|
+
} else {
|
|
1097
|
+
const hlRel = (relative(root, hlAbs) || hlAbs).replaceAll('\\', '/')
|
|
1098
|
+
fail(`${hlRel}: svc-hl.yaml (документ ${i + 1}): Service без metadata (див. k8s.mdc)`)
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
if (hlNames.length === 0) {
|
|
1104
|
+
const hlRel = (relative(root, hlAbs) || hlAbs).replaceAll('\\', '/')
|
|
1105
|
+
fail(`${hlRel}: svc-hl.yaml має містити принаймні один kind: Service (див. k8s.mdc)`)
|
|
1106
|
+
} else {
|
|
1107
|
+
const hlSet = new Set(hlNames)
|
|
1108
|
+
for (const n of svcNames) {
|
|
1109
|
+
const expectHl = `${n}${SVC_HL_NAME_SUFFIX}`
|
|
1110
|
+
if (!hlSet.has(expectHl)) {
|
|
1111
|
+
fail(
|
|
1112
|
+
`${rel}: для Service «${n}» у svc.yaml у svc-hl.yaml має бути Service з metadata.name «${expectHl}» (див. k8s.mdc)`
|
|
1113
|
+
)
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
for (const h of hlNames) {
|
|
1118
|
+
if (h.endsWith(SVC_HL_NAME_SUFFIX)) {
|
|
1119
|
+
const base = h.slice(0, -SVC_HL_NAME_SUFFIX.length)
|
|
1120
|
+
if (!svcNames.includes(base)) {
|
|
1121
|
+
const hlRel = (relative(root, hlAbs) || hlAbs).replaceAll('\\', '/')
|
|
1122
|
+
fail(
|
|
1123
|
+
`${hlRel}: Service «${h}» у svc-hl.yaml не відповідає жодному Service у svc.yaml (очікується базове ім’я «${base}»; див. k8s.mdc)`
|
|
1124
|
+
)
|
|
1125
|
+
}
|
|
1126
|
+
} else {
|
|
1127
|
+
const hlRel = (relative(root, hlAbs) || hlAbs).replaceAll('\\', '/')
|
|
1128
|
+
fail(
|
|
1129
|
+
`${hlRel}: Service «${h}» у svc-hl.yaml: metadata.name має закінчуватися на «${SVC_HL_NAME_SUFFIX}» (див. k8s.mdc)`
|
|
1130
|
+
)
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
} else {
|
|
1137
|
+
fail(`${rel}: поруч обов’язковий svc-hl.yaml (headless-копія з суфіксом -hl у metadata.name; див. k8s.mdc)`)
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
750
1143
|
/**
|
|
751
1144
|
* Для маніфестів, **підключених** до Kustomize (шлях у `resources` / `patches` / …), **metadata.namespace** не додають.
|
|
752
1145
|
* @param {unknown} manifest корінь YAML-документа
|
|
@@ -816,7 +1209,8 @@ export function isK8sBaseManifestYamlPath(rel, baseLower) {
|
|
|
816
1209
|
|
|
817
1210
|
/**
|
|
818
1211
|
* Парсить усі YAML-документи: **metadata.namespace**, **Deployment.resources**, **Hasura image pin**,
|
|
819
|
-
* **Service — заборонені GKE
|
|
1212
|
+
* **Service — заборонені GKE-анотації**, **`svc.yaml`** (**`spec.type: ClusterIP`**), **`svc-hl.yaml`**
|
|
1213
|
+
* (**headless**, суфікс **`-hl`** у **`metadata.name`**).
|
|
820
1214
|
* @param {string} rel відносний шлях
|
|
821
1215
|
* @param {string} baseLower basename файлу (нижній регістр)
|
|
822
1216
|
* @param {string} body вміст після modeline
|
|
@@ -872,6 +1266,18 @@ function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeMan
|
|
|
872
1266
|
if (svcGcpV !== null) {
|
|
873
1267
|
fail(`${rel}: Service (документ ${di + 1}): ${svcGcpV}`)
|
|
874
1268
|
}
|
|
1269
|
+
if (baseLower === 'svc.yaml') {
|
|
1270
|
+
const svcT = serviceSvcYamlClusterIpTypeViolation(obj)
|
|
1271
|
+
if (svcT !== null) {
|
|
1272
|
+
fail(`${rel}: Service (документ ${di + 1}): ${svcT}`)
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (baseLower === 'svc-hl.yaml') {
|
|
1276
|
+
const svcH = serviceSvcHlYamlHeadlessViolation(obj)
|
|
1277
|
+
if (svcH !== null) {
|
|
1278
|
+
fail(`${rel}: Service (документ ${di + 1}): ${svcH}`)
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
875
1281
|
}
|
|
876
1282
|
}
|
|
877
1283
|
}
|
|
@@ -1024,6 +1430,8 @@ async function checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel) {
|
|
|
1024
1430
|
|
|
1025
1431
|
const kustomizeManaged = kustomizeManagedRel.has(rel)
|
|
1026
1432
|
validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeManaged)
|
|
1433
|
+
|
|
1434
|
+
scanGatewayApiRouteBackendRefsInYamlBody(rel, body, fail)
|
|
1027
1435
|
}
|
|
1028
1436
|
|
|
1029
1437
|
/**
|
|
@@ -1111,6 +1519,10 @@ export async function check() {
|
|
|
1111
1519
|
await checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel)
|
|
1112
1520
|
}
|
|
1113
1521
|
|
|
1522
|
+
await validateSvcYamlAndSvcHlPairs(root, yamlFiles, fail)
|
|
1523
|
+
|
|
1524
|
+
await validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail)
|
|
1525
|
+
|
|
1114
1526
|
await ensureBaseKustomizationHasNamespace(root, yamlFiles, fail)
|
|
1115
1527
|
|
|
1116
1528
|
return exitCode
|