Troubleshooting¶
Common issues and solutions for HAPTIC.
Namespace
All kubectl commands below assume the default installation namespace haptic. Replace -n haptic with your namespace if you installed elsewhere.
Quick Symptom Reference¶
| Symptom | Section |
|---|---|
| Pod in CrashLoopBackOff | Controller Not Starting |
| Pods running, no reconciliation activity | Controller Running But Not Processing |
| "template rendering failed" in logs | Invalid Template Syntax |
| "validation failed" / HAProxy errors | Configuration Validation Failures |
| "connection refused" to Dataplane API | Cannot Connect to Dataplane API |
| Controller reports success but HAProxy unchanged | Configuration Not Updating |
| 503 errors / no servers in HAProxy stats | Requests Not Reaching Backend |
| SSL handshake failures | SSL/TLS Issues |
| High CPU or slow reconciliation | Slow Reconciliation |
| OOMKilled / gradual memory growth | High Memory Usage |
| "shm-stats-file-max-objects" / reload failures | Shared Memory Stats Limit |
Controller Issues¶
Controller Not Starting¶
Symptoms: CrashLoopBackOff, repeated restarts, initialization errors
Diagnosis:
kubectl get pods -n haptic -l app.kubernetes.io/name=haptic,app.kubernetes.io/component=controller
kubectl logs -n haptic -l app.kubernetes.io/name=haptic,app.kubernetes.io/component=controller --tail=100
kubectl describe pod -n haptic -l app.kubernetes.io/name=haptic,app.kubernetes.io/component=controller
Common Causes:
| Cause | Check | Solution |
|---|---|---|
| Missing HAProxyTemplateConfig | kubectl get haproxytemplateconfig |
Reinstall Helm chart |
| Invalid credentials Secret | kubectl get secret -n haptic haptic-credentials -o jsonpath='{.data}' (Helm names it <release>-credentials) |
Recreate secret with correct keys |
| RBAC permissions | kubectl auth can-i list ingresses --all-namespaces --as=system:serviceaccount:<ns>:<sa> |
Verify ClusterRole/ClusterRoleBinding |
Controller Running But Not Processing¶
Symptoms: Pods running, no reconciliation activity
Diagnosis:
kubectl logs -n haptic -l app.kubernetes.io/name=haptic,app.kubernetes.io/component=controller | grep -i "watch\|sync complete"
Common Causes:
| Cause | Check | Solution |
|---|---|---|
| Informers not syncing | Logs show "timeout waiting for cache sync" | Check API server connectivity, network policies |
| No matching resources | kubectl get ingresses -A |
Verify resources exist in watched namespaces |
| Ingress class mismatch | kubectl get ingress <name> -o jsonpath='{.spec.ingressClassName}' |
The Ingress must reference the class the chart created; also check any watchedResources.*.fieldSelector namespace filter |
| Leader election (HA) | kubectl get lease -n haptic (the Lease is named after the Helm release) |
Ensure one pod shows is_leader=1 |
Configuration Issues¶
Invalid Template Syntax¶
Symptoms: "template rendering failed" errors
Diagnosis:
kubectl logs -n haptic -l app.kubernetes.io/name=haptic,app.kubernetes.io/component=controller | grep -i "template\|render"
Solution:
- Check template syntax in HAProxyTemplateConfig
-
Inspect the last rendered output via the debug server — port-forward first (see Debugging Guide):
-
See Templating Guide
Configuration Validation Failures¶
Symptoms: "validation failed", HAProxy errors
Common Errors:
| Error | Cause | Solution |
|---|---|---|
backend expects <name> |
Invalid HAProxy syntax | Fix template, test with haproxy -c -f config.cfg |
unable to load file |
Missing map/cert file | Define in maps section, use pathResolver.GetPath() |
invalid address |
Bad server address | Verify EndpointSlices exist, check service names |
Validation Test Failures¶
Symptoms: haptic-controller validate fails
Quick Debugging:
# Step 1: Run with verbose output
haptic-controller validate -f config.yaml --verbose
# Step 2: See full rendered content
haptic-controller validate -f config.yaml --dump-rendered
# Step 3: Check template execution
haptic-controller validate -f config.yaml --trace-templates
See Validation Tests for detailed debugging.
HAProxy Pod Issues¶
Cannot Connect to Dataplane API¶
Symptoms: "connection refused", "timeout", deployment failures
Diagnosis:
HAPROXY_POD=$(kubectl get pods -n haptic -l app.kubernetes.io/component=loadbalancer -o jsonpath='{.items[0].metadata.name}')
kubectl port-forward -n haptic $HAPROXY_POD 5555:5555
# Substitute your actual dataplane password; see spec.credentialsSecretRef
curl -u admin:<password> http://localhost:5555/v3/info
Common Causes:
| Cause | Check | Solution |
|---|---|---|
| Dataplane not running | kubectl logs $HAPROXY_POD -c dataplane |
Verify container started, check port conflicts |
| Wrong credentials | Compare secret vs dataplaneapi.yaml | Update the credentials Secret — credentialsloader picks it up live; also rotate the matching dataplaneapi.yaml on the HAProxy sidecar |
| Network policy | kubectl get networkpolicy |
Update egress rules for controller → HAProxy |
Configuration Not Updating¶
Symptoms: Controller shows success but HAProxy has old config
Diagnosis:
HAPROXY_POD=$(kubectl get pods -n haptic -l app.kubernetes.io/component=loadbalancer -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n haptic $HAPROXY_POD -c haproxy -- ls -lh /etc/haproxy/haproxy.cfg
kubectl logs -n haptic -l app.kubernetes.io/name=haptic,app.kubernetes.io/component=controller | grep -i "deployment.*succeeded"
Common Causes:
| Cause | Check | Solution |
|---|---|---|
| Volume mount issue | kubectl get pod $HAPROXY_POD -o yaml \| grep -A5 volumeMounts |
Ensure both containers share config volume |
| HAProxy not reloading | kubectl logs $HAPROXY_POD -c dataplane |
Check reload command, master socket access |
Shared Memory Stats Limit¶
Opt-in feature
This only applies when haproxy.shmStats.enabled: true is set in Helm values (the default is false) and HAProxy is 3.3+ — the shm-stats file is gated by semver_gte in the chart templates. If you're on the default config you will not see these errors; this section is for operators who turned shm-stats on for performance.
Symptoms: 100% deployment error rate, HAProxy reload failures with shm-stats-file-max-objects errors
Diagnosis:
kubectl logs -n haptic -l app.kubernetes.io/name=haptic,app.kubernetes.io/component=controller | grep "shm-stats"
Look for:
[ALERT] memory error while setting up shared counters for .../SRV_N server:
Cannot add additional object to '/dev/shm/haproxy-stats' file,
maximum number already reached (50000).
Common Causes:
| Cause | Check | Solution |
|---|---|---|
| Too many HAProxy objects for the configured limit | Count ingresses/services: kubectl get ingresses -A --no-headers \| wc -l |
Increase haproxy.shmStats.maxObjects in Helm values |
| Cluster grew beyond initial sizing | Compare object count to maxObjects value |
Recalculate using the formula below |
Solution:
Each HAProxy frontend, backend, and server directive counts as one shm-stats object. The file is fixed-size and cannot be resized on reload. Increase haproxy.shmStats.maxObjects in your Helm values:
Sizing formula: (number of backends + number of servers) × 1.2 safety margin. Each object uses ~4KiB of shared memory. For example, 100,000 objects require ~390Mi in /dev/shm, which counts against the pod's memory limit.
Warning
After changing maxObjects, verify that haproxy.resources.limits.memory is large enough to accommodate the increased /dev/shm usage. The shm volume is memory-backed and counts against the pod's memory limit.
Routing Issues¶
Requests Not Reaching Backend¶
Symptoms: 503 errors, timeouts, no servers in HAProxy stats
Diagnosis:
HAPROXY_POD=$(kubectl get pods -n haptic -l app.kubernetes.io/component=loadbalancer -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n haptic $HAPROXY_POD -c haproxy -- cat /etc/haproxy/haproxy.cfg | grep -A10 "backend"
kubectl get endpointslices -l kubernetes.io/service-name=<service>
Common Causes:
| Cause | Check | Solution |
|---|---|---|
| No endpoints | kubectl get endpointslices |
Verify backend pods running and ready |
| Backend not created | Controller logs for backend errors | Review template logic, check Ingress references |
| Routing not matching | Test with curl -H "Host: ..." |
Verify Host header, check ACLs and map files |
SSL/TLS Issues¶
Symptoms: SSL handshake failures, certificate errors
Diagnosis:
HAPROXY_POD=$(kubectl get pods -n haptic -l app.kubernetes.io/component=loadbalancer -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n haptic $HAPROXY_POD -c haproxy -- ls -lh /etc/haproxy/ssl/
# Port-forward HAProxy's HTTPS port, then probe the handshake.
# Stop the forward with `kill %1` (or Ctrl+C) when done.
kubectl port-forward -n haptic $HAPROXY_POD 443:443 &
openssl s_client -connect localhost:443 -servername your-host.example.com < /dev/null
Common Causes:
| Cause | Check | Solution |
|---|---|---|
| Certificate not deployed | Check sslCertificates section |
Define template, watch secret, use b64decode |
| Wrong cert path | grep "bind.*ssl.*crt" haproxy.cfg |
Use pathResolver.GetPath("cert.pem", "cert") |
Performance Issues¶
Slow Reconciliation¶
Symptoms: Changes take minutes, high CPU
Diagnosis:
kubectl port-forward -n haptic deployment/haptic-controller 9090:9090
curl http://localhost:9090/metrics | grep reconciliation_duration_seconds
Solutions:
- Use namespace restrictions in
watchedResources - Add label selectors to filter resources
- Use cached store for large resources
- Optimize templates: cache values with
{% var %}, reduce nested loops
High Memory Usage¶
Symptoms: OOMKilled events, gradual memory growth
Solutions:
# Filter large fields
watchedResourcesIgnoreFields:
- metadata.managedFields
- metadata.annotations['kubectl.kubernetes.io/last-applied-configuration']
# Use cached store for secrets (fetches on-demand; TTL is auto-derived
# from driftPreventionInterval, not user-configurable)
watchedResources:
secrets:
store: on-demand
# Limit watch scope
watchedResources:
ingresses:
namespace: production
labelSelector: "app=myapp"
Getting Help¶
Collect Diagnostic Information¶
# Controller version
kubectl get deployment -n haptic haptic-controller -o jsonpath='{.spec.template.spec.containers[0].image}'
# Controller logs
kubectl logs -n haptic -l app.kubernetes.io/name=haptic,app.kubernetes.io/component=controller --tail=500 > controller-logs.txt
# Configuration
kubectl get haproxytemplateconfig -n haptic haptic-config -o yaml > config.yaml
# HAProxy config (sanitize sensitive data!)
kubectl exec -n haptic $HAPROXY_POD -c haproxy -- cat /etc/haproxy/haproxy.cfg > haproxy.cfg
Enable Debug Logging¶
The controller supports multiple log levels via the LOG_LEVEL environment variable (case-insensitive):
| Level | Description |
|---|---|
| ERROR | Errors only |
| WARN (or WARNING) | Warnings and errors |
| INFO | Important state changes (default) |
| DEBUG | Detailed debugging information |
| TRACE | Very verbose, per-item iteration logs |
# Enable debug logging
kubectl set env -n haptic deployment/haptic-controller LOG_LEVEL=DEBUG
# Enable trace logging (very verbose)
kubectl set env -n haptic deployment/haptic-controller LOG_LEVEL=TRACE
The log level can also be configured via the HAProxyTemplateConfig CRD's spec.logging.level field. When set, the CRD value takes precedence over the LOG_LEVEL environment variable, and changes take effect without a pod restart:
# In values.yaml
controller:
logLevel: INFO # Initial LOG_LEVEL env var (used until the CRD is loaded)
config:
logging:
level: DEBUG # Written to spec.logging.level — overrides env var at runtime
Note
TRACE level produces extremely verbose output, including per-resource iteration logs, HTTP fetch retries, and test runner details. Use only when debugging specific issues.
Access the Debug Server¶
The Helm chart enables the debug server on port 8080 by default (same port as /healthz). Port-forward to reach it:
/healthz and /debug/* share the same listener, so setting controller.debugPort: 0 disables both and breaks the Kubernetes liveness/readiness probes — restrict access via NetworkPolicy instead. To move both endpoints to a different port, set controller.debugPort: <port> (and update the forward accordingly).
Available endpoints:
/debug/vars— internal state (config, credentials metadata, rendered output, resources, events, uptime)/debug/vars/<name>— a single variable; supports?field={.jsonpath}for subselection/debug/pprof/— Go profiling
See the Debugging Guide for the full endpoint catalogue.