@nitra/cursor 1.8.213 → 1.8.216
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/CHANGELOG.md +25 -0
- package/mdc/k8s.mdc +79 -17
- package/package.json +1 -1
- package/scripts/check-k8s.mjs +299 -76
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.8.216] - 2026-05-09
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **k8s / check-k8s:** орієнтир **`DEFAULT_CONTAINER_MEMORY_REQUEST`** поза base — **`512Mi`** (замість **`512`**).
|
|
12
|
+
|
|
13
|
+
## [1.8.215] - 2026-05-09
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **k8s / check-k8s:** канон **`resources.requests.memory`** у шарі **`…/k8s/…/base/…`** — **`128Mi`** (замість **`128`**, щоб відповідати Quantity у Kubernetes); приймається **`Mi`** без урахування регістру.
|
|
18
|
+
|
|
19
|
+
## [1.8.214] - 2026-05-09
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **k8s / check-k8s:** конвертація image-replace patches → `images:` падала з `byPatch.keys(...).toSorted is not a function`, бо `Map.keys()` повертає ітератор без `toSorted`. Тепер ключі спершу матеріалізуються у масив (`[...byPatch.keys()].toSorted(...)`).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **k8s / check-k8s:** у шарі **`…/k8s/…/base/…`** для **Deployment** жорстко **`resources.requests.cpu: '0.02'`** та **`memory: '128'`**; поза base обов’язкові **cpu** і **memory** (орієнтир **`0.5`** / **`512`** у підказках).
|
|
28
|
+
- **k8s / check-k8s:** заборона **`hpa.yaml`** у каталозі **`…/base/`**; якщо HPA є в дереві base — вимагається strategic-merge **`$patch: delete`** для **HorizontalPodAutoscaler** у **`base/kustomization.yaml`**.
|
|
29
|
+
- **k8s / check-k8s:** прод-оверлей вимагає patches на **HPA** (`minReplicas`/`maxReplicas`) лише якщо успадковане base **не** видаляє HPA через delete-patch; **PDB** **`minAvailable`** — якщо в base є PDB.
|
|
30
|
+
- **k8s.mdc:** оновлено правила та приклади під цю модель.
|
|
31
|
+
|
|
7
32
|
## [1.8.213] - 2026-05-09
|
|
8
33
|
|
|
9
34
|
### Added
|
package/mdc/k8s.mdc
CHANGED
|
@@ -96,19 +96,51 @@ jobs:
|
|
|
96
96
|
run: bun run lint-k8s
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
## Deployment: `resources.requests
|
|
99
|
+
## Deployment: `resources.requests` (CPU і memory)
|
|
100
100
|
|
|
101
|
-
У
|
|
101
|
+
У кожному контейнері **`Deployment`** обов’язкові **`resources.requests.cpu`** і **`resources.requests.memory`** (непорожні скаляри Kubernetes Quantity).
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
### Шар **`…/k8s/…/base/…`** (dev / щільний packing)
|
|
104
|
+
|
|
105
|
+
У **всіх** `Deployment` у файлах під **`…/k8s/…/base/…`** значення **жорстко фіксовані** (для **cpu** допускається число **`0.02`** у YAML):
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
resources:
|
|
109
|
+
requests:
|
|
110
|
+
cpu: '0.02'
|
|
111
|
+
memory: '128Mi'
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**HPA у base не тримаємо** у локальному **`hpa.yaml`** поруч із `Deployment`. Якщо **HorizontalPodAutoscaler** потрапляє в дерево Kustomize з **`resources`** / **`components`** / **`bases`**, у **`…/base/kustomization.yaml`** додай strategic-merge patch з **`$patch: delete`** і **`kind: HorizontalPodAutoscaler`** (і **`metadata.name`**, що збігається з ресурсом, який треба прибрати). **`check k8s`** перевіряє наявність такого patch, коли в дереві base одночасно є **Deployment** і **HPA**.
|
|
115
|
+
|
|
116
|
+
### Поза base (оверлеї, окремі каталоги)
|
|
117
|
+
|
|
118
|
+
Якщо ще не підібрано власні ліміти під сервіс, орієнтир для **`requests`**:
|
|
104
119
|
|
|
105
120
|
```yaml
|
|
106
121
|
resources:
|
|
107
122
|
requests:
|
|
108
|
-
cpu: '0.5'
|
|
123
|
+
cpu: '0.5'
|
|
124
|
+
memory: '512Mi'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
У прод-оверлеях підіймай **`cpu` / `memory`** до реального споживання через **`patches`** або окремі фрагменти маніфестів. **`check k8s`** не вимагає саме **`0.5` / `512Mi`** поза base — лише непорожні **`requests.cpu`** і **`requests.memory`**.
|
|
128
|
+
|
|
129
|
+
```yaml title="k8s/prod/kustomization.yaml (фрагмент)"
|
|
130
|
+
patches:
|
|
131
|
+
- target:
|
|
132
|
+
kind: Deployment
|
|
133
|
+
name: backend-api
|
|
134
|
+
patch: |-
|
|
135
|
+
- op: replace
|
|
136
|
+
path: /spec/template/spec/containers/0/resources/requests/cpu
|
|
137
|
+
value: '500m'
|
|
138
|
+
- op: replace
|
|
139
|
+
path: /spec/template/spec/containers/0/resources/requests/memory
|
|
140
|
+
value: 1Gi
|
|
109
141
|
```
|
|
110
142
|
|
|
111
|
-
|
|
143
|
+
Поле **`imagePullPolicy`** скрипт **не** перевіряє (залишається політиці Kubernetes за тегом образу).
|
|
112
144
|
|
|
113
145
|
Образ **`hasura/graphql-engine`**: дозволений лише канонічний тег із константи **`HASURA_GRAPHQL_ENGINE_IMAGE`** у **`check-k8s.mjs`** (допускається префікс **`docker.io/`**); решта — помилка **check k8s**.
|
|
114
146
|
|
|
@@ -341,15 +373,17 @@ images:
|
|
|
341
373
|
|
|
342
374
|
**`check k8s`:** заборонено **`kind: Ingress`**.
|
|
343
375
|
|
|
344
|
-
## Deployment:
|
|
376
|
+
## Deployment: `pdb.yaml`, `topologySpreadConstraints`, HPA поза dev-base
|
|
345
377
|
|
|
346
|
-
Для **кожного** `kind: Deployment` у каталозі **`…/k8s/…/base/`** (у будь-якому файлі `.yaml`, наприклад **`deploy.yaml`**, **`deployment.yaml`**) у **тому ж каталозі**
|
|
378
|
+
Для **кожного** `kind: Deployment` у каталозі **`…/k8s/…/base/`** (у будь-якому файлі `.yaml`, наприклад **`deploy.yaml`**, **`deployment.yaml`**) у **тому ж каталозі** має бути **`pdb.yaml`** (PDB), а сам Deployment — канонічні **`spec.template.spec.topologySpreadConstraints`**. Локальний **`hpa.yaml`** у каталозі **`…/base/`** **заборонено** — для dev HPA з дерева Kustomize прибирається через **`$patch: delete`** у **`base/kustomization.yaml`** (див. розділ про **`resources.requests`** вище).
|
|
347
379
|
|
|
348
|
-
|
|
380
|
+
У **не-base** оверлеях поруч із `Deployment` лишається звична схема: окремий **`hpa.yaml`**, якщо потрібен HPA для цього середовища. **`check k8s`** звіряє прив’язку за іменами:
|
|
381
|
+
|
|
382
|
+
- **`hpa.yaml`** (поза **`…/base/`**) — `autoscaling/v2`, `HorizontalPodAutoscaler`, `spec.scaleTargetRef.name` **= `metadata.name`** Deployment.
|
|
349
383
|
- **`pdb.yaml`** — `policy/v1`, `PodDisruptionBudget`, `spec.selector.matchLabels.app` **= `spec.selector.matchLabels.app`** Deployment.
|
|
350
384
|
- **`topologySpreadConstraints`** — запис з `maxSkew: 1`, `topologyKey: kubernetes.io/hostname`, `whenUnsatisfiable: ScheduleAnyway`, `labelSelector.matchLabels.app` рівне тій самій мітці `app`.
|
|
351
385
|
|
|
352
|
-
**Kustomize `base` і overlay:** у дереві, зібраному з `k8s/…/base/kustomization.yaml` (`resources` / `bases` / `components` / `crds`, рекурсивно), **HorizontalPodAutoscaler** і **PodDisruptionBudget** допустимі **лише якщо** в тому ж дереві kustomize є хоча б один **`Deployment`** у YAML під **`…/k8s/…/base/`**.
|
|
386
|
+
**Kustomize `base` і overlay:** у дереві, зібраному з `k8s/…/base/kustomization.yaml` (`resources` / `bases` / `components` / `crds`, рекурсивно), **HorizontalPodAutoscaler** і **PodDisruptionBudget** допустимі **лише якщо** в тому ж дереві kustomize є хоча б один **`Deployment`** у YAML під **`…/k8s/…/base/`**. Якщо після збору в дереві є і **Deployment**, і **HPA**, **base** `kustomization.yaml` має містити strategic-merge видалення **HorizontalPodAutoscaler** (`$patch: delete`). У `kustomization.yaml` overlay, який підключає цей `base`, не додавай окремі YAML-файли з HPA / PDB, доки в наслідуваному `base` у дереві не з’явиться такий Deployment. Перевіряє **`check-k8s.mjs`**.
|
|
353
387
|
|
|
354
388
|
**Локальні шляхи в `kustomization.yaml`:** кожен запис без `://` (remote) з `resources` / `bases` / `components` / `crds`, `patchesStrategicMerge`, `patches[].path`, `patchesJson6902[].path`, `configurations[]`, `replacements[].path` має вказувати на **існуючий** у репозиторії файл (`.yaml` / `.yml`) або **каталог**; биті посилання — помилка **`check k8s`**.
|
|
355
389
|
|
|
@@ -359,7 +393,7 @@ images:
|
|
|
359
393
|
|
|
360
394
|
**Dev-like середовища** — сегмент `base`, `dev`, або з суфіксом `-qa` (напр. `tr-qa`):
|
|
361
395
|
|
|
362
|
-
- HPA
|
|
396
|
+
- У **зібраному** маніфесті (після Kustomize), якщо лишився **HPA**: `minReplicas` — рівно **1**, `maxReplicas` — рівно **1**.
|
|
363
397
|
- PDB: `minAvailable` — рівно **0**.
|
|
364
398
|
|
|
365
399
|
**Прод-середовища** — усе інше (будь-який overlay без суфікса `-qa`):
|
|
@@ -369,12 +403,12 @@ images:
|
|
|
369
403
|
|
|
370
404
|
### Прод-оверрайди у `kustomization.yaml`
|
|
371
405
|
|
|
372
|
-
|
|
406
|
+
У прод-накладенні `kustomization.yaml` у `patches[]` **обов’язкові** перевизначення залежно від того, що дає успадковане **base**-дерево:
|
|
373
407
|
|
|
374
|
-
-
|
|
375
|
-
-
|
|
408
|
+
- **`PodDisruptionBudget`**: `spec.minAvailable` — якщо в base-дереві є **Deployment** і **PDB** (типово dev-like `0` → прод ≥1).
|
|
409
|
+
- **`HorizontalPodAutoscaler`**: `spec.minReplicas` **і** `spec.maxReplicas` — **лише якщо** в base-дереві є **HPA**, який **не** прибирається в **base** через **`$patch: delete`**. Якщо base видаляє HPA для dev, у прод-оверлеї додай **HPA** окремим YAML у **`resources`** (або patches на ресурс, що з’являється з іншого шару) з прод-мінімумами.
|
|
376
410
|
|
|
377
|
-
Формат patch — JSON6902 або Strategic Merge; `check k8s` перевіряє **наявність**
|
|
411
|
+
Формат patch — JSON6902 або Strategic Merge; `check k8s` перевіряє **наявність** відповідних JSON Pointer-ів у **`patches[]`**.
|
|
378
412
|
|
|
379
413
|
```yaml title="k8s/prod/kustomization.yaml (фрагмент)"
|
|
380
414
|
patches:
|
|
@@ -397,9 +431,30 @@ patches:
|
|
|
397
431
|
value: 1
|
|
398
432
|
```
|
|
399
433
|
|
|
434
|
+
### Приклад: прибрати HPA у `base/kustomization.yaml`
|
|
435
|
+
|
|
436
|
+
Якщо **HPA** підключений з **`../component`** (або іншого шляху в **`resources`**), а у dev він не потрібен:
|
|
437
|
+
|
|
438
|
+
```yaml title="k8s/base/kustomization.yaml (фрагмент)"
|
|
439
|
+
resources:
|
|
440
|
+
- ../component
|
|
441
|
+
- deploy.yaml
|
|
442
|
+
- pdb.yaml
|
|
443
|
+
patches:
|
|
444
|
+
- target:
|
|
445
|
+
kind: HorizontalPodAutoscaler
|
|
446
|
+
name: backend-api
|
|
447
|
+
patch: |-
|
|
448
|
+
$patch: delete
|
|
449
|
+
apiVersion: autoscaling/v2
|
|
450
|
+
kind: HorizontalPodAutoscaler
|
|
451
|
+
metadata:
|
|
452
|
+
name: backend-api
|
|
453
|
+
```
|
|
454
|
+
|
|
400
455
|
### Приклади
|
|
401
456
|
|
|
402
|
-
```yaml title="k8s/base/hpa.yaml"
|
|
457
|
+
```yaml title="k8s/prod/hpa.yaml (або компонент з HPA — не в …/base/ локальному hpa.yaml)"
|
|
403
458
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.9-standalone-strict/horizontalpodautoscaler-autoscaling-v2.json
|
|
404
459
|
apiVersion: autoscaling/v2
|
|
405
460
|
kind: HorizontalPodAutoscaler
|
|
@@ -410,8 +465,8 @@ spec:
|
|
|
410
465
|
apiVersion: apps/v1
|
|
411
466
|
kind: Deployment
|
|
412
467
|
name: backend-api
|
|
413
|
-
minReplicas:
|
|
414
|
-
maxReplicas:
|
|
468
|
+
minReplicas: 2
|
|
469
|
+
maxReplicas: 10
|
|
415
470
|
metrics:
|
|
416
471
|
- type: Resource
|
|
417
472
|
resource:
|
|
@@ -456,6 +511,13 @@ spec:
|
|
|
456
511
|
spec:
|
|
457
512
|
template:
|
|
458
513
|
spec:
|
|
514
|
+
containers:
|
|
515
|
+
- name: backend-api
|
|
516
|
+
image: example.registry/backend-api:tag
|
|
517
|
+
resources:
|
|
518
|
+
requests:
|
|
519
|
+
cpu: '0.02'
|
|
520
|
+
memory: '128Mi'
|
|
459
521
|
topologySpreadConstraints:
|
|
460
522
|
- maxSkew: 1
|
|
461
523
|
topologyKey: kubernetes.io/hostname
|
package/package.json
CHANGED
package/scripts/check-k8s.mjs
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* (datree за замовчуванням: GitHub Pages `https://datreeio.github.io/CRDs-catalog/…`).
|
|
8
8
|
*
|
|
9
9
|
* Додатково: у кожному YAML-документі з **`kind: Deployment`** у кожного контейнера
|
|
10
|
-
* **`spec.template.spec.containers[]`** має бути
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* **`spec.template.spec.containers[]`** має бути **`resources.requests.cpu`** і **`resources.requests.memory`**
|
|
11
|
+
* (непорожні скаляри). У шарі **`…/k8s/…/base/…`** значення жорстко **`cpu: '0.02'`**, **`memory: '128Mi'`**
|
|
12
|
+
* (для **cpu** допускається число **`0.02`**). Поза base, якщо ще не підібрано власні
|
|
13
|
+
* ліміти — орієнтир **`DEFAULT_CONTAINER_CPU_REQUEST`** = **`"0.5"`**, **`DEFAULT_CONTAINER_MEMORY_REQUEST`**
|
|
14
|
+
* = **`"512Mi"`**. Поле **`imagePullPolicy`**
|
|
13
15
|
* не перевіряється — діють типові правила Kubernetes (`:latest` або коли тег не вказано → **Always**,
|
|
14
16
|
* інші теги → **IfNotPresent**). Якщо серед **`containers`** / **`initContainers`** є образ
|
|
15
17
|
* **`hasura/graphql-engine`**, дозволено лише пін **`HASURA_GRAPHQL_ENGINE_IMAGE`** (див. k8s.mdc).
|
|
@@ -82,21 +84,20 @@
|
|
|
82
84
|
* або рядок `"true"`, без регістрової залежності).
|
|
83
85
|
*
|
|
84
86
|
* **HPA / PDB / topologySpreadConstraints:** для кожного **`Deployment`** у шарі **`…/k8s/…/base/`** (будь-який
|
|
85
|
-
* `.yaml` у цьому
|
|
86
|
-
* **`hpa.yaml
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* `
|
|
92
|
-
* `
|
|
87
|
+
* `.yaml` у цьому каталозі) поруч обов’язкові **`pdb.yaml`** і канонічні **topologySpreadConstraints**. У каталозі
|
|
88
|
+
* **`…/base/`** не тримай **`hpa.yaml`** — для dev HPA прибирається з дерева Kustomize через strategic-merge patch
|
|
89
|
+
* з **`$patch: delete`** і **`kind: HorizontalPodAutoscaler`** у **`base/kustomization.yaml`** (якщо HPA тягнеться з
|
|
90
|
+
* `resources` / `components` / `bases`). Якщо в дереві base є і **Deployment**, і **HorizontalPodAutoscaler**, такий
|
|
91
|
+
* patch обов’язковий. **HPA** поруч із Deployment у **не-base** оверлеях — як раніше (див. k8s.mdc).
|
|
92
|
+
* Env-залежні межі за сегментом після `/k8s/`: **dev-like** (`base`, `dev`, `*-qa`) — для HPA, що лишився після
|
|
93
|
+
* збірки, `minReplicas === 1`, `maxReplicas === 1`, PDB `minAvailable === 0`; **прод** — `minReplicas >= 2`,
|
|
94
|
+
* `maxReplicas >= 2`, `minAvailable >= 1`.
|
|
93
95
|
*
|
|
94
|
-
* **Прод-оверрайди в kustomization.yaml:** для прод overlays (не dev-like) `
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* яке буде оцінено під час збірки Kustomize).
|
|
96
|
+
* **Прод-оверрайди в kustomization.yaml:** для прод overlays (не dev-like) у `patches[]` потрібні перевизначення
|
|
97
|
+
* **`/spec/minReplicas`** і **`/spec/maxReplicas`** для **HorizontalPodAutoscaler** лише якщо успадковане base-дерево
|
|
98
|
+
* містить HPA **і** base **не** видаляє його через **`$patch: delete`**. **`/spec/minAvailable`** для **PDB** —
|
|
99
|
+
* якщо в base-дереві є Deployment і PDB. Формат patch — JSON6902 або Strategic Merge; наявність шляхів —
|
|
100
|
+
* `kustomizationPatchPathsByTargetKind`.
|
|
100
101
|
*
|
|
101
102
|
* **Існування шляхів у `kustomization.yaml`:** кожне локальне посилання (без `://`) з `resources` / `bases` /
|
|
102
103
|
* `components` / `crds`, `patchesStrategicMerge`, `patches[].path`, `patchesJson6902[].path`, `configurations[]`,
|
|
@@ -310,6 +311,10 @@ const KIND_FIELD_RE = /^\s*kind:\s*(\S+)\s*$/
|
|
|
310
311
|
const TYPE_FIELD_RE = /^\s*type:\s*(\S+)\s*$/
|
|
311
312
|
const YAML_DOC_SEPARATOR_LINE_RE = /^---\s*$/
|
|
312
313
|
const HEALTHCHECK_DELETE_RE = /\$patch:\s*delete/u
|
|
314
|
+
/** Strategic-merge patch видалення **HorizontalPodAutoscaler** у kustomization (k8s.mdc). */
|
|
315
|
+
const HPA_STRATEGIC_DELETE_RE = /\$patch:\s*delete/u
|
|
316
|
+
/** Рядок **kind:** для HPA у strategic-merge patch. */
|
|
317
|
+
const HPA_KIND_LINE_RE = /^kind:\s*HorizontalPodAutoscaler\b/m
|
|
313
318
|
const HEALTHCHECK_KIND_RE = /kind:\s*HealthCheckPolicy/u
|
|
314
319
|
const METADATA_LINE_RE = /metadata:/u
|
|
315
320
|
const NAME_NON_EMPTY_RE = /name:\s*\S+/u
|
|
@@ -2144,6 +2149,60 @@ export function ruKustomizationHasHealthCheckDeletePatch(raw) {
|
|
|
2144
2149
|
return true
|
|
2145
2150
|
}
|
|
2146
2151
|
|
|
2152
|
+
/**
|
|
2153
|
+
* Чи вміст strategic-merge patch оголошує видалення **HorizontalPodAutoscaler** (`$patch: delete`, `kind:`).
|
|
2154
|
+
* @param {string} text вміст поля **patch** або файлу merge
|
|
2155
|
+
* @returns {boolean} true, якщо присутні **`$patch: delete`** і **`kind: HorizontalPodAutoscaler`**
|
|
2156
|
+
*/
|
|
2157
|
+
export function patchTextDeclaresHpaStrategicDelete(text) {
|
|
2158
|
+
const t = typeof text === 'string' ? text : ''
|
|
2159
|
+
if (!HPA_STRATEGIC_DELETE_RE.test(t)) return false
|
|
2160
|
+
return HPA_KIND_LINE_RE.test(t)
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
/**
|
|
2164
|
+
* Чи **kustomization.yaml** (inline **patches** / **patchesStrategicMerge**) містить strategic-merge видалення HPA.
|
|
2165
|
+
* @param {string} kustAbs абсолютний шлях до **kustomization.yaml**
|
|
2166
|
+
* @param {string} rootNorm нормалізований корінь репо
|
|
2167
|
+
* @returns {Promise<boolean>} true, якщо знайдено patch видалення HPA
|
|
2168
|
+
*/
|
|
2169
|
+
async function kustomizationDeclaresHpaStrategicDelete(kustAbs, rootNorm) {
|
|
2170
|
+
const kust = await readFirstYamlObject(kustAbs)
|
|
2171
|
+
if (kust === null) return false
|
|
2172
|
+
const kustDir = dirname(kustAbs)
|
|
2173
|
+
const patches = kust.patches
|
|
2174
|
+
if (Array.isArray(patches)) {
|
|
2175
|
+
for (const p of patches) {
|
|
2176
|
+
if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
|
|
2177
|
+
const rec = /** @type {Record<string, unknown>} */ (p)
|
|
2178
|
+
const inline = rec.patch
|
|
2179
|
+
if (typeof inline === 'string' && patchTextDeclaresHpaStrategicDelete(inline)) return true
|
|
2180
|
+
const pathRef = rec.path
|
|
2181
|
+
if (typeof pathRef === 'string' && pathRef.trim() !== '') {
|
|
2182
|
+
const abs = resolve(kustDir, pathRef.trim())
|
|
2183
|
+
if (resolvedFilePathIsUnderRoot(rootNorm, abs)) {
|
|
2184
|
+
const body = await tryReadFileUtf8(abs)
|
|
2185
|
+
if (body !== undefined && patchTextDeclaresHpaStrategicDelete(body)) return true
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
const sm = kust.patchesStrategicMerge
|
|
2192
|
+
if (Array.isArray(sm)) {
|
|
2193
|
+
for (const ref of sm) {
|
|
2194
|
+
if (typeof ref === 'string' && ref.trim() !== '') {
|
|
2195
|
+
const abs = resolve(kustDir, ref.trim())
|
|
2196
|
+
if (resolvedFilePathIsUnderRoot(rootNorm, abs)) {
|
|
2197
|
+
const body = await tryReadFileUtf8(abs)
|
|
2198
|
+
if (body !== undefined && patchTextDeclaresHpaStrategicDelete(body)) return true
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
return false
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2147
2206
|
/**
|
|
2148
2207
|
* Чи абсолютний шлях лежить усередині кореня репозиторію (без виходу через `..`).
|
|
2149
2208
|
* @param {string} rootAbs абсолютний корінь
|
|
@@ -2508,10 +2567,25 @@ function scanForbiddenManifestsInYamlDocuments(rel, body, fail) {
|
|
|
2508
2567
|
}
|
|
2509
2568
|
|
|
2510
2569
|
/**
|
|
2511
|
-
* Рекомендоване
|
|
2570
|
+
* Рекомендоване **`resources.requests.cpu`** поза шарем base для підказок у повідомленнях (k8s.mdc).
|
|
2512
2571
|
*/
|
|
2513
2572
|
export const DEFAULT_CONTAINER_CPU_REQUEST = '0.5'
|
|
2514
2573
|
|
|
2574
|
+
/**
|
|
2575
|
+
* Рекомендоване **`resources.requests.memory`** поза шарем base для підказок у повідомленнях (k8s.mdc).
|
|
2576
|
+
*/
|
|
2577
|
+
export const DEFAULT_CONTAINER_MEMORY_REQUEST = '512Mi'
|
|
2578
|
+
|
|
2579
|
+
/**
|
|
2580
|
+
* Обов’язковий **`resources.requests.cpu`** у **`…/k8s/…/base/…`** (k8s.mdc).
|
|
2581
|
+
*/
|
|
2582
|
+
export const K8S_BASE_CONTAINER_CPU_REQUEST = '0.02'
|
|
2583
|
+
|
|
2584
|
+
/**
|
|
2585
|
+
* Обов’язковий **`resources.requests.memory`** у **`…/k8s/…/base/…`** (k8s.mdc).
|
|
2586
|
+
*/
|
|
2587
|
+
export const K8S_BASE_CONTAINER_MEMORY_REQUEST = '128Mi'
|
|
2588
|
+
|
|
2515
2589
|
/**
|
|
2516
2590
|
* Чи значення `resources.requests.cpu` записане у валідному вигляді:
|
|
2517
2591
|
* непорожній рядок (`"500m"`, `"0.5"`) або додатне число.
|
|
@@ -2525,20 +2599,59 @@ function isValidCpuRequestValue(cpu) {
|
|
|
2525
2599
|
}
|
|
2526
2600
|
|
|
2527
2601
|
/**
|
|
2528
|
-
*
|
|
2529
|
-
*
|
|
2530
|
-
*
|
|
2602
|
+
* Чи значення `resources.requests.memory` записане у валідному вигляді (непорожній рядок або додатне число).
|
|
2603
|
+
* @param {unknown} mem значення поля `resources.requests.memory`
|
|
2604
|
+
* @returns {boolean} true, якщо значення прийнятне
|
|
2605
|
+
*/
|
|
2606
|
+
function isValidMemoryRequestValue(mem) {
|
|
2607
|
+
if (typeof mem === 'string') return mem.trim() !== ''
|
|
2608
|
+
if (typeof mem === 'number') return Number.isFinite(mem) && mem > 0
|
|
2609
|
+
return false
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
/**
|
|
2613
|
+
* Чи CPU у base-шарі збігається з каноном **`0.02`** (рядок або число).
|
|
2614
|
+
* @param {unknown} cpu значення **requests.cpu**
|
|
2615
|
+
* @returns {boolean} true, якщо дорівнює канону base
|
|
2616
|
+
*/
|
|
2617
|
+
function isBaseCanonCpuValue(cpu) {
|
|
2618
|
+
if (typeof cpu === 'number' && Number.isFinite(cpu)) {
|
|
2619
|
+
return cpu === 0.02
|
|
2620
|
+
}
|
|
2621
|
+
if (typeof cpu === 'string' && cpu.trim() !== '') {
|
|
2622
|
+
const t = cpu.trim()
|
|
2623
|
+
if (t === K8S_BASE_CONTAINER_CPU_REQUEST) return true
|
|
2624
|
+
const n = Number(t)
|
|
2625
|
+
return Number.isFinite(n) && n === 0.02
|
|
2626
|
+
}
|
|
2627
|
+
return false
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
/**
|
|
2631
|
+
* Чи memory у base-шарі збігається з каноном **`128Mi`** (рядок Quantity; **`Mi`** без урахування регістру).
|
|
2632
|
+
* @param {unknown} mem значення **requests.memory**
|
|
2633
|
+
* @returns {boolean} true, якщо дорівнює канону base
|
|
2634
|
+
*/
|
|
2635
|
+
function isBaseCanonMemoryValue(mem) {
|
|
2636
|
+
if (typeof mem !== 'string' || mem.trim() === '') return false
|
|
2637
|
+
return /^128Mi$/iu.test(mem.trim())
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
/**
|
|
2641
|
+
* Перевірка поля **`resources`** для одного контейнера **Deployment** (k8s.mdc): **requests.cpu** і **requests.memory**;
|
|
2642
|
+
* у шарі **`…/k8s/…/base/…`** — жорстко **`0.02`** / **`128Mi`**.
|
|
2531
2643
|
* @param {unknown} c елемент **containers[]**
|
|
2532
2644
|
* @param {string} label підпис у повідомленні
|
|
2645
|
+
* @param {boolean} inK8sBaseLayer файл маніфесту під **`…/k8s/…/base/…`**
|
|
2533
2646
|
* @returns {string | null} текст порушення або null
|
|
2534
2647
|
*/
|
|
2535
|
-
function deploymentContainerResourcesViolation(c, label) {
|
|
2648
|
+
function deploymentContainerResourcesViolation(c, label, inK8sBaseLayer) {
|
|
2536
2649
|
if (c === null || c === undefined || typeof c !== 'object' || Array.isArray(c)) {
|
|
2537
2650
|
return null
|
|
2538
2651
|
}
|
|
2539
2652
|
const cont = /** @type {Record<string, unknown>} */ (c)
|
|
2540
2653
|
if (!('resources' in cont)) {
|
|
2541
|
-
return `контейнер "${label}": відсутнє поле resources — додай resources.requests.cpu (за замовчуванням
|
|
2654
|
+
return `контейнер "${label}": відсутнє поле resources — додай resources.requests.cpu та resources.requests.memory (поза base за замовчуванням cpu=${DEFAULT_CONTAINER_CPU_REQUEST}, memory=${DEFAULT_CONTAINER_MEMORY_REQUEST}; у base — cpu='${K8S_BASE_CONTAINER_CPU_REQUEST}', memory='${K8S_BASE_CONTAINER_MEMORY_REQUEST}') (див. k8s.mdc)`
|
|
2542
2655
|
}
|
|
2543
2656
|
const r = cont.resources
|
|
2544
2657
|
if (r === null || typeof r !== 'object' || Array.isArray(r)) {
|
|
@@ -2547,24 +2660,39 @@ function deploymentContainerResourcesViolation(c, label) {
|
|
|
2547
2660
|
const resources = /** @type {Record<string, unknown>} */ (r)
|
|
2548
2661
|
const requests = resources.requests
|
|
2549
2662
|
if (requests === null || requests === undefined || typeof requests !== 'object' || Array.isArray(requests)) {
|
|
2550
|
-
return `контейнер "${label}": додай resources.requests.cpu (за замовчуванням
|
|
2663
|
+
return `контейнер "${label}": додай resources.requests.cpu та resources.requests.memory (поза base за замовчуванням cpu=${DEFAULT_CONTAINER_CPU_REQUEST}, memory=${DEFAULT_CONTAINER_MEMORY_REQUEST}) (див. k8s.mdc)`
|
|
2551
2664
|
}
|
|
2552
2665
|
const req = /** @type {Record<string, unknown>} */ (requests)
|
|
2553
2666
|
if (!('cpu' in req)) {
|
|
2554
|
-
return `контейнер "${label}": додай resources.requests.cpu (за замовчуванням ${DEFAULT_CONTAINER_CPU_REQUEST}) (див. k8s.mdc)`
|
|
2667
|
+
return `контейнер "${label}": додай resources.requests.cpu (поза base за замовчуванням ${DEFAULT_CONTAINER_CPU_REQUEST}) (див. k8s.mdc)`
|
|
2555
2668
|
}
|
|
2556
2669
|
if (!isValidCpuRequestValue(req.cpu)) {
|
|
2557
2670
|
return `контейнер "${label}": resources.requests.cpu має бути непорожнім значенням (наприклад "500m" або ${DEFAULT_CONTAINER_CPU_REQUEST}) (зараз: ${JSON.stringify(req.cpu)}) (див. k8s.mdc)`
|
|
2558
2671
|
}
|
|
2672
|
+
if (!('memory' in req)) {
|
|
2673
|
+
return `контейнер "${label}": додай resources.requests.memory (поза base за замовчуванням ${DEFAULT_CONTAINER_MEMORY_REQUEST}) (див. k8s.mdc)`
|
|
2674
|
+
}
|
|
2675
|
+
if (!isValidMemoryRequestValue(req.memory)) {
|
|
2676
|
+
return `контейнер "${label}": resources.requests.memory має бути непорожнім значенням (наприклад "${DEFAULT_CONTAINER_MEMORY_REQUEST}") (зараз: ${JSON.stringify(req.memory)}) (див. k8s.mdc)`
|
|
2677
|
+
}
|
|
2678
|
+
if (inK8sBaseLayer) {
|
|
2679
|
+
if (!isBaseCanonCpuValue(req.cpu)) {
|
|
2680
|
+
return `контейнер "${label}": у шарі k8s/.../base resources.requests.cpu має бути рівно '${K8S_BASE_CONTAINER_CPU_REQUEST}' (допускається число 0.02) — зараз ${JSON.stringify(req.cpu)} (див. k8s.mdc)`
|
|
2681
|
+
}
|
|
2682
|
+
if (!isBaseCanonMemoryValue(req.memory)) {
|
|
2683
|
+
return `контейнер "${label}": у шарі k8s/.../base resources.requests.memory має бути рівно '${K8S_BASE_CONTAINER_MEMORY_REQUEST}' (суфікс Mi без урахування регістру) — зараз ${JSON.stringify(req.memory)} (див. k8s.mdc)`
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2559
2686
|
return null
|
|
2560
2687
|
}
|
|
2561
2688
|
|
|
2562
2689
|
/**
|
|
2563
2690
|
* Чи порушує маніфест вимогу **`Deployment.spec.template.spec.containers[].resources`** (див. k8s.mdc).
|
|
2564
2691
|
* @param {unknown} manifest корінь YAML-документа як запис JavaScript
|
|
2692
|
+
* @param {boolean} [inK8sBaseLayer] true, якщо файл лежить під **`…/k8s/…/base/…`**
|
|
2565
2693
|
* @returns {string | null} текст порушення для `fail` або null, якщо перевірка не застосовується / ок
|
|
2566
2694
|
*/
|
|
2567
|
-
export function deploymentResourcesViolation(manifest) {
|
|
2695
|
+
export function deploymentResourcesViolation(manifest, inK8sBaseLayer = false) {
|
|
2568
2696
|
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
2569
2697
|
return null
|
|
2570
2698
|
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
@@ -2584,7 +2712,7 @@ export function deploymentResourcesViolation(manifest) {
|
|
|
2584
2712
|
typeof c === 'object' && c !== null && !Array.isArray(c) && typeof c.name === 'string' && c.name !== ''
|
|
2585
2713
|
? c.name
|
|
2586
2714
|
: `#${i + 1}`
|
|
2587
|
-
const v = deploymentContainerResourcesViolation(c, label)
|
|
2715
|
+
const v = deploymentContainerResourcesViolation(c, label, inK8sBaseLayer)
|
|
2588
2716
|
if (v !== null) {
|
|
2589
2717
|
return v
|
|
2590
2718
|
}
|
|
@@ -3845,7 +3973,9 @@ function failIfK8sPolicyNamespaceRulesViolated(rel, docIndex, obj, skipMetaNs, i
|
|
|
3845
3973
|
* @returns {void}
|
|
3846
3974
|
*/
|
|
3847
3975
|
function failIfK8sPolicyResourceRulesViolated(rel, baseLower, docIndex, obj, fail) {
|
|
3848
|
-
const
|
|
3976
|
+
const relPosix = rel.replaceAll('\\', '/')
|
|
3977
|
+
const inK8sBaseLayer = isK8sYamlUnderBaseDirectory(relPosix)
|
|
3978
|
+
const resV = deploymentResourcesViolation(obj, inK8sBaseLayer)
|
|
3849
3979
|
if (resV !== null) {
|
|
3850
3980
|
fail(`${rel}: Deployment (документ ${docIndex}): ${resV}`)
|
|
3851
3981
|
}
|
|
@@ -4977,6 +5107,38 @@ async function verifyK8sBaseKustomizeHpaPdbNeedDeployment(kustAbs, rel, fail, pa
|
|
|
4977
5107
|
}
|
|
4978
5108
|
}
|
|
4979
5109
|
|
|
5110
|
+
/**
|
|
5111
|
+
* У **`…/k8s/…/base/kustomization.yaml`**: якщо з **resources** тягнеться HPA разом із Deployment,
|
|
5112
|
+
* обов’язковий strategic-merge patch з **`$patch: delete`** для **HorizontalPodAutoscaler** (k8s.mdc).
|
|
5113
|
+
* @param {string} kustAbs абсолютний шлях до **kustomization.yaml**
|
|
5114
|
+
* @param {string} rel відносний шлях для повідомлень
|
|
5115
|
+
* @param {string} rootNorm корінь репо
|
|
5116
|
+
* @param {(msg: string) => void} fail callback
|
|
5117
|
+
* @param {(msg: string) => void} passFn success
|
|
5118
|
+
* @param {(kust: string) => Promise<{ hasDeployment: boolean, hasHpa: boolean, hasPdb: boolean }>} getTreeFlags
|
|
5119
|
+
* @returns {Promise<void>}
|
|
5120
|
+
*/
|
|
5121
|
+
async function verifyK8sBaseKustomizeHpaDeletedWhenInherited(
|
|
5122
|
+
kustAbs,
|
|
5123
|
+
rel,
|
|
5124
|
+
rootNorm,
|
|
5125
|
+
fail,
|
|
5126
|
+
passFn,
|
|
5127
|
+
getTreeFlags
|
|
5128
|
+
) {
|
|
5129
|
+
const { hasDeployment, hasHpa } = await getTreeFlags(kustAbs)
|
|
5130
|
+
if (!hasDeployment || !hasHpa) {
|
|
5131
|
+
return
|
|
5132
|
+
}
|
|
5133
|
+
if (await kustomizationDeclaresHpaStrategicDelete(kustAbs, rootNorm)) {
|
|
5134
|
+
passFn(`${rel}: base kustomization містить $patch: delete для HorizontalPodAutoscaler (k8s.mdc)`)
|
|
5135
|
+
} else {
|
|
5136
|
+
fail(
|
|
5137
|
+
`${rel}: у дереві base є HorizontalPodAutoscaler — додай у цей kustomization.yaml strategic-merge patch з $patch: delete та kind: HorizontalPodAutoscaler (k8s.mdc)`
|
|
5138
|
+
)
|
|
5139
|
+
}
|
|
5140
|
+
}
|
|
5141
|
+
|
|
4980
5142
|
/**
|
|
4981
5143
|
* `kustomization` overlay, що посилається на `…/k8s/…/base`, не може додавати HPA / PDB як окремі YAML,
|
|
4982
5144
|
* поки в наслідуваному base немає Deployment.
|
|
@@ -5084,6 +5246,7 @@ async function validateKustomizeHpaPdbOnlyWithBaseDeployment(root, yamlFilesAbs,
|
|
|
5084
5246
|
if (kust !== null) {
|
|
5085
5247
|
if (isK8sBaseKustomizationRelPath(rel)) {
|
|
5086
5248
|
await verifyK8sBaseKustomizeHpaPdbNeedDeployment(kustAbs, rel, fail, passFn, getTreeFlags)
|
|
5249
|
+
await verifyK8sBaseKustomizeHpaDeletedWhenInherited(kustAbs, rel, rootNorm, fail, passFn, getTreeFlags)
|
|
5087
5250
|
} else {
|
|
5088
5251
|
await verifyOverlayHpaPdbFileRefsRespectBaseDeployment(rootNorm, kustAbs, rel, kust, fail, passFn, getTreeFlags)
|
|
5089
5252
|
}
|
|
@@ -5092,80 +5255,108 @@ async function validateKustomizeHpaPdbOnlyWithBaseDeployment(root, yamlFilesAbs,
|
|
|
5092
5255
|
}
|
|
5093
5256
|
|
|
5094
5257
|
/**
|
|
5095
|
-
*
|
|
5258
|
+
* @typedef {{ needsHpaReplicaPatches: boolean, needsPdbMinAvailablePatch: boolean }} ProdOverlayHpaPdbOverrideNeeds
|
|
5259
|
+
*/
|
|
5260
|
+
|
|
5261
|
+
/**
|
|
5262
|
+
* Перевіряє наявність прод-оверрайдів у **kustomization.yaml** залежно від того, що успадковується з base.
|
|
5096
5263
|
* @param {Record<string, unknown>} kust об'єкт kustomization
|
|
5097
5264
|
* @param {string} rel відносний шлях для повідомлень
|
|
5098
5265
|
* @param {(msg: string) => void} fail callback при помилці
|
|
5099
5266
|
* @param {(msg: string) => void} passFn callback при успіху
|
|
5267
|
+
* @param {ProdOverlayHpaPdbOverrideNeeds} needs що саме має бути в **patches[]**
|
|
5100
5268
|
*/
|
|
5101
|
-
function checkProdOverridesInKustomization(kust, rel, fail, passFn) {
|
|
5269
|
+
function checkProdOverridesInKustomization(kust, rel, fail, passFn, needs) {
|
|
5102
5270
|
const byKind = kustomizationPatchPathsByTargetKind(kust)
|
|
5103
5271
|
const hpaPaths = byKind.get('HorizontalPodAutoscaler') ?? new Set()
|
|
5104
5272
|
const pdbPaths = byKind.get('PodDisruptionBudget') ?? new Set()
|
|
5105
5273
|
let ok = true
|
|
5106
|
-
if (
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5274
|
+
if (needs.needsHpaReplicaPatches) {
|
|
5275
|
+
if (!hpaPaths.has('/spec/minReplicas')) {
|
|
5276
|
+
fail(
|
|
5277
|
+
`${rel}: прод-оверлей має перевизначати spec.minReplicas для HorizontalPodAutoscaler (мінімум 2 у проді) (k8s.mdc)`
|
|
5278
|
+
)
|
|
5279
|
+
ok = false
|
|
5280
|
+
}
|
|
5281
|
+
if (!hpaPaths.has('/spec/maxReplicas')) {
|
|
5282
|
+
fail(
|
|
5283
|
+
`${rel}: прод-оверлей має перевизначати spec.maxReplicas для HorizontalPodAutoscaler (мінімум 2 у проді) (k8s.mdc)`
|
|
5284
|
+
)
|
|
5285
|
+
ok = false
|
|
5286
|
+
}
|
|
5117
5287
|
}
|
|
5118
|
-
if (
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5288
|
+
if (needs.needsPdbMinAvailablePatch) {
|
|
5289
|
+
if (!pdbPaths.has('/spec/minAvailable')) {
|
|
5290
|
+
fail(
|
|
5291
|
+
`${rel}: прод-оверлей має перевизначати spec.minAvailable для PodDisruptionBudget (мінімум 1 у проді) (k8s.mdc)`
|
|
5292
|
+
)
|
|
5293
|
+
ok = false
|
|
5294
|
+
}
|
|
5123
5295
|
}
|
|
5124
5296
|
if (ok) {
|
|
5125
|
-
passFn(`${rel}: прод-оверрайди HPA
|
|
5297
|
+
passFn(`${rel}: прод-оверрайди HPA/PDB за потреби присутні (k8s.mdc)`)
|
|
5126
5298
|
}
|
|
5127
5299
|
}
|
|
5128
5300
|
|
|
5129
5301
|
/**
|
|
5130
|
-
*
|
|
5302
|
+
* Які прод-оверрайди потрібні для **kustomization.yaml** (не dev-like), що посилається на **…/k8s/…/base**.
|
|
5131
5303
|
*
|
|
5132
|
-
*
|
|
5133
|
-
*
|
|
5134
|
-
* - `Deployment` (у шарі `…/k8s/…/base/`), і
|
|
5135
|
-
* - `HorizontalPodAutoscaler` і/або `PodDisruptionBudget`.
|
|
5304
|
+
* **HPA:** patches на **`minReplicas`/`maxReplicas`** лише якщо в base-дереві є HPA **і** base **не** містить
|
|
5305
|
+
* strategic-merge **`$patch: delete`** для **HorizontalPodAutoscaler** (k8s.mdc).
|
|
5136
5306
|
*
|
|
5137
|
-
*
|
|
5307
|
+
* **PDB:** **`minAvailable`** — якщо в base-дереві є Deployment і PDB.
|
|
5138
5308
|
* @param {string} rootNorm нормалізований корінь репозиторію
|
|
5139
5309
|
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
5140
|
-
* @returns {Promise<
|
|
5310
|
+
* @returns {Promise<ProdOverlayHpaPdbOverrideNeeds>} прапорці потрібних перевизначень
|
|
5141
5311
|
*/
|
|
5142
|
-
export async function
|
|
5312
|
+
export async function prodOverlayHpaPdbOverrideNeeds(rootNorm, kustAbs) {
|
|
5143
5313
|
const rel = (relative(rootNorm, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
5144
5314
|
const segment = k8sEnvSegmentFromRelPath(rel)
|
|
5145
|
-
if (segment === null || isDevLikeK8sEnvSegment(segment))
|
|
5315
|
+
if (segment === null || isDevLikeK8sEnvSegment(segment)) {
|
|
5316
|
+
return { needsHpaReplicaPatches: false, needsPdbMinAvailablePatch: false }
|
|
5317
|
+
}
|
|
5146
5318
|
|
|
5147
5319
|
const kust = await readFirstYamlObject(kustAbs)
|
|
5148
|
-
if (kust === null) return false
|
|
5320
|
+
if (kust === null) return { needsHpaReplicaPatches: false, needsPdbMinAvailablePatch: false }
|
|
5149
5321
|
|
|
5150
5322
|
const kustDir = dirname(kustAbs)
|
|
5151
5323
|
const pathRefs = resourcePathRefsFromKustomizationObject(kust)
|
|
5152
5324
|
const baseDirs = await k8sBaseDirsFromKustomizeResourcePathRefs(kustDir, pathRefs, rootNorm)
|
|
5153
|
-
if (baseDirs.length === 0) return false
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
)
|
|
5325
|
+
if (baseDirs.length === 0) return { needsHpaReplicaPatches: false, needsPdbMinAvailablePatch: false }
|
|
5326
|
+
|
|
5327
|
+
let needsHpaReplicaPatches = false
|
|
5328
|
+
let needsPdbMinAvailablePatch = false
|
|
5329
|
+
for (const bd of baseDirs) {
|
|
5330
|
+
const bk = join(bd, 'kustomization.yaml')
|
|
5331
|
+
const f = await kustomizeResourceTreeHpaPdbDeploymentFlags(bk, rootNorm)
|
|
5332
|
+
const baseDeletesHpa = await kustomizationDeclaresHpaStrategicDelete(bk, rootNorm)
|
|
5333
|
+
if (f.hasDeployment && f.hasPdb) {
|
|
5334
|
+
needsPdbMinAvailablePatch = true
|
|
5335
|
+
}
|
|
5336
|
+
if (f.hasDeployment && f.hasHpa && !baseDeletesHpa) {
|
|
5337
|
+
needsHpaReplicaPatches = true
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
return { needsHpaReplicaPatches, needsPdbMinAvailablePatch }
|
|
5341
|
+
}
|
|
5158
5342
|
|
|
5159
|
-
|
|
5343
|
+
/**
|
|
5344
|
+
* Чи прод-оверлей потребує **будь-яких** overrides HPA/PDB у **patches[]** (зведений прапорець).
|
|
5345
|
+
* @param {string} rootNorm нормалізований корінь репозиторію
|
|
5346
|
+
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
5347
|
+
* @returns {Promise<boolean>} true, якщо потрібен хоча б один тип оверрайду
|
|
5348
|
+
*/
|
|
5349
|
+
export async function prodOverlayNeedsHpaPdbOverrides(rootNorm, kustAbs) {
|
|
5350
|
+
const n = await prodOverlayHpaPdbOverrideNeeds(rootNorm, kustAbs)
|
|
5351
|
+
return n.needsHpaReplicaPatches || n.needsPdbMinAvailablePatch
|
|
5160
5352
|
}
|
|
5161
5353
|
|
|
5162
5354
|
/**
|
|
5163
|
-
* Для прод kustomization.yaml вимагає patches
|
|
5164
|
-
*
|
|
5355
|
+
* Для прод kustomization.yaml вимагає **patches[]** за потреби: **`/spec/minReplicas`** і **`/spec/maxReplicas`**
|
|
5356
|
+
* для **HorizontalPodAutoscaler** (якщо в успадкованому base лишився HPA без delete-patch), **`/spec/minAvailable`**
|
|
5357
|
+
* для **PDB** (якщо в base є PDB).
|
|
5165
5358
|
*
|
|
5166
5359
|
* Не застосовується до dev-like (base / dev / *-qa).
|
|
5167
|
-
*
|
|
5168
|
-
* Також **не застосовується**, якщо оверлей не наслідує base з Deployment + HPA/PDB (див. `prodOverlayNeedsHpaPdbOverrides`).
|
|
5169
5360
|
* @param {string} root корінь репозиторію
|
|
5170
5361
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
5171
5362
|
* @param {(msg: string) => void} fail callback при помилці
|
|
@@ -5176,9 +5367,10 @@ async function validateProdKustomizationOverrides(root, yamlFilesAbs, fail, pass
|
|
|
5176
5367
|
const kustFiles = yamlFilesAbs.filter(abs => basename(abs) === 'kustomization.yaml')
|
|
5177
5368
|
for (const kustAbs of kustFiles) {
|
|
5178
5369
|
const rel = (relative(rootNorm, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
5179
|
-
|
|
5370
|
+
const needs = await prodOverlayHpaPdbOverrideNeeds(rootNorm, kustAbs)
|
|
5371
|
+
if (!needs.needsHpaReplicaPatches && !needs.needsPdbMinAvailablePatch) continue
|
|
5180
5372
|
const kust = await readFirstYamlObject(kustAbs)
|
|
5181
|
-
if (kust !== null) checkProdOverridesInKustomization(kust, rel, fail, passFn)
|
|
5373
|
+
if (kust !== null) checkProdOverridesInKustomization(kust, rel, fail, passFn, needs)
|
|
5182
5374
|
}
|
|
5183
5375
|
}
|
|
5184
5376
|
|
|
@@ -5272,12 +5464,22 @@ function validatePdbForDeployment(pdbDocs, deployName, appLabel, isDevLike, pdbR
|
|
|
5272
5464
|
* @param {Record<string, unknown>} deployment об'єкт Deployment
|
|
5273
5465
|
* @param {string} deployRel відносний шлях каталогу для повідомлень
|
|
5274
5466
|
* @param {boolean} isDevLike чи середовище dev-like
|
|
5467
|
+
* @param {boolean} isK8sBaseLayer чи каталог під **`…/k8s/…/base/`** (HPA поруч не вимагаємо — прибирається в kustomization)
|
|
5275
5468
|
* @param {Record<string, unknown>[]} hpaDocs HPA-документи каталогу
|
|
5276
5469
|
* @param {Record<string, unknown>[]} pdbDocs PDB-документи каталогу
|
|
5277
5470
|
* @param {(msg: string) => void} fail callback при помилці
|
|
5278
5471
|
* @param {(msg: string) => void} passFn callback при успіху
|
|
5279
5472
|
*/
|
|
5280
|
-
function validateSingleDeploymentHpaPdbTopology(
|
|
5473
|
+
function validateSingleDeploymentHpaPdbTopology(
|
|
5474
|
+
deployment,
|
|
5475
|
+
deployRel,
|
|
5476
|
+
isDevLike,
|
|
5477
|
+
isK8sBaseLayer,
|
|
5478
|
+
hpaDocs,
|
|
5479
|
+
pdbDocs,
|
|
5480
|
+
fail,
|
|
5481
|
+
passFn
|
|
5482
|
+
) {
|
|
5281
5483
|
const deployName = manifestMetadataName(deployment)
|
|
5282
5484
|
const appLabel = deploymentAppLabel(deployment)
|
|
5283
5485
|
if (deployName === null) {
|
|
@@ -5294,7 +5496,9 @@ function validateSingleDeploymentHpaPdbTopology(deployment, deployRel, isDevLike
|
|
|
5294
5496
|
} else {
|
|
5295
5497
|
fail(`${deployRel}: Deployment '${deployName}': ${tscViolation}`)
|
|
5296
5498
|
}
|
|
5297
|
-
|
|
5499
|
+
if (!isK8sBaseLayer) {
|
|
5500
|
+
validateHpaForDeployment(hpaDocs, deployName, isDevLike, `${deployRel}/${HPA_FILENAME}`, fail, passFn)
|
|
5501
|
+
}
|
|
5298
5502
|
validatePdbForDeployment(pdbDocs, deployName, appLabel, isDevLike, `${deployRel}/${PDB_FILENAME}`, fail, passFn)
|
|
5299
5503
|
}
|
|
5300
5504
|
|
|
@@ -5310,11 +5514,30 @@ async function validateDeploymentsInDir(deployments, dir, root, fail, passFn) {
|
|
|
5310
5514
|
const relDir = relative(root, dir).replaceAll('\\', '/')
|
|
5311
5515
|
const segment = k8sEnvSegmentFromRelPath(relDir + '/')
|
|
5312
5516
|
const isDevLike = isDevLikeK8sEnvSegment(segment)
|
|
5517
|
+
const isK8sBaseLayer = isK8sYamlUnderBaseDirectory(`${relDir}/probe.yaml`)
|
|
5518
|
+
if (isK8sBaseLayer && deployments.length > 0) {
|
|
5519
|
+
const hpaAbs = join(dir, HPA_FILENAME)
|
|
5520
|
+
if (existsSync(hpaAbs)) {
|
|
5521
|
+
const prefix = relDir === '' ? '.' : relDir
|
|
5522
|
+
fail(
|
|
5523
|
+
`${prefix}/${HPA_FILENAME}: у шарі k8s/.../base не тримай локальний hpa.yaml — прибери HorizontalPodAutoscaler через $patch: delete у base/kustomization.yaml (k8s.mdc)`
|
|
5524
|
+
)
|
|
5525
|
+
}
|
|
5526
|
+
}
|
|
5313
5527
|
const hpaDocs = await readDocsByKindInDir(dir, 'HorizontalPodAutoscaler', HPA_FILENAME)
|
|
5314
5528
|
const pdbDocs = await readDocsByKindInDir(dir, 'PodDisruptionBudget', PDB_FILENAME)
|
|
5315
5529
|
const deployRel = relDir === '' ? '.' : relDir
|
|
5316
5530
|
for (const deployment of deployments) {
|
|
5317
|
-
validateSingleDeploymentHpaPdbTopology(
|
|
5531
|
+
validateSingleDeploymentHpaPdbTopology(
|
|
5532
|
+
deployment,
|
|
5533
|
+
deployRel,
|
|
5534
|
+
isDevLike,
|
|
5535
|
+
isK8sBaseLayer,
|
|
5536
|
+
hpaDocs,
|
|
5537
|
+
pdbDocs,
|
|
5538
|
+
fail,
|
|
5539
|
+
passFn
|
|
5540
|
+
)
|
|
5318
5541
|
}
|
|
5319
5542
|
}
|
|
5320
5543
|
|
|
@@ -5333,9 +5556,9 @@ async function extractDeploymentsFromFile(filePath) {
|
|
|
5333
5556
|
|
|
5334
5557
|
/**
|
|
5335
5558
|
* Для кожного **Deployment** у шарі **`…/k8s/…/base/`** (будь-який YAML у відповідному каталозі) перевіряє:
|
|
5336
|
-
*
|
|
5337
|
-
*
|
|
5338
|
-
*
|
|
5559
|
+
* заборона локального **`hpa.yaml`**; **`pdb.yaml`** (валідний `policy/v1`); канонічні **topologySpreadConstraints**.
|
|
5560
|
+
* HPA для dev прибирається в **`base/kustomization.yaml`** через **`$patch: delete`** (див. `verifyK8sBaseKustomizeHpaDeletedWhenInherited`).
|
|
5561
|
+
* Env-залежні межі — за сегментом після `/k8s/`: dev-like vs прод.
|
|
5339
5562
|
* @param {string} root корінь репозиторію
|
|
5340
5563
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
5341
5564
|
* @param {(msg: string) => void} fail callback при помилці
|
|
@@ -6018,7 +6241,7 @@ function applyConversionsToDoc(doc, conversions) {
|
|
|
6018
6241
|
byPatch.set(c.index, slot)
|
|
6019
6242
|
}
|
|
6020
6243
|
|
|
6021
|
-
const sortedIdx = byPatch.keys().toSorted((a, b) => b - a)
|
|
6244
|
+
const sortedIdx = [...byPatch.keys()].toSorted((a, b) => b - a)
|
|
6022
6245
|
for (const i of sortedIdx) {
|
|
6023
6246
|
const slot = byPatch.get(i)
|
|
6024
6247
|
if (slot === undefined) continue
|