Skip to content

Backend CRD http_request_rule_list changes don't trigger HAProxy reload #805

@iamdempa

Description

@iamdempa

Backend CRD http_request_rule_list changes don't trigger HAProxy reload

Environment

Component Version
HAProxy Ingress Controller 3.2.6
Helm Chart kubernetes-ingress-1.49.0
HAProxy 3.2.12
Backend CRD API ingress.v3.haproxy.org/v3
Kubernetes 1.30 (AKS)

Description

When modifying only the http_request_rule_list field in a Backend CRD (v3), HAProxy does not reload with the new rules. The rules are written to the controller's in-memory state but never committed to the HAProxy config parser transaction, so haproxy.cfg remains unchanged.

Steps to Reproduce

  1. Create a Backend CRD with an http_request_rule_list:
apiVersion: ingress.v3.haproxy.org/v3
kind: Backend
metadata:
  name: test-backend
  namespace: default
spec:
  name: test-backend
  server_timeout: 30000
  connect_timeout: 5000
  http_request_rule_list:
  - cond: if
    cond_test: '{ req.hdr(X-Block-Me) -m found }'
    deny_status: 403
    type: deny
  1. Create an Ingress referencing this Backend CRD via haproxy.org/cr-backend annotation

  2. Verify the rule appears in haproxy.cfg:

kubectl exec -n <controller-ns> <haproxy-pod> -- grep "X-Block-Me" /etc/haproxy/haproxy.cfg
  1. Modify the Backend CRD http_request_rule_list (e.g., change X-Block-Me to X-Block-Test)

  2. Check HAProxy reload count:

kubectl exec -n <controller-ns> <haproxy-pod> -- sh -c "echo 'show proc' | socat stdio /var/run/haproxy-master.sock"

Expected: Reload count increases, haproxy.cfg contains new rule
Actual: Reload count unchanged, haproxy.cfg still has old rule

Root Cause Analysis

The bug is in pkg/haproxy/api/httprequest.go. The HTTPRequestRulesReplace function for parentType == "backend" only updates the in-memory c.backends map and returns without writing to the config parser transaction:

// pkg/haproxy/api/httprequest.go lines 116-125
func (c *clientNative) HTTPRequestRulesReplace(parentType, parentName string, rules models.HTTPRequestRules) error {
    configuration, _ := c.nativeAPI.Configuration()

    if parentType == "backend" {
        backend, _ := c.backends[parentName]
        backend.HTTPRequestRuleList = rules
        c.backends[parentName] = backend
        return nil   // <-- Returns here, never calls configuration.ReplaceHTTPRequestRules()
    }
    // ... frontend handling ...
    return configuration.ReplaceHTTPRequestRules(parentType, parentName, rules, c.activeTransaction, 0)
}

The flow:

  1. httprequests.Populate() detects rule diff via rules.Diff(currentHTTPRequests)
  2. Calls HTTPRequestRulesReplace("backend", name, rules) — updates only c.backends (in-memory)
  3. Calls instance.Reload() — sets reload flag
  4. But config parser transaction is unchanged → hash unchanged → no commit → haproxy.cfg unchanged
  5. Next cycle: HTTPRequestRulesGet reads from c.backends (now updated) → no diff found → permanently stuck

Workaround

When changing http_request_rule_list, also change a BackendBase field like server_timeout by 1ms:

spec:
  server_timeout: 30001  # was 30000
  http_request_rule_list:
  - ...new rules...

This triggers BackendBase.Diff() → proper reload → new rules take effect.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions