
Helm Chart Builder
- 61 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
Helm Chart Builder is a Claude skill that analyzes and validates Helm charts for Kubernetes, checking structure, values, templates, and dependencies.
About
Helm Chart Builder is a Claude skill that analyzes and validates Helm charts for Kubernetes. It ships scripts that check chart structure, validate values files against Kubernetes best practices, inspect templates, and review dependencies. A DevOps engineer uses it to review a chart before release, validate per-environment values, or add chart checks to CI.
- Analyzes Helm chart structure, metadata, templates, and dependencies
- Validates values.yaml for resource limits, security context, and image tags
- CI-ready JSON output with a pre-release chart workflow
Helm Chart Builder by the numbers
- 61 all-time installs (skills.sh)
- Ranked #651 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
helm-chart-builder capabilities & compatibility
Free; runs local Python scripts, no API keys.
- Capabilities
- helm chart builder · infrastructure compliance auditor
- Works with
- kubernetes
- Use cases
- devops · ci cd
- Pricing
- Free
What helm-chart-builder says it does
The **Helm Chart Builder** skill provides automated analysis of Helm charts including structure validation, values checking, template inspection, and dependency review.
Validates values files against chart expectations and Kubernetes best practices.
Image tags | Flags use of latest or missing image tags
npx skills add https://github.com/borghei/claude-skills --skill helm-chart-builderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 61 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
What it does
Validate Helm chart structure and values files for Kubernetes before release or in CI.
Who is it for?
DevOps engineers reviewing a Helm chart before release or validating per-environment values files in CI.
Skip if: Non-Kubernetes deployments or runtime cluster scanning.
When should I use this skill?
The user asks to analyze Helm charts, validate Helm values, review chart structure, or audit chart dependencies.
What you get
A validated chart with correct structure, secure values, and pinned dependencies ready for helm package.
- Chart structure and quality report (JSON or markdown)
- Per-environment values validation results
By the numbers
- 2 bundled scripts (chart_analyzer, values_validator)
- 5 checks in values_validator.py
Files
Helm Chart Builder
Category: Engineering
Domain: Kubernetes & Helm
Overview
The Helm Chart Builder skill provides automated analysis of Helm charts including structure validation, values checking, template inspection, and dependency review. It helps teams maintain high-quality charts with correct configurations, proper security contexts, and complete documentation.
Quick Start
# Analyze chart structure and quality
python scripts/chart_analyzer.py --path ./charts/my-app
# Validate values.yaml against chart requirements
python scripts/values_validator.py --chart ./charts/my-app --values values-prod.yaml
# JSON output for CI
python scripts/chart_analyzer.py --path ./charts/my-app --format json
# Validate multiple values files
python scripts/values_validator.py --chart ./charts/my-app --values values-dev.yaml values-prod.yamlTools Overview
chart_analyzer.py
Analyzes Helm chart structure, metadata, templates, and dependencies.
| Feature | Description |
|---|---|
| Structure validation | Checks required files exist (Chart.yaml, values.yaml, templates/) |
| Metadata check | Validates Chart.yaml fields, version format, appVersion |
| Template review | Inspects templates for common patterns and issues |
| Dependency analysis | Reviews subchart dependencies and version constraints |
| Documentation check | Verifies NOTES.txt and README presence |
values_validator.py
Validates values files against chart expectations and Kubernetes best practices.
| Feature | Description |
|---|---|
| Resource limits | Checks for CPU/memory requests and limits |
| Security context | Validates runAsNonRoot, readOnlyRootFilesystem |
| Replica count | Checks for production-appropriate replica counts |
| Image tags | Flags use of latest or missing image tags |
| Ingress config | Validates ingress annotations and TLS settings |
Workflows
Chart Review Workflow
1. Analyze structure - Run chart_analyzer.py to check chart organization 2. Validate defaults - Run values_validator.py against default values.yaml 3. Check environments - Validate each environment's values file 4. Review findings - Address critical issues first, then warnings 5. Re-check - Confirm fixes pass validation
Pre-Release Workflow
1. Bump version - Update Chart.yaml version and appVersion 2. Lint chart - Run chart_analyzer.py in strict mode 3. Validate all values - Check every environment's values file 4. Check dependencies - Ensure subchart versions are pinned 5. Package - Chart is ready for helm package
CI Integration
# Structure check
python scripts/chart_analyzer.py --path ./charts/my-app --format json --strict
# Values validation for all environments
for env in dev staging production; do
python scripts/values_validator.py \
--chart ./charts/my-app \
--values "values-${env}.yaml"
doneReference Documentation
- Helm Best Practices - Chart structure, templates, security, dependencies
Common Patterns Quick Reference
Required Chart Structure
my-chart/
Chart.yaml # Required: chart metadata
values.yaml # Required: default values
templates/ # Required: template directory
deployment.yaml
service.yaml
_helpers.tpl # Recommended: template helpers
NOTES.txt # Recommended: post-install notes
charts/ # Optional: subchart dependenciesValues Best Practices
| Setting | Requirement | Why |
|---|---|---|
| resources.limits | Required | Prevents resource exhaustion |
| resources.requests | Required | Enables proper scheduling |
| securityContext.runAsNonRoot | Required | Security baseline |
| image.tag | Required (not latest) | Reproducible deployments |
| replicaCount >= 2 | Recommended for prod | High availability |
| ingress.tls | Recommended | Encrypted traffic |
Chart.yaml Required Fields
| Field | Description |
|---|---|
| apiVersion | v2 for Helm 3 |
| name | Chart name (lowercase) |
| version | SemVer chart version |
| appVersion | Application version |
| description | Brief chart description |
Common Issues
| Issue | Severity | Fix |
|---|---|---|
| Missing Chart.yaml | Critical | Add required chart metadata |
| No resource limits | Warning | Set CPU/memory limits |
| Latest image tag | Warning | Pin specific version |
| No security context | Warning | Add runAsNonRoot: true |
| Missing NOTES.txt | Info | Add post-install notes |
| Unpinned dependencies | Warning | Pin subchart versions |
# Chart.yaml — Sample Helm chart metadata for the helm-chart-builder skill
#
# This chart definition has several issues for the analyzer to flag:
# - Missing appVersion
# - No maintainers listed
# - No home/sources URLs
# - Missing keywords for discoverability
# - Deprecated apiVersion v1 (should be v2)
apiVersion: v1 # ISSUE: should use apiVersion v2 for Helm 3
name: acme-webapp
version: 0.1.0
# ISSUE: missing appVersion field
description: Acme Corp web application
type: application
# ISSUE: no maintainers
# ISSUE: no home URL
# ISSUE: no sources
# ISSUE: no icon
# ISSUE: no keywords
dependencies:
- name: postgresql
version: "12.x.x" # ISSUE: overly broad version constraint
repository: "https://charts.bitnami.com/bitnami"
- name: redis
version: "*" # ISSUE: wildcard version — not reproducible
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled
# values.yaml — Helm values file with common anti-patterns
#
# This values file contains deliberate issues for the helm-chart-builder
# skill analyzer to detect:
# - Missing resource limits/requests
# - No security context
# - Running as root
# - No liveness/readiness probes
# - Hardcoded secrets
# - No pod disruption budget
# - No network policies
# - No topology spread constraints
replicaCount: 1 # ISSUE: single replica for a production workload
image:
repository: acme/webapp
tag: latest # ISSUE: "latest" tag is not reproducible
pullPolicy: Always
# ISSUE: no imagePullSecrets configured for private registry
nameOverride: ""
fullnameOverride: "acme-webapp"
service:
type: LoadBalancer # ISSUE: LoadBalancer exposes directly; consider ClusterIP + Ingress
port: 80
ingress:
enabled: false # ISSUE: no ingress configured despite being a web app
# ISSUE: no securityContext at pod or container level
# Should include:
# runAsNonRoot: true
# readOnlyRootFilesystem: true
# allowPrivilegeEscalation: false
# capabilities: { drop: [ALL] }
# ISSUE: no resource requests or limits defined
# resources:
# requests:
# cpu: 100m
# memory: 128Mi
# limits:
# cpu: 500m
# memory: 512Mi
# ISSUE: no liveness or readiness probes
# livenessProbe:
# httpGet:
# path: /healthz
# port: http
# readinessProbe:
# httpGet:
# path: /ready
# port: http
env:
# ISSUE: hardcoded secrets in values (should use secretRef)
- name: DATABASE_URL
value: "postgres://admin:pr0duction_s3cret@postgres:5432/acme"
- name: REDIS_URL
value: "redis://:redis_pass@redis:6379"
- name: API_SECRET_KEY
value: "sk-live-abc123def456ghi789"
- name: NODE_ENV
value: "production"
- name: LOG_LEVEL
value: "debug" # ISSUE: debug logging in production
# ISSUE: no pod disruption budget
# podDisruptionBudget:
# minAvailable: 1
# ISSUE: no autoscaling configured for production
autoscaling:
enabled: false
# ISSUE: no nodeSelector, tolerations, or affinity rules
nodeSelector: {}
tolerations: []
affinity: {}
# ISSUE: no topology spread constraints for HA
# topologySpreadConstraints: []
# ISSUE: no network policy
# networkPolicy:
# enabled: false
# ISSUE: no service account configuration
serviceAccount:
create: false
# Dependency values
postgresql:
enabled: true
auth:
# ISSUE: hardcoded database password
postgresPassword: "supersecret123"
database: acme
primary:
persistence:
size: 10Gi
# ISSUE: no resource limits on database
redis:
enabled: true
auth:
password: "redis_pass" # ISSUE: hardcoded password
# ISSUE: no resource limits on cache
Helm Best Practices Reference
Chart Structure
Minimum Required Files
my-chart/
Chart.yaml # Required: metadata
values.yaml # Required: defaults
templates/ # Required: manifestsRecommended Structure
my-chart/
Chart.yaml
Chart.lock
values.yaml
values-dev.yaml
values-staging.yaml
values-production.yaml
templates/
deployment.yaml
service.yaml
ingress.yaml
serviceaccount.yaml
hpa.yaml
pdb.yaml
configmap.yaml
secret.yaml
_helpers.tpl
NOTES.txt
tests/
test-connection.yaml
charts/ # Subcharts
crds/ # CRDs
README.mdChart.yaml Best Practices
apiVersion: v2
name: my-app
description: A Helm chart for my application
type: application
version: 1.2.3 # Chart version (SemVer)
appVersion: "2.0.0" # Application version
maintainers:
- name: team
email: team@example.com
dependencies:
- name: postgresql
version: "~12.0"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabledValues Best Practices
Security Defaults
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
podSecurityContext:
fsGroup: 1000
runAsUser: 1000
runAsGroup: 1000Resource Management
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128MiHealth Probes
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 5Template Patterns
_helpers.tpl Standard Labels
{{- define "my-chart.labels" -}}
helm.sh/chart: {{ include "my-chart.chart" . }}
app.kubernetes.io/name: {{ include "my-chart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}Conditional Resources
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
...
{{- end }}Dependency Management
- Always pin dependency versions with range operators:
~1.2.0or>=1.0.0 <2.0.0 - Use
conditionfields to make dependencies optional - Run
helm dependency updateafter modifying Chart.yaml - Commit Chart.lock for reproducible builds
Testing
# Lint chart
helm lint ./my-chart
# Template rendering
helm template my-release ./my-chart --values values-prod.yaml
# Dry run install
helm install my-release ./my-chart --dry-run --debug
# Run chart tests
helm test my-release#!/usr/bin/env python3
"""
Helm Chart Analyzer - Analyze Helm chart structure, metadata, and templates.
Checks for required files, validates Chart.yaml metadata, inspects templates
for common issues, and reviews dependency configurations.
Author: Claude Skills Engineering Team
License: MIT
"""
import argparse
import json
import re
import sys
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import List, Dict, Optional, Any
@dataclass
class Finding:
"""An analysis finding."""
severity: str # critical, warning, info
category: str
message: str
recommendation: str
@dataclass
class ChartMetadata:
"""Parsed Chart.yaml metadata."""
api_version: str
name: str
version: str
app_version: str
description: str
type: str
dependencies: List[Dict[str, str]]
class SimpleYamlParser:
"""Minimal YAML parser for Helm chart files (stdlib only)."""
def parse(self, content: str) -> Dict[str, Any]:
"""Parse simple YAML into a dict."""
result: Dict[str, Any] = {}
current_key = None
current_list: Optional[List] = None
current_list_item: Optional[Dict] = None
for line in content.split("\n"):
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
indent = len(line) - len(line.lstrip())
# Top-level key: value
if indent == 0 and ":" in stripped:
if current_list_item and current_list is not None:
current_list.append(current_list_item)
current_list_item = None
current_list = None
key, _, value = stripped.partition(":")
key = key.strip()
value = value.strip()
current_key = key
if value:
result[key] = value.strip('"').strip("'")
else:
result[key] = None
# List item
elif stripped.startswith("- "):
if current_key and result.get(current_key) is None:
result[current_key] = []
current_list = result[current_key]
if current_list_item and current_list is not None:
current_list.append(current_list_item)
item_content = stripped[2:].strip()
if ":" in item_content:
k, _, v = item_content.partition(":")
current_list_item = {k.strip(): v.strip().strip('"').strip("'")}
elif current_list is not None:
current_list.append(item_content)
current_list_item = None
# Continuation of list item
elif indent >= 4 and current_list_item is not None and ":" in stripped:
k, _, v = stripped.partition(":")
current_list_item[k.strip()] = v.strip().strip('"').strip("'")
if current_list_item and current_list is not None:
current_list.append(current_list_item)
return result
class ChartAnalyzer:
"""Analyzes Helm chart structure and quality."""
REQUIRED_FILES = ["Chart.yaml", "values.yaml"]
REQUIRED_DIRS = ["templates"]
RECOMMENDED_FILES = ["templates/NOTES.txt", "templates/_helpers.tpl"]
CHART_REQUIRED_FIELDS = ["apiVersion", "name", "version"]
SEMVER_PATTERN = re.compile(r'^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$')
def __init__(self, chart_path: Path, strict: bool = False):
self.chart_path = chart_path
self.strict = strict
self.findings: List[Finding] = []
self.metadata: Optional[ChartMetadata] = None
def analyze(self) -> List[Finding]:
"""Run full chart analysis."""
self._check_structure()
self._check_chart_yaml()
self._check_templates()
self._check_dependencies()
self._check_documentation()
return self.findings
def _check_structure(self):
"""Check required files and directories exist."""
for req_file in self.REQUIRED_FILES:
if not (self.chart_path / req_file).exists():
self.findings.append(Finding(
severity="critical",
category="structure",
message=f"Required file missing: {req_file}",
recommendation=f"Create {req_file} in the chart root directory.",
))
for req_dir in self.REQUIRED_DIRS:
if not (self.chart_path / req_dir).is_dir():
self.findings.append(Finding(
severity="critical",
category="structure",
message=f"Required directory missing: {req_dir}/",
recommendation=f"Create {req_dir}/ directory with template files.",
))
for rec_file in self.RECOMMENDED_FILES:
if not (self.chart_path / rec_file).exists():
self.findings.append(Finding(
severity="info",
category="structure",
message=f"Recommended file missing: {rec_file}",
recommendation=f"Add {rec_file} for better chart usability.",
))
# Check for Chart.lock if dependencies exist
if (self.chart_path / "Chart.yaml").exists():
chart_content = (self.chart_path / "Chart.yaml").read_text()
if "dependencies:" in chart_content and not (self.chart_path / "Chart.lock").exists():
self.findings.append(Finding(
severity="warning",
category="dependencies",
message="Chart has dependencies but no Chart.lock file.",
recommendation="Run 'helm dependency update' to generate Chart.lock.",
))
def _check_chart_yaml(self):
"""Validate Chart.yaml metadata."""
chart_file = self.chart_path / "Chart.yaml"
if not chart_file.exists():
return
content = chart_file.read_text()
parser = SimpleYamlParser()
data = parser.parse(content)
# Check required fields
for field_name in self.CHART_REQUIRED_FIELDS:
if field_name not in data or not data[field_name]:
self.findings.append(Finding(
severity="critical",
category="metadata",
message=f"Required field missing in Chart.yaml: {field_name}",
recommendation=f"Add '{field_name}' to Chart.yaml.",
))
# Check apiVersion
api_version = data.get("apiVersion", "")
if api_version and api_version != "v2":
self.findings.append(Finding(
severity="warning",
category="metadata",
message=f"Chart uses apiVersion '{api_version}'. Helm 3 expects 'v2'.",
recommendation="Set apiVersion to 'v2' for Helm 3 compatibility.",
))
# Check version format
version = data.get("version", "")
if version and not self.SEMVER_PATTERN.match(version):
self.findings.append(Finding(
severity="warning",
category="metadata",
message=f"Chart version '{version}' is not valid SemVer.",
recommendation="Use semantic versioning format: MAJOR.MINOR.PATCH",
))
# Check description
if not data.get("description"):
self.findings.append(Finding(
severity="info",
category="metadata",
message="Chart.yaml missing 'description' field.",
recommendation="Add a brief description of the chart's purpose.",
))
# Check appVersion
if not data.get("appVersion"):
self.findings.append(Finding(
severity="info",
category="metadata",
message="Chart.yaml missing 'appVersion' field.",
recommendation="Add appVersion to track the application version deployed.",
))
# Store metadata
deps = data.get("dependencies", [])
if not isinstance(deps, list):
deps = []
self.metadata = ChartMetadata(
api_version=data.get("apiVersion", ""),
name=data.get("name", ""),
version=data.get("version", ""),
app_version=data.get("appVersion", ""),
description=data.get("description", ""),
type=data.get("type", "application"),
dependencies=[d for d in deps if isinstance(d, dict)],
)
def _check_templates(self):
"""Inspect template files for common issues."""
templates_dir = self.chart_path / "templates"
if not templates_dir.is_dir():
return
template_files = list(templates_dir.glob("*.yaml")) + list(templates_dir.glob("*.yml"))
tpl_files = list(templates_dir.glob("*.tpl"))
if not template_files and not tpl_files:
self.findings.append(Finding(
severity="warning",
category="templates",
message="No template files found in templates/ directory.",
recommendation="Add Kubernetes manifest templates (deployment.yaml, service.yaml, etc.).",
))
return
for tpl in template_files:
content = tpl.read_text()
# Check for hardcoded namespace
if re.search(r'namespace:\s*["\']?\w+["\']?\s*$', content, re.MULTILINE):
if "{{ " not in content.split("namespace:")[0].split("\n")[-1]:
nearby = [l for l in content.split("\n") if "namespace:" in l]
for ns_line in nearby:
if "{{" not in ns_line:
self.findings.append(Finding(
severity="warning",
category="templates",
message=f"Hardcoded namespace in {tpl.name}.",
recommendation="Use {{ .Release.Namespace }} for namespace references.",
))
break
# Check for hardcoded image tags
if re.search(r'image:\s*["\']?[\w/.-]+:\w+', content):
img_lines = [l for l in content.split("\n") if "image:" in l]
for img_line in img_lines:
if "{{" not in img_line:
self.findings.append(Finding(
severity="warning",
category="templates",
message=f"Hardcoded image reference in {tpl.name}.",
recommendation="Use templated image: {{ .Values.image.repository }}:{{ .Values.image.tag }}",
))
break
def _check_dependencies(self):
"""Check subchart dependency configurations."""
if not self.metadata or not self.metadata.dependencies:
return
for dep in self.metadata.dependencies:
name = dep.get("name", "unknown")
version = dep.get("version", "")
if not version:
self.findings.append(Finding(
severity="warning",
category="dependencies",
message=f"Dependency '{name}' has no version constraint.",
recommendation=f"Pin dependency '{name}' to a version range.",
))
elif version == "*":
self.findings.append(Finding(
severity="warning",
category="dependencies",
message=f"Dependency '{name}' uses wildcard version '*'.",
recommendation=f"Pin to a specific version range like '~1.2.0' or '>=1.0.0 <2.0.0'.",
))
if not dep.get("repository"):
self.findings.append(Finding(
severity="warning",
category="dependencies",
message=f"Dependency '{name}' has no repository specified.",
recommendation="Add repository URL for the dependency.",
))
def _check_documentation(self):
"""Check for chart documentation."""
readme = self.chart_path / "README.md"
if not readme.exists():
self.findings.append(Finding(
severity="info",
category="documentation",
message="No README.md found in chart directory.",
recommendation="Add README.md documenting chart usage, values, and examples.",
))
def format_text(findings: List[Finding], chart_path: str, metadata: Optional[ChartMetadata]) -> str:
"""Format as human-readable text."""
lines = []
lines.append("=" * 60)
lines.append("HELM CHART ANALYSIS REPORT")
lines.append("=" * 60)
lines.append(f"\nChart: {chart_path}")
if metadata:
lines.append(f" Name: {metadata.name}")
lines.append(f" Version: {metadata.version}")
lines.append(f" App Version: {metadata.app_version}")
lines.append(f" Dependencies: {len(metadata.dependencies)}")
critical = [f for f in findings if f.severity == "critical"]
warnings = [f for f in findings if f.severity == "warning"]
info = [f for f in findings if f.severity == "info"]
lines.append(f"\nFindings: {len(critical)} critical, {len(warnings)} warnings, {len(info)} info")
lines.append("-" * 60)
for severity, group in [("CRITICAL", critical), ("WARNING", warnings), ("INFO", info)]:
if not group:
continue
lines.append(f"\n[{severity}]")
for f in group:
lines.append(f" [{f.category}] {f.message}")
lines.append(f" Fix: {f.recommendation}")
lines.append("")
if not findings:
lines.append("\nNo issues found. Chart follows best practices.")
lines.append("=" * 60)
return "\n".join(lines)
def format_json(findings: List[Finding], chart_path: str, metadata: Optional[ChartMetadata]) -> str:
"""Format as JSON."""
return json.dumps({
"chart_path": chart_path,
"metadata": asdict(metadata) if metadata else None,
"findings": [asdict(f) for f in findings],
"summary": {
"total": len(findings),
"critical": sum(1 for f in findings if f.severity == "critical"),
"warnings": sum(1 for f in findings if f.severity == "warning"),
"info": sum(1 for f in findings if f.severity == "info"),
}
}, indent=2)
def main():
parser = argparse.ArgumentParser(
description="Analyze Helm chart structure, metadata, and templates."
)
parser.add_argument("--path", "-p", required=True, help="Path to Helm chart directory")
parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format")
parser.add_argument("--strict", action="store_true", help="Treat warnings as errors")
args = parser.parse_args()
chart_path = Path(args.path)
if not chart_path.is_dir():
print(f"Error: Not a directory: {args.path}", file=sys.stderr)
sys.exit(2)
analyzer = ChartAnalyzer(chart_path, strict=args.strict)
findings = analyzer.analyze()
if args.format == "json":
print(format_json(findings, str(chart_path), analyzer.metadata))
else:
print(format_text(findings, str(chart_path), analyzer.metadata))
has_critical = any(f.severity == "critical" for f in findings)
has_warning = any(f.severity == "warning" for f in findings)
if has_critical or (args.strict and has_warning):
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Helm Values Validator - Validate values.yaml against chart requirements.
Checks for missing resource limits, security contexts, image tag best practices,
and other Kubernetes configuration requirements.
Author: Claude Skills Engineering Team
License: MIT
"""
import argparse
import json
import re
import sys
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import List, Dict, Any, Optional
@dataclass
class Finding:
"""A validation finding."""
severity: str
category: str
path: str # YAML path like "resources.limits.memory"
message: str
recommendation: str
class ValuesParser:
"""Parse values.yaml with stdlib only."""
def parse(self, content: str) -> Dict[str, Any]:
"""Parse YAML-like content into nested dict."""
result: Dict[str, Any] = {}
stack: List[tuple] = [] # (indent, dict_ref)
current = result
for line in content.split("\n"):
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
indent = len(line) - len(line.lstrip())
# Pop stack to find parent at this indent level
while stack and stack[-1][0] >= indent:
stack.pop()
if stack:
current = stack[-1][1]
else:
current = result
# Handle list items
if stripped.startswith("- "):
continue # Skip list items for this validation
if ":" in stripped:
key, _, value = stripped.partition(":")
key = key.strip()
value = value.strip()
if value:
# Strip quotes
value = value.strip('"').strip("'")
# Try to parse booleans and numbers
if value.lower() == "true":
current[key] = True
elif value.lower() == "false":
current[key] = False
elif value.lower() in ("null", "~"):
current[key] = None
else:
try:
current[key] = int(value)
except ValueError:
try:
current[key] = float(value)
except ValueError:
current[key] = value
else:
# Nested dict
current[key] = {}
stack.append((indent, current))
current = current[key]
stack.append((indent + 2, current))
return result
def get_nested(self, data: Dict, path: str, default=None):
"""Get a nested value by dot-separated path."""
keys = path.split(".")
current = data
for key in keys:
if isinstance(current, dict) and key in current:
current = current[key]
else:
return default
return current
class ValuesValidator:
"""Validates Helm chart values files."""
def __init__(self, values: Dict[str, Any], values_file: str):
self.values = values
self.values_file = values_file
self.findings: List[Finding] = []
self.parser = ValuesParser()
def validate(self) -> List[Finding]:
"""Run all validation checks."""
self._check_resources()
self._check_security_context()
self._check_image()
self._check_replicas()
self._check_service()
self._check_ingress()
self._check_probes()
self._check_autoscaling()
return self.findings
def _get(self, path: str, default=None):
"""Helper to get nested values."""
return self.parser.get_nested(self.values, path, default)
def _check_resources(self):
"""Check for resource limits and requests."""
resources = self._get("resources", {})
if not resources or not isinstance(resources, dict):
self.findings.append(Finding(
severity="warning",
category="resources",
path="resources",
message="No resource limits or requests defined.",
recommendation="Add resources.limits and resources.requests for CPU and memory.",
))
return
limits = resources.get("limits", {})
requests = resources.get("requests", {})
if not limits or not isinstance(limits, dict):
self.findings.append(Finding(
severity="warning",
category="resources",
path="resources.limits",
message="No resource limits defined.",
recommendation="Add resources.limits.cpu and resources.limits.memory.",
))
else:
if "cpu" not in limits:
self.findings.append(Finding(
severity="warning",
category="resources",
path="resources.limits.cpu",
message="CPU limit not set.",
recommendation="Set resources.limits.cpu (e.g., '500m').",
))
if "memory" not in limits:
self.findings.append(Finding(
severity="warning",
category="resources",
path="resources.limits.memory",
message="Memory limit not set.",
recommendation="Set resources.limits.memory (e.g., '256Mi').",
))
if not requests or not isinstance(requests, dict):
self.findings.append(Finding(
severity="info",
category="resources",
path="resources.requests",
message="No resource requests defined.",
recommendation="Add resources.requests for proper scheduling.",
))
def _check_security_context(self):
"""Check security context settings."""
sec_ctx = self._get("securityContext", {})
pod_sec = self._get("podSecurityContext", {})
if not sec_ctx and not pod_sec:
self.findings.append(Finding(
severity="warning",
category="security",
path="securityContext",
message="No security context defined.",
recommendation="Add securityContext with runAsNonRoot, readOnlyRootFilesystem, allowPrivilegeEscalation.",
))
return
if isinstance(sec_ctx, dict):
if sec_ctx.get("runAsNonRoot") is not True:
self.findings.append(Finding(
severity="warning",
category="security",
path="securityContext.runAsNonRoot",
message="runAsNonRoot is not set to true.",
recommendation="Set securityContext.runAsNonRoot: true.",
))
if sec_ctx.get("readOnlyRootFilesystem") is not True:
self.findings.append(Finding(
severity="info",
category="security",
path="securityContext.readOnlyRootFilesystem",
message="readOnlyRootFilesystem is not enabled.",
recommendation="Set securityContext.readOnlyRootFilesystem: true where possible.",
))
if sec_ctx.get("allowPrivilegeEscalation") is not False:
self.findings.append(Finding(
severity="warning",
category="security",
path="securityContext.allowPrivilegeEscalation",
message="allowPrivilegeEscalation is not explicitly set to false.",
recommendation="Set securityContext.allowPrivilegeEscalation: false.",
))
def _check_image(self):
"""Check image configuration."""
image = self._get("image", {})
if not image or not isinstance(image, dict):
return
tag = image.get("tag", "")
if not tag:
self.findings.append(Finding(
severity="warning",
category="image",
path="image.tag",
message="Image tag is empty or not set.",
recommendation="Set image.tag to a specific version (not 'latest').",
))
elif str(tag).lower() == "latest":
self.findings.append(Finding(
severity="warning",
category="image",
path="image.tag",
message="Image tag is 'latest', which is non-deterministic.",
recommendation="Pin image.tag to a specific version for reproducible deployments.",
))
pull_policy = image.get("pullPolicy", "")
if pull_policy == "Always" and tag and str(tag).lower() != "latest":
self.findings.append(Finding(
severity="info",
category="image",
path="image.pullPolicy",
message="pullPolicy is 'Always' with a pinned tag.",
recommendation="Consider 'IfNotPresent' for pinned tags to reduce pull overhead.",
))
def _check_replicas(self):
"""Check replica count."""
replicas = self._get("replicaCount")
autoscaling = self._get("autoscaling", {})
if isinstance(autoscaling, dict) and autoscaling.get("enabled") is True:
return # Autoscaling handles replicas
if replicas is not None and isinstance(replicas, (int, float)):
if replicas < 2:
self.findings.append(Finding(
severity="info",
category="availability",
path="replicaCount",
message=f"replicaCount is {replicas}. Single replica has no redundancy.",
recommendation="Set replicaCount >= 2 for production environments.",
))
def _check_service(self):
"""Check service configuration."""
service = self._get("service", {})
if not service or not isinstance(service, dict):
return
svc_type = service.get("type", "ClusterIP")
if svc_type == "NodePort":
self.findings.append(Finding(
severity="info",
category="networking",
path="service.type",
message="Service type is NodePort.",
recommendation="Consider ClusterIP with Ingress for production. NodePort exposes ports on all nodes.",
))
elif svc_type == "LoadBalancer":
self.findings.append(Finding(
severity="info",
category="networking",
path="service.type",
message="Service type is LoadBalancer (creates cloud LB per service).",
recommendation="Consider using Ingress to consolidate multiple services behind one LB.",
))
def _check_ingress(self):
"""Check ingress configuration."""
ingress = self._get("ingress", {})
if not ingress or not isinstance(ingress, dict):
return
if ingress.get("enabled") is not True:
return
if not ingress.get("tls"):
self.findings.append(Finding(
severity="warning",
category="networking",
path="ingress.tls",
message="Ingress is enabled but TLS is not configured.",
recommendation="Configure ingress.tls for encrypted traffic.",
))
if not ingress.get("className") and not ingress.get("ingressClassName"):
self.findings.append(Finding(
severity="info",
category="networking",
path="ingress.className",
message="No ingress class specified.",
recommendation="Set ingress.className to specify the ingress controller.",
))
def _check_probes(self):
"""Check liveness and readiness probes."""
liveness = self._get("livenessProbe", {})
readiness = self._get("readinessProbe", {})
if not liveness:
self.findings.append(Finding(
severity="info",
category="reliability",
path="livenessProbe",
message="No liveness probe configured.",
recommendation="Add livenessProbe to enable automatic restart on failure.",
))
if not readiness:
self.findings.append(Finding(
severity="info",
category="reliability",
path="readinessProbe",
message="No readiness probe configured.",
recommendation="Add readinessProbe to prevent traffic to unready pods.",
))
def _check_autoscaling(self):
"""Check autoscaling configuration."""
autoscaling = self._get("autoscaling", {})
if not isinstance(autoscaling, dict) or autoscaling.get("enabled") is not True:
return
if not autoscaling.get("minReplicas"):
self.findings.append(Finding(
severity="info",
category="scaling",
path="autoscaling.minReplicas",
message="Autoscaling minReplicas not set.",
recommendation="Set minReplicas >= 2 for production availability.",
))
def format_text(findings: List[Finding], values_file: str) -> str:
"""Format as human-readable text."""
lines = []
lines.append("=" * 60)
lines.append("HELM VALUES VALIDATION REPORT")
lines.append("=" * 60)
lines.append(f"\nValues file: {values_file}")
critical = [f for f in findings if f.severity == "critical"]
warnings = [f for f in findings if f.severity == "warning"]
info = [f for f in findings if f.severity == "info"]
lines.append(f"Findings: {len(critical)} critical, {len(warnings)} warnings, {len(info)} info")
lines.append("-" * 60)
for severity, group in [("CRITICAL", critical), ("WARNING", warnings), ("INFO", info)]:
if not group:
continue
lines.append(f"\n[{severity}]")
for f in group:
lines.append(f" [{f.category}] {f.path}: {f.message}")
lines.append(f" Fix: {f.recommendation}")
lines.append("")
if not findings:
lines.append("\nNo issues found. Values follow best practices.")
lines.append("=" * 60)
return "\n".join(lines)
def format_json(findings: List[Finding], values_file: str) -> str:
"""Format as JSON."""
return json.dumps({
"values_file": values_file,
"findings": [asdict(f) for f in findings],
"summary": {
"total": len(findings),
"critical": sum(1 for f in findings if f.severity == "critical"),
"warnings": sum(1 for f in findings if f.severity == "warning"),
"info": sum(1 for f in findings if f.severity == "info"),
}
}, indent=2)
def main():
parser = argparse.ArgumentParser(
description="Validate Helm values files against best practices."
)
parser.add_argument("--chart", "-c", help="Path to Helm chart directory (for context)")
parser.add_argument("--values", "-v", nargs="+", required=True, help="Path(s) to values file(s)")
parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format")
args = parser.parse_args()
exit_code = 0
all_results = []
for values_file in args.values:
path = Path(values_file)
if not path.exists():
# Try relative to chart directory
if args.chart:
path = Path(args.chart) / values_file
if not path.exists():
print(f"Error: Values file not found: {values_file}", file=sys.stderr)
exit_code = 2
continue
content = path.read_text()
vp = ValuesParser()
values = vp.parse(content)
validator = ValuesValidator(values, str(path))
findings = validator.validate()
if args.format == "json":
all_results.append({
"file": str(path),
"findings": [asdict(f) for f in findings],
})
else:
print(format_text(findings, str(path)))
if any(f.severity == "critical" for f in findings):
exit_code = 1
if args.format == "json":
print(json.dumps({"results": all_results}, indent=2))
sys.exit(exit_code)
if __name__ == "__main__":
main()
Related skills
FAQ
What does values_validator.py check?
CPU and memory resource limits and requests, runAsNonRoot and readOnlyRootFilesystem security context, replica counts, image tags, and ingress TLS.
Can it run in CI?
Yes; chart_analyzer.py supports --format json and a strict mode, and the values validator loops over dev, staging, and production values files.