One example of a charm with configuration too complex to be represented in a structured way
config.yaml, but where a straight base64-encoded config doesn’t quite work either, is cs:haproxy.
The charm makes heavy, invaluable use of relations, but it was clearly designed to infer most of its configuration from those relations: relating a
website creates both frontend and backend stanzas in
haproxy.cfg, with server details and options coming from the remote end of the relation. But the backend services are the least interesting part of configuring haproxy; having primarily relation-oriented configuration is backwards. This means you end up with services’ general
http interfaces emitting haproxy-specific config options, and custom request routing (which is necessary in any non-trivial haproxy website or webservice deployment) is awkward and often requires modifying backend services to shovel the right deployment-specific data over the relation, when all that really should be coming from the other side is a set of ip:port pairs (plus maybe a concurrent request limit, or other unit-specific things like that).
Over the years the haproxy charm has grown the
services option, which allows new stanzas to be defined and options overridden with almost literal haproxy config syntax. But it continues to try to simplify the configuration language: each service entry still creates frontend and backend stanzas, and there’s a hardcoded map of which option goes into which stanza, neither of which is what you always want and which require gross hacks to do what you actually need. In the Snap Store deployment we did a similar thing to cs:squid-reverseproxy except took it one step further to allow disabling implicit generation of stanzas from relations and even rename passed-through relations, for when we had a big multi-service haproxy but only wanted squid to reexport a subset of its services.
Charms often try to be opinionated and avoid exposing users to the complication of the underlying software. But the software didn’t grow that complication for no reason, so it’s frustrating that when I step outside the bounds of what the charm author expected I have to add a new config option, which inevitably just writes the config option out verbatim to the relevant bit of the configuration file. We’ve also seen this in charms that don’t make as much use of relations, e.g. cs:postgresql where dozens of deployment-specific tuning options like
default_statistics_target are deprecated in favour of the literal
extra_pg_conf option, and the charm’s both cheaper to maintain and more useful for that change.
I think what I really want from the haproxy charm is a templated literal config. A relation shouldn’t magically expose a port, but it should show up in the template variables so I can easily generate a
server line for each remote unit. I’ve wondered in the past whether user-configurable options on relations might help, but they don’t solve e.g. the problem in haproxy of having a single frontend routing to a bunch of backends (there’s no Juju object that has a one-to-one mapping with frontend stanzas). So I think exposing the full power of the underlying configuration language is the only sensible option.
This obviously prevents a simple
juju deploy wordpress,
juju deploy haproxy,
juju add-relation wordpress haproxy from doing anything useful, but the community seems to be leaning more and more towards bundles for that sort of thing anyway. The bundle would then just have to include a small haproxy config template to expose the units from the relation on some port. This is another case that shows how charms probably aren’t the right place for high-level logic or policy decisions, and they belong at another level like a bundle or meta-operator.