@nitra/cursor 1.25.3 → 1.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/package.json +1 -1
- package/rules/k8s/js/manifests.mjs +185 -14
- package/rules/k8s/k8s.mdc +57 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.26.1] - 2026-05-26
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`k8s/js/manifests.mjs`**: `Set.prototype.toSorted` не існує — повернено `Array.from(new Set(...)).toSorted(...)` (oxlint `--fix` помилково замінив `[...new Set(...)].sort(...)` на невалідний `new Set(...).toSorted(...)`).
|
|
12
|
+
- **`k8s/js/manifests.mjs`**: hardcoded CIDR-и (`35.191.0.0/16`, `130.211.0.0/22`, `10.0.0.0/8`) у `NETWORK_POLICY_GCLB_INGRESS_FROM` тепер з `// eslint-disable-next-line sonarjs/no-hardcoded-ip` (це канон GCLB, який не змінюється).
|
|
13
|
+
- **`k8s/js/manifests.mjs`**: `() => {}` default callback → іменована константа `noopFail` (eslint `no-empty-function`).
|
|
14
|
+
- **`k8s/js/tests/manifests/tests/check-schema.test.mjs`**: винесено `HR_YAML_RE`, `HTTPROUTE_NP_MAPPING_RE`, `K8S_MDC_RE`, `EXPECTED_GCLB_INGRESS_FROM`, `GCLB_HC_GLOBAL_CIDR` як module-scope константи (e18e `prefer-static-regex` + sonarjs `no-hardcoded-ip`); `mock(() => {})` → `mock(msg => msg)`.
|
|
15
|
+
- **`.cspell.json`**: додано `GCLB`, `gclb`, `байтово` до words.
|
|
16
|
+
|
|
17
|
+
## [1.26.0] - 2026-05-26
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **`k8s/js/manifests.mjs`**: нова `collectHttpRouteIngressForWorkload(dir, appLabel, fail)` — резолвить HTTPRoute → `-hl` Service → `selector.app` mapping і повертає унікальні TCP-порти з `backendRefs[].port` для workload з міткою `appLabel`. Викликається з `appendNetworkPolicyDocuments` і `regenerateLegacyNetworkPolicyDocsInFile` під час `check k8s`.
|
|
22
|
+
- **`k8s/js/manifests.mjs:buildNetworkPolicyYaml`**: опційний 4-й параметр `gclbPorts: number[]` — якщо непорожній, додає ingress-правило з `ipBlock` 35.191.0.0/16, 130.211.0.0/22, 10.0.0.0/8 і TCP-портами (відсортовано). Без параметра output байтово ідентичний baseline canon.
|
|
23
|
+
- **`k8s.mdc` v1.42**: новий розділ «HTTPRoute → NetworkPolicy ingress (GCLB + Envoy)» з описом mapping і прикладом NetworkPolicy для HTTPRoute-paired workload.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **Service blocking via NetworkPolicy** для workload-ів, прив'язаних до `HTTPRoute` через GKE Gateway: попередній canon допускав тільки `podSelector: {}` ingress, що блокувало трафік від Envoy data-plane (`10.10.0.0/23` для `us-central1-proxy-only`) і Google health checks (`35.191.0.0/16`, `130.211.0.0/22`). Тепер правило автоматично додається.
|
|
28
|
+
|
|
7
29
|
## [1.25.3] - 2026-05-26
|
|
8
30
|
|
|
9
31
|
### Fixed
|
package/package.json
CHANGED
|
@@ -4300,20 +4300,55 @@ export function readNetworkPolicySnippet() {
|
|
|
4300
4300
|
return /** @type {any} */ (loadSnippetSpec('deployment'))
|
|
4301
4301
|
}
|
|
4302
4302
|
|
|
4303
|
+
/**
|
|
4304
|
+
* No-op fail-callback (повертає аргумент). Використовується як дефолт у `regenerateLegacyNetworkPolicyDocsInFile`,
|
|
4305
|
+
* коли caller не передає власний `fail` — щоб `collectHttpRouteIngressForWorkload` не падав.
|
|
4306
|
+
* @param {string} msg повідомлення помилки
|
|
4307
|
+
* @returns {string} той самий аргумент
|
|
4308
|
+
*/
|
|
4309
|
+
const noopFail = msg => msg
|
|
4310
|
+
|
|
4311
|
+
/**
|
|
4312
|
+
* `from`-peers для HTTPRoute-aware ingress-правила (GCP HC global + Envoy data-plane / proxy-only subnet).
|
|
4313
|
+
* Порядок зафіксовано детерміністичним (HC-global → 10.0.0.0/8). Див. розділ «HTTPRoute → NetworkPolicy ingress» у k8s.mdc.
|
|
4314
|
+
* @type {ReadonlyArray<{ ipBlock: { cidr: string } }>}
|
|
4315
|
+
*/
|
|
4316
|
+
const NETWORK_POLICY_GCLB_INGRESS_FROM = Object.freeze([
|
|
4317
|
+
// eslint-disable-next-line sonarjs/no-hardcoded-ip
|
|
4318
|
+
{ ipBlock: { cidr: '35.191.0.0/16' } },
|
|
4319
|
+
// eslint-disable-next-line sonarjs/no-hardcoded-ip
|
|
4320
|
+
{ ipBlock: { cidr: '130.211.0.0/22' } },
|
|
4321
|
+
// eslint-disable-next-line sonarjs/no-hardcoded-ip
|
|
4322
|
+
{ ipBlock: { cidr: '10.0.0.0/8' } }
|
|
4323
|
+
])
|
|
4324
|
+
|
|
4303
4325
|
/**
|
|
4304
4326
|
* Канонічний YAML **NetworkPolicy** для workload з іменем `deployName`, міткою `app` і типом `kind`.
|
|
4305
4327
|
* Snippet обирається за `kind` через `KIND_TO_SNIPPET` (без merge — кожен snippet самодостатній).
|
|
4306
4328
|
* Анотація `nitra.dev/workload-kind` додається, щоб rego диспатчив на правильний канон.
|
|
4329
|
+
*
|
|
4330
|
+
* Якщо `gclbPorts` непорожній — після canon ingress-правил додається одне ingress-правило
|
|
4331
|
+
* з фіксованими CIDR-ами (GCP HC global + Envoy data-plane) і відсортованими унікальними TCP-портами
|
|
4332
|
+
* (для HTTPRoute-paired workload-ів; див. `collectHttpRouteIngressForWorkload` і k8s.mdc).
|
|
4307
4333
|
* @param {string} deployName `metadata.name` workload
|
|
4308
4334
|
* @param {string} appLabel `spec.selector.matchLabels.app`
|
|
4309
4335
|
* @param {string} kind `kind` workload (обовʼязковий: Deployment | StatefulSet | Job | CronJob | DaemonSet)
|
|
4336
|
+
* @param {readonly number[]} [gclbPorts] TCP-порти з backendRefs HTTPRoute (опційно)
|
|
4310
4337
|
* @returns {string} вміст `networkpolicy.yaml`
|
|
4311
4338
|
*/
|
|
4312
|
-
export function buildNetworkPolicyYaml(deployName, appLabel, kind) {
|
|
4339
|
+
export function buildNetworkPolicyYaml(deployName, appLabel, kind, gclbPorts) {
|
|
4313
4340
|
const schemaUrl = `${YANNH_BASE}networkpolicy-networking-v1.json`
|
|
4314
4341
|
const snippetName = snippetNameForKind(kind)
|
|
4315
4342
|
const spec = structuredClone(loadSnippetSpec(snippetName))
|
|
4316
4343
|
spec.podSelector.matchLabels = { app: appLabel }
|
|
4344
|
+
if (Array.isArray(gclbPorts) && gclbPorts.length > 0) {
|
|
4345
|
+
const uniqueSorted = [...new Set(gclbPorts)].toSorted((a, b) => a - b)
|
|
4346
|
+
const gclbRule = {
|
|
4347
|
+
from: structuredClone(NETWORK_POLICY_GCLB_INGRESS_FROM),
|
|
4348
|
+
ports: uniqueSorted.map(port => ({ protocol: 'TCP', port }))
|
|
4349
|
+
}
|
|
4350
|
+
spec.ingress = [...(spec.ingress ?? []), gclbRule]
|
|
4351
|
+
}
|
|
4317
4352
|
const specYaml = stringify(spec, { indent: 2 })
|
|
4318
4353
|
.replaceAll(/^(?!$)/gm, ' ')
|
|
4319
4354
|
.trimEnd()
|
|
@@ -4328,6 +4363,127 @@ spec:
|
|
|
4328
4363
|
${specYaml}`
|
|
4329
4364
|
}
|
|
4330
4365
|
|
|
4366
|
+
/**
|
|
4367
|
+
* Збирає унікальні TCP-порти з `HTTPRoute.backendRefs`, які адресують workload з міткою `appLabel`.
|
|
4368
|
+
*
|
|
4369
|
+
* Mapping: `backendRef.name` → `Service.metadata.name` у тому ж каталозі → `service.spec.selector.matchLabels.app === appLabel`.
|
|
4370
|
+
* Використовується для побудови HTTPRoute-aware ingress-правила в NetworkPolicy (GCLB + Envoy data-plane CIDR-и; див. k8s.mdc).
|
|
4371
|
+
* @param {string} dir абсолютний каталог
|
|
4372
|
+
* @param {string} appLabel `spec.selector.matchLabels.app` цільового workload
|
|
4373
|
+
* @param {(msg: string) => void} fail callback при read/parse-помилці YAML у каталозі
|
|
4374
|
+
* @returns {Promise<{ ports: number[] } | null>} відсортовані унікальні TCP-порти або null, якщо HTTPRoute не вказує на цей workload
|
|
4375
|
+
*/
|
|
4376
|
+
export async function collectHttpRouteIngressForWorkload(dir, appLabel, fail) {
|
|
4377
|
+
let entries
|
|
4378
|
+
try {
|
|
4379
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
4380
|
+
} catch {
|
|
4381
|
+
return null
|
|
4382
|
+
}
|
|
4383
|
+
const yamlFiles = entries
|
|
4384
|
+
.filter(e => e.isFile() && (e.name.endsWith('.yaml') || e.name.endsWith('.yml')))
|
|
4385
|
+
.map(e => join(dir, e.name))
|
|
4386
|
+
if (yamlFiles.length === 0) return null
|
|
4387
|
+
|
|
4388
|
+
/** @type {Array<{ name: string, port: number }>} */
|
|
4389
|
+
const allBackendRefs = []
|
|
4390
|
+
/** @type {Map<string, string>} */
|
|
4391
|
+
const servicesByName = new Map()
|
|
4392
|
+
|
|
4393
|
+
for (const abs of yamlFiles) {
|
|
4394
|
+
let raw
|
|
4395
|
+
try {
|
|
4396
|
+
raw = await readFile(abs, 'utf8')
|
|
4397
|
+
} catch (error) {
|
|
4398
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
4399
|
+
fail(`${abs}: не вдалося прочитати для GCLB ingress (HTTPRoute → NetworkPolicy mapping; k8s.mdc): ${msg}`)
|
|
4400
|
+
continue
|
|
4401
|
+
}
|
|
4402
|
+
const lines = toLines(raw)
|
|
4403
|
+
const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
|
|
4404
|
+
/** @type {import('yaml').Document[]} */
|
|
4405
|
+
let docs
|
|
4406
|
+
try {
|
|
4407
|
+
docs = parseAllDocuments(body)
|
|
4408
|
+
} catch (error) {
|
|
4409
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
4410
|
+
fail(`${abs}: не вдалося розпарсити YAML для GCLB ingress (HTTPRoute → NetworkPolicy mapping; k8s.mdc): ${msg}`)
|
|
4411
|
+
continue
|
|
4412
|
+
}
|
|
4413
|
+
for (const doc of docs) {
|
|
4414
|
+
if (doc.errors.length > 0) {
|
|
4415
|
+
fail(
|
|
4416
|
+
`${abs}: YAML містить помилки для GCLB ingress (HTTPRoute → NetworkPolicy mapping; k8s.mdc): ${doc.errors[0].message}`
|
|
4417
|
+
)
|
|
4418
|
+
continue
|
|
4419
|
+
}
|
|
4420
|
+
const rec = asPlainRecord(doc.toJSON())
|
|
4421
|
+
if (rec === null) continue
|
|
4422
|
+
const av = rec.apiVersion
|
|
4423
|
+
if (rec.kind === 'HTTPRoute' && typeof av === 'string' && av.startsWith(GATEWAY_API_GROUP_PREFIX)) {
|
|
4424
|
+
collectHttpRouteBackendRefsInto(rec.spec, allBackendRefs)
|
|
4425
|
+
} else if (rec.kind === 'Service') {
|
|
4426
|
+
const name = manifestMetadataName(rec)
|
|
4427
|
+
if (name !== null) {
|
|
4428
|
+
const app = serviceSelectorAppLabel(rec.spec)
|
|
4429
|
+
if (app !== null) servicesByName.set(name, app)
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4435
|
+
/** @type {Set<number>} */
|
|
4436
|
+
const ports = new Set()
|
|
4437
|
+
for (const { name, port } of allBackendRefs) {
|
|
4438
|
+
const targetApp = servicesByName.get(name)
|
|
4439
|
+
if (targetApp === appLabel) ports.add(port)
|
|
4440
|
+
}
|
|
4441
|
+
if (ports.size === 0) return null
|
|
4442
|
+
return { ports: [...ports].toSorted((a, b) => a - b) }
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
/**
|
|
4446
|
+
* Витягує мітку `app` з `Service.spec.selector.app` (плоский селектор, без `matchLabels`).
|
|
4447
|
+
* Окремий helper від `appLabelFromSpecSelector` (той — для Deployment/StatefulSet, де `selector.matchLabels.app`).
|
|
4448
|
+
* @param {unknown} spec значення `spec` Service
|
|
4449
|
+
* @returns {string | null} мітка `app` або null
|
|
4450
|
+
*/
|
|
4451
|
+
function serviceSelectorAppLabel(spec) {
|
|
4452
|
+
if (spec === null || spec === undefined || typeof spec !== 'object' || Array.isArray(spec)) return null
|
|
4453
|
+
const selector = /** @type {Record<string, unknown>} */ (spec).selector
|
|
4454
|
+
if (selector === null || selector === undefined || typeof selector !== 'object' || Array.isArray(selector)) return null
|
|
4455
|
+
const app = /** @type {Record<string, unknown>} */ (selector).app
|
|
4456
|
+
return typeof app === 'string' && app.trim() !== '' ? app : null
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
/**
|
|
4460
|
+
* Обходить `spec` HTTPRoute (`spec.rules[*].backendRefs[*]`) і додає `(name, port)` у акумулятор.
|
|
4461
|
+
* Дублює walk-логіку `collectGatewayApiRouteBackendServiceNames`, але зберігає `port` поруч з `name`.
|
|
4462
|
+
* @param {unknown} spec значення `spec` маршруту
|
|
4463
|
+
* @param {Array<{ name: string, port: number }>} out акумулятор
|
|
4464
|
+
* @returns {void} результат
|
|
4465
|
+
*/
|
|
4466
|
+
function collectHttpRouteBackendRefsInto(spec, out) {
|
|
4467
|
+
/**
|
|
4468
|
+
* @param {unknown} node вузол для обходу
|
|
4469
|
+
* @returns {void} результат
|
|
4470
|
+
*/
|
|
4471
|
+
function walk(node) {
|
|
4472
|
+
if (node === null || node === undefined) return
|
|
4473
|
+
if (Array.isArray(node)) {
|
|
4474
|
+
for (const x of node) walk(x)
|
|
4475
|
+
return
|
|
4476
|
+
}
|
|
4477
|
+
if (typeof node !== 'object') return
|
|
4478
|
+
if (isGatewayApiBackendRefToService(node)) {
|
|
4479
|
+
const o = /** @type {Record<string, unknown>} */ (node)
|
|
4480
|
+
out.push({ name: String(o.name), port: /** @type {number} */ (o.port) })
|
|
4481
|
+
}
|
|
4482
|
+
for (const v of Object.values(node)) walk(v)
|
|
4483
|
+
}
|
|
4484
|
+
walk(spec)
|
|
4485
|
+
}
|
|
4486
|
+
|
|
4331
4487
|
/**
|
|
4332
4488
|
* Додає `resourceName` у `resources:` kustomization/Component YAML, якщо ще немає; сортує за алфавітом (en).
|
|
4333
4489
|
* @param {string} raw вміст `kustomization.yaml`
|
|
@@ -6289,23 +6445,30 @@ async function existingNetworkPolicyNames(npAbs) {
|
|
|
6289
6445
|
|
|
6290
6446
|
/**
|
|
6291
6447
|
* Дописує відсутні NetworkPolicy-документи у `networkpolicy.yaml` (multi-doc через `---`).
|
|
6448
|
+
* Перед побудовою YAML для кожного workload викликає `collectHttpRouteIngressForWorkload`,
|
|
6449
|
+
* щоб додати GCLB-aware ingress-правило, якщо в каталозі є paired HTTPRoute (k8s.mdc).
|
|
6292
6450
|
* @param {string} npAbs абсолютний шлях до файлу
|
|
6293
6451
|
* @param {Array<{ name: string, appLabel: string, kind: string }>} toAdd workload-и без NP
|
|
6294
6452
|
* @param {string} npRel відносний шлях для повідомлень
|
|
6453
|
+
* @param {(msg: string) => void} fail callback при read/parse-помилках HTTPRoute/Service
|
|
6295
6454
|
* @param {(msg: string) => void} passFn callback при успіху
|
|
6296
6455
|
* @returns {Promise<void>} результат
|
|
6297
6456
|
*/
|
|
6298
|
-
async function appendNetworkPolicyDocuments(npAbs, toAdd, npRel, passFn) {
|
|
6457
|
+
async function appendNetworkPolicyDocuments(npAbs, toAdd, npRel, fail, passFn) {
|
|
6299
6458
|
if (toAdd.length === 0) return
|
|
6300
6459
|
let content = ''
|
|
6301
6460
|
if (existsSync(npAbs)) {
|
|
6302
6461
|
const raw = await readFile(npAbs, 'utf8')
|
|
6303
6462
|
content = raw.trimEnd()
|
|
6304
6463
|
}
|
|
6305
|
-
const
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6464
|
+
const dir = dirname(npAbs)
|
|
6465
|
+
const blocks = []
|
|
6466
|
+
for (const [i, { name, appLabel, kind }] of toAdd.entries()) {
|
|
6467
|
+
const gclb = await collectHttpRouteIngressForWorkload(dir, appLabel, fail)
|
|
6468
|
+
const gclbPorts = gclb === null ? undefined : gclb.ports
|
|
6469
|
+
const block = buildNetworkPolicyYaml(name, appLabel, kind, gclbPorts)
|
|
6470
|
+
blocks.push(i === 0 && content === '' ? block.trimEnd() : stripYamlLanguageServerModeline(block).trimEnd())
|
|
6471
|
+
}
|
|
6309
6472
|
const joined = blocks.join('\n---\n')
|
|
6310
6473
|
content = content === '' ? `${joined}\n` : `${content}\n---\n${joined}\n`
|
|
6311
6474
|
await writeFile(npAbs, content, 'utf8')
|
|
@@ -6360,11 +6523,14 @@ function networkPolicyPodSelectorAppLabel(spec) {
|
|
|
6360
6523
|
|
|
6361
6524
|
/**
|
|
6362
6525
|
* Migrate legacy `networkpolicy.yaml`: якщо хоч один документ має catch-all in-cluster egress —
|
|
6363
|
-
* перезаписати **всі** документи у файлі через `buildNetworkPolicyYaml(name, appLabel)`.
|
|
6526
|
+
* перезаписати **всі** документи у файлі через `buildNetworkPolicyYaml(name, appLabel, kind, gclbPorts)`.
|
|
6527
|
+
* `gclbPorts` витягуються з HTTPRoute paired у тому ж каталозі (див. `collectHttpRouteIngressForWorkload`).
|
|
6528
|
+
* Деталі — k8s.mdc.
|
|
6364
6529
|
* @param {string} npAbs абсолютний шлях до networkpolicy.yaml
|
|
6530
|
+
* @param {(msg: string) => void} [fail] callback при read/parse-помилці HTTPRoute/Service (опційно — для backward compat)
|
|
6365
6531
|
* @returns {Promise<boolean>} true якщо файл переписаний
|
|
6366
6532
|
*/
|
|
6367
|
-
export async function regenerateLegacyNetworkPolicyDocsInFile(npAbs) {
|
|
6533
|
+
export async function regenerateLegacyNetworkPolicyDocsInFile(npAbs, fail) {
|
|
6368
6534
|
if (!existsSync(npAbs)) return false
|
|
6369
6535
|
const docs = await readAllDocsByKindFromFile(npAbs, 'NetworkPolicy')
|
|
6370
6536
|
if (docs.length === 0) return false
|
|
@@ -6392,10 +6558,15 @@ export async function regenerateLegacyNetworkPolicyDocsInFile(npAbs) {
|
|
|
6392
6558
|
if (typeof name === 'string' && name !== '' && appLabel !== '') specs.push({ name, appLabel, kind })
|
|
6393
6559
|
}
|
|
6394
6560
|
if (specs.length === 0) return false
|
|
6395
|
-
const
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
})
|
|
6561
|
+
const dir = dirname(npAbs)
|
|
6562
|
+
const failCb = typeof fail === 'function' ? fail : noopFail
|
|
6563
|
+
const blocks = []
|
|
6564
|
+
for (const [i, { name, appLabel, kind }] of specs.entries()) {
|
|
6565
|
+
const gclb = await collectHttpRouteIngressForWorkload(dir, appLabel, failCb)
|
|
6566
|
+
const gclbPorts = gclb === null ? undefined : gclb.ports
|
|
6567
|
+
const block = buildNetworkPolicyYaml(name, appLabel, kind, gclbPorts)
|
|
6568
|
+
blocks.push(i === 0 ? block.trimEnd() : stripYamlLanguageServerModeline(block).trimEnd())
|
|
6569
|
+
}
|
|
6399
6570
|
await writeFile(npAbs, `${blocks.join('\n---\n')}\n`, 'utf8')
|
|
6400
6571
|
return true
|
|
6401
6572
|
}
|
|
@@ -6415,7 +6586,7 @@ async function ensureNetworkPoliciesForWorkloadsInDir(dir, workloads, rootNorm,
|
|
|
6415
6586
|
const npAbs = join(dir, NETWORK_POLICY_FILENAME)
|
|
6416
6587
|
const npRel = (relative(rootNorm, npAbs) || npAbs).replaceAll('\\', '/')
|
|
6417
6588
|
if (existsSync(npAbs)) {
|
|
6418
|
-
const migrated = await regenerateLegacyNetworkPolicyDocsInFile(npAbs)
|
|
6589
|
+
const migrated = await regenerateLegacyNetworkPolicyDocsInFile(npAbs, fail)
|
|
6419
6590
|
if (migrated) {
|
|
6420
6591
|
passFn(`${npRel}: міграція legacy catch-all egress → канон з явними in-cluster портами (k8s.mdc)`)
|
|
6421
6592
|
}
|
|
@@ -6434,7 +6605,7 @@ async function ensureNetworkPoliciesForWorkloadsInDir(dir, workloads, rootNorm,
|
|
|
6434
6605
|
}
|
|
6435
6606
|
if (toAdd.length === 0) return
|
|
6436
6607
|
try {
|
|
6437
|
-
await appendNetworkPolicyDocuments(npAbs, toAdd, npRel, passFn)
|
|
6608
|
+
await appendNetworkPolicyDocuments(npAbs, toAdd, npRel, fail, passFn)
|
|
6438
6609
|
const kustAbs = join(dir, 'kustomization.yaml')
|
|
6439
6610
|
if (existsSync(kustAbs)) {
|
|
6440
6611
|
const raw = await readFile(kustAbs, 'utf8')
|
package/rules/k8s/k8s.mdc
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: K8s YAML — $schema (yaml-language-server); lint-k8s (kubeconform, kubescape); check-k8s
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.42'
|
|
4
4
|
globs: "**/k8s/**/*.yaml"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -414,6 +414,34 @@ images:
|
|
|
414
414
|
|
|
415
415
|
**Порядок `resources`:** у маніфесті з **`apiVersion: kustomize.config.k8s.io/…`**, **`kind: Kustomization`**, елементи **`resources:`** (лише непорожні рядки) мають бути **відсортовані за алфавітом (англ. локаль, як `localeCompare('en')` у `rules/k8s/fix.mjs`)**. Поля **`bases`**, **`components`**, **`crds`** цією перевіркою **не** впорядковуються.
|
|
416
416
|
|
|
417
|
+
### HTTPRoute → NetworkPolicy ingress (GCLB + Envoy)
|
|
418
|
+
|
|
419
|
+
Якщо в каталозі workload є **`HTTPRoute`** (Gateway API; `apiVersion: gateway.networking.k8s.io/*`) з **`backendRef`** на **`<workload>-hl`** Service (mapping через `service.spec.selector.app`), **`check k8s`** автоматично додає в NetworkPolicy цього workload **ingress-правило** з фіксованим набором CIDR-ів і **TCP-портами з `backendRefs[].port`** (дедуп, відсортовано за зростанням).
|
|
420
|
+
|
|
421
|
+
Без цього правила трафік від **GKE Gateway** (Envoy data-plane з proxy-only subnet регіону, наприклад `us-central1-proxy-only` = `10.10.0.0/23`) і **Google health checks** (`35.191.0.0/16`, `130.211.0.0/22`) блокується базовим NetworkPolicy (бо canon ingress допускає тільки `podSelector: {}` — intra-namespace pod ↔ pod).
|
|
422
|
+
|
|
423
|
+
CIDR-набір зафіксовано (без конфігурації):
|
|
424
|
+
|
|
425
|
+
- `35.191.0.0/16` — GCP HC global
|
|
426
|
+
- `130.211.0.0/22` — GCP HC global (legacy)
|
|
427
|
+
- `10.0.0.0/8` — широкий range, покриває proxy-only subnets усіх регіонів GKE
|
|
428
|
+
|
|
429
|
+
Приклад згенерованого ingress-правила (поверх baseline canon):
|
|
430
|
+
|
|
431
|
+
```yaml
|
|
432
|
+
ingress:
|
|
433
|
+
- from:
|
|
434
|
+
- podSelector: {}
|
|
435
|
+
- from:
|
|
436
|
+
- ipBlock: { cidr: 35.191.0.0/16 }
|
|
437
|
+
- ipBlock: { cidr: 130.211.0.0/22 }
|
|
438
|
+
- ipBlock: { cidr: 10.0.0.0/8 }
|
|
439
|
+
ports:
|
|
440
|
+
- { protocol: TCP, port: 8080 }
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Якщо workload не прив'язаний до жодного HTTPRoute — правило **не** додається; NP лишається baseline (intra-namespace + canon egress). Не-HTTP routes (`GRPCRoute`, `TCPRoute`, `TLSRoute`, `UDPRoute`) поки не покриті — додається лише за HTTPRoute. Алгоритм: функція `collectHttpRouteIngressForWorkload` у **`rules/k8s/js/manifests.mjs`** індексує `HTTPRoute.backendRefs` і `Service` у каталозі, резолвить через `selector.app`, дедуп TCP-портів. Виклики — з `appendNetworkPolicyDocuments` і `regenerateLegacyNetworkPolicyDocsInFile`.
|
|
444
|
+
|
|
417
445
|
### Env-залежні межі (за сегментом після `/k8s/`)
|
|
418
446
|
|
|
419
447
|
**Dev-like середовища** — сегмент `base`, `dev`, або з суфіксом `-qa` (напр. `tr-qa`):
|
|
@@ -554,6 +582,34 @@ spec:
|
|
|
554
582
|
port: 4318
|
|
555
583
|
```
|
|
556
584
|
|
|
585
|
+
```yaml title="k8s/base/networkpolicy.yaml — workload з HTTPRoute (з GCLB ingress)"
|
|
586
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.9-standalone-strict/networkpolicy-networking-v1.json
|
|
587
|
+
apiVersion: networking.k8s.io/v1
|
|
588
|
+
kind: NetworkPolicy
|
|
589
|
+
metadata:
|
|
590
|
+
name: backend-api
|
|
591
|
+
annotations:
|
|
592
|
+
nitra.dev/workload-kind: Deployment
|
|
593
|
+
spec:
|
|
594
|
+
podSelector:
|
|
595
|
+
matchLabels:
|
|
596
|
+
app: backend-api
|
|
597
|
+
policyTypes:
|
|
598
|
+
- Ingress
|
|
599
|
+
- Egress
|
|
600
|
+
ingress:
|
|
601
|
+
- from:
|
|
602
|
+
- podSelector: {}
|
|
603
|
+
- from: # auto-added by check k8s for HTTPRoute-paired workloads
|
|
604
|
+
- ipBlock: { cidr: 35.191.0.0/16 }
|
|
605
|
+
- ipBlock: { cidr: 130.211.0.0/22 }
|
|
606
|
+
- ipBlock: { cidr: 10.0.0.0/8 }
|
|
607
|
+
ports:
|
|
608
|
+
- { protocol: TCP, port: 8080 }
|
|
609
|
+
egress:
|
|
610
|
+
# ... (ідентично до базового прикладу вище)
|
|
611
|
+
```
|
|
612
|
+
|
|
557
613
|
```yaml title="k8s/components/hpa.yaml"
|
|
558
614
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.9-standalone-strict/horizontalpodautoscaler-autoscaling-v2.json
|
|
559
615
|
apiVersion: autoscaling/v2
|