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