From cce32e629eb4c760b635f4e8f94cf6b8acb1cc6b Mon Sep 17 00:00:00 2001 From: Frankie Dintino Date: Sun, 1 Oct 2017 12:44:53 -0400 Subject: [PATCH] Add ability to use pongo2 as an alternative to Go Template refs #177 --- GLOCKFILE | 1 + cmd/docker-gen/main.go | 3 + config.go | 1 + example_pongo2.conf | 21 +++++ template.go | 115 ++++++++++++++++++++--- templates_pongo2/dnsmasq.hosts.conf.tmpl | 8 ++ templates_pongo2/etcd.tmpl | 13 +++ templates_pongo2/fluentd.conf.tmpl | 20 ++++ templates_pongo2/logrotate.tmpl | 27 ++++++ templates_pongo2/nginx.tmpl | 65 +++++++++++++ 10 files changed, 263 insertions(+), 11 deletions(-) create mode 100644 example_pongo2.conf create mode 100644 templates_pongo2/dnsmasq.hosts.conf.tmpl create mode 100644 templates_pongo2/etcd.tmpl create mode 100644 templates_pongo2/fluentd.conf.tmpl create mode 100644 templates_pongo2/logrotate.tmpl create mode 100644 templates_pongo2/nginx.tmpl diff --git a/GLOCKFILE b/GLOCKFILE index a9a6d4a7..fdc57a7a 100644 --- a/GLOCKFILE +++ b/GLOCKFILE @@ -1,6 +1,7 @@ github.com/BurntSushi/toml 056c9bc7be7190eaa7715723883caffa5f8fa3e4 github.com/docker/docker f2afa26235941fd79f40eb1e572e19e4ac2b9bbe github.com/docker/go-units 0dadbb0345b35ec7ef35e228dabb8de89a65bf52 +github.com/flosch/pongo2 1f4be1efe3b3529b7e58861f75d70120a9567dc4 github.com/fsouza/go-dockerclient d2a6d0596004cc01062a2a068540b817f911e6dc github.com/gorilla/mux d391bea3118c9fc17a88d62c9189bb791255e0ef golang.org/x/net a04bdaca5b32abe1c069418fb7088ae607de5bd0 diff --git a/cmd/docker-gen/main.go b/cmd/docker-gen/main.go index f84546a9..b8a5e6f8 100644 --- a/cmd/docker-gen/main.go +++ b/cmd/docker-gen/main.go @@ -25,6 +25,7 @@ var ( notifySigHUPContainerID string onlyExposed bool onlyPublished bool + templateEngine string includeStopped bool configFiles stringslice configs dockergen.ConfigFile @@ -89,6 +90,7 @@ func initFlags() { flag.BoolVar(&watch, "watch", false, "watch for container changes") flag.StringVar(&wait, "wait", "", "minimum and maximum durations to wait (e.g. \"500ms:2s\") before triggering generate") flag.BoolVar(&onlyExposed, "only-exposed", false, "only include containers with exposed ports") + flag.StringVar(&templateEngine, "engine", "go", "engine used to render templates (\"go\" or \"pongo2\")") flag.BoolVar(&onlyPublished, "only-published", false, "only include containers with published ports (implies -only-exposed)") @@ -138,6 +140,7 @@ func main() { config := dockergen.Config{ Template: flag.Arg(0), Dest: flag.Arg(1), + Engine: templateEngine, Watch: watch, Wait: w, NotifyCmd: notifyCmd, diff --git a/config.go b/config.go index 773346c2..38160de2 100644 --- a/config.go +++ b/config.go @@ -11,6 +11,7 @@ import ( type Config struct { Template string Dest string + Engine string Watch bool Wait *Wait NotifyCmd string diff --git a/example_pongo2.conf b/example_pongo2.conf new file mode 100644 index 00000000..8f4f709a --- /dev/null +++ b/example_pongo2.conf @@ -0,0 +1,21 @@ +[[config]] +template = "templates_pongo2/nginx.tmpl" +engine = "pongo2" +dest = "/tmp/nginx.conf" +onlyexposed = true +notifycmd = "/etc/init.d/nginx reload" + +[[config]] +template = "templates_pongo2/fluentd.conf.tmpl" +engine = "pongo2" +dest = "/tmp/fluentd.conf" +watch = true +notifycmd = "echo test" + +[[config]] +template = "templates_pongo2/etcd.tmpl" +engine = "pongo2" +dest = "/tmp/etcd.sh" +watch = true +notifycmd = "/bin/bash /tmp/etcd.sh" +interval = 10 \ No newline at end of file diff --git a/template.go b/template.go index 1eeffaf5..a87b3ecf 100644 --- a/template.go +++ b/template.go @@ -18,6 +18,8 @@ import ( "strings" "syscall" "text/template" + + "github.com/flosch/pongo2" ) func exists(path string) (bool, error) { @@ -453,6 +455,84 @@ func newTemplate(name string) *template.Template { return tmpl } +// Takes a template function that returns an error as its second return value, +// and returns a function that takes a pongo2 ExecutionContext as its first +// argument and calls ExecutionContext.OrigError() if the second return value +// of the original function is not nil when called. Otherwise returns the first +// return value. +func pongoWrap(fn interface{}) func(*pongo2.ExecutionContext, ...interface{}) interface{} { + fv := reflect.ValueOf(fn) + ft := reflect.TypeOf(fn) + return func(ctx *pongo2.ExecutionContext, args ...interface{}) interface{} { + if ft.NumIn() != len(args) { + msg := fmt.Sprintf("Wrong number of arguments; expected %d, got %d", ft.NumIn(), len(args)) + return ctx.Error(msg, nil) + } + vals := make([]reflect.Value, len(args)) + for i, v := range args { + vt := reflect.TypeOf(v) + if !vt.ConvertibleTo(ft.In(i)) { + msg := fmt.Sprintf("Wrong type for argument %d (got %s, expected %s)\n", i, vt, ft.In(i)) + return ctx.Error(msg, nil) + } + vals[i] = reflect.ValueOf(args[i]) + } + retvals := fv.Call(vals) + ret := retvals[0].Interface() + err := retvals[1].Interface() + if err != nil { + return ctx.OrigError(err.(error), nil) + } + return ret + } +} + +func pongoContext(containers Context) pongo2.Context { + context := pongo2.Context{ + "containers": containers, + "env": containers.Env, + "docker": containers.Docker, + "closest": arrayClosest, + "coalesce": coalesce, + "contains": contains, + "dict": pongoWrap(dict), + "dir": pongoWrap(dirList), + "exists": pongoWrap(exists), + "first": arrayFirst, + "groupBy": pongoWrap(groupBy), + "groupByKeys": pongoWrap(groupByKeys), + "groupByMulti": pongoWrap(groupByMulti), + "groupByLabel": pongoWrap(groupByLabel), + "hasPrefix": hasPrefix, + "hasSuffix": hasSuffix, + "json": pongoWrap(marshalJson), + "intersect": intersect, + "keys": pongoWrap(keys), + "last": arrayLast, + "replace": strings.Replace, + "parseBool": strconv.ParseBool, + "parseJson": pongoWrap(unmarshalJson), + "printf": fmt.Sprintf, + "queryEscape": url.QueryEscape, + "sha1": hashSha1, + "split": strings.Split, + "splitN": strings.SplitN, + "trimPrefix": trimPrefix, + "trimSuffix": trimSuffix, + "trim": trim, + "when": pongoWrap(when), + "where": pongoWrap(where), + "whereExist": pongoWrap(whereExist), + "whereNotExist": pongoWrap(whereNotExist), + "whereAny": pongoWrap(whereAny), + "whereAll": pongoWrap(whereAll), + "whereLabelExists": pongoWrap(whereLabelExists), + "whereLabelDoesNotExist": pongoWrap(whereLabelDoesNotExist), + "whereLabelValueMatches": pongoWrap(whereLabelValueMatches), + } + return context +} + func filterRunning(config Config, containers Context) Context { if config.IncludeStopped { return containers @@ -486,7 +566,7 @@ func GenerateFile(config Config, containers Context) bool { filteredContainers = filteredRunningContainers } - contents := executeTemplate(config.Template, filteredContainers) + contents := executeTemplate(config.Template, config.Engine, filteredContainers) if !config.KeepBlankLines { buf := new(bytes.Buffer) @@ -537,16 +617,29 @@ func GenerateFile(config Config, containers Context) bool { return true } -func executeTemplate(templatePath string, containers Context) []byte { - tmpl, err := newTemplate(filepath.Base(templatePath)).ParseFiles(templatePath) - if err != nil { - log.Fatalf("Unable to parse template: %s", err) - } +func executeTemplate(templatePath string, templateEngine string, containers Context) []byte { + if templateEngine == "pongo2" { + context := pongoContext(containers) + tmpl, err := pongo2.FromFile(templatePath) + if err != nil { + log.Fatalf("Unable to parse template: %s", err) + } + contents, err := tmpl.ExecuteBytes(context) + if err != nil { + log.Fatalf("Template error: %s\n", err) + } + return contents + } else { + tmpl, err := newTemplate(filepath.Base(templatePath)).ParseFiles(templatePath) + if err != nil { + log.Fatalf("Unable to parse template: %s", err) + } - buf := new(bytes.Buffer) - err = tmpl.ExecuteTemplate(buf, filepath.Base(templatePath), &containers) - if err != nil { - log.Fatalf("Template error: %s\n", err) + buf := new(bytes.Buffer) + err = tmpl.ExecuteTemplate(buf, filepath.Base(templatePath), &containers) + if err != nil { + log.Fatalf("Template error: %s\n", err) + } + return buf.Bytes() } - return buf.Bytes() } diff --git a/templates_pongo2/dnsmasq.hosts.conf.tmpl b/templates_pongo2/dnsmasq.hosts.conf.tmpl new file mode 100644 index 00000000..1b6880ee --- /dev/null +++ b/templates_pongo2/dnsmasq.hosts.conf.tmpl @@ -0,0 +1,8 @@ +{% set domain = "docker.company.com" %} +{% for container in containers %} +# {{ container.Name }} ({{ container.ID }} from {{ container.Image.Repository }}) +{{ container.IP }} {{ container.Name }}.{{ domain }} +{% if container.IP6Global %} +{{ container.IP6Global }} {{ container.Name }}.{{ domain }} +{% endif %} +{% endfor %} diff --git a/templates_pongo2/etcd.tmpl b/templates_pongo2/etcd.tmpl new file mode 100644 index 00000000..6b122a10 --- /dev/null +++ b/templates_pongo2/etcd.tmpl @@ -0,0 +1,13 @@ +#!/bin/bash + +# Genenerated by {{ env.USER }} +# Docker Version {{ docker.Version }} + +{% for container in containers %} +{% if container.Addresses|length > 0 %} +{% with address = container.Addresses.0 %} +# {{ container.Name }} +curl -XPUT -q -d value="{{ address.IP }}:{{ address.Port }}" -d ttl=15 http://127.0.0.1:4001/v2/keys/backends/{{ container.Image.Repository }}/{{ printf("%.*s", 12, container.ID) }} +{% endwith %} +{% endif %} +{% endfor %} diff --git a/templates_pongo2/fluentd.conf.tmpl b/templates_pongo2/fluentd.conf.tmpl new file mode 100644 index 00000000..7afc4280 --- /dev/null +++ b/templates_pongo2/fluentd.conf.tmpl @@ -0,0 +1,20 @@ + +## File input +## read docker logs with tag=docker.container + +{% for container in containers %} + + type tail + format json + time_key time + path /var/lib/docker/containers/{{ container.ID }}/{{ container.ID }}-json.log + pos_file /var/lib/docker/containers/{{ container.ID }}/{{ container.ID }}-json.log.pos + tag docker.container.{{ printf("%.*s", 12, container.ID) }} + rotate_wait 5 + +{% endfor %} + + + type stdout + + diff --git a/templates_pongo2/logrotate.tmpl b/templates_pongo2/logrotate.tmpl new file mode 100644 index 00000000..a7c7ce74 --- /dev/null +++ b/templates_pongo2/logrotate.tmpl @@ -0,0 +1,27 @@ +{% for container in containers %} +{% set logs = container.Env.LOG_FILES %} +{% if logs %} +{% for logfile in split(logs, ",") %} +/var/lib/docker/containers/{{ container.ID }}/root{{ logfile }}{% endfor %} +{ + daily + missingok + rotate 52 + compress + delaycompress + notifempty + create 644 root root +} +{% endif %} +/var/lib/docker/containers/{{ container.ID }}/{{ container.ID }}-json.log +{ + daily + missingok + rotate 7 + compress + delaycompress + notifempty + create 644 root root +} +{% endfor %} + diff --git a/templates_pongo2/nginx.tmpl b/templates_pongo2/nginx.tmpl new file mode 100644 index 00000000..1ef2525d --- /dev/null +++ b/templates_pongo2/nginx.tmpl @@ -0,0 +1,65 @@ +server { + listen 80 default_server; + server_name _; # This is just an invalid value which will never trigger on a real hostname. + error_log /proc/self/fd/2; + access_log /proc/self/fd/1; + return 503; +} + +{% for host, ctrs in groupByMulti(containers, "Env.VIRTUAL_HOST", ",") %} + +upstream {{ host }} { + +{% for value in ctrs %} + + {% set network = value.Networks.0 %} + + {# If only 1 port exposed, use that #} + {% if value.Addresses|length == 1 %} + {% with address = value.Addresses.0 %} + # {{ value.Name }} + server {{ network.IP }}:{{ address.Port }}; + {% endwith %} + + {# If more than one port exposed, use the one matching VIRTUAL_PORT env var #} + {% elif value.Env.VIRTUAL_PORT %} + {% for address in value.Addresses %} + {% if address.Port == value.Env.VIRTUAL_PORT %} + # {{value.Name}} + server {{ network.IP }}:{{ address.Port }}; + {% endif %} + {% endfor %} + + {# Else default to standard web port 80 #} + {% else %} + {% for address in value.Addresses %} + {% if address.Port == "80" %} + # {{value.Name}} + server {{ network.IP }}:{{ address.Port }}; + {% endif %} + {% endfor %} + {% endif %} +{% endfor %} +} + +server { + gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; + + server_name {{ host }}; + proxy_buffering off; + error_log /proc/self/fd/2; + access_log /proc/self/fd/1; + + location / { + proxy_pass http://{{ trim(host) }}; + proxy_set_header Host http_host; + proxy_set_header X-Real-IP remote_addr; + proxy_set_header X-Forwarded-For proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto scheme; + + # HTTP 1.1 support + proxy_http_version 1.1; + proxy_set_header Connection ""; + } +} +{% endfor %} \ No newline at end of file