hashicorp / consul-template

Template rendering, notifier, and supervisor for @HashiCorp Consul and Vault data.
https://www.hashicorp.com/
Mozilla Public License 2.0
4.76k stars 782 forks source link

Create a work-around to allow the setting of variables #399

Closed jdelic closed 9 years ago

jdelic commented 9 years ago

golang/go#10608 is unfortunately marked as Unplanned. This severly limits consul-templates usecases with service tags. I would love to do something like this:

{{ range services }}
    {{ $servicename := .Name }}
    {{ $port := .Port }}
    {{ $tags := .Tags }}
    {{ range $tags }}
        {{ if . | regexMatch "^smartstack:port:([0-9]+)$" }}
            {{ $parts = . | split ":" }}
            {{ $port = $parts | index 2 }}
        {{ end }}
    {{ end }}
frontend {{$servicename}}
    bind 0.0.0.0:{{$port}}
{{ end }}

Where a tag can override the service's port on the loadbalancer in my example. It would be awesome if consul-template would add something along the lines of http://gohugo.io/extras/scratch to support the above. Or is there another work-around that I don't know about?

sethvargo commented 9 years ago

Hi @jdelic

Thank you for opening an issue. I'm not sure I understand the use case here. You want to override the advertised service port based off of a tag? That seems like an anti-pattern as the Consul API should be the source of truth for which port a service is accessible.

jdelic commented 9 years ago

This is just an example for a haproxy template from the system we're working on. The use-case specifically here is that a developer can override the external port a service will be made available at, but I only used it because that's one that I had open in IntelliJ when I wrote the ticket. The service's port will still be used in the haproxy backend clause which I didn't include. Basically: A developer can deploy a service definition in a service's .deb package that will include tags which consul-template will parse and write to the loadbalancer. That way a service running on port 8443 can be routed to port 443 automatically after the .deb is deployed on an application server without further intervention by Ops.

But the larger point is this: consul-template exposes Golang templates which don't support proper variables, which makes Golang templates a suboptimal API if the template developer can't change the code calling the template. The Go developers stance is: "We're keeping the template system deliberately simple so you have to put all of the logic in the code calling the template", but obviously I don't want to deploy a patched and locally compiled consul-template version. Being able to do simple logic like:

...
{{ $include_ssl_conf := false }}
{{ range .Tags }}
    {{ if eq . "usessl" }}
        {{ $include_ssl_conf = true }}
    {{ end }}
{{ end }}

{{ if $include_ssl_conf }}
...
{{ end }}

would be immensely useful.

sethvargo commented 9 years ago

Hi @jdelic

Would the following work for you?

{{ if .Tags | contains "usessl" }}
# ...
{{ end }}

Go's templating language is intentionally not as functional as something like Ruby's ERB. At first this really bothered me as well. However, in working with the templating language and Consul Template, I have found that wanting the ability to "map-reduce" or perform complex operations in the template is an anti-pattern. It makes the template more difficult to reason about and is often an indication that there exists a problem at a higher level (like in the way the data is structured in Consul itself).

jdelic commented 9 years ago

Would the following work for you?

Ok, that solves indeed the second use-case, but not the first. I would really like to parse data out of tags or to be able to implement a default value.

Another example: We spawn a per-customer backend instance that registers with our loadbalancer using consul/consul-template using docker-registrator. If the customer paid for a custom hostname, the service definition will include a tag "host:[fqdn]", otherwise "host:customer.domain.com". It's super-easy to put that information in the environment of the docker container and basically impossible to put it on the loadbalancer. How would I read this information and use it in a template without proper variables?

jdelic commented 9 years ago

Btw, this would also allow us to find the intersection of tags in templates. Is there any way to do that from the services tag right now? (I am fairly new to consul-template, I admit, so I might be looking in the wrong place)

sethvargo commented 9 years ago

Hey @jdelic

If you are able to put that information into the Docker container's environment variables, can you use the env function to pull it out? Or am I misunderstanding what you mean?

For the use case you described, I would recommend writing a Consul Template plugin instead.

I agree that Go's templating language is incredibly limiting, but I find that to be an asset more than a hinderance.

Tags are not a first-class thing in Consul (like you cannot query by a tag), so I think they may not be the best way to store this type of data. The template would be much simpler if you mapped the service to a value in the KV store and then you could use a simple or clause for default values.

Even if we solve this particular problem, you will undoubtably hit another roadblock because of the way your data is structured. I think you'll save some headaches if you either restructure your data or write custom plugins instead :smile:.

jdelic commented 9 years ago

If you are able to put that information into the Docker container's environment variables, can you use the env function to pull it out? Or am I misunderstanding what you mean?

No, those are separate servers.

Even if we solve this particular problem, you will undoubtably hit another roadblock because of the way your data is structured. I think you'll save some headaches if you either restructure your data or write custom plugins instead :smile:.

Hmm, I don't agree. Having the ability to set a variable across two nested scopes in a template would fix all of the current problems that I'm having with consul-template :confused:.

For the use case you described, I would recommend writing a Consul Template plugin instead.

From reading the docs, I could write a plugin that given a {{ services | byTag }} map returns a JSON object accomplishing the same tasks like with my above examples. However, that seems to be impossible since I can't get services to serialize using toJSON. For example

{{ services | byTag | toJSON }}

results in

executing "test.tpl" at <toJSON>: wrong type for value; expected map[string]interface {}; got map[string][]interface {}

How would I get to the service catalog for a plug-in? (Sorry, this is now terribly off topic from the original ticket :disappointed:, but I really appreciate the help!)

sethvargo commented 9 years ago

Hi @jdelic

It looks like plugins only currently work with map[string]interface{}. Sorry about that - they are mostly designed for the KV store. Ignore that suggestion since it won't work.

Having the ability to set a variable across two nested scopes in a template would fix all of the current problems that I'm having with consul-template.

I understand your frustration there, but there is a reason Go's templating language does not expose that functionality. I really trust the Go authors and think they have the right opinion that templates should not be mutating or mapping the data they receive.

I do not have a good solution or workaround. Consul Template operates on the assumption that your data is structured in a semantic way. If you're operating outside the bounds of those assumptions, Consul Template may not be the right tool for your job. You're more than welcome to submit a PR for this "scratch" functionality, but I do not think this is something we will be implementing ourselves. The watcher and dependency libraries are also entirely open source and separate from Consul Template. You could easily build your own tool that satisfies your exact use case without duplicating a ton of logic. There are a number of community projects that leverage the packages in this repo outside the scope of Consul Template including envconsul, consul-replicate, and fsconsul :smile:.

I would be happy to revisit this if there is a serious demand, but that has not been the case so far. Thanks!

jdelic commented 9 years ago

Thanks for the discussion and the references, though, I really appreciate it. Just for the record: I think the Golang devs got that one wrong, just like Django templates and numerous other template languages before them, but we'll see :).

I guess we might use consul-template to render a Jinja template and then use the "command" portion of the -template parameter to actually render the configuration file. Again: thanks for all the help!

tzz commented 9 years ago

@sethvargo here's another example of where variables or plugins everywhere would be useful:

We have X application servers and Y data servers (HTTP protocol, idempotent). The haproxy configuration we want on each application server is:

server data_master 1.2.3.4:5000 check
server data_slave1 1.2.3.5:5000 check backup
server data_slave2 1.2.3.6:5000 check backup
...
server data_slaveY 1.2.3.9:5000 check backup

In other words, pick one data server as the stable primary but fail over to the rest if the primary data_master is down. Furthermore we'd like the data servers to be stable (so the haproxy.cfg file doesn't change constantly) and we'd like to have a stable master depending on the application server (to distribute the load). In Puppet, for instance, the fqdn_rotate() function can be applied to a sorted array to get exactly this effect.

In a external plugin this is not hard, but we apparently can't use plugins against a service call in Consul Template. So this is IMO a deficiency in Consul Template; it seems easiest to remedy it with plugins so I hope that's possible in the future.

tzz commented 9 years ago

Here's the current template code, but that doesn't do what we need. The list is not sorted or rotated per application server so everyone will hit an unpredictable data server and the haproxy configuration will be rewritten if the list order changes. I may be misunderstanding something fundamental, so please let me know if so.

{{ range $i, $s := service "dataservice" }}
{{ if eq $i 0 }}
        server dsmaster {{$s.Address}}:9200 check
{{else}}
        server dsslave_{{$s.ID}} {{$s.Address}}:9200 check backup
{{end}}
{{ end }}

(btw the data service is Elastic Search but I'm trying to keep the example abstract :)

jdelic commented 9 years ago

@tzz Perhaps you're interested: We now render a Python script from consul-template which we invoke using the "command" portion of the consul-template template definition which then renders a Jinja2 template and executes another command. That way we get a sane templating system without having to patch/extend/recompile and then ship our own version of consul-template . Basically we use it like this:

consul-template -consul localhost:8500 -template \
    'servicerenderer.ctmpl.py:/tmp/example.py:chmod 700 /tmp/example.py;/tmp/example.py --has "servicestack:internal" -o /etc/haproxy/haproxy.conf --cmd "systemctl reload haproxy" haproxy.jinja.cfg'

Here is the sourcecode if you want to have a look.

tzz commented 9 years ago

@jdelic oh that's clever. I would use && to ensure that the commands all succeed but otherwise it looks great. I'll think about using it for our case as well. Thanks!

tgeens commented 8 years ago

I'm trying to build up some dynamics urls based on tags provided by the service. It uses a few conditionals based on tags. I can build up the url just fine, all though that line is a little long and hairy.

Now the problem is: I need that in 6-7 places, so assigning all that to a variable would be a lot more DRY and maintainable

tgeens commented 8 years ago

As a workaround, I'm using a macro right now.

See {{ define "NAME" }}...{{ end }}

jdelic commented 7 years ago

Sorry for commenting on this long-closed issue, but people might come here through Google.

I have since published the above work-around in consul-smartstack.

(@tgeens @tzz perhaps you're also interested in this)