@nitra/cursor 1.8.61 → 1.8.64
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 +53 -25
- package/package.json +1 -1
- package/scripts/check-k8s.mjs +422 -10
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,26 +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`** не
|
|
111
|
+
У **`kind: Service`** не додавай у **`metadata.annotations`** **`cloud.google.com/neg`** і **`cloud.google.com/backend-config`** (legacy під Ingress / старе балансування GKE). **check k8s** падає, якщо ключ є.
|
|
112
|
+
|
|
113
|
+
## Service: `svc.yaml` і `svc-hl.yaml` (Gateway API)
|
|
114
114
|
|
|
115
|
-
- **`
|
|
116
|
-
- **`cloud.google.com/backend-config`**
|
|
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**).
|
|
117
116
|
|
|
118
|
-
|
|
117
|
+
**Точні умови та повідомлення `fail`** — верхній JSDoc **`npm/scripts/check-k8s.mjs`**.
|
|
119
118
|
|
|
120
119
|
## Kustomize: структура каталогів (`base` / overlays)
|
|
121
120
|
|
|
@@ -134,11 +133,11 @@ resources: {}
|
|
|
134
133
|
|
|
135
134
|
### Namespace
|
|
136
135
|
|
|
137
|
-
- **`base/kustomization.yaml`:**
|
|
136
|
+
- **`base/kustomization.yaml`:** поле **`namespace:`** має бути **непорожнім** (перевіряє **check k8s**, якщо файл є).
|
|
138
137
|
|
|
139
|
-
- **Де не дублювати `metadata.namespace`:** у YAML
|
|
138
|
+
- **Де не дублювати `metadata.namespace`:** у YAML, досяжних через **граф Kustomize** (шляхи з **`kustomization.yaml`**, як у логіці **`collectKustomizeManagedRelPaths`** / **check k8s**). **Namespace** задає **`namespace:`** у kustomization.
|
|
140
139
|
|
|
141
|
-
- **Коли `metadata.namespace`
|
|
140
|
+
- **Коли `metadata.namespace` обов’язковий у файлі:** YAML під **`k8s`**, який **не** в графі жодного kustomization — непорожній **`metadata.namespace`** для namespaced **kind** (винятки — кластерні **kind**, перелік **`CLUSTER_SCOPED_KINDS`** у **`check-k8s.mjs`**). Якщо namespace у маніфесті не потрібен — підключи файл через **`resources`** / **`patches`** тощо.
|
|
142
141
|
|
|
143
142
|
- **Не додавай** окремі **patches** Kustomize, які лише змінюють **namespace**: **namespace** визначає Kustomize; у overlays додаткові зміни — без дублювання логіки **namespace**.
|
|
144
143
|
|
|
@@ -174,27 +173,56 @@ resources: {}
|
|
|
174
173
|
|
|
175
174
|
**`check k8s`:** заборонено **`kind: Ingress`**.
|
|
176
175
|
|
|
177
|
-
|
|
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
|
+
```
|
|
178
195
|
|
|
179
|
-
|
|
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
|
+
```
|
|
208
|
+
|
|
209
|
+
## Перевірка
|
|
180
210
|
|
|
181
|
-
|
|
211
|
+
**`npx @nitra/cursor check k8s`** — програмні критерії в **JSDoc на початку** **`npm/scripts/check-k8s.mjs`**. Якщо під **`k8s`** немає **`*.yaml`** — крок пропущено. Канон **`$schema`** для редактора — розділ **«Визначення схеми YAML`** нижче.
|
|
182
212
|
|
|
183
|
-
|
|
213
|
+
**Не входить у check k8s:** наприклад **ReferenceGrant** і доступ між **namespace** (лише рекомендації тут), **kubeconform** / **kubescape** — це **`bun run lint-k8s`**.
|
|
184
214
|
|
|
185
|
-
|
|
215
|
+
## Що саме в скрипті `check-k8s.mjs`
|
|
186
216
|
|
|
187
|
-
|
|
217
|
+
Повний перелік умов, константи (**`YANNH_PIN`**, **`CLUSTER_SCOPED_KINDS`**, **`HASURA_GRAPHQL_ENGINE_IMAGE`** тощо) і допоміжні функції — у файлі скрипта; змінив вимогу для **check** — онови **JSDoc** і за потреби тести в **`npm/tests/check-k8s-schema.test.mjs`**.
|
|
188
218
|
|
|
189
|
-
При зміні **PIN** версії Kubernetes
|
|
219
|
+
При зміні **PIN** версії Kubernetes узгодь **`check-k8s.mjs`**, **`run-k8s.mjs`** (**`KUBERNETES_VERSION`**, **`DATREE_CRD_SCHEMA_LOCATION`**) і цей файл (**lint-k8s**, **Визначення схеми YAML**).
|
|
190
220
|
|
|
191
221
|
## Коли застосовувати (агентам)
|
|
192
222
|
|
|
193
|
-
-
|
|
194
|
-
-
|
|
195
|
-
-
|
|
196
|
-
- Дотримуйся структури **Kustomize** (`base` = dev, overlays без дублювання `base`, коментарі для рядків, що змінюються в overlay). У **`base/kustomization.yaml`** завжди задавай непорожній **`namespace:`**. У **`k8s/base`** у кожному ресурсному YAML має бути явний **`metadata.namespace`**. Поза **base**, якщо не хочеш **`metadata.namespace`** у файлі — підключи його до kustomization (**`resources`** / **`patches`** тощо); інакше додай явний **`metadata.namespace`**.
|
|
197
|
-
- Після міграції на нову структуру **видали** застарілі файли та каталоги, які вже замінені (див. **Міграція зі старої структури**).
|
|
223
|
+
- Після змін у k8s YAML: **`npx @nitra/cursor check k8s`** і за наявності правила — **`bun run lint-k8s`**.
|
|
224
|
+
- Оновив **`apiVersion` / `kind`** — підправ **перший** рядок **`$schema`** (див. **Визначення схеми YAML**).
|
|
225
|
+
- Дотримуйся **Kustomize** з цього правила; деталі **namespace** / графа ресурсів — **check k8s** + підказки в JSDoc скрипта.
|
|
198
226
|
|
|
199
227
|
## Визначення схеми YAML (канон)
|
|
200
228
|
|
package/package.json
CHANGED
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
|
*
|
|
@@ -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[]} масив успішно розібраних коренів YAML-документів
|
|
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
|
|
@@ -610,7 +722,7 @@ function scanIngressInYamlDocuments(rel, body, fail) {
|
|
|
610
722
|
|
|
611
723
|
/**
|
|
612
724
|
* Чи порушує маніфест вимогу **`Deployment.spec.template.spec.containers[].resources`** (див. k8s.mdc).
|
|
613
|
-
* @param {unknown} manifest корінь YAML-документа як
|
|
725
|
+
* @param {unknown} manifest корінь YAML-документа як запис JavaScript
|
|
614
726
|
* @returns {string | null} текст порушення для `fail` або null, якщо перевірка не застосовується / ок
|
|
615
727
|
*/
|
|
616
728
|
export function deploymentResourcesViolation(manifest) {
|
|
@@ -640,7 +752,7 @@ export function deploymentResourcesViolation(manifest) {
|
|
|
640
752
|
}
|
|
641
753
|
const r = cont.resources
|
|
642
754
|
if (r === null || typeof r !== 'object' || Array.isArray(r)) {
|
|
643
|
-
return `контейнер "${label}": resources має бути
|
|
755
|
+
return `контейнер "${label}": resources має бути записом у YAML (наприклад порожній: resources: {})`
|
|
644
756
|
}
|
|
645
757
|
}
|
|
646
758
|
}
|
|
@@ -649,7 +761,7 @@ export function deploymentResourcesViolation(manifest) {
|
|
|
649
761
|
}
|
|
650
762
|
|
|
651
763
|
/**
|
|
652
|
-
* Прибирає digest з посилання на образ (`@sha256:…`) для порівняння
|
|
764
|
+
* Прибирає digest з посилання на образ (`@sha256:…`) для порівняння тегу образу.
|
|
653
765
|
* @param {string} image значення поля `image`
|
|
654
766
|
* @returns {string} той самий рядок без суфікса `@…` (digest), з `.trim()`
|
|
655
767
|
*/
|
|
@@ -659,7 +771,7 @@ function stripImageDigest(image) {
|
|
|
659
771
|
}
|
|
660
772
|
|
|
661
773
|
/**
|
|
662
|
-
* Чи рядок `image` вказує на репозиторій **hasura/graphql-engine** (будь-який тег / без
|
|
774
|
+
* Чи рядок `image` вказує на репозиторій **hasura/graphql-engine** (будь-який тег / без вказаного тегу).
|
|
663
775
|
* @param {string} image значення поля `image`
|
|
664
776
|
* @returns {boolean} true, якщо шлях образу закінчується на `hasura/graphql-engine` з тегом або без
|
|
665
777
|
*/
|
|
@@ -696,7 +808,7 @@ function hasuraGraphqlEngineViolationInContainerList(list, containers) {
|
|
|
696
808
|
}
|
|
697
809
|
|
|
698
810
|
/**
|
|
699
|
-
* Чи порушує **Deployment** вимогу
|
|
811
|
+
* Чи порушує **Deployment** вимогу щодо зафіксованого образу **hasura/graphql-engine** (k8s.mdc).
|
|
700
812
|
* @param {unknown} manifest корінь YAML-документа
|
|
701
813
|
* @returns {string | null} текст порушення або null, якщо не Deployment / образу немає / ок
|
|
702
814
|
*/
|
|
@@ -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
|