@nitra/cursor 1.8.116 → 1.8.117
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 +36 -6
- package/package.json +1 -1
- package/scripts/check-k8s.mjs +186 -3
package/mdc/k8s.mdc
CHANGED
|
@@ -298,13 +298,43 @@ data:
|
|
|
298
298
|
|
|
299
299
|
**Dev-like середовища** — сегмент `base`, `dev`, або з суфіксом `-qa` (напр. `tr-qa`):
|
|
300
300
|
|
|
301
|
-
- HPA: `minReplicas
|
|
302
|
-
- PDB: `minAvailable
|
|
301
|
+
- HPA: `minReplicas` — рівно **1**, `maxReplicas` — рівно **1** (у деві не масштабуємо).
|
|
302
|
+
- PDB: `minAvailable` — рівно **0**.
|
|
303
303
|
|
|
304
304
|
**Прод-середовища** — усе інше (будь-який overlay без суфікса `-qa`):
|
|
305
305
|
|
|
306
|
-
- HPA: `minReplicas
|
|
307
|
-
- PDB: `minAvailable
|
|
306
|
+
- HPA: `minReplicas` — мінімум **2**, `maxReplicas` — мінімум **2**.
|
|
307
|
+
- PDB: `minAvailable` — мінімум **1**.
|
|
308
|
+
|
|
309
|
+
### Прод-оверрайди у `kustomization.yaml`
|
|
310
|
+
|
|
311
|
+
Оскільки `base/` (і dev-like середовища) тримають dev-значення (`1`/`1`/`0`), у **кожному** прод-оверлеї `kustomization.yaml` у `patches[]` **обов'язково** має бути перевизначення:
|
|
312
|
+
|
|
313
|
+
- для `HorizontalPodAutoscaler`: `spec.minReplicas` **і** `spec.maxReplicas` (щоб у проді вийшло ≥2).
|
|
314
|
+
- для `PodDisruptionBudget`: `spec.minAvailable` (щоб у проді вийшло ≥1).
|
|
315
|
+
|
|
316
|
+
Формат patch — JSON6902 або Strategic Merge; `check k8s` перевіряє **наявність** оверрайду відповідного поля. Конкретне значення має задовольняти прод-мінімуми — це видно у вмісті patch і остаточно матеріалізується під час збірки Kustomize.
|
|
317
|
+
|
|
318
|
+
```yaml title="k8s/prod/kustomization.yaml (фрагмент)"
|
|
319
|
+
patches:
|
|
320
|
+
- target:
|
|
321
|
+
kind: HorizontalPodAutoscaler
|
|
322
|
+
name: backend-api
|
|
323
|
+
patch: |-
|
|
324
|
+
- op: replace
|
|
325
|
+
path: /spec/minReplicas
|
|
326
|
+
value: 3
|
|
327
|
+
- op: replace
|
|
328
|
+
path: /spec/maxReplicas
|
|
329
|
+
value: 10
|
|
330
|
+
- target:
|
|
331
|
+
kind: PodDisruptionBudget
|
|
332
|
+
name: backend-api
|
|
333
|
+
patch: |-
|
|
334
|
+
- op: replace
|
|
335
|
+
path: /spec/minAvailable
|
|
336
|
+
value: 1
|
|
337
|
+
```
|
|
308
338
|
|
|
309
339
|
### Приклади
|
|
310
340
|
|
|
@@ -319,8 +349,8 @@ spec:
|
|
|
319
349
|
apiVersion: apps/v1
|
|
320
350
|
kind: Deployment
|
|
321
351
|
name: backend-api
|
|
322
|
-
minReplicas: 1 #
|
|
323
|
-
maxReplicas:
|
|
352
|
+
minReplicas: 1 # dev-like; прод-оверлеї перевизначають на ≥ 2
|
|
353
|
+
maxReplicas: 1 # dev-like; прод-оверлеї перевизначають на ≥ 2
|
|
324
354
|
metrics:
|
|
325
355
|
- type: Resource
|
|
326
356
|
resource:
|
package/package.json
CHANGED
package/scripts/check-k8s.mjs
CHANGED
|
@@ -68,10 +68,17 @@
|
|
|
68
68
|
* обов'язкові **`hpa.yaml`** (`autoscaling/v2`, `HorizontalPodAutoscaler`, `scaleTargetRef.name` = ім'я Deployment)
|
|
69
69
|
* і **`pdb.yaml`** (`policy/v1`, `PodDisruptionBudget`, `selector.matchLabels.app` = мітка `app` Deployment).
|
|
70
70
|
* Env-залежні межі за сегментом після `/k8s/`: **dev-like** (`base`, `dev`, `*-qa`) — `minReplicas === 1`,
|
|
71
|
-
* `minAvailable === 0`; **прод** (решта) — `minReplicas >= 2`, `maxReplicas >= 2`,
|
|
72
|
-
* Сам Deployment має мати у `spec.template.spec.topologySpreadConstraints` запис
|
|
71
|
+
* `maxReplicas === 1`, `minAvailable === 0`; **прод** (решта) — `minReplicas >= 2`, `maxReplicas >= 2`,
|
|
72
|
+
* `minAvailable >= 1`. Сам Deployment має мати у `spec.template.spec.topologySpreadConstraints` запис
|
|
73
73
|
* `maxSkew: 1`, `topologyKey: kubernetes.io/hostname`, `whenUnsatisfiable: ScheduleAnyway`,
|
|
74
74
|
* `labelSelector.matchLabels.app` рівне `spec.selector.matchLabels.app` Deployment.
|
|
75
|
+
*
|
|
76
|
+
* **Прод-оверрайди в kustomization.yaml:** для прод-оверлеїв (не dev-like) `kustomization.yaml` у своїх
|
|
77
|
+
* inline `patches[]` повинен змінювати `/spec/minReplicas` і `/spec/maxReplicas` для
|
|
78
|
+
* **HorizontalPodAutoscaler**, і `/spec/minAvailable` для **PodDisruptionBudget** — щоб прод-мінімуми
|
|
79
|
+
* з (`>=2`, `>=2`, `>=1`) не залишалися на dev-значеннях із base. Формат patch — JSON6902 або Strategic Merge;
|
|
80
|
+
* наявність перевіряється через `kustomizationPatchPathsByTargetKind` (конкретне значення — у вмісті patch,
|
|
81
|
+
* яке буде оцінено під час збірки Kustomize).
|
|
75
82
|
*/
|
|
76
83
|
import { existsSync } from 'node:fs'
|
|
77
84
|
import { readFile, readdir, stat, unlink } from 'node:fs/promises'
|
|
@@ -3521,7 +3528,7 @@ export function k8sEnvSegmentFromRelPath(relPath) {
|
|
|
3521
3528
|
export function isDevLikeK8sEnvSegment(segment) {
|
|
3522
3529
|
if (typeof segment !== 'string' || segment === '') return false
|
|
3523
3530
|
if (segment === 'base' || segment === 'dev') return true
|
|
3524
|
-
return
|
|
3531
|
+
return segment.endsWith('-qa')
|
|
3525
3532
|
}
|
|
3526
3533
|
|
|
3527
3534
|
/**
|
|
@@ -3606,6 +3613,7 @@ export function hpaManifestViolations(manifest, expectedDeployName, isDevLike) {
|
|
|
3606
3613
|
}
|
|
3607
3614
|
if (isDevLike) {
|
|
3608
3615
|
if (minR !== null && minR !== 1) errs.push(`spec.minReplicas для dev-like (base/dev/*-qa) має бути 1 (зараз: ${minR})`)
|
|
3616
|
+
if (maxR !== null && maxR !== 1) errs.push(`spec.maxReplicas для dev-like (base/dev/*-qa) має бути 1 (зараз: ${maxR})`)
|
|
3609
3617
|
} else {
|
|
3610
3618
|
if (minR !== null && minR < 2) errs.push(`spec.minReplicas для прод середовища має бути мінімум 2 (зараз: ${minR})`)
|
|
3611
3619
|
if (maxR !== null && maxR < 2) errs.push(`spec.maxReplicas для прод середовища має бути мінімум 2 (зараз: ${maxR})`)
|
|
@@ -3770,6 +3778,179 @@ async function readDocsByKindInDir(dirPath, kind, filenameFilter) {
|
|
|
3770
3778
|
return out
|
|
3771
3779
|
}
|
|
3772
3780
|
|
|
3781
|
+
/**
|
|
3782
|
+
* Збирає шляхи **JSON Pointer**, які змінює один inline `patch` у **`patches[]`** kustomization.yaml.
|
|
3783
|
+
* Підтримка двох форматів:
|
|
3784
|
+
* — **JSON6902** (масив операцій): беремо `path` кожної операції (через `collectJson6902OperationsFromPatchText`).
|
|
3785
|
+
* — **Strategic Merge** (YAML-обʼєкт): плоскі шляхи до всіх листових полів (наприклад
|
|
3786
|
+
* `spec.minReplicas: 2` → `/spec/minReplicas`). Проміжні обʼєкти не вважаються «зміненими» — лише листки.
|
|
3787
|
+
* @param {string} patchText вміст поля `patch`
|
|
3788
|
+
* @returns {Set<string>} шляхи JSON Pointer (наприклад `/spec/minReplicas`)
|
|
3789
|
+
*/
|
|
3790
|
+
export function kustomizePatchModifiedPaths(patchText) {
|
|
3791
|
+
/** @type {Set<string>} */
|
|
3792
|
+
const out = new Set()
|
|
3793
|
+
const t = typeof patchText === 'string' ? patchText.trim() : ''
|
|
3794
|
+
if (t === '') return out
|
|
3795
|
+
const ops = collectJson6902OperationsFromPatchText(patchText)
|
|
3796
|
+
if (ops.length > 0) {
|
|
3797
|
+
for (const { path } of ops) {
|
|
3798
|
+
if (path) out.add(path)
|
|
3799
|
+
}
|
|
3800
|
+
return out
|
|
3801
|
+
}
|
|
3802
|
+
let parsed
|
|
3803
|
+
try {
|
|
3804
|
+
for (const d of parseAllDocuments(t)) {
|
|
3805
|
+
if (d.errors.length === 0) {
|
|
3806
|
+
parsed = d.toJSON()
|
|
3807
|
+
break
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
} catch {
|
|
3811
|
+
return out
|
|
3812
|
+
}
|
|
3813
|
+
if (parsed === null || parsed === undefined || typeof parsed !== 'object' || Array.isArray(parsed)) return out
|
|
3814
|
+
/**
|
|
3815
|
+
* Рекурсивний обхід: шлях додаємо лише для листків (скаляр / масив).
|
|
3816
|
+
* @param {Record<string, unknown>} obj
|
|
3817
|
+
* @param {string} prefix
|
|
3818
|
+
*/
|
|
3819
|
+
const walk = (obj, prefix) => {
|
|
3820
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
3821
|
+
const p = `${prefix}/${k}`
|
|
3822
|
+
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
3823
|
+
walk(/** @type {Record<string, unknown>} */ (v), p)
|
|
3824
|
+
} else {
|
|
3825
|
+
out.add(p)
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
walk(/** @type {Record<string, unknown>} */ (parsed), '')
|
|
3830
|
+
return out
|
|
3831
|
+
}
|
|
3832
|
+
|
|
3833
|
+
/**
|
|
3834
|
+
* Читає `kind` з inline **`patch`** у форматі Strategic Merge (для випадків, коли **`target.kind`** не заданий).
|
|
3835
|
+
* @param {string} patchText вміст поля `patch`
|
|
3836
|
+
* @returns {string | null} значення `kind` першого документа або null
|
|
3837
|
+
*/
|
|
3838
|
+
function strategicMergePatchKind(patchText) {
|
|
3839
|
+
const t = typeof patchText === 'string' ? patchText.trim() : ''
|
|
3840
|
+
if (t === '') return null
|
|
3841
|
+
try {
|
|
3842
|
+
for (const d of parseAllDocuments(t)) {
|
|
3843
|
+
if (d.errors.length === 0) {
|
|
3844
|
+
const obj = d.toJSON()
|
|
3845
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
3846
|
+
const k = /** @type {Record<string, unknown>} */ (obj).kind
|
|
3847
|
+
if (typeof k === 'string' && k !== '') return k
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
} catch {
|
|
3852
|
+
return null
|
|
3853
|
+
}
|
|
3854
|
+
return null
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3857
|
+
/**
|
|
3858
|
+
* Збирає шляхи, змінені всіма inline `patches[]` у kustomization, згрупованими за `kind` цілі.
|
|
3859
|
+
* `kind` визначається з `target.kind` (канон) або, якщо відсутній — з `kind:` у тілі Strategic Merge patch.
|
|
3860
|
+
* @param {Record<string, unknown>} kust об'єкт kustomization.yaml
|
|
3861
|
+
* @returns {Map<string, Set<string>>} `kind` → шляхи JSON Pointer, які overrides змінюють
|
|
3862
|
+
*/
|
|
3863
|
+
export function kustomizationPatchPathsByTargetKind(kust) {
|
|
3864
|
+
/** @type {Map<string, Set<string>>} */
|
|
3865
|
+
const byKind = new Map()
|
|
3866
|
+
const patches = kust.patches
|
|
3867
|
+
if (!Array.isArray(patches)) return byKind
|
|
3868
|
+
for (const p of patches) {
|
|
3869
|
+
if (p === null || typeof p !== 'object' || Array.isArray(p)) continue
|
|
3870
|
+
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
3871
|
+
if (typeof pr.patch !== 'string') continue
|
|
3872
|
+
/** @type {string | null} */
|
|
3873
|
+
let kind = null
|
|
3874
|
+
const target = pr.target
|
|
3875
|
+
if (target !== null && typeof target === 'object' && !Array.isArray(target)) {
|
|
3876
|
+
const tk = /** @type {Record<string, unknown>} */ (target).kind
|
|
3877
|
+
if (typeof tk === 'string' && tk !== '') kind = tk
|
|
3878
|
+
}
|
|
3879
|
+
if (kind === null) kind = strategicMergePatchKind(pr.patch)
|
|
3880
|
+
if (kind === null) continue
|
|
3881
|
+
const paths = kustomizePatchModifiedPaths(pr.patch)
|
|
3882
|
+
if (!byKind.has(kind)) byKind.set(kind, new Set())
|
|
3883
|
+
const set = byKind.get(kind)
|
|
3884
|
+
for (const x of paths) set.add(x)
|
|
3885
|
+
}
|
|
3886
|
+
return byKind
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
/**
|
|
3890
|
+
* Для прод kustomization.yaml вимагає патчі, що перевизначають **`/spec/minReplicas`** і **`/spec/maxReplicas`**
|
|
3891
|
+
* на **HorizontalPodAutoscaler**, а також **`/spec/minAvailable`** на **PodDisruptionBudget**. Не застосовується
|
|
3892
|
+
* до dev-like (base / dev / *-qa) — там ці значення беруть з base (див. k8s.mdc).
|
|
3893
|
+
* @param {string} root корінь репозиторію
|
|
3894
|
+
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
3895
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
3896
|
+
* @param {(msg: string) => void} passFn callback при успіху
|
|
3897
|
+
*/
|
|
3898
|
+
async function validateProdKustomizationOverrides(root, yamlFilesAbs, fail, passFn) {
|
|
3899
|
+
const kustFiles = yamlFilesAbs.filter(abs => basename(abs) === 'kustomization.yaml')
|
|
3900
|
+
for (const kustAbs of kustFiles) {
|
|
3901
|
+
const rel = relative(root, kustAbs).replaceAll('\\', '/')
|
|
3902
|
+
const segment = k8sEnvSegmentFromRelPath(rel)
|
|
3903
|
+
if (segment === null || isDevLikeK8sEnvSegment(segment)) continue
|
|
3904
|
+
let raw
|
|
3905
|
+
try {
|
|
3906
|
+
raw = await readFile(kustAbs, 'utf8')
|
|
3907
|
+
} catch {
|
|
3908
|
+
continue
|
|
3909
|
+
}
|
|
3910
|
+
/** @type {Record<string, unknown> | undefined} */
|
|
3911
|
+
let kust
|
|
3912
|
+
try {
|
|
3913
|
+
for (const doc of parseAllDocuments(raw)) {
|
|
3914
|
+
if (doc.errors.length === 0) {
|
|
3915
|
+
const obj = doc.toJSON()
|
|
3916
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
3917
|
+
kust = /** @type {Record<string, unknown>} */ (obj)
|
|
3918
|
+
break
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
} catch {
|
|
3923
|
+
continue
|
|
3924
|
+
}
|
|
3925
|
+
if (kust === undefined) continue
|
|
3926
|
+
const byKind = kustomizationPatchPathsByTargetKind(kust)
|
|
3927
|
+
const hpaPaths = byKind.get('HorizontalPodAutoscaler') ?? new Set()
|
|
3928
|
+
const pdbPaths = byKind.get('PodDisruptionBudget') ?? new Set()
|
|
3929
|
+
let ok = true
|
|
3930
|
+
if (!hpaPaths.has('/spec/minReplicas')) {
|
|
3931
|
+
fail(
|
|
3932
|
+
`${rel}: прод-оверлей має перевизначати spec.minReplicas для HorizontalPodAutoscaler (мінімум 2 у проді) (k8s.mdc)`
|
|
3933
|
+
)
|
|
3934
|
+
ok = false
|
|
3935
|
+
}
|
|
3936
|
+
if (!hpaPaths.has('/spec/maxReplicas')) {
|
|
3937
|
+
fail(
|
|
3938
|
+
`${rel}: прод-оверлей має перевизначати spec.maxReplicas для HorizontalPodAutoscaler (мінімум 2 у проді) (k8s.mdc)`
|
|
3939
|
+
)
|
|
3940
|
+
ok = false
|
|
3941
|
+
}
|
|
3942
|
+
if (!pdbPaths.has('/spec/minAvailable')) {
|
|
3943
|
+
fail(
|
|
3944
|
+
`${rel}: прод-оверлей має перевизначати spec.minAvailable для PodDisruptionBudget (мінімум 1 у проді) (k8s.mdc)`
|
|
3945
|
+
)
|
|
3946
|
+
ok = false
|
|
3947
|
+
}
|
|
3948
|
+
if (ok) {
|
|
3949
|
+
passFn(`${rel}: прод-оверрайди HPA minReplicas/maxReplicas і PDB minAvailable присутні (k8s.mdc)`)
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3773
3954
|
/**
|
|
3774
3955
|
* Для кожного **Deployment** під `k8s/` перевіряє: у тому ж каталозі повинні бути
|
|
3775
3956
|
* `hpa.yaml` (валідний `autoscaling/v2`) і `pdb.yaml` (валідний `policy/v1`), а сам Deployment
|
|
@@ -3929,5 +4110,7 @@ export async function check() {
|
|
|
3929
4110
|
|
|
3930
4111
|
await validateDeploymentHpaPdbAndTopology(root, yamlFiles, fail, pass)
|
|
3931
4112
|
|
|
4113
|
+
await validateProdKustomizationOverrides(root, yamlFiles, fail, pass)
|
|
4114
|
+
|
|
3932
4115
|
return reporter.getExitCode()
|
|
3933
4116
|
}
|