P8.3: Add values.schema.json rejection rules for incompatible configs
Schema-enforced rules (helm lint --strict): - Rule 1: miroir.replicas > 1 requires taskStore.backend=redis - Rule 2: hpa.enabled requires replicas >= 2 AND taskStore.backend=redis - Rule 3: search_ui.rate_limit.backend=local rejected when replicas > 1 - Rule 4: admin_ui.login_rate_limit.backend=local rejected when replicas > 1 Template-enforced rule (helm template): - Rule 5: scoped_key_rotate_before_expiry_days < scoped_key_max_age_days (JSON Schema draft-7 cannot compare sibling properties) 11 test cases: 7 bad configs rejected, 4 good configs pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
7c13091a27
commit
5b9ae4fa02
11 changed files with 293 additions and 20 deletions
|
|
@ -188,3 +188,17 @@ Return "true" if CDC PVC should be created, "false" otherwise.
|
|||
{{- define "miroir.cdcPvcEnabled" -}}
|
||||
{{- if .Values.cdcPvc.enabled -}}true{{- else -}}false{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Cross-field validations that JSON Schema draft-7 cannot express.
|
||||
Rendered as an empty ConfigMap; fails template rendering on invalid config.
|
||||
*/}}
|
||||
{{- define "miroir.validate.values" -}}
|
||||
{{- if .Values.search_ui -}}
|
||||
{{- if and (hasKey .Values.search_ui "scoped_key_rotate_before_expiry_days") (hasKey .Values.search_ui "scoped_key_max_age_days") -}}
|
||||
{{- if ge (int .Values.search_ui.scoped_key_rotate_before_expiry_days) (int .Values.search_ui.scoped_key_max_age_days) -}}
|
||||
{{- fail (printf "search_ui.scoped_key_rotate_before_expiry_days (%d) must be strictly less than scoped_key_max_age_days (%d); otherwise rotation fires before/at key issuance, producing a continuous rotation loop" (int .Values.search_ui.scoped_key_rotate_before_expiry_days) (int .Values.search_ui.scoped_key_max_age_days)) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
|
|
|||
5
charts/miroir/templates/miroir-validate.yaml
Normal file
5
charts/miroir/templates/miroir-validate.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{{/*
|
||||
Render-time value validations (cross-field checks that JSON Schema cannot express).
|
||||
This produces no output on success; calls fail() on invalid config.
|
||||
*/}}
|
||||
{{- include "miroir.validate.values" . -}}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Rule 4 bad: admin_ui.login_rate_limit.backend=local with multiple replicas
|
||||
# Expected: FAIL — local login rate limits not shared across pods
|
||||
miroir:
|
||||
replicas: 2
|
||||
taskStore:
|
||||
backend: redis
|
||||
admin_ui:
|
||||
login_rate_limit:
|
||||
backend: local
|
||||
8
charts/miroir/tests/bad-hpa-no-redis.yaml
Normal file
8
charts/miroir/tests/bad-hpa-no-redis.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Rule 2 bad: HPA enabled with SQLite backend
|
||||
# Expected: FAIL — HPA requires taskStore.backend=redis
|
||||
miroir:
|
||||
replicas: 2
|
||||
taskStore:
|
||||
backend: sqlite
|
||||
hpa:
|
||||
enabled: true
|
||||
8
charts/miroir/tests/bad-hpa-single-replica.yaml
Normal file
8
charts/miroir/tests/bad-hpa-single-replica.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Rule 2 bad: HPA enabled with only 1 replica
|
||||
# Expected: FAIL — HPA requires miroir.replicas >= 2
|
||||
miroir:
|
||||
replicas: 1
|
||||
taskStore:
|
||||
backend: redis
|
||||
hpa:
|
||||
enabled: true
|
||||
9
charts/miroir/tests/bad-scoped-key-rotate-gt-max.yaml
Normal file
9
charts/miroir/tests/bad-scoped-key-rotate-gt-max.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Rule 5 bad: scoped_key_rotate_before_expiry_days > scoped_key_max_age_days
|
||||
# Expected: FAIL — rotation window is negative, producing a continuous rotation loop
|
||||
miroir:
|
||||
replicas: 1
|
||||
taskStore:
|
||||
backend: sqlite
|
||||
search_ui:
|
||||
scoped_key_max_age_days: 30
|
||||
scoped_key_rotate_before_expiry_days: 60
|
||||
9
charts/miroir/tests/bad-scoped-key-rotate-gte-max.yaml
Normal file
9
charts/miroir/tests/bad-scoped-key-rotate-gte-max.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Rule 5 bad: scoped_key_rotate_before_expiry_days >= scoped_key_max_age_days
|
||||
# Expected: FAIL — rotation fires before/at key issuance, producing a continuous rotation loop
|
||||
miroir:
|
||||
replicas: 1
|
||||
taskStore:
|
||||
backend: sqlite
|
||||
search_ui:
|
||||
scoped_key_max_age_days: 30
|
||||
scoped_key_rotate_before_expiry_days: 30
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Rule 3 bad: search_ui.rate_limit.backend=local with multiple replicas
|
||||
# Expected: FAIL — local rate limiting with multi-replica gives per_ip × pod_count effective rate
|
||||
miroir:
|
||||
replicas: 2
|
||||
taskStore:
|
||||
backend: redis
|
||||
search_ui:
|
||||
rate_limit:
|
||||
backend: local
|
||||
6
charts/miroir/tests/good-dev-no-ui.yaml
Normal file
6
charts/miroir/tests/good-dev-no-ui.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Good: dev defaults (single replica, SQLite, no HPA, no UI)
|
||||
# Expected: PASS
|
||||
miroir:
|
||||
replicas: 1
|
||||
taskStore:
|
||||
backend: sqlite
|
||||
18
charts/miroir/tests/good-production.yaml
Normal file
18
charts/miroir/tests/good-production.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Good: full production config with all features enabled correctly
|
||||
# Expected: PASS
|
||||
miroir:
|
||||
replicas: 3
|
||||
taskStore:
|
||||
backend: redis
|
||||
hpa:
|
||||
enabled: true
|
||||
minReplicas: 3
|
||||
maxReplicas: 10
|
||||
search_ui:
|
||||
scoped_key_max_age_days: 60
|
||||
scoped_key_rotate_before_expiry_days: 30
|
||||
rate_limit:
|
||||
backend: redis
|
||||
admin_ui:
|
||||
login_rate_limit:
|
||||
backend: redis
|
||||
|
|
@ -196,34 +196,212 @@
|
|||
"size": { "type": "string" },
|
||||
"storageClass": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"search_ui": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"scoped_key_max_age_days": { "type": "integer", "minimum": 2 },
|
||||
"scoped_key_rotate_before_expiry_days": { "type": "integer", "minimum": 1 },
|
||||
"rate_limit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"backend": { "type": "string", "enum": ["local", "redis"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"admin_ui": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"login_rate_limit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"backend": { "type": "string", "enum": ["local", "redis"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"if": {
|
||||
"properties": {
|
||||
"miroir": {
|
||||
"allOf": [
|
||||
{
|
||||
"description": "Rule 1: miroir.replicas > 1 requires taskStore.backend: redis",
|
||||
"if": {
|
||||
"properties": {
|
||||
"replicas": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 1
|
||||
"miroir": {
|
||||
"properties": {
|
||||
"replicas": { "type": "integer", "exclusiveMinimum": 1 }
|
||||
},
|
||||
"required": ["replicas"]
|
||||
}
|
||||
},
|
||||
"required": ["replicas"]
|
||||
}
|
||||
},
|
||||
"required": ["miroir"]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"taskStore": {
|
||||
"required": ["miroir"]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"backend": {
|
||||
"const": "redis",
|
||||
"errorMessage": "SQLite task store cannot run with multiple replicas; set taskStore.backend=redis"
|
||||
"taskStore": {
|
||||
"properties": {
|
||||
"backend": {
|
||||
"const": "redis",
|
||||
"errorMessage": "SQLite task store cannot run with multiple replicas; set taskStore.backend=redis"
|
||||
}
|
||||
},
|
||||
"required": ["backend"]
|
||||
}
|
||||
},
|
||||
"required": ["backend"]
|
||||
"required": ["taskStore"]
|
||||
}
|
||||
},
|
||||
"required": ["taskStore"]
|
||||
}
|
||||
{
|
||||
"description": "Rule 2: hpa.enabled requires replicas >= 2 AND taskStore.backend: redis",
|
||||
"if": {
|
||||
"properties": {
|
||||
"hpa": {
|
||||
"properties": {
|
||||
"enabled": { "const": true }
|
||||
},
|
||||
"required": ["enabled"]
|
||||
}
|
||||
},
|
||||
"required": ["hpa"]
|
||||
},
|
||||
"then": {
|
||||
"allOf": [
|
||||
{
|
||||
"properties": {
|
||||
"miroir": {
|
||||
"properties": {
|
||||
"replicas": {
|
||||
"type": "integer",
|
||||
"minimum": 2,
|
||||
"errorMessage": "HPA requires miroir.replicas >= 2; increase replicas or disable hpa"
|
||||
}
|
||||
},
|
||||
"required": ["replicas"]
|
||||
}
|
||||
},
|
||||
"required": ["miroir"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"taskStore": {
|
||||
"properties": {
|
||||
"backend": {
|
||||
"const": "redis",
|
||||
"errorMessage": "HPA requires taskStore.backend=redis; SQLite is single-writer and cannot be shared across autoscaled pods"
|
||||
}
|
||||
},
|
||||
"required": ["backend"]
|
||||
}
|
||||
},
|
||||
"required": ["taskStore"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Rule 3: search_ui.rate_limit.backend: local rejected when miroir.replicas > 1",
|
||||
"if": {
|
||||
"allOf": [
|
||||
{
|
||||
"properties": {
|
||||
"miroir": {
|
||||
"properties": {
|
||||
"replicas": { "type": "integer", "exclusiveMinimum": 1 }
|
||||
},
|
||||
"required": ["replicas"]
|
||||
}
|
||||
},
|
||||
"required": ["miroir"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"search_ui": {
|
||||
"properties": {
|
||||
"rate_limit": {
|
||||
"properties": {
|
||||
"backend": { "const": "local" }
|
||||
},
|
||||
"required": ["backend"]
|
||||
}
|
||||
},
|
||||
"required": ["rate_limit"]
|
||||
}
|
||||
},
|
||||
"required": ["search_ui"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"search_ui": {
|
||||
"properties": {
|
||||
"rate_limit": {
|
||||
"properties": {
|
||||
"backend": {
|
||||
"not": { "const": "local" },
|
||||
"description": "Local rate limiting is incompatible with multiple replicas — effective per_ip limit is multiplied by pod count. Use backend: redis instead."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Rule 4: admin_ui.login_rate_limit.backend: local rejected when miroir.replicas > 1",
|
||||
"if": {
|
||||
"allOf": [
|
||||
{
|
||||
"properties": {
|
||||
"miroir": {
|
||||
"properties": {
|
||||
"replicas": { "type": "integer", "exclusiveMinimum": 1 }
|
||||
},
|
||||
"required": ["replicas"]
|
||||
}
|
||||
},
|
||||
"required": ["miroir"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"admin_ui": {
|
||||
"properties": {
|
||||
"login_rate_limit": {
|
||||
"properties": {
|
||||
"backend": { "const": "local" }
|
||||
},
|
||||
"required": ["backend"]
|
||||
}
|
||||
},
|
||||
"required": ["login_rate_limit"]
|
||||
}
|
||||
},
|
||||
"required": ["admin_ui"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"admin_ui": {
|
||||
"properties": {
|
||||
"login_rate_limit": {
|
||||
"properties": {
|
||||
"backend": {
|
||||
"not": { "const": "local" },
|
||||
"description": "Local login rate limiting is incompatible with multiple replicas — limits are not shared across pods. Use backend: redis instead."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Rule 5: scoped_key_rotate_before_expiry_days must be strictly less than scoped_key_max_age_days (enforced at render time via _helpers.tpl since JSON Schema draft-7 cannot compare sibling properties)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue