@nitra/cursor 1.8.63 → 1.8.69
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/abie.mdc +48 -18
- package/mdc/k8s.mdc +52 -32
- package/package.json +1 -1
- package/scripts/check-abie.mjs +224 -4
- package/scripts/check-k8s.mjs +10 -20
package/mdc/abie.mdc
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Правила для проєктів
|
|
2
|
+
description: Правила для проєктів AbInBev Efes
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.2'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Правило **abie** для споживачів **@nitra/cursor**: **k8s** (**Deployment** + **HealthCheckPolicy**), overlay **ua** / **ru** (**nodeSelector**, видалення **HealthCheckPolicy** у **ru**) і гілки в **clean-merged-branch**. **`npx @nitra/cursor check abie`** виконується лише якщо в **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## k8s: `hc.yaml` поруч із Deployment
|
|
10
|
+
|
|
11
|
+
Якщо під **`k8s`** є **`kind: Deployment`**, у **тій самій директорії** має бути **`hc.yaml`** з **HealthCheckPolicy** (**`networking.gke.io/v1`**): коректний modeline **`$schema`**, **`/healthz`**, порт **8080**, **`targetRef.name`** = **`metadata.name`**. Деталі — **`validateAbieHcYaml`** у **`npm/scripts/check-abie.mjs`**.
|
|
10
12
|
|
|
11
13
|
```yaml title="hc.yaml"
|
|
12
14
|
# yaml-language-server: $schema=https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json
|
|
13
15
|
apiVersion: networking.gke.io/v1
|
|
14
16
|
kind: HealthCheckPolicy
|
|
15
17
|
metadata:
|
|
16
|
-
name:
|
|
17
|
-
namespace: dev #
|
|
18
|
+
name: СЕРВІС
|
|
19
|
+
namespace: dev # kustomize overlay
|
|
18
20
|
spec:
|
|
19
21
|
default:
|
|
20
22
|
config:
|
|
@@ -25,34 +27,62 @@ spec:
|
|
|
25
27
|
targetRef:
|
|
26
28
|
group: ''
|
|
27
29
|
kind: Service
|
|
28
|
-
name:
|
|
30
|
+
name: СЕРВІС
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
## k8s: overlay **`ru`** і HealthCheckPolicy
|
|
34
|
+
|
|
35
|
+
Якщо в дереві **k8s** є **HealthCheckPolicy**, **check abie** вимагає **`ru/kustomization.yaml`** з patch **`$patch: delete`** для політики (узгоджено з **k8s.mdc** / **check-k8s**, **`ruKustomizationHasHealthCheckDeletePatch`** у **`npm/scripts/check-k8s.mjs`**). Підстав реальне ім’я замість **`СЕРВІС`**:
|
|
32
36
|
|
|
33
|
-
```yaml title="kustomization.yaml"
|
|
37
|
+
```yaml title="…/ru/kustomization.yaml (фрагмент)"
|
|
34
38
|
patches:
|
|
35
39
|
- target:
|
|
36
40
|
kind: HealthCheckPolicy
|
|
41
|
+
name: СЕРВІС
|
|
37
42
|
patch: |-
|
|
38
43
|
kind: HealthCheckPolicy
|
|
39
44
|
metadata:
|
|
40
|
-
name:
|
|
45
|
+
name: СЕРВІС
|
|
41
46
|
$patch: delete
|
|
42
47
|
```
|
|
43
48
|
|
|
44
|
-
##
|
|
49
|
+
## k8s: overlay **`ua`** / **`ru`** і nodeSelector
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
Якщо під **`k8s`** є **Deployment**, у **кожному** **`…/ua/kustomization.yaml`** та **`…/ru/kustomization.yaml`** має бути inline **JSON6902** у **`patches[].patch`** на **`target.kind: Deployment`** з **nodeSelector** за політикою abie (**ua** — **add** + **preem** false; **ru** — **replace** + **yandex.cloud/preemptible** false). Критерії збігу — **`kustomizationHasAbieDeploymentNodeSelectorPatch`** у **`npm/scripts/check-abie.mjs`**.
|
|
47
52
|
|
|
48
|
-
```yaml title="
|
|
49
|
-
|
|
53
|
+
```yaml title="…/ua/kustomization.yaml (фрагмент)"
|
|
54
|
+
patches:
|
|
55
|
+
- target:
|
|
56
|
+
kind: Deployment
|
|
57
|
+
name: my-app
|
|
58
|
+
patch: |-
|
|
59
|
+
- op: add
|
|
60
|
+
path: /spec/template/spec/nodeSelector
|
|
61
|
+
value:
|
|
62
|
+
preem: 'false'
|
|
50
63
|
```
|
|
51
64
|
|
|
52
|
-
|
|
65
|
+
```yaml title="…/ru/kustomization.yaml (фрагмент)"
|
|
66
|
+
patches:
|
|
67
|
+
- target:
|
|
68
|
+
kind: Deployment
|
|
69
|
+
name: my-app
|
|
70
|
+
patch: |-
|
|
71
|
+
- op: replace
|
|
72
|
+
path: /spec/template/spec/nodeSelector
|
|
73
|
+
value:
|
|
74
|
+
yandex.cloud/preemptible: "false"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Git branches
|
|
53
78
|
|
|
54
|
-
|
|
79
|
+
У **`.github/workflows/clean-merged-branch.yml`** у кроці **`phpdocker-io/github-actions-delete-abandoned-branches`** значення **`with.ignore_branches`** має містити **dev**, **ua** та **ru** (разом з іншими гілками, якщо потрібно), наприклад:
|
|
55
80
|
|
|
56
|
-
|
|
81
|
+
```yaml title=".github/workflows/clean-merged-branch.yml (фрагмент)"
|
|
82
|
+
with:
|
|
83
|
+
ignore_branches: main,dev,ua,ru
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Перевірка
|
|
57
87
|
|
|
58
|
-
|
|
88
|
+
**`npx @nitra/cursor check abie`**
|
package/mdc/k8s.mdc
CHANGED
|
@@ -42,7 +42,7 @@ alwaysApply: false
|
|
|
42
42
|
}
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
Якщо правило **`k8s`** підключено в **`.n-cursor.json`** (масив **`rules`**), у **кореневому** `package.json`
|
|
45
|
+
Якщо правило **`k8s`** підключено в **`.n-cursor.json`** (масив **`rules`**), у **кореневому** `package.json` **мають** бути скрипт **`lint-k8s`** і виклик **`bun run lint-k8s`** у агрегованому **`lint`** (див. **`bun.mdc`**). Це перевіряє **`npx @nitra/cursor check bun`**.
|
|
46
46
|
|
|
47
47
|
Шлях до скрипта підстав свій (`./scripts/…` після копіювання, `node_modules/@nitra/cursor/scripts/…` якщо пакет у залежностях).
|
|
48
48
|
|
|
@@ -96,34 +96,25 @@ jobs:
|
|
|
96
96
|
|
|
97
97
|
## Deployment: `resources`
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
У **`Deployment`** у кожному **`containers`** / **`initContainers`** має бути **`resources`**; якщо лімітів ще немає — мінімум порожній запис:
|
|
100
100
|
|
|
101
101
|
```yaml
|
|
102
102
|
resources: {}
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
**`check k8s`** перевіряє наявність **`resources`** у кожному документі **Deployment** під **`k8s`**. Поле **`imagePullPolicy`** скрипт **не** перевіряє (залишається політиці Kubernetes за тегом образу).
|
|
106
106
|
|
|
107
|
-
**`
|
|
108
|
-
|
|
109
|
-
Якщо в **`Deployment`** у **`spec.template.spec.containers`** або **`initContainers`** задано образ **`hasura/graphql-engine`**, у полі **`image`** має бути саме **`hasura/graphql-engine:v2.48.15.ubi.amd64`** (для еквівалента Docker Hub допускається префікс **`docker.io/`**). Інші теги або сторонні реєстри з тим самим репозиторієм **`hasura/graphql-engine`** — порушення **`check k8s`**.
|
|
107
|
+
Образ **`hasura/graphql-engine`**: дозволений лише канонічний тег із константи **`HASURA_GRAPHQL_ENGINE_IMAGE`** у **`check-k8s.mjs`** (допускається префікс **`docker.io/`**); решта — помилка **check k8s**.
|
|
110
108
|
|
|
111
109
|
## Service: заборонені анотації GKE
|
|
112
110
|
|
|
113
|
-
У **`kind: Service`** не
|
|
114
|
-
|
|
115
|
-
- **`cloud.google.com/neg`**
|
|
116
|
-
- **`cloud.google.com/backend-config`**
|
|
117
|
-
|
|
118
|
-
Вони потрібні для інтеграції з **Ingress** / класичним балансуванням GKE; після переходу на **Gateway API** їх слід **прибрати** з маніфестів. **`check k8s`** завершиться помилкою, якщо хоча б один із цих ключів присутній.
|
|
111
|
+
У **`kind: Service`** не додавай у **`metadata.annotations`** **`cloud.google.com/neg`** і **`cloud.google.com/backend-config`** (legacy під Ingress / старе балансування GKE). **check k8s** падає, якщо ключ є.
|
|
119
112
|
|
|
120
113
|
## Service: `svc.yaml` і `svc-hl.yaml` (Gateway API)
|
|
121
114
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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`**).
|
|
115
|
+
Пара файлів для кластерного й headless **Service**: **`svc.yaml`** + **`svc-hl.yaml`** в одному каталозі, **`spec.type: ClusterIP`** / **`clusterIP: None`**, імена **`-hl`**, узгодженість пар, маршрути **`gateway.networking.k8s.io`** (**HTTPRoute**, **GRPCRoute**, **TCPRoute**, **TLSRoute**, **UDPRoute**) — **backendRef** лише на сервіси з суфіксом **`-hl`**; якщо **kustomization** посилається на **`svc.yaml`**, у **тому ж** **`kustomization.yaml`** має бути посилання на sibling **`svc-hl.yaml`**. Скрипт **не** створює файли — додай **`svc-hl.yaml`** вручну (копія з правками **name** / **clusterIP**).
|
|
116
|
+
|
|
117
|
+
**Точні умови та повідомлення `fail`** — верхній JSDoc **`npm/scripts/check-k8s.mjs`**.
|
|
127
118
|
|
|
128
119
|
## Kustomize: структура каталогів (`base` / overlays)
|
|
129
120
|
|
|
@@ -142,11 +133,11 @@ resources: {}
|
|
|
142
133
|
|
|
143
134
|
### Namespace
|
|
144
135
|
|
|
145
|
-
- **`base/kustomization.yaml`:**
|
|
136
|
+
- **`base/kustomization.yaml`:** поле **`namespace:`** має бути **непорожнім** (перевіряє **check k8s**, якщо файл є).
|
|
146
137
|
|
|
147
|
-
- **Де не дублювати `metadata.namespace`:** у YAML
|
|
138
|
+
- **Де не дублювати `metadata.namespace`:** у YAML, досяжних через **граф Kustomize** (шляхи з **`kustomization.yaml`**, як у логіці **`collectKustomizeManagedRelPaths`** / **check k8s**). **Namespace** задає **`namespace:`** у kustomization.
|
|
148
139
|
|
|
149
|
-
- **Коли `metadata.namespace`
|
|
140
|
+
- **Коли `metadata.namespace` обов’язковий у файлі:** YAML під **`k8s`**, який **не** в графі жодного kustomization — непорожній **`metadata.namespace`** для namespaced **kind** (винятки — кластерні **kind**, перелік **`CLUSTER_SCOPED_KINDS`** у **`check-k8s.mjs`**). Якщо namespace у маніфесті не потрібен — підключи файл через **`resources`** / **`patches`** тощо.
|
|
150
141
|
|
|
151
142
|
- **Не додавай** окремі **patches** Kustomize, які лише змінюють **namespace**: **namespace** визначає Kustomize; у overlays додаткові зміни — без дублювання логіки **namespace**.
|
|
152
143
|
|
|
@@ -182,27 +173,56 @@ resources: {}
|
|
|
182
173
|
|
|
183
174
|
**`check k8s`:** заборонено **`kind: Ingress`**.
|
|
184
175
|
|
|
185
|
-
|
|
176
|
+
3. Якщо **HTTPRoute** посилається на **Service** в іншому **namespace**, потрібен **ReferenceGrant** (дозвіл). Наприклад **HTTPRoute** в **`contract`** і **Service** в **`dev`** — додай маніфест на кшталт **`base/rg.yaml`** (фрагмент нижче).
|
|
177
|
+
|
|
178
|
+
```yaml title="base/rg.yaml"
|
|
179
|
+
# yaml-language-server: $schema=https://datreeio.github.io/CRDs-catalog/gateway.networking.k8s.io/referencegrant_v1beta1.json
|
|
180
|
+
apiVersion: gateway.networking.k8s.io/v1beta1
|
|
181
|
+
kind: ReferenceGrant
|
|
182
|
+
metadata:
|
|
183
|
+
name: contract-to-dev
|
|
184
|
+
namespace: dev
|
|
185
|
+
spec:
|
|
186
|
+
from:
|
|
187
|
+
- group: gateway.networking.k8s.io
|
|
188
|
+
kind: HTTPRoute
|
|
189
|
+
namespace: contract
|
|
190
|
+
to:
|
|
191
|
+
- group: ''
|
|
192
|
+
kind: Service
|
|
193
|
+
# Якщо name не вказано, доступ дозволено до всіх Service в цьому namespace
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**ReferenceGrant** має бути в **namespace** тих **Service**, до яких відкривається доступ (у прикладі — **`dev`**). Якщо **`namespace:`** у **overlay** не збігається з **namespace** гранта, **не** додавай **`base/rg.yaml`** у **`resources:`** того overlay — Kustomize може перезаписати **`metadata.namespace`** гранта. Натомість застосуй **`patches`** (JSON patch), щоб явно виставити потрібний **namespace**:
|
|
197
|
+
|
|
198
|
+
```yaml title="overlay/kustomization.yaml (фрагмент)"
|
|
199
|
+
patches:
|
|
200
|
+
- target:
|
|
201
|
+
kind: ReferenceGrant
|
|
202
|
+
name: contract-to-dev
|
|
203
|
+
patch: |-
|
|
204
|
+
- op: replace
|
|
205
|
+
path: /metadata/namespace
|
|
206
|
+
value: dev
|
|
207
|
+
```
|
|
186
208
|
|
|
187
|
-
|
|
209
|
+
## Перевірка
|
|
188
210
|
|
|
189
|
-
|
|
211
|
+
**`npx @nitra/cursor check k8s`** — програмні критерії в **JSDoc на початку** **`npm/scripts/check-k8s.mjs`**. Якщо під **`k8s`** немає **`*.yaml`** — крок пропущено. Канон **`$schema`** для редактора — розділ **«Визначення схеми YAML`** нижче.
|
|
190
212
|
|
|
191
|
-
|
|
213
|
+
**Не входить у check k8s:** наприклад **ReferenceGrant** і доступ між **namespace** (лише рекомендації тут), **kubeconform** / **kubescape** — це **`bun run lint-k8s`**.
|
|
192
214
|
|
|
193
|
-
|
|
215
|
+
## Що саме в скрипті `check-k8s.mjs`
|
|
194
216
|
|
|
195
|
-
|
|
217
|
+
Повний перелік умов, константи (**`YANNH_PIN`**, **`CLUSTER_SCOPED_KINDS`**, **`HASURA_GRAPHQL_ENGINE_IMAGE`** тощо) і допоміжні функції — у файлі скрипта; змінив вимогу для **check** — онови **JSDoc** і за потреби тести в **`npm/tests/check-k8s-schema.test.mjs`**.
|
|
196
218
|
|
|
197
|
-
При зміні **PIN** версії Kubernetes
|
|
219
|
+
При зміні **PIN** версії Kubernetes узгодь **`check-k8s.mjs`**, **`run-k8s.mjs`** (**`KUBERNETES_VERSION`**, **`DATREE_CRD_SCHEMA_LOCATION`**) і цей файл (**lint-k8s**, **Визначення схеми YAML**).
|
|
198
220
|
|
|
199
221
|
## Коли застосовувати (агентам)
|
|
200
222
|
|
|
201
|
-
-
|
|
202
|
-
-
|
|
203
|
-
-
|
|
204
|
-
- Дотримуйся структури **Kustomize** (`base` = dev, overlays без дублювання `base`, коментарі для рядків, що змінюються в overlay). У **`base/kustomization.yaml`** завжди задавай непорожній **`namespace:`**. У **`k8s/base`** у кожному ресурсному YAML має бути явний **`metadata.namespace`**. Поза **base**, якщо не хочеш **`metadata.namespace`** у файлі — підключи його до kustomization (**`resources`** / **`patches`** тощо); інакше додай явний **`metadata.namespace`**.
|
|
205
|
-
- Після міграції на нову структуру **видали** застарілі файли та каталоги, які вже замінені (див. **Міграція зі старої структури**).
|
|
223
|
+
- Після змін у k8s YAML: **`npx @nitra/cursor check k8s`** і за наявності правила — **`bun run lint-k8s`**.
|
|
224
|
+
- Оновив **`apiVersion` / `kind`** — підправ **перший** рядок **`$schema`** (див. **Визначення схеми YAML**).
|
|
225
|
+
- Дотримуйся **Kustomize** з цього правила; деталі **namespace** / графа ресурсів — **check k8s** + підказки в JSDoc скрипта.
|
|
206
226
|
|
|
207
227
|
## Визначення схеми YAML (канон)
|
|
208
228
|
|
package/package.json
CHANGED
package/scripts/check-abie.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Перевіряє відповідність проєкту правилу abie.mdc (проєкти
|
|
2
|
+
* Перевіряє відповідність проєкту правилу abie.mdc (проєкти AbInBev Efes).
|
|
3
3
|
*
|
|
4
4
|
* Застосовується лише якщо у **`.n-cursor.json`** у масиві **`rules`** є **`abie`** — інакше вихід **0**
|
|
5
5
|
* без перевірок (щоб не суперечити типовому **ga.mdc** з **`ignore_branches: main,dev`**).
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
* має існувати **`hc.yaml`** із **`HealthCheckPolicy`** (**`networking.gke.io/v1`**), modeline **`$schema`**
|
|
13
13
|
* як у abie.mdc, **`/healthz`**, порт **8080**, **`targetRef`** на **Service** з тим самим **`metadata.name`**.
|
|
14
14
|
* Якщо в дереві **k8s** є **HealthCheckPolicy**, перевіряється **`ru/kustomization.yaml`** з patch **`$patch: delete`**
|
|
15
|
-
* (
|
|
15
|
+
* (логіка вмісту — **`ruKustomizationHasHealthCheckDeletePatch`** у **check-k8s.mjs**, узгоджено з **k8s.mdc**).
|
|
16
|
+
*
|
|
17
|
+
* **nodeSelector:** якщо є **Deployment** під **k8s**, у кожному **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`**
|
|
18
|
+
* має бути inline **JSON6902** patch на **`kind: Deployment`**: для **ua** — **`op: add`**, **`path: /spec/template/spec/nodeSelector`**,
|
|
19
|
+
* **`preem: false`**; для **ru** — **`op: replace`**, той самий **path**, **`yandex.cloud/preemptible: false`** (див. abie.mdc).
|
|
16
20
|
*/
|
|
17
21
|
import { existsSync } from 'node:fs'
|
|
18
22
|
import { readFile } from 'node:fs/promises'
|
|
@@ -20,7 +24,7 @@ import { dirname, join, relative } from 'node:path'
|
|
|
20
24
|
|
|
21
25
|
import { parseAllDocuments } from 'yaml'
|
|
22
26
|
|
|
23
|
-
import {
|
|
27
|
+
import { pathHasK8sSegment, ruKustomizationHasHealthCheckDeletePatch } from './check-k8s.mjs'
|
|
24
28
|
import { pass } from './utils/pass.mjs'
|
|
25
29
|
import { flattenWorkflowSteps, getStepUses, parseWorkflowYaml } from './utils/gha-workflow.mjs'
|
|
26
30
|
import { walkDir } from './utils/walkDir.mjs'
|
|
@@ -35,6 +39,26 @@ const MODELINE_RE = /^#\s*yaml-language-server:\s*\$schema=(\S+)\s*$/
|
|
|
35
39
|
/** Гілки, які мають бути в **`ignore_branches`** за abie.mdc. */
|
|
36
40
|
export const ABIE_REQUIRED_IGNORE_BRANCHES = ['dev', 'ua', 'ru']
|
|
37
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Чи відносний шлях вказує на **`ru/kustomization.yaml`** (сегмент **`ru`** перед ім’ям файлу) — специфіка abie overlay.
|
|
44
|
+
* @param {string} rel шлях від кореня репозиторію
|
|
45
|
+
* @returns {boolean} true, якщо це `…/ru/kustomization.yaml`
|
|
46
|
+
*/
|
|
47
|
+
export function isRuKustomizationPath(rel) {
|
|
48
|
+
const norm = rel.replaceAll('\\', '/')
|
|
49
|
+
return /(^|\/)ru\/kustomization\.yaml$/u.test(norm)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Чи відносний шлях вказує на **`ua/kustomization.yaml`** (сегмент **`ua`** перед ім’ям файлу) — специфіка abie overlay.
|
|
54
|
+
* @param {string} rel шлях від кореня репозиторію
|
|
55
|
+
* @returns {boolean} true, якщо це `…/ua/kustomization.yaml`
|
|
56
|
+
*/
|
|
57
|
+
export function isUaKustomizationPath(rel) {
|
|
58
|
+
const norm = rel.replaceAll('\\', '/')
|
|
59
|
+
return /(^|\/)ua\/kustomization\.yaml$/u.test(norm)
|
|
60
|
+
}
|
|
61
|
+
|
|
38
62
|
/**
|
|
39
63
|
* Чи увімкнено правило **abie** у конфігу репозиторію.
|
|
40
64
|
* @param {string} root корінь репозиторію (cwd)
|
|
@@ -198,6 +222,136 @@ function stripBom(s) {
|
|
|
198
222
|
return s.startsWith('\uFEFF') ? s.slice(1) : s
|
|
199
223
|
}
|
|
200
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Чи рядок inline JSON6902 patch містить очікуваний **ua** nodeSelector (**op: add**, **preem: false**).
|
|
227
|
+
* @param {string} patchText поле **patch** у kustomization
|
|
228
|
+
* @returns {boolean} true, якщо критерії abie.mdc виконано
|
|
229
|
+
*/
|
|
230
|
+
function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
|
|
231
|
+
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
if (!/op:\s*add\b/u.test(patchText)) {
|
|
235
|
+
return false
|
|
236
|
+
}
|
|
237
|
+
if (!/path:\s*\/spec\/template\/spec\/nodeSelector\b/u.test(patchText)) {
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
if (!/\bpreem:\s*['"]?false['"]?\b/u.test(patchText)) {
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
return true
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Чи рядок inline JSON6902 patch містить очікуваний **ru** nodeSelector (**op: replace**, **yandex.cloud/preemptible: false**).
|
|
248
|
+
* @param {string} patchText поле **patch** у kustomization
|
|
249
|
+
* @returns {boolean} true, якщо критерії abie.mdc виконано
|
|
250
|
+
*/
|
|
251
|
+
function jsonPatchTextHasRuDeploymentNodeSelector(patchText) {
|
|
252
|
+
if (typeof patchText !== 'string' || patchText.trim() === '') {
|
|
253
|
+
return false
|
|
254
|
+
}
|
|
255
|
+
if (!/op:\s*replace\b/u.test(patchText)) {
|
|
256
|
+
return false
|
|
257
|
+
}
|
|
258
|
+
if (!/path:\s*\/spec\/template\/spec\/nodeSelector\b/u.test(patchText)) {
|
|
259
|
+
return false
|
|
260
|
+
}
|
|
261
|
+
if (!/yandex\.cloud\/preemptible:\s*['"]?false['"]?/u.test(patchText)) {
|
|
262
|
+
return false
|
|
263
|
+
}
|
|
264
|
+
return true
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Чи один елемент **patches** у kustomization відповідає abie nodeSelector для заданого **mode**.
|
|
269
|
+
* @param {unknown} p елемент масиву **patches**
|
|
270
|
+
* @param {'ua' | 'ru'} mode який overlay перевіряти
|
|
271
|
+
* @returns {boolean} true, якщо patch відповідає abie для **mode**
|
|
272
|
+
*/
|
|
273
|
+
function inlineKustomizationPatchMatchesAbieMode(p, mode) {
|
|
274
|
+
if (p === null || typeof p !== 'object' || Array.isArray(p)) {
|
|
275
|
+
return false
|
|
276
|
+
}
|
|
277
|
+
const pr = /** @type {Record<string, unknown>} */ (p)
|
|
278
|
+
const target = pr.target
|
|
279
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) {
|
|
280
|
+
return false
|
|
281
|
+
}
|
|
282
|
+
const tg = /** @type {Record<string, unknown>} */ (target)
|
|
283
|
+
if (tg.kind !== 'Deployment') {
|
|
284
|
+
return false
|
|
285
|
+
}
|
|
286
|
+
const patchStr = pr.patch
|
|
287
|
+
if (typeof patchStr !== 'string') {
|
|
288
|
+
return false
|
|
289
|
+
}
|
|
290
|
+
if (mode === 'ua' && jsonPatchTextHasUaDeploymentNodeSelector(patchStr)) {
|
|
291
|
+
return true
|
|
292
|
+
}
|
|
293
|
+
if (mode === 'ru' && jsonPatchTextHasRuDeploymentNodeSelector(patchStr)) {
|
|
294
|
+
return true
|
|
295
|
+
}
|
|
296
|
+
return false
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Чи один YAML-документ kustomization містить відповідний inline patch на Deployment.
|
|
301
|
+
* @param {import('yaml').Document} doc документ після **parseAllDocuments**
|
|
302
|
+
* @param {'ua' | 'ru'} mode який overlay перевіряти
|
|
303
|
+
* @returns {boolean} true, якщо знайдено відповідний patch
|
|
304
|
+
*/
|
|
305
|
+
function kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode) {
|
|
306
|
+
if (doc.errors.length > 0) {
|
|
307
|
+
return false
|
|
308
|
+
}
|
|
309
|
+
const root = doc.toJSON()
|
|
310
|
+
if (root === null || typeof root !== 'object' || Array.isArray(root)) {
|
|
311
|
+
return false
|
|
312
|
+
}
|
|
313
|
+
const rec = /** @type {Record<string, unknown>} */ (root)
|
|
314
|
+
if (rec.kind !== 'Kustomization') {
|
|
315
|
+
return false
|
|
316
|
+
}
|
|
317
|
+
const patches = rec.patches
|
|
318
|
+
if (!Array.isArray(patches)) {
|
|
319
|
+
return false
|
|
320
|
+
}
|
|
321
|
+
for (const p of patches) {
|
|
322
|
+
if (inlineKustomizationPatchMatchesAbieMode(p, mode)) {
|
|
323
|
+
return true
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return false
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Чи **kustomization.yaml** містить inline **patches** на **Deployment** з nodeSelector за abie.mdc (**ua** або **ru**).
|
|
331
|
+
* @param {string} raw повний текст файлу
|
|
332
|
+
* @param {'ua' | 'ru'} mode який overlay перевіряти
|
|
333
|
+
* @returns {boolean} true, якщо знайдено відповідний patch
|
|
334
|
+
*/
|
|
335
|
+
export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
|
|
336
|
+
const body = stripBom(raw)
|
|
337
|
+
const lines = body.split(/\r?\n/u)
|
|
338
|
+
const first = lines[0] ?? ''
|
|
339
|
+
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
340
|
+
/** @type {import('yaml').Document[]} */
|
|
341
|
+
let docs
|
|
342
|
+
try {
|
|
343
|
+
docs = parseAllDocuments(rest)
|
|
344
|
+
} catch {
|
|
345
|
+
return false
|
|
346
|
+
}
|
|
347
|
+
for (const doc of docs) {
|
|
348
|
+
if (kustomizationDocumentHasAbieDeploymentNodeSelectorPatch(doc, mode)) {
|
|
349
|
+
return true
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return false
|
|
353
|
+
}
|
|
354
|
+
|
|
201
355
|
/**
|
|
202
356
|
* Перевіряє **hc.yaml** на відповідність abie.mdc.
|
|
203
357
|
* @param {string} raw повний текст файлу
|
|
@@ -341,7 +495,7 @@ async function collectHealthCheckPolicyRelPaths(root, yamlAbs) {
|
|
|
341
495
|
}
|
|
342
496
|
|
|
343
497
|
/**
|
|
344
|
-
* Якщо є **HealthCheckPolicy**, вимагає **ru/kustomization.yaml** з patch видалення (
|
|
498
|
+
* Якщо є **HealthCheckPolicy**, вимагає **ru/kustomization.yaml** з patch видалення (**ruKustomizationHasHealthCheckDeletePatch** у **check-k8s**).
|
|
345
499
|
* @param {string} root корінь
|
|
346
500
|
* @param {string[]} yamlFilesAbs абсолютні шляхи yaml k8s
|
|
347
501
|
* @param {string[]} healthCheckPolicyRelativePaths відносні шляхи
|
|
@@ -377,6 +531,67 @@ async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, health
|
|
|
377
531
|
)
|
|
378
532
|
}
|
|
379
533
|
|
|
534
|
+
/**
|
|
535
|
+
* Якщо є **Deployment** під **k8s**, вимагає в кожному overlay **ua** та **ru** (**kustomization.yaml**) JSON6902 patch nodeSelector (abie.mdc).
|
|
536
|
+
* @param {string} root корінь репозиторію
|
|
537
|
+
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
538
|
+
* @param {(msg: string) => void} fail callback
|
|
539
|
+
* @returns {Promise<void>}
|
|
540
|
+
*/
|
|
541
|
+
async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail) {
|
|
542
|
+
const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
543
|
+
if (uaAbsList.length === 0) {
|
|
544
|
+
fail(
|
|
545
|
+
'Є Deployment у k8s — додай ua/kustomization.yaml з inline patch на Deployment: op add, path /spec/template/spec/nodeSelector, preem false (abie.mdc)'
|
|
546
|
+
)
|
|
547
|
+
return
|
|
548
|
+
}
|
|
549
|
+
for (const abs of uaAbsList) {
|
|
550
|
+
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
551
|
+
let raw
|
|
552
|
+
try {
|
|
553
|
+
raw = await readFile(abs, 'utf8')
|
|
554
|
+
} catch (error) {
|
|
555
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
556
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
|
|
560
|
+
fail(
|
|
561
|
+
`${rel}: потрібен patch target kind Deployment з op: add, path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`
|
|
562
|
+
)
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
pass(`${rel}: nodeSelector patch (ua) відповідає abie.mdc`)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
569
|
+
if (ruAbsList.length === 0) {
|
|
570
|
+
fail(
|
|
571
|
+
'Є Deployment у k8s — додай ru/kustomization.yaml з inline patch на Deployment: op replace, path /spec/template/spec/nodeSelector, yandex.cloud/preemptible false (abie.mdc)'
|
|
572
|
+
)
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
for (const abs of ruAbsList) {
|
|
576
|
+
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
577
|
+
let raw
|
|
578
|
+
try {
|
|
579
|
+
raw = await readFile(abs, 'utf8')
|
|
580
|
+
} catch (error) {
|
|
581
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
582
|
+
fail(`${rel}: не вдалося прочитати (${msg})`)
|
|
583
|
+
return
|
|
584
|
+
}
|
|
585
|
+
if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ru')) {
|
|
586
|
+
fail(
|
|
587
|
+
`${rel}: потрібен patch target kind Deployment з op: replace, path /spec/template/spec/nodeSelector та yandex.cloud/preemptible: false (abie.mdc)`
|
|
588
|
+
)
|
|
589
|
+
return
|
|
590
|
+
}
|
|
591
|
+
pass(`${rel}: nodeSelector patch (ru) відповідає abie.mdc`)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
380
595
|
/**
|
|
381
596
|
* Перевіряє відповідність проєкту правилам abie.mdc.
|
|
382
597
|
* @returns {Promise<number>} 0 — OK, 1 — є порушення
|
|
@@ -464,5 +679,10 @@ export async function check() {
|
|
|
464
679
|
const healthCheckPolicyRelativePaths = await collectHealthCheckPolicyRelPaths(root, yamlFiles)
|
|
465
680
|
await ensureRuKustomizationHealthCheckDelete(root, yamlFiles, healthCheckPolicyRelativePaths, fail)
|
|
466
681
|
|
|
682
|
+
if (deploymentDirs.size > 0) {
|
|
683
|
+
pass('Є Deployment — перевіряємо nodeSelector у ua/ru kustomization (abie.mdc)')
|
|
684
|
+
await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, fail)
|
|
685
|
+
}
|
|
686
|
+
|
|
467
687
|
return exitCode
|
|
468
688
|
}
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* (datree за замовчуванням: GitHub Pages `https://datreeio.github.io/CRDs-catalog/…`).
|
|
7
7
|
*
|
|
8
8
|
* Додатково: у кожному YAML-документі з **`kind: Deployment`** у кожного контейнера
|
|
9
|
-
* **`spec.template.spec.containers[]`** має бути ключ **`resources`** (значення —
|
|
9
|
+
* **`spec.template.spec.containers[]`** має бути ключ **`resources`** (значення — об’єкт, допускається
|
|
10
10
|
* порожній **`{}`**). Поле **`imagePullPolicy`** не перевіряється — діють типові правила Kubernetes
|
|
11
|
-
* (`:latest` або
|
|
11
|
+
* (`:latest` або коли тег не вказано → **Always**, інші теги → **IfNotPresent**). Якщо серед **`containers`** /
|
|
12
12
|
* **`initContainers`** є образ **`hasura/graphql-engine`**, дозволено лише пін **`HASURA_GRAPHQL_ENGINE_IMAGE`**
|
|
13
13
|
* (див. k8s.mdc).
|
|
14
14
|
*
|
|
@@ -327,7 +327,7 @@ export function kustomizationSvcYamlMissingSvcHlViolation(kustomizationDir, path
|
|
|
327
327
|
if (basename(abs).toLowerCase() === 'svc.yaml') {
|
|
328
328
|
const hlAbs = resolve(dirname(abs), 'svc-hl.yaml')
|
|
329
329
|
if (!resolved.has(hlAbs)) {
|
|
330
|
-
return `kustomization посилається на «${ref}» — додай у тому ж kustomization.yaml посилання на відповідний svc-hl.yaml (очікуваний шлях поруч,
|
|
330
|
+
return `kustomization посилається на «${ref}» — додай у тому ж kustomization.yaml посилання на відповідний svc-hl.yaml (очікуваний шлях поруч, наприклад той самий префікс каталогу + svc-hl.yaml; див. k8s.mdc)`
|
|
331
331
|
}
|
|
332
332
|
}
|
|
333
333
|
}
|
|
@@ -627,9 +627,9 @@ async function readK8sYamlBodyAfterModelineForSvcPair(abs) {
|
|
|
627
627
|
}
|
|
628
628
|
|
|
629
629
|
/**
|
|
630
|
-
* Розбирає YAML на корені
|
|
630
|
+
* Розбирає YAML на корені документів (ігнорує зламані документи).
|
|
631
631
|
* @param {string} body фрагмент YAML
|
|
632
|
-
* @returns {unknown[]} масив
|
|
632
|
+
* @returns {unknown[]} масив успішно розібраних коренів YAML-документів
|
|
633
633
|
*/
|
|
634
634
|
function parseK8sYamlDocumentObjectRoots(body) {
|
|
635
635
|
try {
|
|
@@ -666,16 +666,6 @@ function extractApiVersionAndKind(doc) {
|
|
|
666
666
|
}
|
|
667
667
|
}
|
|
668
668
|
|
|
669
|
-
/**
|
|
670
|
-
* Чи відносний шлях вказує на **`ru/kustomization.yaml`** (сегмент **`ru`** перед ім’ям файлу).
|
|
671
|
-
* @param {string} rel шлях від кореня репозиторію
|
|
672
|
-
* @returns {boolean} true, якщо це `…/ru/kustomization.yaml`
|
|
673
|
-
*/
|
|
674
|
-
export function isRuKustomizationPath(rel) {
|
|
675
|
-
const norm = rel.replaceAll('\\', '/')
|
|
676
|
-
return /(^|\/)ru\/kustomization\.yaml$/u.test(norm)
|
|
677
|
-
}
|
|
678
|
-
|
|
679
669
|
/**
|
|
680
670
|
* Чи вміст overlay **`ru/kustomization.yaml`** містить Kustomize patch видалення **HealthCheckPolicy**.
|
|
681
671
|
* @param {string} raw повний текст файлу
|
|
@@ -722,7 +712,7 @@ function scanIngressInYamlDocuments(rel, body, fail) {
|
|
|
722
712
|
|
|
723
713
|
/**
|
|
724
714
|
* Чи порушує маніфест вимогу **`Deployment.spec.template.spec.containers[].resources`** (див. k8s.mdc).
|
|
725
|
-
* @param {unknown} manifest корінь YAML-документа як
|
|
715
|
+
* @param {unknown} manifest корінь YAML-документа як запис JavaScript
|
|
726
716
|
* @returns {string | null} текст порушення для `fail` або null, якщо перевірка не застосовується / ок
|
|
727
717
|
*/
|
|
728
718
|
export function deploymentResourcesViolation(manifest) {
|
|
@@ -752,7 +742,7 @@ export function deploymentResourcesViolation(manifest) {
|
|
|
752
742
|
}
|
|
753
743
|
const r = cont.resources
|
|
754
744
|
if (r === null || typeof r !== 'object' || Array.isArray(r)) {
|
|
755
|
-
return `контейнер "${label}": resources має бути
|
|
745
|
+
return `контейнер "${label}": resources має бути записом у YAML (наприклад порожній: resources: {})`
|
|
756
746
|
}
|
|
757
747
|
}
|
|
758
748
|
}
|
|
@@ -761,7 +751,7 @@ export function deploymentResourcesViolation(manifest) {
|
|
|
761
751
|
}
|
|
762
752
|
|
|
763
753
|
/**
|
|
764
|
-
* Прибирає digest з посилання на образ (`@sha256:…`) для порівняння
|
|
754
|
+
* Прибирає digest з посилання на образ (`@sha256:…`) для порівняння тегу образу.
|
|
765
755
|
* @param {string} image значення поля `image`
|
|
766
756
|
* @returns {string} той самий рядок без суфікса `@…` (digest), з `.trim()`
|
|
767
757
|
*/
|
|
@@ -771,7 +761,7 @@ function stripImageDigest(image) {
|
|
|
771
761
|
}
|
|
772
762
|
|
|
773
763
|
/**
|
|
774
|
-
* Чи рядок `image` вказує на репозиторій **hasura/graphql-engine** (будь-який тег / без
|
|
764
|
+
* Чи рядок `image` вказує на репозиторій **hasura/graphql-engine** (будь-який тег / без вказаного тегу).
|
|
775
765
|
* @param {string} image значення поля `image`
|
|
776
766
|
* @returns {boolean} true, якщо шлях образу закінчується на `hasura/graphql-engine` з тегом або без
|
|
777
767
|
*/
|
|
@@ -808,7 +798,7 @@ function hasuraGraphqlEngineViolationInContainerList(list, containers) {
|
|
|
808
798
|
}
|
|
809
799
|
|
|
810
800
|
/**
|
|
811
|
-
* Чи порушує **Deployment** вимогу
|
|
801
|
+
* Чи порушує **Deployment** вимогу щодо зафіксованого образу **hasura/graphql-engine** (k8s.mdc).
|
|
812
802
|
* @param {unknown} manifest корінь YAML-документа
|
|
813
803
|
* @returns {string | null} текст порушення або null, якщо не Deployment / образу немає / ок
|
|
814
804
|
*/
|