Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add WASM module templates #596

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 90 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,53 +93,55 @@ Generate files from docker container meta-data

Options:
-config value
config files with template directives. Config files will be merged if this option is specified multiple times. (default [])
config files with template directives. Config files will be merged if this option is specified multiple times.
-endpoint string
docker api endpoint (tcp|unix://..). Default unix:///var/run/docker.sock
docker api endpoint (tcp|unix://..). Default unix:///var/run/docker.sock
-include-stopped
include stopped containers
-interval int
notify command interval (secs)
notify command interval (secs)
-keep-blank-lines
keep blank lines in the output file
keep blank lines in the output file
-notify restart xyz
run command after template is regenerated (e.g restart xyz)
run command after template is regenerated (e.g restart xyz)
-notify-container string
container to send a signal to
-notify-output
log the output(stdout/stderr) of notify command
-notify-container container-ID
container to send a signal to
-notify-signal signal
signal to send to the -notify-container. -1 to call docker restart. Defaults to 1 aka. HUP.
All available signals available on the [dockerclient](https://github.com/fsouza/go-dockerclient/blob/01804dec8a84d0a77e63611f2b62d33e9bb2b64a/signal.go)
log the output(stdout/stderr) of notify command
-notify-sighup container-ID
send HUP signal to container. Equivalent to 'docker kill -s HUP container-ID', or `-notify-container container-ID -notify-signal 1`
send HUP signal to container. Equivalent to docker kill -s HUP container-ID
-notify-signal int
signal to send to the notify-container. Defaults to SIGHUP (default 1)
-only-exposed
only include containers with exposed ports
only include containers with exposed ports
-only-published
only include containers with published ports (implies -only-exposed)
-include-stopped
include stopped containers
only include containers with published ports (implies -only-exposed)
-tlscacert string
path to TLS CA certificate file (default "/Users/jason/.docker/machine/machines/default/ca.pem")
path to TLS CA certificate file (default "/root/.docker/ca.pem")
-tlscert string
path to TLS client certificate file (default "/Users/jason/.docker/machine/machines/default/cert.pem")
path to TLS client certificate file (default "/root/.docker/cert.pem")
-tlskey string
path to TLS client key file (default "/Users/jason/.docker/machine/machines/default/key.pem")
path to TLS client key file (default "/root/.docker/key.pem")
-tlsverify
verify docker daemon's TLS certicate (default true)
verify docker daemon's TLS certicate
-version
show version
show version
-wait string
minimum and maximum durations to wait (e.g. "500ms:2s") before triggering generate
-wasmcache string
path to cache directory for compiled wasm modules (default "/tmp/docker-gen-wasm-cache")
-watch
watch for container changes
-wait
minimum (and/or maximum) duration to wait after each container change before triggering
watch for container changes

Arguments:
template - path to a template to generate
dest - path to write the template. If not specfied, STDOUT is used
dest - path to a write the template. If not specfied, STDOUT is used

Environment Variables:
DOCKER_HOST - default value for -endpoint
DOCKER_CERT_PATH - directory path containing key.pem, cert.pm and ca.pem
DOCKER_TLS_VERIFY - enable client TLS verification]
DOCKER_CERT_PATH - directory path containing key.pem, cert.pem and ca.pem
DOCKER_TLS_VERIFY - enable client TLS verification
WASM_CACHE_DIR - path to cache directory for compiled wasm modules, default for -wasmcache
```

If no `<dest>` file is specified, the output is sent to stdout. Mainly useful for debugging.
Expand Down Expand Up @@ -404,6 +406,67 @@ For example, this is a JSON version of an emitted RuntimeContainer struct:
* *`whereLabelExists $containers $label`*: Filters a slice of containers based on the existence of the label `$label`.
* *`whereLabelDoesNotExist $containers $label`*: Filters a slice of containers based on the non-existence of the label `$label`.
* *`whereLabelValueMatches $containers $label $pattern`*: Filters a slice of containers based on the existence of the label `$label` with values matching the regular expression `$pattern`.

===

### Templating via WASM modules

`docker-gen` can run arbitrary Webassembly (WASI) modules. The module takes json-serialized representation of docker containers on its standard input (`os.Stdin`), emits a file on standard output (`os.Stdout`) and can output messages direcly on `docker-gen`'s standatd error stream (`os.Stderr`). You can develop your own wasm module using `github.com/nginx-proxy/docker-gen/plugin` library. It automates parsing operations and emits output and errors. It also configures Go's standard `log` module to output on `os.Stderr`, so it can be used to show errors and warnings. WASM plugin module receives the following data structure:

```go
type PluginContext struct {
Containers []*RuntimeContainer
Env map[string]string
Docker Docker
}
```

All other structures are the same as shown for the templates.

Here is a simple example of a plugin module:


```go
// file main.go
package main

import (
"bytes"
"fmt"

"github.com/nginx-proxy/docker-gen/plugin"
)


func main() {
plugin.Main(func(in *plugin.PluginContext) ([]byte, error) {
bout := bytes.Buffer{}
for _, container := range in.Containers {
bout.WriteString(fmt.Sprintf(
"Container %s is runnning: %t(%s)\n",
container.Name,
container.State.Running,
container.State.Health.Status))
}
return bout.Bytes(), nil
})
}
```

You can compile it to webassembly with tinygo:

```shell
tinygo build -o ./docker-ps-like.wasm -target wasi main.go
```

After compilation you can use the compiled plugin as a template:

```shell
docker-gen ./docker-ps-like.wasm
```

A more sophisticated example of a webassembly plugin module can be found [here](/examples/nginx-wasm-example/). It outputs the same content as [`nginx.tmpl`](/templates/nginx.tmpl), but provides several warnings on misconfiguration of `VIRTUAL_HOST` and `VIRTUAL_PORT` variables on standart errors (`os.Stderr`).


===

Expand Down
8 changes: 8 additions & 0 deletions cmd/docker-gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
docker "github.com/fsouza/go-dockerclient"
"github.com/nginx-proxy/docker-gen/internal/config"
"github.com/nginx-proxy/docker-gen/internal/generator"
"github.com/nginx-proxy/docker-gen/internal/template"
)

type stringslice []string
Expand All @@ -38,6 +39,7 @@ var (
tlsKey string
tlsCaCert string
tlsVerify bool
wasmCacheDir *string = &template.WasmCacheDir
)

func (strings *stringslice) String() string {
Expand Down Expand Up @@ -68,6 +70,7 @@ Environment Variables:
DOCKER_HOST - default value for -endpoint
DOCKER_CERT_PATH - directory path containing key.pem, cert.pem and ca.pem
DOCKER_TLS_VERIFY - enable client TLS verification
WASM_CACHE_DIR - path to cache directory for compiled wasm modules, default for -wasmcache
`)
println(`For more information, see https://github.com/nginx-proxy/docker-gen`)
}
Expand Down Expand Up @@ -109,6 +112,11 @@ func initFlags() {
flag.StringVar(&tlsCert, "tlscert", filepath.Join(certPath, "cert.pem"), "path to TLS client certificate file")
flag.StringVar(&tlsKey, "tlskey", filepath.Join(certPath, "key.pem"), "path to TLS client key file")
flag.StringVar(&tlsCaCert, "tlscacert", filepath.Join(certPath, "ca.pem"), "path to TLS CA certificate file")
wasmCacheEnv := os.Getenv("WASM_CACHE_DIR")
if wasmCacheEnv != "" {
*wasmCacheDir = wasmCacheEnv
}
flag.StringVar(wasmCacheDir, "wasmcache", *wasmCacheDir, "path to cache directory for compiled wasm modules")
flag.BoolVar(&tlsVerify, "tlsverify", os.Getenv("DOCKER_TLS_VERIFY") != "", "verify docker daemon's TLS certicate")

flag.Usage = usage
Expand Down
12 changes: 12 additions & 0 deletions examples/nginx-wasm-example/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module github.com/nginx-proxy/docker-gen/nginx-wasm-example

go 1.21

require github.com/nginx-proxy/docker-gen/plugin v0.0.0-00010101000000-000000000000

require (
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
)

replace github.com/nginx-proxy/docker-gen/plugin => ../../plugin
4 changes: 4 additions & 0 deletions examples/nginx-wasm-example/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
131 changes: 131 additions & 0 deletions examples/nginx-wasm-example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package main

import (
"bytes"
"log"
"os"
"strings"

"github.com/nginx-proxy/docker-gen/plugin"
)

var defaultHttpServer = `
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;
}
`

var upstreamServer = `
# ${NAME}
server ${IP}:${PORT};
`

var proxiedServer = `
upstream ${HOST} {
${UPSTREAMS}
}

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 ${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 "";
}
}
`

func virtualhost(t *plugin.RuntimeContainer) (*string, error) {
if s, ok := t.Env["VIRTUAL_HOST"]; ok {
return &s, nil
}
return nil, nil
}

func findAddressWithPort(c *plugin.RuntimeContainer, port string) bool {
for _, address := range c.Addresses {
if address.Port == port {
return true
}
}
return false
}

func NginxGen(in *plugin.PluginContext) (out []byte, err error) {
bout := bytes.NewBuffer(make([]byte, 4096))
bout.WriteString(defaultHttpServer)

containersWithHosts, err := plugin.GroupByMulti(in.Containers, virtualhost, ",")
if err != nil {
return
}
for host, containers := range containersWithHosts {
upstreams := make([]string, 0, len(containers))
for _, c := range containers {
if c.State.Health.Status != "" && c.State.Health.Status != "healthy" {
log.Printf("Container %s is unhealthy", c.Name)
continue
}
if len(c.Networks) == 0 {
log.Printf("Container %s has no networks, but it is a virtual host %s", c.Name, host)
continue
}
vars := map[string]string{"NAME": c.Name, "IP": c.Networks[0].IP, "PORT": ""}
if len(c.Addresses) == 1 {
// If only 1 port exposed, use that
vars["PORT"] = c.Addresses[0].Port
} else if port, ok := c.Env["VIRTUAL_PORT"]; ok {
if findAddressWithPort(c, port) {
vars["PORT"] = port
} else {
log.Printf("Container %s (vhost %s) has VIRTUAL_PORT %s, but it does not expose it", c.Name, host, port)
continue
}
} else {
// Else default to standard web port 80
port := "80"
if findAddressWithPort(c, port) {
vars["PORT"] = port
} else {
log.Printf("Container %s (vhost %s) does not declare VIRTUAL_PORT, exposes multiple ports and does not expose port 80", c.Name, host)
continue
}
}
upstreams = append(upstreams, os.Expand(upstreamServer, func(s string) string { return vars[s] }))
}
expanded := os.Expand(proxiedServer, func(s string) string {
switch s {
case "HOST":
return host
case "UPSTREAMS":
return strings.Join(upstreams, "\n")
default:
return "$" + s
}
})
bout.WriteString(expanded)
}

out = bout.Bytes()
return
}

func main() {
plugin.Main(NginxGen)
}
Binary file not shown.
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ require (
github.com/BurntSushi/toml v1.3.2
github.com/Masterminds/sprig/v3 v3.2.3
github.com/fsouza/go-dockerclient v1.10.2
github.com/nginx-proxy/docker-gen/plugin v0.0.0-00010101000000-000000000000
github.com/stretchr/testify v1.8.4
github.com/tetratelabs/wazero v1.6.0
)

replace github.com/nginx-proxy/docker-gen/plugin => ./plugin

require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
Expand All @@ -25,8 +29,10 @@ require (
github.com/gorilla/mux v1.8.1 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.15.9 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
Expand All @@ -56,6 +58,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
Expand Down Expand Up @@ -94,6 +98,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tetratelabs/wazero v1.6.0 h1:z0H1iikCdP8t+q341xqepY4EWvHEw8Es7tlqiVzlP3g=
github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
Expand Down
5 changes: 5 additions & 0 deletions go.work
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
go 1.21

use .
use plugin
use examples/nginx-wasm-example
Loading