@nitra/cursor 1.8.221 → 1.8.228

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.
Files changed (44) hide show
  1. package/.claude-template/npm-CLAUDE.md +4 -0
  2. package/CHANGELOG.md +69 -0
  3. package/bin/auto-rules.md +2 -0
  4. package/bin/n-cursor.js +3 -2
  5. package/mdc/abie.mdc +13 -0
  6. package/mdc/ci4.mdc +8 -0
  7. package/mdc/tauri.mdc +20 -0
  8. package/package.json +1 -1
  9. package/policy/abie/base_deployment_preem/base_deployment_preem.rego +56 -0
  10. package/policy/abie/base_deployment_preem/base_deployment_preem_test.rego +60 -0
  11. package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches.rego +100 -0
  12. package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches_test.rego +48 -0
  13. package/policy/abie/health_check_policy/health_check_policy.rego +91 -22
  14. package/policy/abie/health_check_policy/health_check_policy_test.rego +99 -0
  15. package/policy/abie/http_route_base/http_route_base_test.rego +64 -0
  16. package/policy/k8s/base_kustomization/base_kustomization.rego +40 -0
  17. package/policy/k8s/base_kustomization/base_kustomization_test.rego +36 -0
  18. package/policy/k8s/base_manifest/base_manifest.rego +154 -0
  19. package/policy/k8s/base_manifest/base_manifest_test.rego +94 -0
  20. package/policy/k8s/gateway/gateway.rego +151 -0
  21. package/policy/k8s/gateway/gateway_test.rego +122 -0
  22. package/policy/k8s/hasura_configmap/hasura_configmap.rego +69 -0
  23. package/policy/k8s/hasura_configmap/hasura_configmap_test.rego +49 -0
  24. package/policy/k8s/hasura_httproute/hasura_httproute.rego +298 -0
  25. package/policy/k8s/hasura_httproute/hasura_httproute_test.rego +148 -0
  26. package/policy/k8s/hpa_pdb/hpa_pdb.rego +139 -0
  27. package/policy/k8s/hpa_pdb/hpa_pdb_test.rego +101 -0
  28. package/policy/k8s/kustomization/kustomization.rego +220 -0
  29. package/policy/k8s/kustomization/kustomization_test.rego +128 -0
  30. package/policy/k8s/kustomize_managed/kustomize_managed.rego +31 -0
  31. package/policy/k8s/kustomize_managed/kustomize_managed_test.rego +30 -0
  32. package/policy/k8s/manifest/manifest.rego +151 -4
  33. package/policy/k8s/manifest/manifest_test.rego +309 -0
  34. package/policy/k8s/svc_hl_yaml/svc_hl_yaml.rego +51 -0
  35. package/policy/k8s/svc_hl_yaml/svc_hl_yaml_test.rego +42 -0
  36. package/policy/k8s/svc_yaml/svc_yaml.rego +31 -0
  37. package/policy/k8s/svc_yaml/svc_yaml_test.rego +41 -0
  38. package/scripts/check-abie.mjs +102 -369
  39. package/scripts/check-ga.mjs +89 -9
  40. package/scripts/check-k8s.mjs +128 -569
  41. package/scripts/lint-conftest.mjs +98 -3
  42. package/scripts/lint-ga.mjs +18 -132
  43. package/scripts/lint-rego.mjs +19 -4
  44. package/scripts/utils/run-conftest-batch.mjs +117 -0
@@ -0,0 +1,139 @@
1
+ # Порт **структурних** пер-документних перевірок HPA та PDB з
2
+ # `npm/scripts/check-k8s.mjs` (k8s.mdc). Перевіряє лише ті властивості, що
3
+ # не залежать від cross-file контексту (`expectedDeployName`, `expectedAppLabel`,
4
+ # `isDevLike`-сегмента). Cross-file перевірки лишаються в JS
5
+ # (`hpaManifestViolations`, `pdbManifestViolations`,
6
+ # `validateDeploymentHpaPdbAndTopology`).
7
+ #
8
+ # Запуск (локально, по одному файлу):
9
+ # conftest test path/to/hpa.yaml -p npm/policy/k8s/hpa_pdb \
10
+ # --namespace k8s.hpa_pdb
11
+ #
12
+ # Перевіряє:
13
+ # - HPA: `apiVersion: autoscaling/v2`, `kind: HorizontalPodAutoscaler`;
14
+ # `spec` присутній; `spec.behavior.scaleUp` / `scaleDown` з непорожніми
15
+ # `policies`; `spec.metrics` — непорожній масив;
16
+ # - PDB: `apiVersion: policy/v1`, `kind: PodDisruptionBudget`;
17
+ # `spec.selector.matchLabels` присутній.
18
+ #
19
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
20
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`.
21
+ package k8s.hpa_pdb
22
+
23
+ import rego.v1
24
+
25
+ hpa_kind_template := "kind має бути HorizontalPodAutoscaler (зараз: %v) (k8s.mdc)"
26
+
27
+ hpa_api_template := "apiVersion має бути autoscaling/v2 (зараз: %v) (k8s.mdc)"
28
+
29
+ pdb_kind_template := "kind має бути PodDisruptionBudget (зараз: %v) (k8s.mdc)"
30
+
31
+ pdb_api_template := "apiVersion має бути policy/v1 (зараз: %v) (k8s.mdc)"
32
+
33
+ # ── HPA: apiVersion / kind / spec ────────────────────────────────────────
34
+
35
+ deny contains msg if {
36
+ is_hpa_doc
37
+ input.kind != "HorizontalPodAutoscaler"
38
+ msg := sprintf(hpa_kind_template, [input.kind])
39
+ }
40
+
41
+ deny contains msg if {
42
+ is_hpa_doc
43
+ input.apiVersion != "autoscaling/v2"
44
+ msg := sprintf(hpa_api_template, [input.apiVersion])
45
+ }
46
+
47
+ deny contains "spec відсутній або некоректний (HPA; k8s.mdc)" if {
48
+ is_hpa_doc
49
+ not is_object(object.get(input, "spec", null))
50
+ }
51
+
52
+ # ── HPA: spec.behavior.scaleUp / scaleDown з policies ────────────────────
53
+
54
+ deny contains "spec.behavior відсутній (має містити scaleUp і scaleDown) (HPA; k8s.mdc)" if {
55
+ is_hpa_doc
56
+ spec := object.get(input, "spec", null)
57
+ is_object(spec)
58
+ not is_object(object.get(spec, "behavior", null))
59
+ }
60
+
61
+ deny contains msg if {
62
+ is_hpa_doc
63
+ behavior := object.get(object.get(input, "spec", {}), "behavior", null)
64
+ is_object(behavior)
65
+ some key in {"scaleUp", "scaleDown"}
66
+ not is_object(object.get(behavior, key, null))
67
+ msg := sprintf("spec.behavior.%s відсутній (HPA; k8s.mdc)", [key])
68
+ }
69
+
70
+ deny contains msg if {
71
+ is_hpa_doc
72
+ behavior := object.get(object.get(input, "spec", {}), "behavior", null)
73
+ is_object(behavior)
74
+ some key in {"scaleUp", "scaleDown"}
75
+ branch := object.get(behavior, key, null)
76
+ is_object(branch)
77
+ not is_non_empty_array(object.get(branch, "policies", null))
78
+ msg := sprintf("spec.behavior.%s.policies має бути непорожнім масивом (HPA; k8s.mdc)", [key])
79
+ }
80
+
81
+ # ── HPA: spec.metrics — непорожній масив ──────────────────────────────────
82
+
83
+ deny contains "spec.metrics має бути непорожнім масивом (наприклад Resource/cpu/Utilization) (HPA; k8s.mdc)" if {
84
+ is_hpa_doc
85
+ spec := object.get(input, "spec", null)
86
+ is_object(spec)
87
+ not is_non_empty_array(object.get(spec, "metrics", null))
88
+ }
89
+
90
+ # ── PDB: apiVersion / kind / spec.selector.matchLabels ───────────────────
91
+
92
+ deny contains msg if {
93
+ is_pdb_doc
94
+ input.kind != "PodDisruptionBudget"
95
+ msg := sprintf(pdb_kind_template, [input.kind])
96
+ }
97
+
98
+ deny contains msg if {
99
+ is_pdb_doc
100
+ input.apiVersion != "policy/v1"
101
+ msg := sprintf(pdb_api_template, [input.apiVersion])
102
+ }
103
+
104
+ deny contains "spec відсутній або некоректний (PDB; k8s.mdc)" if {
105
+ is_pdb_doc
106
+ not is_object(object.get(input, "spec", null))
107
+ }
108
+
109
+ deny contains "spec.selector відсутній (PDB; k8s.mdc)" if {
110
+ is_pdb_doc
111
+ spec := object.get(input, "spec", null)
112
+ is_object(spec)
113
+ not is_object(object.get(spec, "selector", null))
114
+ }
115
+
116
+ deny contains "spec.selector.matchLabels відсутній (PDB; k8s.mdc)" if {
117
+ is_pdb_doc
118
+ selector := object.get(object.get(input, "spec", {}), "selector", null)
119
+ is_object(selector)
120
+ not is_object(object.get(selector, "matchLabels", null))
121
+ }
122
+
123
+ # ── helpers ───────────────────────────────────────────────────────────────
124
+
125
+ # Кваліфікуємо як HPA-документ: входить hint `kind == "HorizontalPodAutoscaler"`
126
+ # або apiVersion з префіксом autoscaling/. Це уникає false-positive на ConfigMap
127
+ # чи інших kind у тому самому файлі.
128
+ is_hpa_doc if input.kind == "HorizontalPodAutoscaler"
129
+
130
+ is_hpa_doc if startswith(object.get(input, "apiVersion", ""), "autoscaling/")
131
+
132
+ is_pdb_doc if input.kind == "PodDisruptionBudget"
133
+
134
+ is_pdb_doc if input.apiVersion == "policy/v1"
135
+
136
+ is_non_empty_array(x) if {
137
+ is_array(x)
138
+ count(x) > 0
139
+ }
@@ -0,0 +1,101 @@
1
+ # Тести для `k8s.hpa_pdb`. Запуск:
2
+ # conftest verify -p npm/policy/k8s/hpa_pdb --namespace k8s.hpa_pdb
3
+ package k8s.hpa_pdb_test
4
+
5
+ import rego.v1
6
+
7
+ import data.k8s.hpa_pdb
8
+
9
+ valid_hpa := {
10
+ "apiVersion": "autoscaling/v2",
11
+ "kind": "HorizontalPodAutoscaler",
12
+ "metadata": {"name": "api"},
13
+ "spec": {
14
+ "scaleTargetRef": {"apiVersion": "apps/v1", "kind": "Deployment", "name": "api"},
15
+ "minReplicas": 1,
16
+ "maxReplicas": 1,
17
+ "metrics": [{
18
+ "type": "Resource",
19
+ "resource": {"name": "cpu", "target": {"type": "Utilization", "averageUtilization": 75}},
20
+ }],
21
+ "behavior": {
22
+ "scaleUp": {"policies": [{"type": "Pods", "value": 1, "periodSeconds": 60}]},
23
+ "scaleDown": {"policies": [{"type": "Pods", "value": 1, "periodSeconds": 60}]},
24
+ },
25
+ },
26
+ }
27
+
28
+ valid_pdb := {
29
+ "apiVersion": "policy/v1",
30
+ "kind": "PodDisruptionBudget",
31
+ "metadata": {"name": "api"},
32
+ "spec": {
33
+ "minAvailable": 1,
34
+ "selector": {"matchLabels": {"app": "api"}},
35
+ },
36
+ }
37
+
38
+ # ── HPA позитив / негатив ────────────────────────────────────────────────
39
+
40
+ test_allow_valid_hpa if {
41
+ count(hpa_pdb.deny) == 0 with input as valid_hpa
42
+ }
43
+
44
+ test_deny_hpa_v1 if {
45
+ count(hpa_pdb.deny) > 0 with input as object.union(valid_hpa, {"apiVersion": "autoscaling/v1"})
46
+ }
47
+
48
+ test_deny_hpa_missing_metrics if {
49
+ bad := json.patch(valid_hpa, [{"op": "remove", "path": "/spec/metrics"}])
50
+ count(hpa_pdb.deny) > 0 with input as bad
51
+ }
52
+
53
+ test_deny_hpa_empty_metrics if {
54
+ bad := json.patch(valid_hpa, [{"op": "replace", "path": "/spec/metrics", "value": []}])
55
+ count(hpa_pdb.deny) > 0 with input as bad
56
+ }
57
+
58
+ test_deny_hpa_missing_behavior if {
59
+ bad := json.patch(valid_hpa, [{"op": "remove", "path": "/spec/behavior"}])
60
+ count(hpa_pdb.deny) > 0 with input as bad
61
+ }
62
+
63
+ test_deny_hpa_empty_scale_up_policies if {
64
+ bad := json.patch(valid_hpa, [{"op": "replace", "path": "/spec/behavior/scaleUp/policies", "value": []}])
65
+ count(hpa_pdb.deny) > 0 with input as bad
66
+ }
67
+
68
+ test_deny_hpa_missing_scale_down if {
69
+ bad := json.patch(valid_hpa, [{"op": "remove", "path": "/spec/behavior/scaleDown"}])
70
+ count(hpa_pdb.deny) > 0 with input as bad
71
+ }
72
+
73
+ # ── PDB позитив / негатив ────────────────────────────────────────────────
74
+
75
+ test_allow_valid_pdb if {
76
+ count(hpa_pdb.deny) == 0 with input as valid_pdb
77
+ }
78
+
79
+ test_deny_pdb_wrong_api_version if {
80
+ bad := object.union(valid_pdb, {"apiVersion": "policy/v1beta1"})
81
+ count(hpa_pdb.deny) > 0 with input as bad
82
+ }
83
+
84
+ test_deny_pdb_missing_selector if {
85
+ bad := json.patch(valid_pdb, [{"op": "remove", "path": "/spec/selector"}])
86
+ count(hpa_pdb.deny) > 0 with input as bad
87
+ }
88
+
89
+ test_deny_pdb_missing_match_labels if {
90
+ bad := json.patch(valid_pdb, [{"op": "replace", "path": "/spec/selector", "value": {}}])
91
+ count(hpa_pdb.deny) > 0 with input as bad
92
+ }
93
+
94
+ # Не той kind/apiVersion — пакет не діє.
95
+ test_allow_unrelated_manifest if {
96
+ count(hpa_pdb.deny) == 0 with input as {
97
+ "apiVersion": "v1",
98
+ "kind": "ConfigMap",
99
+ "metadata": {"name": "x"},
100
+ }
101
+ }
@@ -0,0 +1,220 @@
1
+ # Порт пер-документних структурних перевірок `kustomization.yaml` з
2
+ # `npm/scripts/check-k8s.mjs` (k8s.mdc).
3
+ #
4
+ # Запуск (локально, на одному kustomization.yaml):
5
+ # conftest test path/to/kustomization.yaml -p npm/policy/k8s/kustomization \
6
+ # --namespace k8s.kustomization
7
+ #
8
+ # Перевіряє (тільки для `kind: Kustomization`, `apiVersion: kustomize.config.k8s.io/...`):
9
+ # - `resources[]` — за алфавітом (en, case-insensitive); порожні рядки ігноруються;
10
+ # - `patches[]` — за tuple `(target.kind, target.name, target.namespace, path)`;
11
+ # - всередині одного inline `patch` (JSON6902) не може бути одночасно
12
+ # операцій `op: remove` і `op: add` на той самий `path` — k8s.mdc вимагає
13
+ # `op: replace` у такому випадку.
14
+ #
15
+ # JS authoritative: повна резолюція kustomize-дерева, перевірка існування
16
+ # refs на диску, парність `svc.yaml`/`svc-hl.yaml`, вибір conftest-цілей за
17
+ # patternом `kustomization.yaml` — у `check-k8s.mjs`.
18
+ #
19
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
20
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
21
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
22
+ package k8s.kustomization
23
+
24
+ import rego.v1
25
+
26
+ # Префікс apiVersion маніфесту Kustomize Kustomization.
27
+ api_prefix := "kustomize.config.k8s.io/"
28
+
29
+ resources_unsorted_template := concat(" ", [
30
+ "Kustomization.resources має бути за алфавітом (en, case-insensitive).",
31
+ "Зараз: %s; очікувано: %s (k8s.mdc)",
32
+ ])
33
+
34
+ resources_not_array_template := "Kustomization.resources має бути масивом (k8s.mdc)"
35
+
36
+ resources_item_not_string_template := "Kustomization.resources[%d] — очікується рядок-шлях (k8s.mdc)"
37
+
38
+ patches_unsorted_template := concat(" ", [
39
+ "Kustomization.patches має бути за алфавітом",
40
+ "(target.kind → target.name → target.namespace → path).",
41
+ "Зараз: %s; очікувано: %s (k8s.mdc)",
42
+ ])
43
+
44
+ patches_not_array_template := "Kustomization.patches має бути масивом (k8s.mdc)"
45
+
46
+ json6902_conflict_template := concat(" ", [
47
+ "Kustomization.patches[%d]: у наборі JSON6902 операцій є і remove,",
48
+ "і add на той самий path %q — використай op: replace (k8s.mdc)",
49
+ ])
50
+
51
+ # ── deny: resources[] не масив / нечитаний елемент ───────────────────────
52
+
53
+ deny contains resources_not_array_template if {
54
+ is_kustomization
55
+ resources_present
56
+ not is_array(input.resources)
57
+ }
58
+
59
+ deny contains msg if {
60
+ is_kustomization
61
+ some i, item in input.resources
62
+ not is_string(item)
63
+ msg := sprintf(resources_item_not_string_template, [i])
64
+ }
65
+
66
+ # ── deny: resources[] не за алфавітом ────────────────────────────────────
67
+
68
+ deny contains msg if {
69
+ is_kustomization
70
+ is_array(input.resources)
71
+ count(non_empty_resource_paths) >= 2
72
+ not resources_sorted
73
+ msg := sprintf(
74
+ resources_unsorted_template,
75
+ [concat(", ", non_empty_resource_paths), concat(", ", sorted_resource_paths_alpha)],
76
+ )
77
+ }
78
+
79
+ # ── deny: patches[] не масив ─────────────────────────────────────────────
80
+
81
+ deny contains patches_not_array_template if {
82
+ is_kustomization
83
+ patches_present
84
+ not is_array(input.patches)
85
+ }
86
+
87
+ # ── deny: patches[] не за tuple-сортом ───────────────────────────────────
88
+
89
+ deny contains msg if {
90
+ is_kustomization
91
+ is_array(input.patches)
92
+ count(input.patches) >= 2
93
+ not patches_sorted
94
+ msg := sprintf(
95
+ patches_unsorted_template,
96
+ [concat(", ", patches_have_labels), concat(", ", patches_want_labels)],
97
+ )
98
+ }
99
+
100
+ # ── deny: JSON6902 — remove+add на той самий path всередині одного patch ──
101
+
102
+ deny contains msg if {
103
+ is_kustomization
104
+ is_array(object.get(input, "patches", []))
105
+ some i, p in input.patches
106
+ patch_text := patch_inline_text(p)
107
+ patch_text != ""
108
+ some conflict_path in json6902_remove_add_conflicts(patch_text)
109
+ msg := sprintf(json6902_conflict_template, [i, conflict_path])
110
+ }
111
+
112
+ # ── helpers ───────────────────────────────────────────────────────────────
113
+
114
+ is_kustomization if {
115
+ input.kind == "Kustomization"
116
+ startswith(object.get(input, "apiVersion", ""), api_prefix)
117
+ }
118
+
119
+ resources_present if {
120
+ _ := input.resources
121
+ }
122
+
123
+ patches_present if {
124
+ _ := input.patches
125
+ }
126
+
127
+ # Список непорожніх рядкових шляхів resources у порядку файлу (для повідомлення).
128
+ non_empty_resource_paths := [trim_space(item) |
129
+ some item in input.resources
130
+ is_string(item)
131
+ trim_space(item) != ""
132
+ ]
133
+
134
+ resources_sorted if {
135
+ non_empty_resource_paths == sorted_resource_paths_alpha
136
+ }
137
+
138
+ # Case-insensitive en-сорт: будуємо tuple `[lower(s), s]`, сортуємо
139
+ # rego-built-in `sort` (за першою позицією — lower-case), повертаємо оригінали.
140
+ # Це відповідає JS `localeCompare('en', { sensitivity: 'base' })` для ASCII.
141
+ sorted_resource_paths_alpha := [pair[1] | some pair in sorted_lowered_pairs]
142
+
143
+ sorted_lowered_pairs := sort([[lower(s), s] | some s in non_empty_resource_paths])
144
+
145
+ # ── patches sort helpers ─────────────────────────────────────────────────
146
+
147
+ patch_keys := [patch_sort_key(p) | some p in input.patches]
148
+
149
+ patch_sort_key(p) := key if {
150
+ target := object.get(p, "target", {})
151
+ key := [
152
+ lower(string_or_empty(target, "kind")),
153
+ lower(string_or_empty(target, "name")),
154
+ lower(string_or_empty(target, "namespace")),
155
+ lower(string_or_empty(p, "path")),
156
+ ]
157
+ }
158
+
159
+ string_or_empty(obj, k) := v if {
160
+ v := object.get(obj, k, "")
161
+ is_string(v)
162
+ } else := ""
163
+
164
+ patches_sorted if {
165
+ patch_keys == sort(patch_keys)
166
+ }
167
+
168
+ # Лейбли для повідомлень: «kind/name» якщо обидва є; «path=…» якщо є path; «#i» інакше.
169
+ patches_have_labels := [patch_label(input.patches[i], i) |
170
+ some i, _ in input.patches
171
+ ]
172
+
173
+ patches_want_labels := [patch_label(pair[1], pair[2]) | some pair in sorted_patches_with_index]
174
+
175
+ sorted_patches_with_index := sort([[patch_sort_key(input.patches[i]), input.patches[i], i] |
176
+ some i, _ in input.patches
177
+ ])
178
+
179
+ patch_label(p, i) := sprintf("%s/%s", [kind, name]) if {
180
+ target := object.get(p, "target", {})
181
+ kind := string_or_empty(target, "kind")
182
+ kind != ""
183
+ name := string_or_empty(target, "name")
184
+ name != ""
185
+ } else := sprintf("path=%s", [path]) if {
186
+ path := string_or_empty(p, "path")
187
+ path != ""
188
+ } else := sprintf("#%d", [i])
189
+
190
+ # ── JSON6902 conflict helpers ────────────────────────────────────────────
191
+
192
+ # Текст inline `patch` у одному записі patches[]. Якщо немає поля `patch` або
193
+ # не рядок — повертаємо "" (зовнішні patch-файли через `path` тут не читаємо;
194
+ # JS дивиться їх окремо).
195
+ patch_inline_text(p) := v if {
196
+ v := object.get(p, "patch", "")
197
+ is_string(v)
198
+ } else := ""
199
+
200
+ # Витягує `op`/`path` пари з тексту JSON6902. Спершу пробуємо JSON, потім YAML
201
+ # (типова форма kustomization — YAML literal block).
202
+ json6902_ops(text) := ops if {
203
+ yaml.is_valid(text)
204
+ parsed := yaml.unmarshal(text)
205
+ is_array(parsed)
206
+ ops := [{"op": lower(item.op), "path": item.path} |
207
+ some item in parsed
208
+ is_string(object.get(item, "op", null))
209
+ is_string(object.get(item, "path", null))
210
+ trim_space(item.path) != ""
211
+ ]
212
+ } else := []
213
+
214
+ # Шляхи з конфліктом `remove` + `add` у одному наборі операцій.
215
+ json6902_remove_add_conflicts(text) := paths if {
216
+ ops := json6902_ops(text)
217
+ remove_paths := {op.path | some op in ops; op.op == "remove"}
218
+ add_paths := {op.path | some op in ops; op.op == "add"}
219
+ paths := remove_paths & add_paths
220
+ }
@@ -0,0 +1,128 @@
1
+ # Тести для `k8s.kustomization`. Запуск:
2
+ # conftest verify -p npm/policy/k8s/kustomization --namespace k8s.kustomization
3
+ package k8s.kustomization_test
4
+
5
+ import rego.v1
6
+
7
+ import data.k8s.kustomization
8
+
9
+ base_kust := {
10
+ "apiVersion": "kustomize.config.k8s.io/v1beta1",
11
+ "kind": "Kustomization",
12
+ }
13
+
14
+ # ── resources sort ───────────────────────────────────────────────────────
15
+
16
+ test_deny_resources_unsorted if {
17
+ count(kustomization.deny) > 0 with input as object.union(base_kust, {"resources": [
18
+ "deployment.yaml",
19
+ "configmap.yaml",
20
+ ]})
21
+ }
22
+
23
+ test_allow_resources_sorted if {
24
+ count(kustomization.deny) == 0 with input as object.union(base_kust, {"resources": [
25
+ "configmap.yaml",
26
+ "deployment.yaml",
27
+ ]})
28
+ }
29
+
30
+ test_allow_resources_case_insensitive_sorted if {
31
+ count(kustomization.deny) == 0 with input as object.union(base_kust, {"resources": [
32
+ "AAA.yaml",
33
+ "bbb.yaml",
34
+ "CCC.yaml",
35
+ ]})
36
+ }
37
+
38
+ test_deny_resources_not_array if {
39
+ count(kustomization.deny) > 0 with input as object.union(base_kust, {"resources": "string-not-array"})
40
+ }
41
+
42
+ test_deny_resources_item_not_string if {
43
+ count(kustomization.deny) > 0 with input as object.union(base_kust, {"resources": [
44
+ "a.yaml",
45
+ {"obj": "not allowed"},
46
+ ]})
47
+ }
48
+
49
+ # ── patches sort ─────────────────────────────────────────────────────────
50
+
51
+ test_deny_patches_unsorted_by_kind_name if {
52
+ count(kustomization.deny) > 0 with input as object.union(base_kust, {"patches": [
53
+ {"target": {"kind": "Deployment", "name": "z"}},
54
+ {"target": {"kind": "Deployment", "name": "a"}},
55
+ ]})
56
+ }
57
+
58
+ test_allow_patches_sorted_by_kind_name if {
59
+ count(kustomization.deny) == 0 with input as object.union(base_kust, {"patches": [
60
+ {"target": {"kind": "Deployment", "name": "a"}},
61
+ {"target": {"kind": "Deployment", "name": "z"}},
62
+ ]})
63
+ }
64
+
65
+ test_deny_patches_not_array if {
66
+ count(kustomization.deny) > 0 with input as object.union(base_kust, {"patches": "string-not-array"})
67
+ }
68
+
69
+ # ── JSON6902 remove+add conflict ─────────────────────────────────────────
70
+
71
+ test_deny_json6902_remove_and_add_same_path if {
72
+ patch_text := concat("\n", [
73
+ "- op: remove",
74
+ " path: /spec/replicas",
75
+ "- op: add",
76
+ " path: /spec/replicas",
77
+ " value: 3",
78
+ ])
79
+ count(kustomization.deny) > 0 with input as object.union(base_kust, {"patches": [{
80
+ "target": {"kind": "Deployment", "name": "api"},
81
+ "patch": patch_text,
82
+ }]})
83
+ }
84
+
85
+ test_allow_json6902_replace_same_path if {
86
+ patch_text := concat("\n", [
87
+ "- op: replace",
88
+ " path: /spec/replicas",
89
+ " value: 3",
90
+ ])
91
+ count(kustomization.deny) == 0 with input as object.union(base_kust, {"patches": [{
92
+ "target": {"kind": "Deployment", "name": "api"},
93
+ "patch": patch_text,
94
+ }]})
95
+ }
96
+
97
+ test_allow_json6902_remove_and_add_different_paths if {
98
+ patch_text := concat("\n", [
99
+ "- op: remove",
100
+ " path: /spec/replicas",
101
+ "- op: add",
102
+ " path: /spec/strategy",
103
+ " value: {}",
104
+ ])
105
+ count(kustomization.deny) == 0 with input as object.union(base_kust, {"patches": [{
106
+ "target": {"kind": "Deployment", "name": "api"},
107
+ "patch": patch_text,
108
+ }]})
109
+ }
110
+
111
+ # Маніфест не Kustomization — правила не діють.
112
+ test_allow_non_kustomization if {
113
+ count(kustomization.deny) == 0 with input as {
114
+ "apiVersion": "v1",
115
+ "kind": "ConfigMap",
116
+ "metadata": {"name": "x"},
117
+ "data": {"key": "value"},
118
+ }
119
+ }
120
+
121
+ # Не той apiVersion — правила не діють.
122
+ test_allow_kustomization_other_api_version if {
123
+ count(kustomization.deny) == 0 with input as {
124
+ "apiVersion": "other.example.com/v1",
125
+ "kind": "Kustomization",
126
+ "resources": ["z.yaml", "a.yaml"],
127
+ }
128
+ }
@@ -0,0 +1,31 @@
1
+ # Порт перевірки `metadataNamespaceForbiddenViolation` з
2
+ # `npm/scripts/check-k8s.mjs` (k8s.mdc): для файлів, які підключено до якогось
3
+ # `kustomization.yaml` через `resources` / `patches` / `…`, поле
4
+ # `metadata.namespace` забороняється — namespace задає сам kustomization.
5
+ #
6
+ # Запуск (локально, лише для одного kustomize-managed YAML):
7
+ # conftest test path/to/manifest.yaml -p npm/policy/k8s/kustomize_managed \
8
+ # --namespace k8s.kustomize_managed
9
+ #
10
+ # JS відбирає kustomize-managed файли через `collectKustomizeManagedRelPaths`
11
+ # і викликає conftest з цією намеспейс. JS authoritative
12
+ # (`check-k8s.mjs`: `metadataNamespaceForbiddenViolation`,
13
+ # `failIfK8sPolicyNamespaceRulesViolated`).
14
+ #
15
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
16
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`.
17
+ package k8s.kustomize_managed
18
+
19
+ import rego.v1
20
+
21
+ namespace_forbidden_msg := concat(" ", [
22
+ "metadata.namespace заборонено — namespace задає kustomization.yaml",
23
+ "(поле namespace); файл підключено через resources / patches / …",
24
+ "(k8s.mdc)",
25
+ ])
26
+
27
+ deny contains namespace_forbidden_msg if {
28
+ meta := object.get(input, "metadata", null)
29
+ is_object(meta)
30
+ "namespace" in object.keys(meta)
31
+ }
@@ -0,0 +1,30 @@
1
+ # Тести для `k8s.kustomize_managed`. Запуск:
2
+ # conftest verify -p npm/policy/k8s/kustomize_managed --namespace k8s.kustomize_managed
3
+ package k8s.kustomize_managed_test
4
+
5
+ import rego.v1
6
+
7
+ import data.k8s.kustomize_managed
8
+
9
+ test_deny_metadata_with_namespace if {
10
+ count(kustomize_managed.deny) > 0 with input as {
11
+ "apiVersion": "v1",
12
+ "kind": "ConfigMap",
13
+ "metadata": {"name": "cm", "namespace": "dev"},
14
+ }
15
+ }
16
+
17
+ test_allow_metadata_without_namespace if {
18
+ count(kustomize_managed.deny) == 0 with input as {
19
+ "apiVersion": "v1",
20
+ "kind": "ConfigMap",
21
+ "metadata": {"name": "cm"},
22
+ }
23
+ }
24
+
25
+ test_allow_no_metadata if {
26
+ count(kustomize_managed.deny) == 0 with input as {
27
+ "apiVersion": "v1",
28
+ "kind": "ConfigMap",
29
+ }
30
+ }