@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.
- package/.claude-template/npm-CLAUDE.md +4 -0
- package/CHANGELOG.md +69 -0
- package/bin/auto-rules.md +2 -0
- package/bin/n-cursor.js +3 -2
- package/mdc/abie.mdc +13 -0
- package/mdc/ci4.mdc +8 -0
- package/mdc/tauri.mdc +20 -0
- package/package.json +1 -1
- package/policy/abie/base_deployment_preem/base_deployment_preem.rego +56 -0
- package/policy/abie/base_deployment_preem/base_deployment_preem_test.rego +60 -0
- package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches.rego +100 -0
- package/policy/abie/clean_merged_ignore_branches/clean_merged_ignore_branches_test.rego +48 -0
- package/policy/abie/health_check_policy/health_check_policy.rego +91 -22
- package/policy/abie/health_check_policy/health_check_policy_test.rego +99 -0
- package/policy/abie/http_route_base/http_route_base_test.rego +64 -0
- package/policy/k8s/base_kustomization/base_kustomization.rego +40 -0
- package/policy/k8s/base_kustomization/base_kustomization_test.rego +36 -0
- package/policy/k8s/base_manifest/base_manifest.rego +154 -0
- package/policy/k8s/base_manifest/base_manifest_test.rego +94 -0
- package/policy/k8s/gateway/gateway.rego +151 -0
- package/policy/k8s/gateway/gateway_test.rego +122 -0
- package/policy/k8s/hasura_configmap/hasura_configmap.rego +69 -0
- package/policy/k8s/hasura_configmap/hasura_configmap_test.rego +49 -0
- package/policy/k8s/hasura_httproute/hasura_httproute.rego +298 -0
- package/policy/k8s/hasura_httproute/hasura_httproute_test.rego +148 -0
- package/policy/k8s/hpa_pdb/hpa_pdb.rego +139 -0
- package/policy/k8s/hpa_pdb/hpa_pdb_test.rego +101 -0
- package/policy/k8s/kustomization/kustomization.rego +220 -0
- package/policy/k8s/kustomization/kustomization_test.rego +128 -0
- package/policy/k8s/kustomize_managed/kustomize_managed.rego +31 -0
- package/policy/k8s/kustomize_managed/kustomize_managed_test.rego +30 -0
- package/policy/k8s/manifest/manifest.rego +151 -4
- package/policy/k8s/manifest/manifest_test.rego +309 -0
- package/policy/k8s/svc_hl_yaml/svc_hl_yaml.rego +51 -0
- package/policy/k8s/svc_hl_yaml/svc_hl_yaml_test.rego +42 -0
- package/policy/k8s/svc_yaml/svc_yaml.rego +31 -0
- package/policy/k8s/svc_yaml/svc_yaml_test.rego +41 -0
- package/scripts/check-abie.mjs +102 -369
- package/scripts/check-ga.mjs +89 -9
- package/scripts/check-k8s.mjs +128 -569
- package/scripts/lint-conftest.mjs +98 -3
- package/scripts/lint-ga.mjs +18 -132
- package/scripts/lint-rego.mjs +19 -4
- 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
|
+
}
|