A Helm plugin that uses Common Expression Language (CEL) to validate values. Instead of using JSON Schema in values.schema.json
, you can write more expressive validation rules using CEL in values.cel.yaml
.
helm plugin install https://github.com/idsulik/helm-cel
Create a values.cel.yaml
file in your chart directory alongside your values.yaml
file:
rules:
- expr: "has(values.service) && has(values.service.port)"
desc: "service port is required"
- expr: "values.service.port >= 1 && values.service.port <= 65535"
desc: "service port must be between 1 and 65535"
- expr: "!(has(values.replicaCount)) || values.replicaCount >= 1"
desc: "if replicaCount is set, it must be at least 1"
Then run validation:
helm cel /path/to/your/chart
Given this values.yaml
:
service:
type: ClusterIP
port: 80
replicaCount: 1
image:
repository: nginx
tag: latest
And this values.cel.yaml
:
rules:
- expr: "has(values.service) && has(values.service.port)"
desc: "service port is required"
- expr: "values.service.port >= 1 && values.service.port <= 65535"
desc: "service port must be between 1 and 65535"
- expr: "values.replicaCount >= 1"
desc: "replica count must be at least 1"
- expr: |
has(values.image) &&
has(values.image.repository) &&
has(values.image.tag)
desc: "image repository and tag are required"
If validation fails, you'll get a clear error message:
❌ Validation failed: replica count must be at least 1
Rule: values.replicaCount >= 1
Path: replicaCount
Current value: 0
Each rule in values.cel.yaml
consists of:
expr
: A CEL expression that should evaluate totrue
for valid valuesdesc
: A description of what the rule validatesseverity
: Optional severity level ("error" or "warning", defaults to "error")
The values.cel.yaml
can also contain named expressions that can be reused across rules:
expressions:
# Named expressions for reuse
portRange: 'values.service.port >= 1 && values.service.port <= 65535'
nodePortRange: 'values.service.nodePort >= 30000 && values.service.nodePort <= 32767'
rules:
- expr: "${portRange}"
desc: "Service port must be between 1 and 65535"
- expr: 'values.service.type == "NodePort" ? ${nodePortRange} : true'
desc: "NodePort must be between 30000 and 32767 when service type is NodePort"
- expr: "values.replicaCount >= 2"
desc: "Consider running multiple replicas for high availability"
severity: warning
Rules can have two severity levels:
error
: Validation fails if the rule is not satisfied (default)warning
: Shows a warning but allows validation to pass
Example output with warnings:
Found 1 warning(s):
⚠️ Service port must be between 1 and 65535 and replica count must be within min and max bounds
Rule: (values.service.port >= 1 && values.service.port <= 65535) && (values.replicaCount >= values.minReplicas && values.replicaCount <= values.maxReplicas)
Path: service.port
Current value: 80801111111
-------------------------------------------------
⚠️✅ Values validation successful with warnings!
- Required fields:
- expr: "has(values.fieldName)"
desc: "fieldName is required"
- Value constraints:
- expr: "values.number >= 0 && values.number <= 100"
desc: "number must be between 0 and 100"
- Conditional requirements:
- expr: "!(has(values.optional)) || values.optional >= 0"
desc: "if optional is set, it must be non-negative"
- Type validation:
- expr: "type(values.ports) == list"
desc: "ports must be a list"
- Complex object validation:
- expr: |
has(values.container) &&
has(values.container.image) &&
has(values.container.tag)
desc: "container must have image and tag"
- Resource validation:
expressions:
validateResources: '
has(values.resources) &&
has(values.resources.requests) &&
has(values.resources.limits) &&
matches(string(values.resources.requests.memory), r"^[0-9]+(Mi|Gi)$") &&
matches(string(values.resources.limits.memory), r"^[0-9]+(Mi|Gi)$") &&
matches(string(values.resources.requests.cpu), r"^[0-9]+m$|^[0-9]+$") &&
matches(string(values.resources.limits.cpu), r"^[0-9]+m$|^[0-9]+$")
'
rules:
- expr: "${validateResources}"
desc: "Resource requests and limits must be properly formatted"
- Complex service validation:
expressions:
portRange: 'values.service.port >= 1 && values.service.port <= 65535'
rules:
- expr: 'values.service.type in ["ClusterIP", "NodePort", "LoadBalancer"]'
desc: "Service type must be valid"
- expr: "${portRange}"
desc: "Service port must be valid"
- expr: '!values.ingress.enabled || has(values.ingress.className)'
desc: "Ingress className should be specified when enabled"
severity: warning
- Conditional validation with reusable expressions:
expressions:
nodePortRange: 'values.service.nodePort >= 30000 && values.service.nodePort <= 32767'
rules:
- expr: 'values.service.type == "NodePort" ? ${nodePortRange} : true'
desc: "NodePort must be in valid range when service type is NodePort"
Requirements:
- Go 1.22 or later
Build:
make build
Install locally:
make install
Run tests:
make test
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
Distributed under the MIT License. See LICENSE
for more information.