@nitra/cursor 1.8.56 → 1.8.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mdc/k8s.mdc CHANGED
@@ -106,6 +106,17 @@ resources: {}
106
106
 
107
107
  У кожному контейнері **`Deployment`** має бути **`imagePullPolicy: Always`** (див. **`check k8s`**).
108
108
 
109
+ Якщо в **`Deployment`** у **`spec.template.spec.containers`** або **`initContainers`** задано образ **`hasura/graphql-engine`**, у полі **`image`** має бути саме **`hasura/graphql-engine:v2.48.15.ubi.amd64`** (для еквівалента Docker Hub допускається префікс **`docker.io/`**). Інші теги або сторонні реєстри з тим самим репозиторієм **`hasura/graphql-engine`** — порушення **`check k8s`**.
110
+
111
+ ## Service: заборонені анотації GKE
112
+
113
+ У **`kind: Service`** не використовуй у **`metadata.annotations`** ключі:
114
+
115
+ - **`cloud.google.com/neg`**
116
+ - **`cloud.google.com/backend-config`**
117
+
118
+ Вони потрібні для інтеграції з **Ingress** / класичним балансуванням GKE; після переходу на **Gateway API** їх слід **прибрати** з маніфестів. **`check k8s`** завершиться помилкою, якщо хоча б один із цих ключів присутній.
119
+
109
120
  ## Kustomize: структура каталогів (`base` / overlays)
110
121
 
111
122
  Трансформуй дерева **`**/k8s`**, щоб **винести спільне** через [Kustomize](https://kustomize.io/): один канонічний **`base`** і тонкі **overlays** для інших середовищ.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.56",
3
+ "version": "1.8.58",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -7,7 +7,8 @@
7
7
  *
8
8
  * Додатково: у кожному YAML-документі з **`kind: Deployment`** у кожного контейнера
9
9
  * **`spec.template.spec.containers[]`** має бути ключ **`resources`** (значення — об'єкт, допускається
10
- * порожній **`{}`**) та **`imagePullPolicy: Always`**.
10
+ * порожній **`{}`**) та **`imagePullPolicy: Always`**. Якщо серед **`containers`** / **`initContainers`**
11
+ * є образ **`hasura/graphql-engine`**, дозволено лише пін **`HASURA_GRAPHQL_ENGINE_IMAGE`** (див. k8s.mdc).
11
12
  *
12
13
  * **Namespace і Kustomize:** YAML у **`…/k8s/base/`** (окрім імені **`kustomization.yaml`**)
13
14
  * завжди має **непорожній** **`metadata.namespace`** у відповідних документах (узгоджено з dev у репозиторії),
@@ -18,6 +19,9 @@
18
19
  *
19
20
  * **`kind: Ingress`** заборонено (потрібен перехід на Gateway API).
20
21
  *
22
+ * У **`kind: Service`** у **`metadata.annotations`** не повинно бути ключів **`cloud.google.com/neg`**
23
+ * та **`cloud.google.com/backend-config`** (див. k8s.mdc).
24
+ *
21
25
  * Структура **Kustomize** (див. k8s.mdc): заборона шляхів **`…/k8s/dev/…`**; у **`k8s/base/kustomization.yaml`**
22
26
  * завжди має бути непорожнє поле **`namespace:`** (перевірка, якщо файл існує).
23
27
  *
@@ -38,6 +42,27 @@ import { walkDir } from './utils/walkDir.mjs'
38
42
  /** Версія набору схем yannh — узгоджено з k8s.mdc */
39
43
  const YANNH_PIN = 'v1.33.9-standalone-strict'
40
44
 
45
+ /**
46
+ * Дозволений образ **hasura/graphql-engine** у Deployment (узгоджено з k8s.mdc).
47
+ * Еквівалент **`docker.io/…`** також приймається.
48
+ */
49
+ export const HASURA_GRAPHQL_ENGINE_IMAGE = 'hasura/graphql-engine:v2.48.15.ubi.amd64'
50
+
51
+ /** Набір прийнятних рядків `image` без digest (`@sha256:…`). */
52
+ const HASURA_GRAPHQL_ENGINE_ALLOWED_IMAGES = new Set([
53
+ HASURA_GRAPHQL_ENGINE_IMAGE,
54
+ `docker.io/${HASURA_GRAPHQL_ENGINE_IMAGE}`
55
+ ])
56
+
57
+ /**
58
+ * Ключі анотацій GKE (NEG / BackendConfig) у **Service** — заборонені (узгоджено з k8s.mdc).
59
+ * @type {readonly string[]}
60
+ */
61
+ export const SERVICE_FORBIDDEN_GCP_ANNOTATION_KEYS = Object.freeze([
62
+ 'cloud.google.com/neg',
63
+ 'cloud.google.com/backend-config'
64
+ ])
65
+
41
66
  /** Гілка репозиторію yannh/kubernetes-json-schema для raw.githubusercontent.com (каталог набору в URL одразу після ref). */
42
67
  const YANNH_REF = 'master'
43
68
 
@@ -570,6 +595,105 @@ export function deploymentImagePullPolicyViolation(manifest) {
570
595
  return null
571
596
  }
572
597
 
598
+ /**
599
+ * Прибирає digest з посилання на образ (`@sha256:…`) для порівняння тега.
600
+ * @param {string} image значення поля `image`
601
+ * @returns {string} той самий рядок без суфікса `@…` (digest), з `.trim()`
602
+ */
603
+ function stripImageDigest(image) {
604
+ const at = image.indexOf('@')
605
+ return (at === -1 ? image : image.slice(0, at)).trim()
606
+ }
607
+
608
+ /**
609
+ * Чи рядок `image` вказує на репозиторій **hasura/graphql-engine** (будь-який тег / без тега).
610
+ * @param {string} image значення поля `image`
611
+ * @returns {boolean} true, якщо шлях образу закінчується на `hasura/graphql-engine` з тегом або без
612
+ */
613
+ function isHasuraGraphqlEngineImageRef(image) {
614
+ const s = stripImageDigest(image)
615
+ return /(^|\/)hasura\/graphql-engine(?:[:]|$)/u.test(s)
616
+ }
617
+
618
+ /**
619
+ * Перевіряє пін образу Hasura у одному списку контейнерів Pod spec.
620
+ * @param {string} list ім’я поля для повідомлення (`containers` / `initContainers`)
621
+ * @param {unknown} containers значення з маніфесту
622
+ * @returns {string | null} текст порушення або null
623
+ */
624
+ function hasuraGraphqlEngineViolationInContainerList(list, containers) {
625
+ if (!Array.isArray(containers)) return null
626
+ for (const [i, c] of containers.entries()) {
627
+ const label =
628
+ typeof c === 'object' && c !== null && !Array.isArray(c) && typeof c.name === 'string' && c.name !== ''
629
+ ? c.name
630
+ : `#${i + 1}`
631
+ if (c !== null && c !== undefined && typeof c === 'object' && !Array.isArray(c)) {
632
+ const cont = /** @type {Record<string, unknown>} */ (c)
633
+ const image = cont.image
634
+ if (typeof image === 'string' && image.trim() !== '' && isHasuraGraphqlEngineImageRef(image)) {
635
+ const normalized = stripImageDigest(image)
636
+ if (!HASURA_GRAPHQL_ENGINE_ALLOWED_IMAGES.has(normalized)) {
637
+ return `${list} "${label}": образ hasura/graphql-engine має бути ${HASURA_GRAPHQL_ENGINE_IMAGE} (зараз: ${image}) (див. k8s.mdc)`
638
+ }
639
+ }
640
+ }
641
+ }
642
+ return null
643
+ }
644
+
645
+ /**
646
+ * Чи порушує **Deployment** вимогу пінованого образу **hasura/graphql-engine** (k8s.mdc).
647
+ * @param {unknown} manifest корінь YAML-документа
648
+ * @returns {string | null} текст порушення або null, якщо не Deployment / образу немає / ок
649
+ */
650
+ export function deploymentHasuraGraphqlEngineImageViolation(manifest) {
651
+ if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
652
+ return null
653
+ const rec = /** @type {Record<string, unknown>} */ (manifest)
654
+ if (rec.kind !== 'Deployment') return null
655
+ const spec = rec.spec
656
+ if (spec === null || spec === undefined || typeof spec !== 'object' || Array.isArray(spec)) return null
657
+ const template = /** @type {Record<string, unknown>} */ (spec).template
658
+ if (template === null || template === undefined || typeof template !== 'object' || Array.isArray(template))
659
+ return null
660
+ const podSpecRaw = /** @type {Record<string, unknown>} */ (template).spec
661
+ if (podSpecRaw === null || podSpecRaw === undefined || typeof podSpecRaw !== 'object' || Array.isArray(podSpecRaw))
662
+ return null
663
+ const podSpec = /** @type {Record<string, unknown>} */ (podSpecRaw)
664
+
665
+ const main = hasuraGraphqlEngineViolationInContainerList('containers', podSpec.containers)
666
+ if (main !== null) return main
667
+ return hasuraGraphqlEngineViolationInContainerList('initContainers', podSpec.initContainers)
668
+ }
669
+
670
+ /**
671
+ * Чи **Service** містить заборонені анотації GKE у **`metadata.annotations`** (k8s.mdc).
672
+ * @param {unknown} manifest корінь YAML-документа
673
+ * @returns {string | null} текст порушення або null, якщо не Service / анотацій немає / ок
674
+ */
675
+ export function serviceForbiddenGcpAnnotationsViolation(manifest) {
676
+ if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
677
+ return null
678
+ const rec = /** @type {Record<string, unknown>} */ (manifest)
679
+ if (rec.kind !== 'Service') return null
680
+ const meta = rec.metadata
681
+ if (meta === null || meta === undefined || typeof meta !== 'object' || Array.isArray(meta)) return null
682
+ const m = /** @type {Record<string, unknown>} */ (meta)
683
+ const ann = m.annotations
684
+ if (ann === null || ann === undefined || typeof ann !== 'object' || Array.isArray(ann)) return null
685
+ const a = /** @type {Record<string, unknown>} */ (ann)
686
+ /** @type {string[]} */
687
+ const found = []
688
+ for (const key of SERVICE_FORBIDDEN_GCP_ANNOTATION_KEYS) {
689
+ if (Object.hasOwn(a, key)) {
690
+ found.push(key)
691
+ }
692
+ }
693
+ if (found.length === 0) return null
694
+ return `metadata.annotations: прибери заборонені ключі GKE: ${found.join(', ')} (див. k8s.mdc)`
695
+ }
696
+
573
697
  /**
574
698
  * Для маніфестів, **підключених** до Kustomize (шлях у `resources` / `patches` / …), **metadata.namespace** не додають.
575
699
  * @param {unknown} manifest корінь YAML-документа
@@ -638,7 +762,8 @@ export function isK8sBaseManifestYamlPath(rel, baseLower) {
638
762
  }
639
763
 
640
764
  /**
641
- * Парсить усі YAML-документи: **metadata.namespace**, **Deployment.resources**, **imagePullPolicy**.
765
+ * Парсить усі YAML-документи: **metadata.namespace**, **Deployment.resources**, **imagePullPolicy**, **Hasura image pin**,
766
+ * **Service — заборонені GKE-анотації**.
642
767
  * @param {string} rel відносний шлях
643
768
  * @param {string} baseLower basename файлу (нижній регістр)
644
769
  * @param {string} body вміст після modeline
@@ -690,6 +815,14 @@ function validateK8sYamlPolicyDocuments(rel, baseLower, body, fail, kustomizeMan
690
815
  if (pullV !== null) {
691
816
  fail(`${rel}: Deployment (документ ${di + 1}): ${pullV}`)
692
817
  }
818
+ const hasuraV = deploymentHasuraGraphqlEngineImageViolation(obj)
819
+ if (hasuraV !== null) {
820
+ fail(`${rel}: Deployment (документ ${di + 1}): ${hasuraV}`)
821
+ }
822
+ const svcGcpV = serviceForbiddenGcpAnnotationsViolation(obj)
823
+ if (svcGcpV !== null) {
824
+ fail(`${rel}: Service (документ ${di + 1}): ${svcGcpV}`)
825
+ }
693
826
  }
694
827
  }
695
828
  }