@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.25.3",
3
+ "version": "1.26.1",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -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 blocks = toAdd.map(({ name, appLabel, kind }, i) => {
6306
- const block = buildNetworkPolicyYaml(name, appLabel, kind)
6307
- return i === 0 && content === '' ? block.trimEnd() : stripYamlLanguageServerModeline(block).trimEnd()
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)`. Деталі — k8s.mdc.
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 blocks = specs.map(({ name, appLabel, kind }, i) => {
6396
- const block = buildNetworkPolicyYaml(name, appLabel, kind)
6397
- return i === 0 ? block.trimEnd() : stripYamlLanguageServerModeline(block).trimEnd()
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.41'
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