@nitra/cursor 1.8.105 → 1.8.108
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/bin/auto-rules.md +2 -2
- package/bin/n-cursor.js +5 -15
- package/mdc/js-pino.mdc +2 -2
- package/mdc/k8s.mdc +22 -12
- package/mdc/text.mdc +3 -3
- package/package.json +1 -1
- package/scripts/check-abie.mjs +515 -528
- package/scripts/check-bun.mjs +106 -78
- package/scripts/check-ga.mjs +151 -119
- package/scripts/check-js-lint.mjs +256 -179
- package/scripts/check-js-pino.mjs +48 -3
- package/scripts/check-k8s.mjs +403 -34
- package/scripts/check-nginx-default-tpl.mjs +109 -91
- package/scripts/check-npm-module.mjs +163 -116
- package/scripts/check-style-lint.mjs +74 -61
- package/scripts/check-text.mjs +289 -209
- package/scripts/check-vue.mjs +108 -67
- package/scripts/utils/bunyan-imports.mjs +182 -0
- package/scripts/utils/gha-workflow.mjs +3 -1
package/scripts/check-k8s.mjs
CHANGED
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
* компонент). Спочатку шукається збіг за фактичним `type`, потім за **`*`**.
|
|
56
56
|
* Dockerfile — правило docker.mdc, скрипт check-docker.mjs.
|
|
57
57
|
*
|
|
58
|
-
*
|
|
58
|
+
* **Структура `HTTPRoute` для Hasura-Deployment:** звіряється канон 4 правил у **`spec.rules`** (редиректи **`<prefix>/ql`** і **`<prefix>/ql/`** на **`<prefix>/ql/console`** 302, **`PathPrefix <prefix>/ql`** + **URLRewrite** на **`/`**, окреме WebSocket-правило з **`RequestHeaderModifier`** remove **`Authorization`**). **Префікс параметризовано** (рядок перед **`/ql`** у першому Hasura-правилі). **Прив'язка** — за **`metadata.name`** у тому ж каталозі, що й **Deployment** з образом **`hasura/graphql-engine`** (див. k8s.mdc). **Додаткові правила** поверх канону дозволені.
|
|
59
59
|
*/
|
|
60
60
|
import { existsSync } from 'node:fs'
|
|
61
61
|
import { readFile, stat, unlink } from 'node:fs/promises'
|
|
@@ -949,10 +949,7 @@ function formatKustomizePatchTargetForMessage(target) {
|
|
|
949
949
|
*/
|
|
950
950
|
function failIfExplicitPatchTargetsNotInCatalog(rel, first, catalog, fail) {
|
|
951
951
|
for (const { section, index, target } of extractExplicitPatchTargetsFromKustomization(first)) {
|
|
952
|
-
if (
|
|
953
|
-
shouldValidateKustomizePatchTarget(target) &&
|
|
954
|
-
!kustomizeResourceCatalogMatchesPatchTarget(catalog, target)
|
|
955
|
-
) {
|
|
952
|
+
if (shouldValidateKustomizePatchTarget(target) && !kustomizeResourceCatalogMatchesPatchTarget(catalog, target)) {
|
|
956
953
|
fail(
|
|
957
954
|
`${rel}: ${section}[${index}].target — немає відповідного ресурсу в resources/bases/components/crds (рекурсивно): ${formatKustomizePatchTargetForMessage(target)}`
|
|
958
955
|
)
|
|
@@ -1163,7 +1160,16 @@ async function validatePatchTargetsOneKustomizationFile(root, kustAbs, rootNorm,
|
|
|
1163
1160
|
const kustNs = typeof rec.namespace === 'string' && rec.namespace.trim() !== '' ? rec.namespace.trim() : ''
|
|
1164
1161
|
failIfExplicitPatchTargetsNotInCatalog(rel, first, catalog, fail)
|
|
1165
1162
|
await failIfPathOnlyPatchesNotInCatalog(rel, rec.patches, kustDir, rootNorm, root, catalog, kustNs, fail)
|
|
1166
|
-
await failIfStrategicMergePatchesNotInCatalog(
|
|
1163
|
+
await failIfStrategicMergePatchesNotInCatalog(
|
|
1164
|
+
rel,
|
|
1165
|
+
rec.patchesStrategicMerge,
|
|
1166
|
+
kustDir,
|
|
1167
|
+
rootNorm,
|
|
1168
|
+
root,
|
|
1169
|
+
catalog,
|
|
1170
|
+
kustNs,
|
|
1171
|
+
fail
|
|
1172
|
+
)
|
|
1167
1173
|
}
|
|
1168
1174
|
|
|
1169
1175
|
/**
|
|
@@ -1607,7 +1613,9 @@ async function auditJson6902PatchExternalFile(rel, resolved, root, patchRef, fai
|
|
|
1607
1613
|
return
|
|
1608
1614
|
}
|
|
1609
1615
|
const relPatch = (relative(root, resolved) || patchRef).replaceAll('\\', '/')
|
|
1610
|
-
fail(
|
|
1616
|
+
fail(
|
|
1617
|
+
`${rel}: patch-файл «${relPatch}»: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
|
|
1618
|
+
)
|
|
1611
1619
|
}
|
|
1612
1620
|
|
|
1613
1621
|
/**
|
|
@@ -1621,15 +1629,7 @@ async function auditJson6902PatchExternalFile(rel, resolved, root, patchRef, fai
|
|
|
1621
1629
|
* @param {(msg: string) => void} fail реєстрація порушення
|
|
1622
1630
|
* @returns {Promise<void>}
|
|
1623
1631
|
*/
|
|
1624
|
-
async function auditOneKustomizationJson6902Patch(
|
|
1625
|
-
rel,
|
|
1626
|
-
pr,
|
|
1627
|
-
patchIdx,
|
|
1628
|
-
kustAbs,
|
|
1629
|
-
rootNorm,
|
|
1630
|
-
root,
|
|
1631
|
-
fail
|
|
1632
|
-
) {
|
|
1632
|
+
async function auditOneKustomizationJson6902Patch(rel, pr, patchIdx, kustAbs, rootNorm, root, fail) {
|
|
1633
1633
|
if (typeof pr.patch === 'string' && pr.patch.trim() !== '') {
|
|
1634
1634
|
failIfJson6902RemoveAddConflictOnSamePath(rel, `patches[${patchIdx}] inline JSON6902`, pr.patch, fail)
|
|
1635
1635
|
}
|
|
@@ -1924,6 +1924,41 @@ export function deploymentHasuraGraphqlEngineImageViolation(manifest) {
|
|
|
1924
1924
|
return hasuraGraphqlEngineViolationInContainerList('initContainers', podSpec.initContainers)
|
|
1925
1925
|
}
|
|
1926
1926
|
|
|
1927
|
+
/**
|
|
1928
|
+
* Чи у списку контейнерів є хоча б один з образом **hasura/graphql-engine** (будь-який тег).
|
|
1929
|
+
* @param {unknown} containers значення **containers** / **initContainers** із podSpec
|
|
1930
|
+
* @returns {boolean} true — якщо знайдено хоча б один контейнер з образом Hasura
|
|
1931
|
+
*/
|
|
1932
|
+
function containerListHasHasuraImage(containers) {
|
|
1933
|
+
if (!Array.isArray(containers)) return false
|
|
1934
|
+
for (const c of containers) {
|
|
1935
|
+
if (c !== null && typeof c === 'object' && !Array.isArray(c)) {
|
|
1936
|
+
const image = /** @type {Record<string, unknown>} */ (c).image
|
|
1937
|
+
if (typeof image === 'string' && image !== '' && isHasuraGraphqlEngineImageRef(image)) return true
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
return false
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
/**
|
|
1944
|
+
* Чи **Deployment** використовує образ **hasura/graphql-engine** у будь-якому контейнері (маркер для прив'язки HTTPRoute-канона).
|
|
1945
|
+
* @param {unknown} manifest корінь YAML-документа
|
|
1946
|
+
* @returns {boolean} true — для Deployment з Hasura-контейнером у containers / initContainers
|
|
1947
|
+
*/
|
|
1948
|
+
export function isHasuraDeploymentManifest(manifest) {
|
|
1949
|
+
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) return false
|
|
1950
|
+
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
1951
|
+
if (rec.kind !== 'Deployment') return false
|
|
1952
|
+
const spec = rec.spec
|
|
1953
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return false
|
|
1954
|
+
const template = /** @type {Record<string, unknown>} */ (spec).template
|
|
1955
|
+
if (template === null || typeof template !== 'object' || Array.isArray(template)) return false
|
|
1956
|
+
const podSpec = /** @type {Record<string, unknown>} */ (template).spec
|
|
1957
|
+
if (podSpec === null || typeof podSpec !== 'object' || Array.isArray(podSpec)) return false
|
|
1958
|
+
const p = /** @type {Record<string, unknown>} */ (podSpec)
|
|
1959
|
+
return containerListHasHasuraImage(p.containers) || containerListHasHasuraImage(p.initContainers)
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1927
1962
|
/**
|
|
1928
1963
|
* Чи **Service** містить заборонені анотації GKE у **`metadata.annotations`** (k8s.mdc).
|
|
1929
1964
|
* @param {unknown} manifest корінь YAML-документа
|
|
@@ -2154,6 +2189,247 @@ function scanGatewayApiRouteBackendRefsInYamlBody(rel, body, fail) {
|
|
|
2154
2189
|
}
|
|
2155
2190
|
}
|
|
2156
2191
|
|
|
2192
|
+
/**
|
|
2193
|
+
* Звузити `unknown` до `Record<string, unknown>` (`null`, масиви, примітиви → null).
|
|
2194
|
+
* @param {unknown} node довільний вузол YAML-документа
|
|
2195
|
+
* @returns {Record<string, unknown> | null} plain-об'єкт або null, якщо це не plain-запис
|
|
2196
|
+
*/
|
|
2197
|
+
function asPlainRecord(node) {
|
|
2198
|
+
if (node === null || node === undefined || typeof node !== 'object' || Array.isArray(node)) return null
|
|
2199
|
+
return /** @type {Record<string, unknown>} */ (node)
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
/**
|
|
2203
|
+
* Чи `match` — рівно один шлях заданого типу з потрібним значенням, **без** `headers`.
|
|
2204
|
+
* @param {unknown} rule одне правило `HTTPRoute`
|
|
2205
|
+
* @param {'Exact' | 'PathPrefix'} pathType очікуваний `path.type`
|
|
2206
|
+
* @param {string} pathValue очікуваний `path.value`
|
|
2207
|
+
* @returns {boolean} true — якщо `matches` рівно один і відповідає критерію
|
|
2208
|
+
*/
|
|
2209
|
+
function hasuraRuleMatchesSinglePathNoHeaders(rule, pathType, pathValue) {
|
|
2210
|
+
const r = asPlainRecord(rule)
|
|
2211
|
+
if (r === null) return false
|
|
2212
|
+
const matches = r.matches
|
|
2213
|
+
if (!Array.isArray(matches) || matches.length !== 1) return false
|
|
2214
|
+
const m = asPlainRecord(matches[0])
|
|
2215
|
+
if (m === null) return false
|
|
2216
|
+
if (m.headers !== undefined) return false
|
|
2217
|
+
const p = asPlainRecord(m.path)
|
|
2218
|
+
if (p === null) return false
|
|
2219
|
+
return p.type === pathType && p.value === pathValue
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
/**
|
|
2223
|
+
* Чи **filters** — рівно один `RequestRedirect` з `ReplaceFullPath` на `toPath` і `statusCode: 302`.
|
|
2224
|
+
* @param {unknown} rule одне правило `HTTPRoute`
|
|
2225
|
+
* @param {string} toPath очікуваний `requestRedirect.path.replaceFullPath`
|
|
2226
|
+
* @returns {boolean} true — якщо filters відповідають канону редиректу
|
|
2227
|
+
*/
|
|
2228
|
+
function hasuraRuleHasExactRedirect(rule, toPath) {
|
|
2229
|
+
const r = asPlainRecord(rule)
|
|
2230
|
+
if (r === null) return false
|
|
2231
|
+
const filters = r.filters
|
|
2232
|
+
if (!Array.isArray(filters) || filters.length !== 1) return false
|
|
2233
|
+
const f = asPlainRecord(filters[0])
|
|
2234
|
+
if (f === null || f.type !== 'RequestRedirect') return false
|
|
2235
|
+
const rr = asPlainRecord(f.requestRedirect)
|
|
2236
|
+
if (rr === null || rr.statusCode !== 302) return false
|
|
2237
|
+
const p = asPlainRecord(rr.path)
|
|
2238
|
+
return p !== null && p.type === 'ReplaceFullPath' && p.replaceFullPath === toPath
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
/**
|
|
2242
|
+
* Чи серед **filters** є `URLRewrite` з `ReplacePrefixMatch: /`.
|
|
2243
|
+
* @param {unknown[]} filters масив filters з одного правила `HTTPRoute`
|
|
2244
|
+
* @returns {boolean} true — якщо фільтр `URLRewrite` має `ReplacePrefixMatch: /`
|
|
2245
|
+
*/
|
|
2246
|
+
function hasuraFiltersIncludeUrlRewriteToSlash(filters) {
|
|
2247
|
+
for (const f of filters) {
|
|
2248
|
+
const fr = asPlainRecord(f)
|
|
2249
|
+
if (fr !== null && fr.type === 'URLRewrite') {
|
|
2250
|
+
const rw = asPlainRecord(fr.urlRewrite)
|
|
2251
|
+
if (rw === null) return false
|
|
2252
|
+
const p = asPlainRecord(rw.path)
|
|
2253
|
+
return p !== null && p.type === 'ReplacePrefixMatch' && p.replacePrefixMatch === '/'
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
return false
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
/**
|
|
2260
|
+
* Чи серед **filters** є `RequestHeaderModifier` з `remove: [Authorization]`.
|
|
2261
|
+
* @param {unknown[]} filters масив filters з одного правила `HTTPRoute`
|
|
2262
|
+
* @returns {boolean} true — якщо фільтр `RequestHeaderModifier` видаляє саме `Authorization`
|
|
2263
|
+
*/
|
|
2264
|
+
function hasuraFiltersRemoveAuthorization(filters) {
|
|
2265
|
+
for (const f of filters) {
|
|
2266
|
+
const fr = asPlainRecord(f)
|
|
2267
|
+
if (fr !== null && fr.type === 'RequestHeaderModifier') {
|
|
2268
|
+
const mod = asPlainRecord(fr.requestHeaderModifier)
|
|
2269
|
+
if (mod === null) return false
|
|
2270
|
+
const remove = mod.remove
|
|
2271
|
+
if (!Array.isArray(remove) || remove.length !== 1) return false
|
|
2272
|
+
return remove[0] === 'Authorization'
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
return false
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
/**
|
|
2279
|
+
* Ім'я єдиного `backendRef` у правилі (або null, якщо backend-ів не рівно один).
|
|
2280
|
+
* @param {unknown} rule одне правило `HTTPRoute`
|
|
2281
|
+
* @returns {string | null} `backendRefs[0].name` або null, якщо backend-ів не рівно один
|
|
2282
|
+
*/
|
|
2283
|
+
function hasuraRuleSingleBackendName(rule) {
|
|
2284
|
+
const r = asPlainRecord(rule)
|
|
2285
|
+
if (r === null) return null
|
|
2286
|
+
const refs = r.backendRefs
|
|
2287
|
+
if (!Array.isArray(refs) || refs.length !== 1) return null
|
|
2288
|
+
const b = asPlainRecord(refs[0])
|
|
2289
|
+
if (b === null || typeof b.name !== 'string') return null
|
|
2290
|
+
return b.name
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
/**
|
|
2294
|
+
* Правило 3: `PathPrefix <qlPath>` + **filters** = 1 × `URLRewrite(ReplacePrefixMatch: /)`.
|
|
2295
|
+
* @param {unknown} rule одне правило `HTTPRoute`
|
|
2296
|
+
* @param {string} qlPath очікуваний `path.value` (`<prefix>/ql`)
|
|
2297
|
+
* @returns {boolean} true — якщо правило відповідає канону пункту 3
|
|
2298
|
+
*/
|
|
2299
|
+
function hasuraRuleIsQlUrlRewrite(rule, qlPath) {
|
|
2300
|
+
if (!hasuraRuleMatchesSinglePathNoHeaders(rule, 'PathPrefix', qlPath)) return false
|
|
2301
|
+
const r = asPlainRecord(rule)
|
|
2302
|
+
if (r === null) return false
|
|
2303
|
+
const filters = r.filters
|
|
2304
|
+
if (!Array.isArray(filters) || filters.length !== 1) return false
|
|
2305
|
+
return hasuraFiltersIncludeUrlRewriteToSlash(filters)
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
/**
|
|
2309
|
+
* Правило 4: WebSocket — `PathPrefix <qlPath>` + `Upgrade: websocket`, **filters** = `URLRewrite` + `RequestHeaderModifier(remove Authorization)`.
|
|
2310
|
+
* @param {unknown} rule одне правило `HTTPRoute`
|
|
2311
|
+
* @param {string} qlPath очікуваний `path.value` (`<prefix>/ql`)
|
|
2312
|
+
* @returns {boolean} true — якщо правило відповідає канону пункту 4 (WebSocket)
|
|
2313
|
+
*/
|
|
2314
|
+
function hasuraRuleIsWebsocket(rule, qlPath) {
|
|
2315
|
+
const r = asPlainRecord(rule)
|
|
2316
|
+
if (r === null) return false
|
|
2317
|
+
const matches = r.matches
|
|
2318
|
+
if (!Array.isArray(matches) || matches.length !== 1) return false
|
|
2319
|
+
const m = asPlainRecord(matches[0])
|
|
2320
|
+
if (m === null) return false
|
|
2321
|
+
const p = asPlainRecord(m.path)
|
|
2322
|
+
if (p === null || p.type !== 'PathPrefix' || p.value !== qlPath) return false
|
|
2323
|
+
const headers = m.headers
|
|
2324
|
+
if (!Array.isArray(headers) || headers.length !== 1) return false
|
|
2325
|
+
const h = asPlainRecord(headers[0])
|
|
2326
|
+
if (h === null || h.type !== 'Exact' || h.name !== 'Upgrade' || h.value !== 'websocket') return false
|
|
2327
|
+
const filters = r.filters
|
|
2328
|
+
if (!Array.isArray(filters) || filters.length !== 2) return false
|
|
2329
|
+
return hasuraFiltersIncludeUrlRewriteToSlash(filters) && hasuraFiltersRemoveAuthorization(filters)
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
/**
|
|
2333
|
+
* Знаходить перше правило з **`matches`** = `[{ path: { type: 'Exact', value: '<prefix>/ql' } }]` (без headers),
|
|
2334
|
+
* повертає `<prefix>` (може бути порожнім) і позицію правила 1.
|
|
2335
|
+
* @param {unknown[]} rules вміст `spec.rules` HTTPRoute
|
|
2336
|
+
* @returns {{ prefix: string, startIndex: number } | null} виявлений префікс і позиція правила 1 або null
|
|
2337
|
+
*/
|
|
2338
|
+
function findHasuraCanonStart(rules) {
|
|
2339
|
+
for (let i = 0; i < rules.length; i++) {
|
|
2340
|
+
const r = asPlainRecord(rules[i])
|
|
2341
|
+
const matches = r === null ? null : r.matches
|
|
2342
|
+
if (!Array.isArray(matches) || matches.length !== 1) {
|
|
2343
|
+
// наступне правило
|
|
2344
|
+
} else {
|
|
2345
|
+
const m = asPlainRecord(matches[0])
|
|
2346
|
+
const p = m === null || m.headers !== undefined ? null : asPlainRecord(m.path)
|
|
2347
|
+
if (
|
|
2348
|
+
p !== null &&
|
|
2349
|
+
p.type === 'Exact' &&
|
|
2350
|
+
typeof p.value === 'string' &&
|
|
2351
|
+
p.value.endsWith('/ql')
|
|
2352
|
+
) {
|
|
2353
|
+
return { prefix: p.value.slice(0, -'/ql'.length), startIndex: i }
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
return null
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
/**
|
|
2361
|
+
* Знаходить перше правило за індексом ≥ `from`, що задовольняє `predicate`. Повертає індекс або -1.
|
|
2362
|
+
* @param {unknown[]} rules вміст `spec.rules` HTTPRoute
|
|
2363
|
+
* @param {number} from мінімальний індекс, з якого починати пошук
|
|
2364
|
+
* @param {(rule: unknown) => boolean} predicate предикат на одне правило
|
|
2365
|
+
* @returns {number} індекс знайденого правила або -1
|
|
2366
|
+
*/
|
|
2367
|
+
function findHasuraRule(rules, from, predicate) {
|
|
2368
|
+
for (let i = from; i < rules.length; i++) {
|
|
2369
|
+
if (predicate(rules[i])) return i
|
|
2370
|
+
}
|
|
2371
|
+
return -1
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
/**
|
|
2375
|
+
* Чи **`HTTPRoute`** порушує канон 4 правил Hasura (див. k8s.mdc).
|
|
2376
|
+
* Повертає текст порушення або null, якщо канон витримано. Додаткові правила поверх канону допускаються.
|
|
2377
|
+
* @param {unknown} manifest корінь YAML-документа
|
|
2378
|
+
* @returns {string | null} текст порушення або null, якщо канон витримано
|
|
2379
|
+
*/
|
|
2380
|
+
export function httpRouteHasuraCanonViolation(manifest) {
|
|
2381
|
+
const rec = asPlainRecord(manifest)
|
|
2382
|
+
if (rec === null) return null
|
|
2383
|
+
const spec = asPlainRecord(rec.spec)
|
|
2384
|
+
if (spec === null) return 'HTTPRoute без spec — канон Hasura вимагає 4 правил (див. k8s.mdc)'
|
|
2385
|
+
const rules = spec.rules
|
|
2386
|
+
if (!Array.isArray(rules) || rules.length === 0) {
|
|
2387
|
+
return 'spec.rules порожній — канон Hasura вимагає 4 правил у порядку (див. k8s.mdc)'
|
|
2388
|
+
}
|
|
2389
|
+
const start = findHasuraCanonStart(rules)
|
|
2390
|
+
if (start === null) {
|
|
2391
|
+
return 'не знайдено правило 1 Hasura-канона: Exact "<prefix>/ql" + RequestRedirect ReplaceFullPath "<prefix>/ql/console" statusCode 302 (див. k8s.mdc)'
|
|
2392
|
+
}
|
|
2393
|
+
const { prefix, startIndex } = start
|
|
2394
|
+
const qlPath = `${prefix}/ql`
|
|
2395
|
+
const qlSlashPath = `${prefix}/ql/`
|
|
2396
|
+
const consolePath = `${prefix}/ql/console`
|
|
2397
|
+
|
|
2398
|
+
if (!hasuraRuleHasExactRedirect(rules[startIndex], consolePath)) {
|
|
2399
|
+
return `правило 1 Hasura-канона (rules[${startIndex}], prefix «${prefix}»): Exact "${qlPath}" має мати RequestRedirect ReplaceFullPath "${consolePath}" statusCode 302 (див. k8s.mdc)`
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
const i2 = findHasuraRule(
|
|
2403
|
+
rules,
|
|
2404
|
+
startIndex + 1,
|
|
2405
|
+
r => hasuraRuleMatchesSinglePathNoHeaders(r, 'Exact', qlSlashPath) && hasuraRuleHasExactRedirect(r, consolePath)
|
|
2406
|
+
)
|
|
2407
|
+
if (i2 === -1) {
|
|
2408
|
+
return `правило 2 Hasura-канона: після правила 1 має бути Exact "${qlSlashPath}" + RequestRedirect ReplaceFullPath "${consolePath}" statusCode 302 (див. k8s.mdc)`
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
const i3 = findHasuraRule(
|
|
2412
|
+
rules,
|
|
2413
|
+
i2 + 1,
|
|
2414
|
+
r => hasuraRuleIsQlUrlRewrite(r, qlPath) && hasuraRuleSingleBackendName(r) !== null
|
|
2415
|
+
)
|
|
2416
|
+
if (i3 === -1) {
|
|
2417
|
+
return `правило 3 Hasura-канона: після правила 2 має бути PathPrefix "${qlPath}" + URLRewrite ReplacePrefixMatch "/" + один backendRef на headless Service (див. k8s.mdc)`
|
|
2418
|
+
}
|
|
2419
|
+
const backendName = /** @type {string} */ (hasuraRuleSingleBackendName(rules[i3]))
|
|
2420
|
+
|
|
2421
|
+
const i4 = findHasuraRule(
|
|
2422
|
+
rules,
|
|
2423
|
+
i3 + 1,
|
|
2424
|
+
r => hasuraRuleIsWebsocket(r, qlPath) && hasuraRuleSingleBackendName(r) === backendName
|
|
2425
|
+
)
|
|
2426
|
+
if (i4 === -1) {
|
|
2427
|
+
return `правило 4 Hasura-канона (WebSocket): після правила 3 має бути PathPrefix "${qlPath}" + header "Upgrade: websocket" + URLRewrite ReplacePrefixMatch "/" + RequestHeaderModifier remove [Authorization] + backendRef «${backendName}» (див. k8s.mdc)`
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
return null
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2157
2433
|
/**
|
|
2158
2434
|
* Збирає **`metadata.name`** для **kind: Service** у коренях документів; при помилці викликає **fail** і повертає false.
|
|
2159
2435
|
* @param {Record<string, unknown>[]} roots корені YAML-документів
|
|
@@ -2300,6 +2576,113 @@ async function validateSvcYamlAndSvcHlPairs(root, yamlFiles, fail) {
|
|
|
2300
2576
|
}
|
|
2301
2577
|
}
|
|
2302
2578
|
|
|
2579
|
+
/**
|
|
2580
|
+
* Індексує Hasura-Deployment-и за каталогом (ключ — абсолютний шлях каталогу, значення — множина `metadata.name`).
|
|
2581
|
+
* Паралельно збирає всі `kind: HTTPRoute` Gateway API (`gateway.networking.k8s.io/*`) із doc-індексом.
|
|
2582
|
+
* @param {string[]} yamlFiles абсолютні шляхи до `*.yaml` під `k8s`
|
|
2583
|
+
* @returns {Promise<{
|
|
2584
|
+
* hasuraByDir: Map<string, Set<string>>,
|
|
2585
|
+
* httpRoutes: { abs: string, dir: string, docIndex: number, obj: Record<string, unknown> }[]
|
|
2586
|
+
* }>} індекс Hasura-Deployment-ів за каталогом і список HTTPRoute-документів
|
|
2587
|
+
*/
|
|
2588
|
+
async function collectHasuraDeploymentsAndHttpRoutes(yamlFiles) {
|
|
2589
|
+
/** @type {Map<string, Set<string>>} */
|
|
2590
|
+
const hasuraByDir = new Map()
|
|
2591
|
+
/** @type {{ abs: string, dir: string, docIndex: number, obj: Record<string, unknown> }[]} */
|
|
2592
|
+
const httpRoutes = []
|
|
2593
|
+
|
|
2594
|
+
for (const abs of yamlFiles) {
|
|
2595
|
+
await indexOneK8sYamlForHasuraCanon(abs, hasuraByDir, httpRoutes)
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
return { hasuraByDir, httpRoutes }
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
/**
|
|
2602
|
+
* Читає один YAML і додає Hasura-Deployment-и / HTTPRoute-документи до відповідних колекцій (нещасливі читання ігнорує).
|
|
2603
|
+
* @param {string} abs абсолютний шлях до файлу
|
|
2604
|
+
* @param {Map<string, Set<string>>} hasuraByDir індекс Hasura Deployment-ів за каталогом
|
|
2605
|
+
* @param {{ abs: string, dir: string, docIndex: number, obj: Record<string, unknown> }[]} httpRoutes колектор HTTPRoute-документів
|
|
2606
|
+
* @returns {Promise<void>}
|
|
2607
|
+
*/
|
|
2608
|
+
async function indexOneK8sYamlForHasuraCanon(abs, hasuraByDir, httpRoutes) {
|
|
2609
|
+
let raw
|
|
2610
|
+
try {
|
|
2611
|
+
raw = await readFile(abs, 'utf8')
|
|
2612
|
+
} catch {
|
|
2613
|
+
return
|
|
2614
|
+
}
|
|
2615
|
+
const lines = toLines(raw)
|
|
2616
|
+
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
2617
|
+
/** @type {import('yaml').Document[]} */
|
|
2618
|
+
let docs
|
|
2619
|
+
try {
|
|
2620
|
+
docs = parseAllDocuments(body)
|
|
2621
|
+
} catch {
|
|
2622
|
+
return
|
|
2623
|
+
}
|
|
2624
|
+
const dir = dirname(abs)
|
|
2625
|
+
|
|
2626
|
+
for (const [di, doc] of docs.entries()) {
|
|
2627
|
+
if (doc.errors.length === 0) {
|
|
2628
|
+
const rec = asPlainRecord(doc.toJSON())
|
|
2629
|
+
if (rec !== null) {
|
|
2630
|
+
recordHasuraDeploymentName(rec, dir, hasuraByDir)
|
|
2631
|
+
const av = rec.apiVersion
|
|
2632
|
+
if (rec.kind === 'HTTPRoute' && typeof av === 'string' && av.startsWith(GATEWAY_API_GROUP_PREFIX)) {
|
|
2633
|
+
httpRoutes.push({ abs, dir, docIndex: di + 1, obj: rec })
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
/**
|
|
2641
|
+
* Якщо документ — Hasura-Deployment із непорожнім `metadata.name`, додає ім'я до індексу за каталогом.
|
|
2642
|
+
* @param {Record<string, unknown>} rec корінь YAML-документа
|
|
2643
|
+
* @param {string} dir абсолютний шлях до каталогу файлу
|
|
2644
|
+
* @param {Map<string, Set<string>>} hasuraByDir індекс Hasura Deployment-ів за каталогом (мутується)
|
|
2645
|
+
* @returns {void}
|
|
2646
|
+
*/
|
|
2647
|
+
function recordHasuraDeploymentName(rec, dir, hasuraByDir) {
|
|
2648
|
+
if (!isHasuraDeploymentManifest(rec)) return
|
|
2649
|
+
const meta = asPlainRecord(rec.metadata)
|
|
2650
|
+
const name = meta === null ? undefined : meta.name
|
|
2651
|
+
if (typeof name !== 'string' || name === '') return
|
|
2652
|
+
let set = hasuraByDir.get(dir)
|
|
2653
|
+
if (set === undefined) {
|
|
2654
|
+
set = new Set()
|
|
2655
|
+
hasuraByDir.set(dir, set)
|
|
2656
|
+
}
|
|
2657
|
+
set.add(name)
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
/**
|
|
2661
|
+
* Для кожного `kind: HTTPRoute`, що прив'язаний до **Hasura-Deployment** у тому самому каталозі за **`metadata.name`**,
|
|
2662
|
+
* звіряє канон 4 правил (див. `httpRouteHasuraCanonViolation` і k8s.mdc).
|
|
2663
|
+
* @param {string} root корінь репозиторію
|
|
2664
|
+
* @param {string[]} yamlFiles абсолютні шляхи до `*.yaml` під `k8s`
|
|
2665
|
+
* @param {(msg: string) => void} fail callback реєстрації помилки
|
|
2666
|
+
* @returns {Promise<void>}
|
|
2667
|
+
*/
|
|
2668
|
+
async function validateHasuraHttpRouteCanon(root, yamlFiles, fail) {
|
|
2669
|
+
const { hasuraByDir, httpRoutes } = await collectHasuraDeploymentsAndHttpRoutes(yamlFiles)
|
|
2670
|
+
if (hasuraByDir.size === 0 || httpRoutes.length === 0) return
|
|
2671
|
+
|
|
2672
|
+
for (const hr of httpRoutes) {
|
|
2673
|
+
const meta = asPlainRecord(hr.obj.metadata)
|
|
2674
|
+
const name = meta === null ? undefined : meta.name
|
|
2675
|
+
const set = typeof name === 'string' && name !== '' ? hasuraByDir.get(hr.dir) : undefined
|
|
2676
|
+
if (set !== undefined && typeof name === 'string' && set.has(name)) {
|
|
2677
|
+
const v = httpRouteHasuraCanonViolation(hr.obj)
|
|
2678
|
+
if (v !== null) {
|
|
2679
|
+
const rel = (relative(root, hr.abs) || hr.abs).replaceAll('\\', '/')
|
|
2680
|
+
fail(`${rel}: HTTPRoute «${name}» (документ ${hr.docIndex}; прив'язано до Hasura-Deployment у тому ж каталозі): ${v}`)
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2303
2686
|
/**
|
|
2304
2687
|
* Для маніфестів, **підключених** до Kustomize (шлях у `resources` / `patches` / …), **metadata.namespace** не додають.
|
|
2305
2688
|
* @param {unknown} manifest корінь YAML-документа
|
|
@@ -2378,15 +2761,7 @@ export function isK8sBaseManifestYamlPath(rel, baseLower) {
|
|
|
2378
2761
|
* @param {(msg: string) => void} fail реєстрація помилки
|
|
2379
2762
|
* @returns {void}
|
|
2380
2763
|
*/
|
|
2381
|
-
function failIfK8sPolicyNamespaceRulesViolated(
|
|
2382
|
-
rel,
|
|
2383
|
-
docIndex,
|
|
2384
|
-
obj,
|
|
2385
|
-
skipMetaNs,
|
|
2386
|
-
inBaseManifest,
|
|
2387
|
-
kustomizeManaged,
|
|
2388
|
-
fail
|
|
2389
|
-
) {
|
|
2764
|
+
function failIfK8sPolicyNamespaceRulesViolated(rel, docIndex, obj, skipMetaNs, inBaseManifest, kustomizeManaged, fail) {
|
|
2390
2765
|
if (skipMetaNs) {
|
|
2391
2766
|
return
|
|
2392
2767
|
}
|
|
@@ -2479,15 +2854,7 @@ function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeMan
|
|
|
2479
2854
|
fail(`${rel}: YAML (документ ${di + 1}): ${doc.errors.map(e => e.message).join('; ')}`)
|
|
2480
2855
|
} else {
|
|
2481
2856
|
const obj = doc.toJSON()
|
|
2482
|
-
failIfK8sPolicyNamespaceRulesViolated(
|
|
2483
|
-
rel,
|
|
2484
|
-
di + 1,
|
|
2485
|
-
obj,
|
|
2486
|
-
skipMetaNs,
|
|
2487
|
-
inBaseManifest,
|
|
2488
|
-
kustomizeManaged,
|
|
2489
|
-
fail
|
|
2490
|
-
)
|
|
2857
|
+
failIfK8sPolicyNamespaceRulesViolated(rel, di + 1, obj, skipMetaNs, inBaseManifest, kustomizeManaged, fail)
|
|
2491
2858
|
failIfK8sPolicyResourceRulesViolated(rel, baseLower, di + 1, obj, fail)
|
|
2492
2859
|
}
|
|
2493
2860
|
}
|
|
@@ -2827,6 +3194,8 @@ export async function check() {
|
|
|
2827
3194
|
|
|
2828
3195
|
await validateSvcYamlAndSvcHlPairs(root, yamlFiles, fail)
|
|
2829
3196
|
|
|
3197
|
+
await validateHasuraHttpRouteCanon(root, yamlFiles, fail)
|
|
3198
|
+
|
|
2830
3199
|
await validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail)
|
|
2831
3200
|
|
|
2832
3201
|
await validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFiles, fail)
|