@nitra/cursor 1.8.127 → 1.8.129
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 +2 -2
- package/mdc/k8s.mdc +2 -0
- package/package.json +1 -1
- package/scripts/check-abie.mjs +72 -81
- package/scripts/check-docker.mjs +6 -9
- package/scripts/check-ga.mjs +3 -5
- package/scripts/check-k8s.mjs +206 -123
- package/scripts/utils/docker-mirror.mjs +22 -24
package/mdc/abie.mdc
CHANGED
|
@@ -36,7 +36,7 @@ spec:
|
|
|
36
36
|
|
|
37
37
|
## k8s: overlay **HTTPRoute** (**ua** / **ru**)
|
|
38
38
|
|
|
39
|
-
За наявності **Deployment** під **k8s** і наявності **Vite** (**`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`** у каталозі пакета) у **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`** цього пакета потрібні **inline JSON6902** у **`patches`**: **target** **`kind: HTTPRoute`**, **непорожній `name`** (як у маніфесті маршруту). Мають бути зміни **`/spec/hostnames`** (домени abie — у скрипті) та **`/spec/parentRefs/0/namespace`** (**`ua`** / **`ru`**). Для **ru** — анотація **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`** лише якщо в **тому ж** **`ru/kustomization.yaml`** є згадка **`HASURA_GRAPHQL_JWT_SECRET`** (типово patch на **ConfigMap** Hasura). Як обирати **`op`** (**add** / **replace** тощо) у patch — **k8s.mdc** (розділ про JSON patch у kustomization).
|
|
39
|
+
За наявності **Deployment** під **k8s** і наявності **Vite** (**`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`** у каталозі пакета) у **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`** цього пакета потрібні **inline JSON6902** у **`patches`**: **target** **`kind: HTTPRoute`**, **непорожній `name`** (як у маніфесті маршруту). Мають бути зміни **`/spec/hostnames`** (домени abie — у скрипті) та **`/spec/parentRefs/0/namespace`** (**`ua`** / **`ru`**, також дозволені префікси **`ua-*`** / **`ru-*`**, наприклад **`ua-b2b`**, **`ru-b2b`**). Для **ru** — анотація **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`** лише якщо в **тому ж** **`ru/kustomization.yaml`** є згадка **`HASURA_GRAPHQL_JWT_SECRET`** (типово patch на **ConfigMap** Hasura). Як обирати **`op`** (**add** / **replace** тощо) у patch — **k8s.mdc** (розділ про JSON patch у kustomization).
|
|
40
40
|
|
|
41
41
|
### HTTPRoute: спільні сервіси **`auth-run-hl`**, **`file-link-hl`**
|
|
42
42
|
|
|
@@ -60,7 +60,7 @@ spec:
|
|
|
60
60
|
port: 8080
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
У **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`** додай до того самого **inline** patch на **`HTTPRoute`** (той самий **`target.name`**) операції **JSON6902** з **`path`**: **`/spec/rules/<i>/backendRefs/<j>/namespace`**, де **`<i>`** / **`<j>`** — індекси відповідно до порядку **`spec.rules`** та **`backendRefs`** у base-файлі; **`value`**: **`ua`**
|
|
63
|
+
У **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`** додай до того самого **inline** patch на **`HTTPRoute`** (той самий **`target.name`**) операції **JSON6902** з **`path`**: **`/spec/rules/<i>/backendRefs/<j>/namespace`**, де **`<i>`** / **`<j>`** — індекси відповідно до порядку **`spec.rules`** та **`backendRefs`** у base-файлі; **`value`**: **`ua`** / **`ru`** (також дозволені **`ua-*`** / **`ru-*`**). Якщо кілька таких **`backendRefs`**, потрібна окрема операція для кожного.
|
|
64
64
|
|
|
65
65
|
```yaml title="…/ua/kustomization.yaml (фрагмент)"
|
|
66
66
|
- target:
|
package/mdc/k8s.mdc
CHANGED
|
@@ -298,6 +298,8 @@ data:
|
|
|
298
298
|
|
|
299
299
|
**Локальні шляхи в `kustomization.yaml`:** кожен запис без `://` (remote) з `resources` / `bases` / `components` / `crds`, `patchesStrategicMerge`, `patches[].path`, `patchesJson6902[].path`, `configurations[]`, `replacements[].path` має вказувати на **існуючий** у репозиторії файл (`.yaml` / `.yml`) або **каталог**; биті посилання — помилка **`check k8s`**.
|
|
300
300
|
|
|
301
|
+
**Порядок `resources`:** у маніфесті з **`apiVersion: kustomize.config.k8s.io/…`**, **`kind: Kustomization`**, елементи **`resources:`** (лише непорожні рядки) мають бути **відсортовані за алфавітом (англ. локаль, як `localeCompare('en')` у `check-k8s.mjs`)**. Поля **`bases`**, **`components`**, **`crds`** цією перевіркою **не** впорядковуються.
|
|
302
|
+
|
|
301
303
|
### Env-залежні межі (за сегментом після `/k8s/`)
|
|
302
304
|
|
|
303
305
|
**Dev-like середовища** — сегмент `base`, `dev`, або з суфіксом `-qa` (напр. `tr-qa`):
|
package/package.json
CHANGED
package/scripts/check-abie.mjs
CHANGED
|
@@ -86,10 +86,11 @@ const PATCH_PREEM_FALSE_RE = /\bpreem:\s*['"]?false['"]?\b/u
|
|
|
86
86
|
const PATCH_YANDEX_PREEMPTIBLE_FALSE_RE = /yandex\.cloud\/preemptible:\s*['"]?false['"]?/u
|
|
87
87
|
const TRAILING_SLASH_RE = /\/$/u
|
|
88
88
|
const PATCH_HOSTNAMES_PATH_RE = /path:\s*\/spec\/hostnames\b/mu
|
|
89
|
+
// Overlay namespaces: allow ua/ru and ua-*/ru-* (e.g. ua-b2b, ru-b2b).
|
|
89
90
|
const PATCH_PARENT_REF_NS_UA_RE =
|
|
90
|
-
/path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua['"]?(?:\s|$)/
|
|
91
|
+
/path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/imu
|
|
91
92
|
const PATCH_PARENT_REF_NS_RU_RE =
|
|
92
|
-
/path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru['"]?(?:\s|$)/
|
|
93
|
+
/path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/imu
|
|
93
94
|
const WEBSOCKET_ANNOTATION_RE = /gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/mu
|
|
94
95
|
const LEADING_EMPTY_LINE_RE = /^\s*\n/u
|
|
95
96
|
const REMOVE_CLUSTER_IP_AFTER_OP_RE = /op:\s*remove\b[\s\S]{0,200}?path:\s*\/spec\/clusterIP\b/mu
|
|
@@ -225,6 +226,20 @@ export function isAllowedAbieBaseDevHostname(hostname) {
|
|
|
225
226
|
return false
|
|
226
227
|
}
|
|
227
228
|
|
|
229
|
+
/**
|
|
230
|
+
* @param {unknown} hostnames значення поля spec.hostnames
|
|
231
|
+
* @returns {string[]} непорожні рядки-хости
|
|
232
|
+
*/
|
|
233
|
+
function collectAbieHostnames(hostnames) {
|
|
234
|
+
if (Array.isArray(hostnames)) {
|
|
235
|
+
return hostnames.filter(h => typeof h === 'string' && h.trim() !== '')
|
|
236
|
+
}
|
|
237
|
+
if (typeof hostnames === 'string' && hostnames.trim() !== '') {
|
|
238
|
+
return [hostnames]
|
|
239
|
+
}
|
|
240
|
+
return []
|
|
241
|
+
}
|
|
242
|
+
|
|
228
243
|
/**
|
|
229
244
|
* Повідомлення про недопустимі **spec.hostnames** у **HTTPRoute** у шляху **…/base/…** (abie.mdc).
|
|
230
245
|
* @param {unknown} obj корінь YAML-документа
|
|
@@ -232,49 +247,23 @@ export function isAllowedAbieBaseDevHostname(hostname) {
|
|
|
232
247
|
* @returns {string[]} порожньо, якщо перевірка не застосовується або hostnames коректні
|
|
233
248
|
*/
|
|
234
249
|
export function abieBaseHttpRouteHostnamesErrors(obj, rel) {
|
|
235
|
-
if (!isAbieK8sBaseYamlPath(rel))
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
239
|
-
return []
|
|
240
|
-
}
|
|
250
|
+
if (!isAbieK8sBaseYamlPath(rel)) return []
|
|
251
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return []
|
|
241
252
|
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
242
|
-
if (rec.kind !== 'HTTPRoute')
|
|
243
|
-
return []
|
|
244
|
-
}
|
|
253
|
+
if (rec.kind !== 'HTTPRoute') return []
|
|
245
254
|
const spec = rec.spec
|
|
246
|
-
if (spec === null || typeof spec !== 'object' || Array.isArray(spec))
|
|
247
|
-
return []
|
|
248
|
-
}
|
|
255
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return []
|
|
249
256
|
const hostnames = /** @type {Record<string, unknown>} */ (spec).hostnames
|
|
250
|
-
if (hostnames === undefined)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
/** @type {string[]} */
|
|
254
|
-
const hosts = []
|
|
255
|
-
if (Array.isArray(hostnames)) {
|
|
256
|
-
for (const h of hostnames) {
|
|
257
|
-
if (typeof h === 'string' && h.trim() !== '') {
|
|
258
|
-
hosts.push(h)
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
} else if (typeof hostnames === 'string' && hostnames.trim() !== '') {
|
|
262
|
-
hosts.push(hostnames)
|
|
263
|
-
}
|
|
264
|
-
if (hosts.length === 0) {
|
|
265
|
-
return []
|
|
266
|
-
}
|
|
257
|
+
if (hostnames === undefined) return []
|
|
258
|
+
const hosts = collectAbieHostnames(hostnames)
|
|
259
|
+
if (hosts.length === 0) return []
|
|
267
260
|
const root = ABIE_BASE_DEV_HTTPROUTE_HOST_ROOT
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
errors.push(
|
|
261
|
+
return hosts
|
|
262
|
+
.filter(h => !isAllowedAbieBaseDevHostname(h))
|
|
263
|
+
.map(
|
|
264
|
+
h =>
|
|
273
265
|
`${rel}: HTTPRoute у base (dev): hostname "${h}" недопустимий — дозволені лише ${root} та піддомени, зокрема *.${root} (abie.mdc)`
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return errors
|
|
266
|
+
)
|
|
278
267
|
}
|
|
279
268
|
|
|
280
269
|
/**
|
|
@@ -1157,8 +1146,8 @@ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yam
|
|
|
1157
1146
|
function countAbieHttpRouteBackendRefNamespacePatchesInCombined(combined, mode) {
|
|
1158
1147
|
const re =
|
|
1159
1148
|
mode === 'ua'
|
|
1160
|
-
? /path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua['"]?(?:\s|$)/
|
|
1161
|
-
: /path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru['"]?(?:\s|$)/
|
|
1149
|
+
? /path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/gimu
|
|
1150
|
+
: /path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru(?:-[a-z0-9][a-z0-9-]*)?['"]?(?:\s|$)/gimu
|
|
1162
1151
|
return [...combined.matchAll(re)].length
|
|
1163
1152
|
}
|
|
1164
1153
|
|
|
@@ -1591,6 +1580,37 @@ async function checkHttpRouteKustomization(abs, rel, mode, root, yamlFilesAbs, c
|
|
|
1591
1580
|
return true
|
|
1592
1581
|
}
|
|
1593
1582
|
|
|
1583
|
+
/**
|
|
1584
|
+
* @param {unknown} json YAML-документ
|
|
1585
|
+
* @returns {boolean} true, якщо HTTPRoute має непорожні spec.hostnames
|
|
1586
|
+
*/
|
|
1587
|
+
function httpRouteHasNonEmptyHostnames(json) {
|
|
1588
|
+
if (json === null || typeof json !== 'object' || Array.isArray(json)) return false
|
|
1589
|
+
const rec = /** @type {Record<string, unknown>} */ (json)
|
|
1590
|
+
if (rec.kind !== 'HTTPRoute') return false
|
|
1591
|
+
const spec = rec.spec
|
|
1592
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return false
|
|
1593
|
+
const hostnames = /** @type {Record<string, unknown>} */ (spec).hostnames
|
|
1594
|
+
return collectAbieHostnames(hostnames).length > 0
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* @param {import('yaml').Document} doc YAML-документ з файлу
|
|
1599
|
+
* @param {string} rel відносний шлях для повідомлень
|
|
1600
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
1601
|
+
* @returns {{ hasErrors: boolean, hasHostnames: boolean }} результат обробки документа
|
|
1602
|
+
*/
|
|
1603
|
+
function processBaseHttpRouteDoc(doc, rel, fail) {
|
|
1604
|
+
if (doc.errors.length !== 0) return { hasErrors: false, hasHostnames: false }
|
|
1605
|
+
const json = doc.toJSON()
|
|
1606
|
+
const errs = abieBaseHttpRouteHostnamesErrors(json, rel)
|
|
1607
|
+
if (errs.length > 0) {
|
|
1608
|
+
for (const e of errs) fail(e)
|
|
1609
|
+
return { hasErrors: true, hasHostnames: false }
|
|
1610
|
+
}
|
|
1611
|
+
return { hasErrors: false, hasHostnames: httpRouteHasNonEmptyHostnames(json) }
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1594
1614
|
/**
|
|
1595
1615
|
* Для кожного **HTTPRoute** у **`…/k8s/base/…`** з непорожніми **`spec.hostnames`** — лише **aiml.live** та піддомени (abie.mdc).
|
|
1596
1616
|
* @param {string} root корінь репозиторію
|
|
@@ -1601,39 +1621,15 @@ async function checkHttpRouteKustomization(abs, rel, mode, root, yamlFilesAbs, c
|
|
|
1601
1621
|
*/
|
|
1602
1622
|
async function ensureAbieBaseHttpRouteHostnames(root, yamlFilesAbs, fail, passFn) {
|
|
1603
1623
|
let baseHttpRoutesWithHostnames = 0
|
|
1604
|
-
|
|
1624
|
+
const baseFiles = yamlFilesAbs.filter(abs => isAbieK8sBaseYamlPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
1625
|
+
for (const abs of baseFiles) {
|
|
1605
1626
|
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
if (doc.errors.length === 0) {
|
|
1613
|
-
const json = doc.toJSON()
|
|
1614
|
-
const errs = abieBaseHttpRouteHostnamesErrors(json, rel)
|
|
1615
|
-
if (errs.length > 0) {
|
|
1616
|
-
for (const e of errs) {
|
|
1617
|
-
fail(e)
|
|
1618
|
-
}
|
|
1619
|
-
return
|
|
1620
|
-
}
|
|
1621
|
-
if (json !== null && typeof json === 'object' && !Array.isArray(json)) {
|
|
1622
|
-
const rec = /** @type {Record<string, unknown>} */ (json)
|
|
1623
|
-
if (rec.kind === 'HTTPRoute') {
|
|
1624
|
-
const spec = rec.spec
|
|
1625
|
-
if (spec !== null && typeof spec === 'object' && !Array.isArray(spec)) {
|
|
1626
|
-
const hostnames = /** @type {Record<string, unknown>} */ (spec).hostnames
|
|
1627
|
-
if (Array.isArray(hostnames) && hostnames.some(h => typeof h === 'string' && h.trim() !== '')) {
|
|
1628
|
-
baseHttpRoutesWithHostnames++
|
|
1629
|
-
} else if (typeof hostnames === 'string' && hostnames.trim() !== '') {
|
|
1630
|
-
baseHttpRoutesWithHostnames++
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1627
|
+
const docs = await readAndParseYamlDocs(abs, rel, fail)
|
|
1628
|
+
if (!docs) return
|
|
1629
|
+
for (const doc of docs) {
|
|
1630
|
+
const { hasErrors, hasHostnames } = processBaseHttpRouteDoc(doc, rel, fail)
|
|
1631
|
+
if (hasErrors) return
|
|
1632
|
+
if (hasHostnames) baseHttpRoutesWithHostnames++
|
|
1637
1633
|
}
|
|
1638
1634
|
}
|
|
1639
1635
|
if (baseHttpRoutesWithHostnames > 0) {
|
|
@@ -1685,7 +1681,6 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
|
|
|
1685
1681
|
/**
|
|
1686
1682
|
* Перевіряє відсутність артефактів Firebase Hosting у **кожному** **підкаталозі першого рівня** від кореня
|
|
1687
1683
|
* (не в самому корені репозиторію) — abie.mdc. Каталоги **`.git`** і **`node_modules`** у скануванні пропускаються.
|
|
1688
|
-
*
|
|
1689
1684
|
* @param {string} root корінь репозиторію
|
|
1690
1685
|
* @param {(msg: string) => void} passFn успішне повідомлення
|
|
1691
1686
|
* @param {(msg: string) => void} failFn повідомлення про порушення
|
|
@@ -1700,9 +1695,7 @@ async function ensureNoFirebaseHostingArtifacts(root, passFn, failFn) {
|
|
|
1700
1695
|
failFn(`Не вдалося прочитати ${root} для перевірки Firebase Hosting: ${msg} (abie.mdc)`)
|
|
1701
1696
|
return
|
|
1702
1697
|
}
|
|
1703
|
-
const topDirs = entries.filter(
|
|
1704
|
-
e => e.isDirectory() && !ABIE_FIREBASE_HOSTING_SCAN_SKIP_TOP_DIR_NAMES.has(e.name)
|
|
1705
|
-
)
|
|
1698
|
+
const topDirs = entries.filter(e => e.isDirectory() && !ABIE_FIREBASE_HOSTING_SCAN_SKIP_TOP_DIR_NAMES.has(e.name))
|
|
1706
1699
|
let hasViolation = false
|
|
1707
1700
|
for (const e of topDirs) {
|
|
1708
1701
|
for (const name of ['.firebaserc', 'firebase.json']) {
|
|
@@ -1720,9 +1713,7 @@ async function ensureNoFirebaseHostingArtifacts(root, passFn, failFn) {
|
|
|
1720
1713
|
if (hasViolation) {
|
|
1721
1714
|
return
|
|
1722
1715
|
}
|
|
1723
|
-
passFn(
|
|
1724
|
-
'Підкаталоги кореня (1-й рівень, без .git/node_modules): артефактів Firebase Hosting не знайдено (abie.mdc)'
|
|
1725
|
-
)
|
|
1716
|
+
passFn('Підкаталоги кореня (1-й рівень, без .git/node_modules): артефактів Firebase Hosting не знайдено (abie.mdc)')
|
|
1726
1717
|
}
|
|
1727
1718
|
|
|
1728
1719
|
/**
|
package/scripts/check-docker.mjs
CHANGED
|
@@ -24,6 +24,8 @@ import { lintDockerfileWithHadolint, posixRel } from './utils/docker-hadolint.mj
|
|
|
24
24
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
25
25
|
import { walkDir } from './utils/walkDir.mjs'
|
|
26
26
|
|
|
27
|
+
const NEWLINE_RE = /\r?\n/
|
|
28
|
+
|
|
27
29
|
/**
|
|
28
30
|
* @typedef {{
|
|
29
31
|
* line: number
|
|
@@ -59,30 +61,25 @@ export async function findDockerfilePaths(root) {
|
|
|
59
61
|
|
|
60
62
|
/**
|
|
61
63
|
* Витягує всі `FROM <image>` зі вмісту Dockerfile/Containerfile.
|
|
62
|
-
*
|
|
63
64
|
* @param {string} fileContent вміст Dockerfile/Containerfile
|
|
64
65
|
* @returns {FromStage[]} список знайдених FROM-інструкцій
|
|
65
66
|
*/
|
|
66
67
|
export function parseFromStages(fileContent) {
|
|
67
68
|
const out = []
|
|
68
|
-
const lines = fileContent.split(
|
|
69
|
-
for (
|
|
70
|
-
const image = getFromImageToken(
|
|
69
|
+
const lines = fileContent.split(NEWLINE_RE)
|
|
70
|
+
for (const [i, line] of lines.entries()) {
|
|
71
|
+
const image = getFromImageToken(line)
|
|
71
72
|
if (image) out.push({ line: i + 1, image })
|
|
72
73
|
}
|
|
73
74
|
return out
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
const RUNTIME_IMAGES = /** @type {const} */ ([
|
|
77
|
-
'mirror.gcr.io/library/alpine',
|
|
78
|
-
'mirror.gcr.io/library/nginx'
|
|
79
|
-
])
|
|
77
|
+
const RUNTIME_IMAGES = /** @type {const} */ (['mirror.gcr.io/library/alpine', 'mirror.gcr.io/library/nginx'])
|
|
80
78
|
|
|
81
79
|
/**
|
|
82
80
|
* Перевіряє базові вимоги до структури Dockerfile:
|
|
83
81
|
* - multistage: мінімум 2 FROM
|
|
84
82
|
* - фінальний FROM: alpine або nginx з mirror.gcr.io
|
|
85
|
-
*
|
|
86
83
|
* @param {string} fileContent вміст Dockerfile/Containerfile
|
|
87
84
|
* @returns {string | null} повідомлення помилки або null
|
|
88
85
|
*/
|
package/scripts/check-ga.mjs
CHANGED
|
@@ -136,12 +136,12 @@ function verifyNoRunShellLineContinuationBackslash(relPath, content, failFn, pas
|
|
|
136
136
|
}
|
|
137
137
|
const hits = findRunStepsWithShellLineContinuationBackslash(root)
|
|
138
138
|
if (hits.length === 0) {
|
|
139
|
-
passFn(`${relPath}: run без shell-продовження через
|
|
139
|
+
passFn(String.raw`${relPath}: run без shell-продовження через \ (ga.mdc)`)
|
|
140
140
|
return
|
|
141
141
|
}
|
|
142
142
|
for (const h of hits) {
|
|
143
143
|
failFn(
|
|
144
|
-
`${relPath}: job ${h.jobId}, крок ${h.stepIndex + 1}: у run заборонено продовження рядків через зворотний сліш; довгі команди оформи як folded block (run: >-) без
|
|
144
|
+
String.raw`${relPath}: job ${h.jobId}, крок ${h.stepIndex + 1}: у run заборонено продовження рядків через зворотний сліш; довгі команди оформи як folded block (run: >-) без \ на кінцях рядків (ga.mdc)`
|
|
145
145
|
)
|
|
146
146
|
}
|
|
147
147
|
}
|
|
@@ -237,9 +237,7 @@ async function checkVscodeSettingsForGa(passFn, failFn) {
|
|
|
237
237
|
}
|
|
238
238
|
const block = /** @type {Record<string, unknown>} */ (settings)['[github-actions-workflow]']
|
|
239
239
|
if (!block || typeof block !== 'object' || block === null || Array.isArray(block)) {
|
|
240
|
-
failFn(
|
|
241
|
-
`${rel}: додай "[github-actions-workflow]": { "editor.defaultFormatter": "oxc.oxc-vscode" } (ga.mdc)`
|
|
242
|
-
)
|
|
240
|
+
failFn(`${rel}: додай "[github-actions-workflow]": { "editor.defaultFormatter": "oxc.oxc-vscode" } (ga.mdc)`)
|
|
243
241
|
return
|
|
244
242
|
}
|
|
245
243
|
const df = String(/** @type {Record<string, unknown>} */ (block)['editor.defaultFormatter'] ?? '')
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
* в **тому ж каталозі**, що й **`svc.yaml`** (логіка збігається з **`pathsFromKustomizationObject`**).
|
|
43
43
|
*
|
|
44
44
|
* Структура **Kustomize** (див. k8s.mdc): заборона шляхів **`…/k8s/dev/…`**; у **`k8s/base/kustomization.yaml`**
|
|
45
|
-
* завжди має бути непорожнє поле **`namespace:`** (перевірка, якщо файл існує).
|
|
45
|
+
* завжди має бути непорожнє поле **`namespace:`** (перевірка, якщо файл існує). У **`apiVersion: kustomize.config.k8s.io/…`**, **`kind: Kustomization`**
|
|
46
|
+
* перелік **`resources:`** (лише непорожні рядки) має бути відсортовано за алфавітом (**en**, `localeCompare`).
|
|
46
47
|
*
|
|
47
48
|
* **Inline JSON6902** у **`patches`** (і зовнішні файли з **`patches[].path`** під **`k8s`**, якщо вміст — масив JSON Patch): не допускається пара **`remove`** і **`add`**
|
|
48
49
|
* на один і той самий **`path`** у межах одного фрагмента — потрібен **`op: replace`** (k8s.mdc). **check-k8s** це перевіряє.
|
|
@@ -365,6 +366,77 @@ function pushStringPaths(arr, acc) {
|
|
|
365
366
|
}
|
|
366
367
|
}
|
|
367
368
|
|
|
369
|
+
/** Префікс `apiVersion` для маніфесту Kustomize **Kustomization**. */
|
|
370
|
+
const KUSTOMIZE_CONFIG_API_PREFIX = 'kustomize.config.k8s.io/'
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Чи послідовність непорожніх рядків зростаюча за `localeCompare` (en).
|
|
374
|
+
* @param {string[]} paths
|
|
375
|
+
* @returns {boolean}
|
|
376
|
+
*/
|
|
377
|
+
function stringPathsAreSortedEn(paths) {
|
|
378
|
+
for (let i = 1; i < paths.length; i++) {
|
|
379
|
+
if (paths[i - 1].localeCompare(paths[i], 'en', { sensitivity: 'base' }) > 0) {
|
|
380
|
+
return false
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return true
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Порушення сорту **`resources`**: лише для **`kustomize.config.k8s.io/…`**, **`kind: Kustomization`**.
|
|
388
|
+
* Порожні рядки в списку ігноруються (як у `pushStringPaths`).
|
|
389
|
+
* @param {unknown} obj корінь першого YAML-документа
|
|
390
|
+
* @returns {string | null} причина або `null`, якщо обмеження не застосовується
|
|
391
|
+
*/
|
|
392
|
+
export function kustomizationResourcesSortedAlphabeticallyViolation(obj) {
|
|
393
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return null
|
|
394
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
395
|
+
if (rec.kind !== 'Kustomization') return null
|
|
396
|
+
const av = rec.apiVersion
|
|
397
|
+
if (typeof av !== 'string' || !av.startsWith(KUSTOMIZE_CONFIG_API_PREFIX)) return null
|
|
398
|
+
const res = rec.resources
|
|
399
|
+
if (res === undefined) return null
|
|
400
|
+
if (!Array.isArray(res)) {
|
|
401
|
+
return 'Kustomization.resources має бути масивом (k8s.mdc)'
|
|
402
|
+
}
|
|
403
|
+
/** @type {string[]} */
|
|
404
|
+
const paths = []
|
|
405
|
+
for (let i = 0; i < res.length; i++) {
|
|
406
|
+
const item = res[i]
|
|
407
|
+
if (typeof item !== 'string') {
|
|
408
|
+
return `Kustomization.resources[${i}] — очікується рядок-шлях (k8s.mdc)`
|
|
409
|
+
}
|
|
410
|
+
const t = item.trim()
|
|
411
|
+
if (t !== '') paths.push(t)
|
|
412
|
+
}
|
|
413
|
+
if (paths.length < 2) return null
|
|
414
|
+
if (!stringPathsAreSortedEn(paths)) {
|
|
415
|
+
const want = [...paths].sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }))
|
|
416
|
+
return `Kustomization.resources має бути за алфавітом (en). Зараз: ${paths.join(', ')}; очікувано: ${want.join(', ')} (k8s.mdc)`
|
|
417
|
+
}
|
|
418
|
+
return null
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Усі **`kustomization.yaml`**: **`resources`**, відсортовані за en.
|
|
423
|
+
* @param {string} root корінь репо
|
|
424
|
+
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
425
|
+
* @param {(msg: string) => void} fail
|
|
426
|
+
* @returns {Promise<void>}
|
|
427
|
+
*/
|
|
428
|
+
async function validateKustomizationResourcesSortedAlphabetically(root, yamlFilesAbs, fail) {
|
|
429
|
+
for (const kustAbs of yamlFilesAbs.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')) {
|
|
430
|
+
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
431
|
+
const kust = await readFirstYamlObject(kustAbs)
|
|
432
|
+
if (kust === null) continue
|
|
433
|
+
const v = kustomizationResourcesSortedAlphabeticallyViolation(kust)
|
|
434
|
+
if (v !== null) {
|
|
435
|
+
fail(`${rel}: ${v}`)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
368
440
|
/**
|
|
369
441
|
* Шляхи з полів Kustomization для resolve відносно каталогу **`kustomization.yaml`**.
|
|
370
442
|
* @param {unknown} obj корінь першого документа Kustomization
|
|
@@ -397,6 +469,35 @@ function pathsFromKustomizationObject(obj) {
|
|
|
397
469
|
return out
|
|
398
470
|
}
|
|
399
471
|
|
|
472
|
+
/**
|
|
473
|
+
* @param {unknown} arr масив (може бути не масивом)
|
|
474
|
+
* @param {string[]} out вихідний масив
|
|
475
|
+
*/
|
|
476
|
+
function collectObjectPathFields(arr, out) {
|
|
477
|
+
if (!Array.isArray(arr)) return
|
|
478
|
+
for (const item of arr) {
|
|
479
|
+
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
|
|
480
|
+
const pth = /** @type {Record<string, unknown>} */ (item).path
|
|
481
|
+
if (typeof pth === 'string' && pth.trim() !== '') {
|
|
482
|
+
out.push(pth.trim())
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* @param {unknown} arr масив (може бути не масивом)
|
|
490
|
+
* @param {string[]} out вихідний масив
|
|
491
|
+
*/
|
|
492
|
+
function collectStringPaths(arr, out) {
|
|
493
|
+
if (!Array.isArray(arr)) return
|
|
494
|
+
for (const c of arr) {
|
|
495
|
+
if (typeof c === 'string' && c.trim() !== '') {
|
|
496
|
+
out.push(c.trim())
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
400
501
|
/**
|
|
401
502
|
* Унікальні локальні шляхи з `kustomization.yaml` для перевірки існування на диску:
|
|
402
503
|
* як у `pathsFromKustomizationObject`, плюс **`patchesJson6902[].path`**, плюс **`configurations[]`**
|
|
@@ -410,37 +511,48 @@ export function kustomizePathRefsForExistenceCheck(obj) {
|
|
|
410
511
|
}
|
|
411
512
|
const fromPaths = pathsFromKustomizationObject(obj)
|
|
412
513
|
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
514
|
+
collectObjectPathFields(rec.patchesJson6902, fromPaths)
|
|
515
|
+
collectStringPaths(rec.configurations, fromPaths)
|
|
516
|
+
collectObjectPathFields(rec.replacements, fromPaths)
|
|
517
|
+
return [...new Set(fromPaths)]
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* @param {string} rel відносний шлях файлу
|
|
522
|
+
* @param {string} r посилання з kustomization
|
|
523
|
+
* @param {string} kustDir каталог kustomization.yaml
|
|
524
|
+
* @param {string} rootNorm нормалізований корінь
|
|
525
|
+
* @param {(msg: string) => void} fail callback
|
|
526
|
+
* @returns {Promise<void>}
|
|
527
|
+
*/
|
|
528
|
+
async function validateKustomizationRef(rel, r, kustDir, rootNorm, fail) {
|
|
529
|
+
const target = resolve(kustDir, r.trim())
|
|
530
|
+
if (!resolvedFilePathIsUnderRoot(rootNorm, target)) {
|
|
531
|
+
fail(
|
|
532
|
+
`${rel}: посилання «${r}» виходить за межі репозиторію (resolve: ${(
|
|
533
|
+
relative(rootNorm, target) || target
|
|
534
|
+
).replaceAll('\\', '/')}) (k8s.mdc)`
|
|
535
|
+
)
|
|
536
|
+
return
|
|
423
537
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
538
|
+
/** @type {import('node:fs').Stats | undefined} */
|
|
539
|
+
let st
|
|
540
|
+
try {
|
|
541
|
+
st = await stat(target)
|
|
542
|
+
} catch {
|
|
543
|
+
st = undefined
|
|
431
544
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
}
|
|
545
|
+
if (st === undefined) {
|
|
546
|
+
fail(`${rel}: посилання «${r}» вказує на неіснуючий ресурс (очікувано файл або каталог; k8s.mdc)`)
|
|
547
|
+
} else if (st.isFile()) {
|
|
548
|
+
if (!YAML_EXTENSION_RE.test(target)) {
|
|
549
|
+
fail(
|
|
550
|
+
`${rel}: «${r}» — за правилами k8s у kustomization для файлів дозволені лише розширення .yaml / .yml (k8s.mdc)`
|
|
551
|
+
)
|
|
441
552
|
}
|
|
553
|
+
} else if (!st.isDirectory()) {
|
|
554
|
+
fail(`${rel}: «${r}» — ні файл, ні каталог (k8s.mdc)`)
|
|
442
555
|
}
|
|
443
|
-
return [...new Set(fromPaths)]
|
|
444
556
|
}
|
|
445
557
|
|
|
446
558
|
/**
|
|
@@ -454,44 +566,14 @@ export function kustomizePathRefsForExistenceCheck(obj) {
|
|
|
454
566
|
async function validateOneKustomizationPathRefsExist(root, kustAbs, rootNorm, fail) {
|
|
455
567
|
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
456
568
|
const kust = await readFirstYamlObject(kustAbs)
|
|
457
|
-
if (kust === null) {
|
|
458
|
-
return
|
|
459
|
-
}
|
|
460
|
-
if (kust.kind !== 'Kustomization') {
|
|
569
|
+
if (kust === null || kust.kind !== 'Kustomization') {
|
|
461
570
|
return
|
|
462
571
|
}
|
|
463
572
|
const refs = kustomizePathRefsForExistenceCheck(kust)
|
|
464
573
|
const kustDir = dirname(resolve(kustAbs))
|
|
465
574
|
for (const r of refs) {
|
|
466
575
|
if (typeof r === 'string' && !r.includes('://') && r.trim() !== '') {
|
|
467
|
-
|
|
468
|
-
if (resolvedFilePathIsUnderRoot(rootNorm, target)) {
|
|
469
|
-
/** @type {import('node:fs').Stats | undefined} */
|
|
470
|
-
let st
|
|
471
|
-
try {
|
|
472
|
-
st = await stat(target)
|
|
473
|
-
} catch {
|
|
474
|
-
st = undefined
|
|
475
|
-
}
|
|
476
|
-
if (st === undefined) {
|
|
477
|
-
fail(
|
|
478
|
-
`${rel}: посилання «${r}» вказує на неіснуючий ресурс (очікувано файл або каталог; k8s.mdc)`
|
|
479
|
-
)
|
|
480
|
-
} else if (st.isFile()) {
|
|
481
|
-
if (!YAML_EXTENSION_RE.test(target)) {
|
|
482
|
-
fail(
|
|
483
|
-
`${rel}: «${r}» — за правилами k8s у kustomization для файлів дозволені лише розширення .yaml / .yml (k8s.mdc)`
|
|
484
|
-
)
|
|
485
|
-
}
|
|
486
|
-
} else if (!st.isDirectory()) {
|
|
487
|
-
fail(`${rel}: «${r}» — ні файл, ні каталог (k8s.mdc)`)
|
|
488
|
-
}
|
|
489
|
-
} else {
|
|
490
|
-
fail(
|
|
491
|
-
`${rel}: посилання «${r}» виходить за межі репозиторію (resolve: ${(relative(rootNorm, target) || target).replaceAll('\\', '/')
|
|
492
|
-
}) (k8s.mdc)`
|
|
493
|
-
)
|
|
494
|
-
}
|
|
576
|
+
await validateKustomizationRef(rel, r, kustDir, rootNorm, fail)
|
|
495
577
|
}
|
|
496
578
|
}
|
|
497
579
|
}
|
|
@@ -4279,6 +4361,24 @@ function isResolvedFileUnderDirectory(dirAbs, fileAbs) {
|
|
|
4279
4361
|
return !r.startsWith('../') && r !== '..'
|
|
4280
4362
|
}
|
|
4281
4363
|
|
|
4364
|
+
/**
|
|
4365
|
+
* @param {string} resolved абсолютний шлях
|
|
4366
|
+
* @param {string} rootNorm нормалізований корінь
|
|
4367
|
+
* @returns {Promise<boolean>} true, якщо resolved є k8s base-каталогом з kustomization.yaml
|
|
4368
|
+
*/
|
|
4369
|
+
async function isK8sBaseDir(resolved, rootNorm) {
|
|
4370
|
+
if (basename(resolved) !== 'base') return false
|
|
4371
|
+
if (!existsSync(join(resolved, 'kustomization.yaml'))) return false
|
|
4372
|
+
if (!isUnderK8sPathRelToRoot(rootNorm, resolved)) return false
|
|
4373
|
+
let st
|
|
4374
|
+
try {
|
|
4375
|
+
st = await stat(resolved)
|
|
4376
|
+
} catch {
|
|
4377
|
+
return false
|
|
4378
|
+
}
|
|
4379
|
+
return st.isDirectory()
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4282
4382
|
/**
|
|
4283
4383
|
* За списку посилань kustomize повертає каталоги `.../base` з `kustomization.yaml` (наслідування base).
|
|
4284
4384
|
* @param {string} kustDir каталог kustomization.yaml
|
|
@@ -4292,22 +4392,8 @@ async function k8sBaseDirsFromKustomizeResourcePathRefs(kustDir, pathRefs, rootN
|
|
|
4292
4392
|
for (const ref of pathRefs) {
|
|
4293
4393
|
if (typeof ref === 'string' && !ref.includes('://') && ref.trim() !== '') {
|
|
4294
4394
|
const resolved = resolve(kustDir, ref.trim())
|
|
4295
|
-
if (resolvedFilePathIsUnderRoot(rootNorm, resolved)) {
|
|
4296
|
-
|
|
4297
|
-
try {
|
|
4298
|
-
st = await stat(resolved)
|
|
4299
|
-
} catch {
|
|
4300
|
-
st = undefined
|
|
4301
|
-
}
|
|
4302
|
-
if (
|
|
4303
|
-
st !== undefined
|
|
4304
|
-
&& st.isDirectory()
|
|
4305
|
-
&& basename(resolved) === 'base'
|
|
4306
|
-
&& existsSync(join(resolved, 'kustomization.yaml'))
|
|
4307
|
-
&& isUnderK8sPathRelToRoot(rootNorm, resolved)
|
|
4308
|
-
) {
|
|
4309
|
-
out.push(resolved)
|
|
4310
|
-
}
|
|
4395
|
+
if (resolvedFilePathIsUnderRoot(rootNorm, resolved) && (await isK8sBaseDir(resolved, rootNorm))) {
|
|
4396
|
+
out.push(resolved)
|
|
4311
4397
|
}
|
|
4312
4398
|
}
|
|
4313
4399
|
}
|
|
@@ -4368,9 +4454,7 @@ async function verifyK8sBaseKustomizeHpaPdbNeedDeployment(kustAbs, rel, fail, pa
|
|
|
4368
4454
|
const { hasDeployment, hasHpa, hasPdb } = await getTreeFlags(kustAbs)
|
|
4369
4455
|
if (hasHpa || hasPdb) {
|
|
4370
4456
|
if (hasDeployment) {
|
|
4371
|
-
passFn(
|
|
4372
|
-
`${rel}: у дереві kustomize base є HPA/PDB і Deployment (k8s.mdc)`
|
|
4373
|
-
)
|
|
4457
|
+
passFn(`${rel}: у дереві kustomize base є HPA/PDB і Deployment (k8s.mdc)`)
|
|
4374
4458
|
} else {
|
|
4375
4459
|
fail(
|
|
4376
4460
|
`${rel}: у base є HorizontalPodAutoscaler і/або PodDisruptionBudget у resources/bases/…, але дерева kustomize не містить Deployment — HPA і PDB дозволені тільки разом із Deployment (k8s.mdc)`
|
|
@@ -4407,47 +4491,52 @@ async function verifyOverlayHpaPdbFileRefsRespectBaseDeployment(
|
|
|
4407
4491
|
return
|
|
4408
4492
|
}
|
|
4409
4493
|
|
|
4410
|
-
const
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
if (h) {
|
|
4414
|
-
return true
|
|
4415
|
-
}
|
|
4416
|
-
}
|
|
4417
|
-
return false
|
|
4418
|
-
})()
|
|
4494
|
+
const treeFlags = await Promise.all(baseDirs.map(bd => getTreeFlags(join(bd, 'kustomization.yaml'))))
|
|
4495
|
+
const anyBaseHasDep = treeFlags.some(f => f.hasDeployment)
|
|
4496
|
+
|
|
4419
4497
|
for (const ref of pathRefs) {
|
|
4420
4498
|
if (typeof ref === 'string' && !ref.includes('://') && ref.trim() !== '') {
|
|
4421
|
-
|
|
4422
|
-
if (resolvedFilePathIsUnderRoot(root, fAbs) && existsSync(fAbs)) {
|
|
4423
|
-
let st
|
|
4424
|
-
try {
|
|
4425
|
-
st = await stat(fAbs)
|
|
4426
|
-
} catch {
|
|
4427
|
-
st = undefined
|
|
4428
|
-
}
|
|
4429
|
-
if (st !== undefined && st.isFile() && YAML_EXTENSION_RE.test(fAbs)) {
|
|
4430
|
-
const fUnderSomeBase = baseDirs.some(bd => isResolvedFileUnderDirectory(bd, fAbs))
|
|
4431
|
-
if (!fUnderSomeBase) {
|
|
4432
|
-
const hpaPdb = await yamlFileContainsHpaOrPdbDocument(fAbs)
|
|
4433
|
-
if (hpaPdb) {
|
|
4434
|
-
if (anyBaseHasDep) {
|
|
4435
|
-
passFn(
|
|
4436
|
-
`${rel}: overlay-файл «${(relative(root, fAbs) || ref).replaceAll('\\', '/')}» з HPA/PDB, base містить Deployment (k8s.mdc)`
|
|
4437
|
-
)
|
|
4438
|
-
} else {
|
|
4439
|
-
fail(
|
|
4440
|
-
`${rel}: посилання «${ref}» містить HorizontalPodAutoscaler і/або PodDisruptionBudget, а наслідуваний k8s/base не дає у дереві Deployment — прибери HPA/PDB або додай Deployment у base (k8s.mdc)`
|
|
4441
|
-
)
|
|
4442
|
-
}
|
|
4443
|
-
}
|
|
4444
|
-
}
|
|
4445
|
-
}
|
|
4446
|
-
}
|
|
4499
|
+
await checkOverlayRefHpaPdb(root, kustDir, rel, ref, baseDirs, anyBaseHasDep, fail, passFn)
|
|
4447
4500
|
}
|
|
4448
4501
|
}
|
|
4449
4502
|
}
|
|
4450
4503
|
|
|
4504
|
+
/**
|
|
4505
|
+
* @param {string} root нормалізований корінь
|
|
4506
|
+
* @param {string} kustDir каталог kustomization.yaml
|
|
4507
|
+
* @param {string} rel відносний шлях для повідомлень
|
|
4508
|
+
* @param {string} ref посилання з pathRefs
|
|
4509
|
+
* @param {string[]} baseDirs масив base-каталогів
|
|
4510
|
+
* @param {boolean} anyBaseHasDep чи є Deployment у base
|
|
4511
|
+
* @param {(msg: string) => void} fail callback
|
|
4512
|
+
* @param {(msg: string) => void} passFn callback
|
|
4513
|
+
* @returns {Promise<void>}
|
|
4514
|
+
*/
|
|
4515
|
+
async function checkOverlayRefHpaPdb(root, kustDir, rel, ref, baseDirs, anyBaseHasDep, fail, passFn) {
|
|
4516
|
+
const fAbs = resolve(kustDir, ref.trim())
|
|
4517
|
+
if (!resolvedFilePathIsUnderRoot(root, fAbs) || !existsSync(fAbs)) return
|
|
4518
|
+
let st
|
|
4519
|
+
try {
|
|
4520
|
+
st = await stat(fAbs)
|
|
4521
|
+
} catch {
|
|
4522
|
+
return
|
|
4523
|
+
}
|
|
4524
|
+
if (!st.isFile() || !YAML_EXTENSION_RE.test(fAbs)) return
|
|
4525
|
+
const fUnderSomeBase = baseDirs.some(bd => isResolvedFileUnderDirectory(bd, fAbs))
|
|
4526
|
+
if (fUnderSomeBase) return
|
|
4527
|
+
const hpaPdb = await yamlFileContainsHpaOrPdbDocument(fAbs)
|
|
4528
|
+
if (!hpaPdb) return
|
|
4529
|
+
if (anyBaseHasDep) {
|
|
4530
|
+
passFn(
|
|
4531
|
+
`${rel}: overlay-файл «${(relative(root, fAbs) || ref).replaceAll('\\', '/')}» з HPA/PDB, base містить Deployment (k8s.mdc)`
|
|
4532
|
+
)
|
|
4533
|
+
} else {
|
|
4534
|
+
fail(
|
|
4535
|
+
`${rel}: посилання «${ref}» містить HorizontalPodAutoscaler і/або PodDisruptionBudget, а наслідуваний k8s/base не дає у дереві Deployment — прибери HPA/PDB або додай Deployment у base (k8s.mdc)`
|
|
4536
|
+
)
|
|
4537
|
+
}
|
|
4538
|
+
}
|
|
4539
|
+
|
|
4451
4540
|
/**
|
|
4452
4541
|
* Перевіряє всі кастомізації: (1) у k8s/base дереві HPA/PDB тільки з Deployment; (2) overlay, що
|
|
4453
4542
|
* посилається на base, не додає HPA/PDB без Deployment у base.
|
|
@@ -4482,15 +4571,7 @@ async function validateKustomizeHpaPdbOnlyWithBaseDeployment(root, yamlFilesAbs,
|
|
|
4482
4571
|
if (isK8sBaseKustomizationRelPath(rel)) {
|
|
4483
4572
|
await verifyK8sBaseKustomizeHpaPdbNeedDeployment(kustAbs, rel, fail, passFn, getTreeFlags)
|
|
4484
4573
|
} else {
|
|
4485
|
-
await verifyOverlayHpaPdbFileRefsRespectBaseDeployment(
|
|
4486
|
-
rootNorm,
|
|
4487
|
-
kustAbs,
|
|
4488
|
-
rel,
|
|
4489
|
-
kust,
|
|
4490
|
-
fail,
|
|
4491
|
-
passFn,
|
|
4492
|
-
getTreeFlags
|
|
4493
|
-
)
|
|
4574
|
+
await verifyOverlayHpaPdbFileRefsRespectBaseDeployment(rootNorm, kustAbs, rel, kust, fail, passFn, getTreeFlags)
|
|
4494
4575
|
}
|
|
4495
4576
|
}
|
|
4496
4577
|
}
|
|
@@ -4767,6 +4848,8 @@ export async function check() {
|
|
|
4767
4848
|
|
|
4768
4849
|
await validateKustomizationPathRefsExistOnDisk(root, yamlFiles, fail)
|
|
4769
4850
|
|
|
4851
|
+
await validateKustomizationResourcesSortedAlphabetically(root, yamlFiles, fail)
|
|
4852
|
+
|
|
4770
4853
|
await validateKustomizationPatchTargetsResolved(root, yamlFiles, fail)
|
|
4771
4854
|
|
|
4772
4855
|
await validateKustomizeHpaPdbOnlyWithBaseDeployment(root, yamlFiles, fail, pass)
|
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
* `mirror.gcr.io/library/{alpine,nginx,node}`.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
const FROM_LINE_RE = /^\s*FROM\s+([^\n]+)/i
|
|
15
|
+
const TOKEN_RE = /(?:[^\s"]+|"[^"]*")+/g
|
|
16
|
+
const MIRROR_GCR_RE = /^mirror\.gcr\.io\//i
|
|
17
|
+
const IP_LIKE_RE = /^\d+\.\d+/
|
|
18
|
+
const HOST_PORT_RE = /^\S+:\d+$/
|
|
19
|
+
const DOCKER_IO_PREFIX_RE = /^(docker\.io|index\.docker\.io)\//
|
|
20
|
+
const NEWLINE_SPLIT_RE = /\r?\n/
|
|
21
|
+
|
|
14
22
|
/**
|
|
15
23
|
* @param {string} t — токен образу в лапках або без
|
|
16
24
|
* @returns {string} токен без зовнішніх лапок
|
|
@@ -25,18 +33,16 @@ function stripFromImageQuotes(t) {
|
|
|
25
33
|
/**
|
|
26
34
|
* Виділяє токен образу з рядка `FROM` (після зняття inline-коментаря, без AS).
|
|
27
35
|
* Підтримує прапорець `--platform=…` і форму `--platform` + значення.
|
|
28
|
-
*
|
|
29
36
|
* @param {string} line — рядок Dockerfile
|
|
30
37
|
* @returns {string | null} токен образу або null, якщо рядок не `FROM`
|
|
31
38
|
*/
|
|
32
39
|
export function getFromImageToken(line) {
|
|
33
40
|
const withoutComment = line.split('#')[0].trim()
|
|
34
41
|
if (!withoutComment) return null
|
|
35
|
-
const m = withoutComment.match(
|
|
42
|
+
const m = withoutComment.match(FROM_LINE_RE)
|
|
36
43
|
if (!m) return null
|
|
37
44
|
const raw = m[1].trim()
|
|
38
|
-
const
|
|
39
|
-
const tokens = raw.match(tokenRe) || []
|
|
45
|
+
const tokens = raw.match(TOKEN_RE) || []
|
|
40
46
|
let i = 0
|
|
41
47
|
while (i < tokens.length) {
|
|
42
48
|
const t = tokens[i]
|
|
@@ -64,13 +70,12 @@ export function getFromImageToken(line) {
|
|
|
64
70
|
/**
|
|
65
71
|
* Схоже на звернення до Docker Hub (коротке ім’я, `docker.io/…`, не mirror.gcr.io).
|
|
66
72
|
* Не вважати Hub: явний чужий реєстр (`gcr.io/…`, `reg.example.com:5000/…`).
|
|
67
|
-
*
|
|
68
73
|
* @param {string} imageToken — ref образу (FROM)
|
|
69
74
|
* @returns {boolean} true, якщо схоже на pull з Docker Hub
|
|
70
75
|
*/
|
|
71
76
|
export function isDockerHubStyleImageRef(imageToken) {
|
|
72
77
|
if (!imageToken) return false
|
|
73
|
-
if (
|
|
78
|
+
if (MIRROR_GCR_RE.test(imageToken)) return false
|
|
74
79
|
const noDigest = imageToken.split('@')[0] || ''
|
|
75
80
|
if (!noDigest.includes('/')) {
|
|
76
81
|
return true
|
|
@@ -78,8 +83,8 @@ export function isDockerHubStyleImageRef(imageToken) {
|
|
|
78
83
|
const first = noDigest.split('/')[0] || ''
|
|
79
84
|
if (first === 'docker.io' || first === 'index.docker.io') return true
|
|
80
85
|
if (first.includes('.')) return false
|
|
81
|
-
if (first === 'localhost' ||
|
|
82
|
-
if (first.includes(':') &&
|
|
86
|
+
if (first === 'localhost' || IP_LIKE_RE.test(first)) return false
|
|
87
|
+
if (first.includes(':') && HOST_PORT_RE.test(first)) {
|
|
83
88
|
return false
|
|
84
89
|
}
|
|
85
90
|
return true
|
|
@@ -87,13 +92,12 @@ export function isDockerHubStyleImageRef(imageToken) {
|
|
|
87
92
|
|
|
88
93
|
/**
|
|
89
94
|
* Нормалізує шлях репозиторію (без тега/digest) для порівняння: `library/node`, `oven/bun`, …
|
|
90
|
-
*
|
|
91
95
|
* @param {string} imageToken — ref образу
|
|
92
96
|
* @returns {string} нормалізований шлях репозиторію без тега
|
|
93
97
|
*/
|
|
94
98
|
export function normalizeHubRepoPath(imageToken) {
|
|
95
99
|
let s = (imageToken.split('@')[0] || '').toLowerCase()
|
|
96
|
-
s = s.replace(
|
|
100
|
+
s = s.replace(DOCKER_IO_PREFIX_RE, '')
|
|
97
101
|
if (!s.includes('/')) {
|
|
98
102
|
return `library/${s.split(':')[0]}`
|
|
99
103
|
}
|
|
@@ -105,12 +109,9 @@ export function normalizeHubRepoPath(imageToken) {
|
|
|
105
109
|
return s
|
|
106
110
|
}
|
|
107
111
|
|
|
108
|
-
const HUB_REPOS_REQUIRING_MIRROR = /** @type {const} */ (
|
|
109
|
-
'oven/bun',
|
|
110
|
-
|
|
111
|
-
'library/nginx',
|
|
112
|
-
'library/node'
|
|
113
|
-
])
|
|
112
|
+
const HUB_REPOS_REQUIRING_MIRROR = /** @type {const} */ (
|
|
113
|
+
new Set(['oven/bun', 'library/alpine', 'library/nginx', 'library/node'])
|
|
114
|
+
)
|
|
114
115
|
|
|
115
116
|
const EXPECTED_MIRROR = /** @type {const} */ ({
|
|
116
117
|
'oven/bun': 'mirror.gcr.io/oven/bun',
|
|
@@ -120,17 +121,16 @@ const EXPECTED_MIRROR = /** @type {const} */ ({
|
|
|
120
121
|
})
|
|
121
122
|
|
|
122
123
|
/**
|
|
123
|
-
* Якщо образ
|
|
124
|
-
*
|
|
124
|
+
* Якщо образ тягнеться з Hub і підлягає дзеркалу — повертає рекомендовану заміну, інакше `null`.
|
|
125
125
|
* @param {string} imageToken — ref після `FROM`
|
|
126
126
|
* @returns {string | null} рекомендований `mirror.gcr.io/...` (без тега) або null
|
|
127
127
|
*/
|
|
128
128
|
export function getRequiredMirrorGcrImage(imageToken) {
|
|
129
129
|
if (!imageToken) return null
|
|
130
|
-
if (
|
|
130
|
+
if (MIRROR_GCR_RE.test(imageToken)) return null
|
|
131
131
|
if (!isDockerHubStyleImageRef(imageToken)) return null
|
|
132
132
|
const norm = normalizeHubRepoPath(imageToken)
|
|
133
|
-
if (!HUB_REPOS_REQUIRING_MIRROR.
|
|
133
|
+
if (!HUB_REPOS_REQUIRING_MIRROR.has(/** @type {keyof typeof EXPECTED_MIRROR} */ (norm))) {
|
|
134
134
|
return null
|
|
135
135
|
}
|
|
136
136
|
return EXPECTED_MIRROR[/** @type {keyof typeof EXPECTED_MIRROR} */ (norm)]
|
|
@@ -138,14 +138,12 @@ export function getRequiredMirrorGcrImage(imageToken) {
|
|
|
138
138
|
|
|
139
139
|
/**
|
|
140
140
|
* Сканує вміст Dockerfile / Containerfile — повертає рядок помилки або `null`.
|
|
141
|
-
*
|
|
142
141
|
* @param {string} fileContent — повний вміст Dockerfile
|
|
143
142
|
* @returns {string | null} повідомлення з номером рядка або null
|
|
144
143
|
*/
|
|
145
144
|
export function getMirrorGcrHint(fileContent) {
|
|
146
|
-
const lines = fileContent.split(
|
|
147
|
-
for (
|
|
148
|
-
const line = lines[n]
|
|
145
|
+
const lines = fileContent.split(NEWLINE_SPLIT_RE)
|
|
146
|
+
for (const [n, line] of lines.entries()) {
|
|
149
147
|
const image = getFromImageToken(line)
|
|
150
148
|
const expected = getRequiredMirrorGcrImage(image)
|
|
151
149
|
if (expected) {
|