Structural diff for HAProxy configuration

(This is an English translation of a previous post)

We use HAProxy for all our incoming HTTP and other TCP connections.

Its configuration is the most complex we have. That’s why we do not write it by hand, but rather generate it from metadata and includes. Such a generator makes the configuration easier — but it also makes it harder to see what the actual changes will be in the end.

For situations like that, we like tooling that will show us a diff of the generated configuration. Because the HAProxy configuration is a single text file, we could use the diff program to compare two versions.

The downside of a textual diff is that it shows meaningless differences, for example if only order or comments change.

For example, if we compare these two files:

global
	log stdout format raw local0 info

defaults
	timeout connect 5s
	timeout server 10s
	timeout client 20s

# Example config
frontend fe_proxy
	bind 0.0.0.0:80

	default_backend be_proxy

backend be_proxy
	server localhost 127.0.0.1:8080

with

global
	log stdout format raw local0 info

defaults
	timeout client 10s
	timeout server 10s
	timeout connect 5s


backend be_proxy
	server localhost 127.0.0.1:8080


# Example config with a different comment
frontend fe_proxy
	bind 0.0.0.0:80

	default_backend be_proxy

Then diff will give us this output:

$ diff -u examples/old.cfg examples/new.cfg
--- examples/old.cfg    2024-02-19 15:53:45
+++ examples/new.cfg    2024-02-19 15:54:09
@@ -2,15 +2,17 @@
    log stdout format raw local0 info
 
 defaults
-   timeout connect 5s
+   timeout client 10s
    timeout server 10s
-   timeout client 20s
+   timeout connect 5s
 
-# Example config
+
+backend be_proxy
+   server localhost 127.0.0.1:8080
+
+
+# Example config with a different comment
 frontend fe_proxy
    bind 0.0.0.0:80
 
    default_backend be_proxy
-
-backend be_proxy
-   server localhost 127.0.0.1:8080

There is a relevant change here – but it’s lost between multiple textual changes that have no effect.

Therefore we have created a small Go program which roughly reads the configuration into data structures and then compares them:

$ go run . examples/old.cfg examples/new.cfg 
&main.config{
    "backend": {"be_proxy": {"server": {"localhost 127.0.0.1:8080"}}},
    "defaults": {
        "": {
            "timeout": {
-               "client 20s",
+               "client 10s",
                "connect 5s",
                "server 10s",
            },
        },
    },
    "frontend": {"fe_proxy": {"bind": {"0.0.0.0:80"}, "default_backend": {"be_proxy"}}},
    "global":   {"": {"log": {"stdout format raw local0 info"}}},
  }

In this output we can clearly see what actually changed. Of course this is a contrived example – usually you would do these unrelated changes in different commits. But even then the tool can be helpful; to ensure that one did not make actual changes when one just meant to rearrange.

The program can be found on GitHub. Please be advised, that it is only meant as an example – we have no plans to maintain the repository.

If you’re located in Germany, we’re hiring (up to 100% remote work from Germany).

(Header-Bild: Prtksxna, CC BY-SA 4.0 , via Wikimedia Commons)